Vis

Provider extensions

Use a provider extension to connect Vis to an LLM provider that needs custom sign-in or model discovery. You register it with vis.Provider, and Vis can select its models alongside the built-in providers.

When to use

  • Your LLM service needs an interactive sign-in or tokens that expire. Handle sign-in and renewal in callbacks such as auth_fn and refresh_token_fn.
  • The service's models should come from the service itself. Extend the model list with enrich_models_fn, described in Callbacks.
  • The extension should own the provider's credentials, outside state.yml. Register a managed provider.

If your service needs only an endpoint and API key, use Configuration instead. No extension is needed.

Before you start

The example needs no external Python dependencies, but its endpoint and model names are placeholders: replace them with a service you are authorized to use.

Declare and load a provider

  1. Save this complete entry as .vis/extensions/example_provider.py in your project. It reads EXAMPLE_API_KEY only when a callback runs, not while declaring the provider.
import os
import blockether.vis.extension as vis


def credential() -> vis.ProviderCredential | None:
    token = os.environ.get("EXAMPLE_API_KEY")
    return vis.ProviderCredential(token) if token else None


def status() -> vis.ProviderStatus:
    return vis.ProviderStatus(is_authenticated=bool(os.environ.get("EXAMPLE_API_KEY")),
                              source="env-var")


vis.register_extension(vis.Extension(
    name="provider-example",
    description="An OpenAI-compatible provider.",
    env=["EXAMPLE_API_KEY"],
    providers=[vis.Provider(
        id="example",
        label="Example AI",
        preset=vis.ProviderPreset(
            base_url="https://gateway.example.com/v1",
            api_style="openai",
            default_models=["example-model"],
        ),
        get_token_fn=credential,
        status_fn=status,
    )],
))
  1. Supply EXAMPLE_API_KEY through the project's environment configuration, a local .env file or the gateway's startup environment. Do not commit the value. See environment resolution.
  2. Start Vis in that project or run /reload, then inspect vis-agent providers status example in the terminal. The status callback reports whether a credential is present. It does not verify it with the service.
  3. Add example through Add provider or the providers configuration. Select the real model name. Send a request to check the connection.

The preset supplies endpoint and model defaults. Callbacks supply credentials and status. Registration or a positive local status is not proof that the service accepts the key. No interactive login is defined in this minimal example.

Records

Record Fields
ProviderPreset base_url, api_style, default_models, responses_path, llm_headers, extra_body, is_hidden, JSON-only extra
ProviderCredential token, optional api_url, api_style, responses_path, llm_headers, source
ProviderStatus is_authenticated, source, display-only extra
ProviderModel name, context, is_tool_calls_supported, is_vision_supported, JSON-only extra
ProviderLimits limits (list of ProviderLimit), rpm, tpm, note, error

api_style accepts the same values as configuration: anthropic, openai, openai-responses, gemini and their aliases. Configuration overrides the credential, which overrides the preset. A credential can therefore supply an endpoint discovered during authentication while still allowing a user override. Header maps replace the entire field at each level.

A model can use another wire format than its provider. For example, a gateway can serve Claude models on /messages and other models on the Responses API. Put api_style in the extra of that ProviderModel, as in extra={"api_style": "anthropic"}. It accepts the same values as the preset. If a value names no wire format, Vis ignores the whole model list from enrich_models_fn, logs a warning and keeps the configured models.

ProviderModel.is_vision_supported tells Vis if a model reads images. With True, Vis sends attached pictures to the model. With False, Vis sends a text description of each picture. With None, the model catalog decides. This field works like is_vision_supported on a model in configuration.

Vis also reads the old names is_tool_call and is_image_input. Use the new names in new code.

Token and header fields are excluded from a record's repr. Never log credentials or put them in status metadata. A missing credential is None, not a record with an empty token.

Model prices

Set the price of a model when your provider bills other rates than the public list prices. Put a pricing map in the extra of ProviderModel:

vis.ProviderModel(
    name="claude-haiku-5-5",
    context=1000000,
    is_tool_calls_supported=True,
    extra={"pricing": {"input": 0.11, "output": 0.55,
                       "input_over_100k": 0.55, "output_over_100k": 2.75}},
)
  • Give each rate in USD for 1 million tokens.
  • Use the keys input, output, cached_input, cache_write, cache_write_5m and cache_write_1h.
  • Add _over_100k, _over_200k or _over_272k to a key for the rate above that prompt size.

Vis prices each response with the rates of the provider that served it. Without a pricing map, Vis uses the built-in price list. A rate that is not a non-negative number stops the model list of the provider, and Vis logs a warning.

Callbacks

Callback Signature Purpose
get_token_fn, detect_fn () -> ProviderCredential | None passive credential read that never starts a login
refresh_token_fn (rejected: str | None) -> ProviderCredential | None renew a token (a zero-argument form also works)
status_fn () -> ProviderStatus | None connection state, not quota
limits_fn () -> ProviderLimits | None usage report
auth_fn (printer) -> str | bool | None interactive login (print instructions with printer(line))
auth_prompt_fn () -> Sequence[str] | str | None static login guidance
logout_fn () -> None discard the credential
enrich_models_fn (provider, router_opts) -> Sequence[ProviderModel] | None extend the model list (None keeps the defaults)
on_selected_fn (event) -> None notification after selection

Callbacks are optional and synchronous. Async functions and invalid signatures are rejected at declaration. Passive callback errors are logged and return no result. Authentication errors are returned to the caller.

Startup, status probes and limits polling run without a session. vis.shell and vis.jailed_shell work there, but vis.ask does not.

Report usage with the limits contract:

def limits() -> vis.ProviderLimits:
    return vis.ProviderLimits(limits=[
        vis.ProviderLimit("daily-tokens", "Daily tokens",
                          scope="account", kind="tokens",
                          precision="exact", source="provider-api",
                          used=25.49, limit=100,
                          window=vis.ProviderLimitWindow("calendar", unit="day", size=1)),
    ])

ProviderLimit also accepts remaining, is_unlimited, subject and note. Inspect the result with vis-agent providers limits <id>.

Managed providers

A managed provider is registered when its extension loads. The extension defines its endpoint and models and stores credentials outside state.yml. Set is_managed=True:

vis.Provider(
    id="corp-gateway",
    label="Corp Gateway",
    is_managed=True,
    preset=vis.ProviderPreset(base_url="https://gateway.example.com/v1",
                              api_style="openai",
                              default_models=["corp-large", "corp-small"]),
    get_token_fn=_issued_token,
    auth_fn=_browser_oauth,   # optional: provider-owned first-use login
)
Ordinary provider Managed
Owner user configuration the extension
Add provider dialog listed never listed
Appears after configuration when the extension loads
Authentication configured flow runtime credential, or the extension's auth_fn

To implement one:

  1. Set is_managed=True and supply a preset with default_models.
  2. Make get_token_fn() a passive read that returns ProviderCredential(token) or None while signed out.
  3. When sign-in is needed, supply auth_fn(printer). It completes the interactive flow, stores the credential where get_token_fn() reads it, and returns "ok" or "already-authenticated".
  4. Keep status_fn() and limits_fn() passive. Put renewal in refresh_token_fn().

With auth_fn, a request that finds no usable token runs the login once, calls get_token_fn() again and resumes. Concurrent requests share the flow. Status probes, startup and opening the model picker never run auth_fn. Test the explicit path with vis-agent providers auth <id>, then make a request while signed out to verify first-use login.

You can also start this login from the provider's row: Sign in in the Companion or Authenticate in the TUI. Vis runs auth_fn on the gateway machine, and the client shows each line that auth_fn passes to printer. Print a fallback sign-in URL, so that a person can finish sign-in when no browser opens on that machine.

See also