Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.provider.auth

Model-provider adapter to the shared gateway authentication lifecycle in flow. Registered providers own protocol start/complete/await and credential persistence; flow owns callback transport, expiry, cancellation, single exchange and verdicts. This adapter handles provider eligibility, API-key storage and fleet invalidation. No browser, relay, token or PKCE verifier crosses the public flow view.

Model-provider adapter to the shared gateway authentication lifecycle in `flow`.
Registered providers own protocol start/complete/await and credential persistence;
`flow` owns callback transport, expiry, cancellation, single exchange and verdicts.
This adapter handles provider eligibility, API-key storage and fleet invalidation.
No browser, relay, token or PKCE verifier crosses the public flow view.
raw docstring

com.blockether.vis.internal.provider.callback

Short-lived OAuth loopback receivers. They carry only a callback, never tokens.

listen! binds a numeric loopback address, validates the exact destination and state before accepting ONE response, and closes on completion, stop or expiry. The browser sees a receipt, not a claim that token exchange has succeeded. No request values are reflected into HTML or logged. Shared by provider and MCP auth.

Short-lived OAuth loopback receivers. They carry only a callback, never tokens.

`listen!` binds a numeric loopback address, validates the exact destination and
state before accepting ONE response, and closes on completion, stop or expiry.
The browser sees a receipt, not a claim that token exchange has succeeded.
No request values are reflected into HTML or logged. Shared by provider and MCP auth.
raw docstring

com.blockether.vis.internal.provider.credential-command

Command-backed provider credentials — the api_key_command config key.

A static api_key (or a ${NAME} reference to one) is only good for a long-lived secret. Short-lived SSO/gateway tokens come from a credential HELPER instead: a small program that prints a fresh token on stdout. This namespace runs that helper for a provider and hands the trimmed stdout back as the API key.

Three contracts hold this together:

  • No shell, ever. The configured value is a structured argv that is passed to ProcessBuilder verbatim. It is never joined, never split on whitespace, and never handed to sh -c, so a token containing shell metacharacters — or a config written by someone else — cannot become command injection.
  • The credential is write-once, in memory. Resolved stdout is returned to the caller and cached HERE. It is never persisted (nothing writes it back into :api-key), never logged, and never placed in an error message. Every diagnostic this namespace produces is built from argv[0], the exit code, and the helper's stderr — never its stdout.
  • Bounded and single-flight. One helper invocation per provider at a time, bounded by timeout-ms, with a successful token cached for success-ttl-ms and a failure remembered for failure-ttl-ms. A long-running gateway must not fork a token helper per turn, and an interactive helper must never be launched twice concurrently.

resolve! never throws: callers use its :error to render a provider as unavailable (providers/provider-status, doctor) or to drop it from the router build, exactly as an unresolved ${NAME} is handled today.

Command-backed provider credentials — the `api_key_command` config key.

A static `api_key` (or a `${NAME}` reference to one) is only good for a
long-lived secret. Short-lived SSO/gateway tokens come from a credential
HELPER instead: a small program that prints a fresh token on stdout. This
namespace runs that helper for a provider and hands the trimmed stdout back
as the API key.

Three contracts hold this together:

  - **No shell, ever.** The configured value is a structured argv that is
    passed to `ProcessBuilder` verbatim. It is never joined, never split on
    whitespace, and never handed to `sh -c`, so a token containing shell
    metacharacters — or a config written by someone else — cannot become
    command injection.
  - **The credential is write-once, in memory.** Resolved stdout is returned
    to the caller and cached HERE. It is never persisted (nothing writes it
    back into `:api-key`), never logged, and never placed in an error
    message. Every diagnostic this namespace produces is built from argv[0],
    the exit code, and the helper's stderr — never its stdout.
  - **Bounded and single-flight.** One helper invocation per provider at a
    time, bounded by `timeout-ms`, with a successful token cached for
    `success-ttl-ms` and a failure remembered for `failure-ttl-ms`. A
    long-running gateway must not fork a token helper per turn, and an
    interactive helper must never be launched twice concurrently.

`resolve!` never throws: callers use its `:error` to render a provider as
unavailable (`providers/provider-status`, `doctor`) or to drop it from the
router build, exactly as an unresolved `${NAME}` is handled today.
raw docstring

com.blockether.vis.internal.provider.error

Single source of truth for provider-error presentation.

Typed provider-error content and per-iteration trace rows derive their wording and facts from this namespace, so a failure reads identically everywhere.

err is the error map carried on a trace entry / ex-info: {:message .. :data {:status .. :body .. :request-id ..} ..}. Every helper tolerates the bare ex-info shape too (via ex-message).

CLASSIFICATION IS SVAR'S. svar-classification wraps svar.internal.failure/classify — the single owner of failure families, retry safety and :reached-model? for everything svar transports. This namespace owns WORDING, plus the handful of failures svar cannot see (its typed empty-content and stream-watchdog outcomes, gateway tool-field rejections, tool-schema defects). Never grow a second copy of svar's heuristics here.

Single source of truth for provider-error presentation.

Typed provider-error content and per-iteration trace rows derive their wording
and facts from this namespace, so a failure reads identically everywhere.

`err` is the error map carried on a trace entry / ex-info:
`{:message .. :data {:status .. :body .. :request-id ..} ..}`. Every
helper tolerates the bare ex-info shape too (via `ex-message`).

CLASSIFICATION IS SVAR'S. `svar-classification` wraps
`svar.internal.failure/classify` — the single owner of failure families,
retry safety and `:reached-model?` for everything svar transports. This
namespace owns WORDING, plus the handful of failures svar cannot see (its
typed empty-content and stream-watchdog outcomes, gateway tool-field
rejections, tool-schema defects). Never grow a second copy of svar's
heuristics here.
raw docstring

com.blockether.vis.internal.provider.flow

Gateway-owned authentication lifecycle shared by MCP and model adapters.

start! takes an owner [domain id] and protocol legs :start (0-arg), :complete (private flow, input), :await (private flow), :settle (0-arg). Start returns :kind, private :flow, and allowlisted presentation fields. An adapter doing dynamic registration can allocate callback-transport! before constructing its authorization URL; all other adapters let us do it.

One live attempt per owner. All completion paths serialize on the same state; terminal verdicts remain pollable until expiry/cancellation. Private flow data and adapter results never cross the public allowlist. No browser or relay here. Cancellation stops workers and rejects late verdicts; adapters must also obey interruption/expiry before persisting credentials during an in-flight exchange.

Gateway-owned authentication lifecycle shared by MCP and model adapters.

`start!` takes an owner `[domain id]` and protocol legs `:start` (0-arg),
`:complete` (private flow, input), `:await` (private flow), `:settle` (0-arg).
Start returns `:kind`, private `:flow`, and allowlisted presentation fields.
An adapter doing dynamic registration can allocate `callback-transport!`
before constructing its authorization URL; all other adapters let us do it.

One live attempt per owner. All completion paths serialize on the same state;
terminal verdicts remain pollable until expiry/cancellation. Private flow data
and adapter results never cross the public allowlist. No browser or relay here.
Cancellation stops workers and rejects late verdicts; adapters must also obey
interruption/expiry before persisting credentials during an in-flight exchange.
raw docstring

com.blockether.vis.internal.provider.key-store

The STATIC API-KEY provider shape, owned once.

A vendor that authenticates with a plain key per plan (Alibaba Model Studio, Z.ai) needs the same things: a file under ~/.vis, a per-plan slice inside it, one lookup order (TUI/config key, env var, that file), a token envelope for the router, a status report that never prints the key, the interactive vis-agent providers auth flow, a per-plan logout and the extension entry map. Only the STRINGS and the plan table differ, so a provider extension declares a BOOK and this namespace owns the behaviour:

{:vendor "Alibaba" ; how a message names it :file "alibaba-auth.json" ; lives under ~/.vis :file-shape :flat ; or :by-plan (the default) :key-hint "<your-alibaba-api-key>" ; the export line's value :error-type :vis/alibaba-not-authenticated ; ex-info :type when no key :auth-notes [" The key is plan-scoped …"] ; extra prompt lines, optional :plans {:coding {:provider-id :alibaba-coding-plan :label "Alibaba (Coding Plan)" :base-url "https://…" :default-models ["…"] :env-keys ["ALIBABA_CODING_PLAN_API_KEY"]}}}

The plan TAG (:coding) is local to the file and the :provider-id is the catalog id; the two never merge and no lookup ever falls back to a sibling plan, because a key issued for one plan is rejected by the other's endpoint.

:file-shape decides where a slice LIVES. :by-plan (the default) nests each plan under its tag, because those keys are separate credentials. :flat hands the whole file to a book with ONE credential: the key sits at the root ({"api_key" …}), the plan tag never reaches disk, and no message shows a plan vocabulary the user has nothing to choose between. Declared, never inferred - growing a second plan is a deliberate change of file shape, not a silent one that orphans every key already stored.

What a provider still owns: its plan table, its :provider/limits-fn (a quota endpoint is vendor-specific) and its own namespace docstring.

The STATIC API-KEY provider shape, owned once.

A vendor that authenticates with a plain key per plan (Alibaba Model Studio,
Z.ai) needs the same things: a file under `~/.vis`, a per-plan slice inside
it, one lookup order (TUI/config key, env var, that file), a token envelope
for the router, a status report that never prints the key, the interactive
`vis-agent providers auth` flow, a per-plan logout and the extension entry
map. Only the STRINGS and the plan table differ, so a provider extension
declares a BOOK and this namespace owns the behaviour:

  {:vendor     "Alibaba"                      ; how a message names it
   :file       "alibaba-auth.json"            ; lives under ~/.vis
   :file-shape :flat                          ; or :by-plan (the default)
   :key-hint   "<your-alibaba-api-key>"       ; the export line's value
   :error-type :vis/alibaba-not-authenticated ; ex-info :type when no key
   :auth-notes ["  The key is plan-scoped …"]  ; extra prompt lines, optional
   :plans      {:coding {:provider-id :alibaba-coding-plan
                         :label "Alibaba (Coding Plan)"
                         :base-url "https://…"
                         :default-models ["…"]
                         :env-keys ["ALIBABA_CODING_PLAN_API_KEY"]}}}

The plan TAG (`:coding`) is local to the file and the `:provider-id` is the
catalog id; the two never merge and no lookup ever falls back to a sibling
plan, because a key issued for one plan is rejected by the other's endpoint.

`:file-shape` decides where a slice LIVES. `:by-plan` (the default) nests each
plan under its tag, because those keys are separate credentials. `:flat` hands
the whole file to a book with ONE credential: the key sits at the root
(`{"api_key" …}`), the plan tag never reaches disk, and no message shows a
plan vocabulary the user has nothing to choose between. Declared, never
inferred - growing a second plan is a deliberate change of file shape, not a
silent one that orphans every key already stored.

What a provider still owns: its plan table, its `:provider/limits-fn` (a
quota endpoint is vendor-specific) and its own namespace docstring.
raw docstring

com.blockether.vis.internal.provider.limits

Normalized provider limits surface.

Providers may optionally expose :provider/limits-fn in the global registry. The function returns provider-specific limit/quota data; this namespace wraps it in one validated envelope and augments it with static provider metadata from svar's catalog (currently RPM / TPM).

The limits vocabulary and the report shape belong to com.blockether.vis.contract.provider; what stays here is fetching, caching and normalizing whatever a provider answered.

Goals:

  • one host-level shape for all providers,
  • explicit support for providers that only know static limits,
  • contract validation of every returned report,
  • graceful error envelopes instead of exploding the caller when a provider-specific implementation is absent or malformed.
Normalized provider limits surface.

Providers may optionally expose `:provider/limits-fn` in the global
registry. The function returns provider-specific limit/quota data;
this namespace wraps it in one validated envelope and augments it
with static provider metadata from svar's catalog (currently RPM /
TPM).

The limits vocabulary and the report shape belong to
`com.blockether.vis.contract.provider`; what stays here is fetching, caching and
normalizing whatever a provider answered.

Goals:
- one host-level shape for all providers,
- explicit support for providers that only know static limits,
- contract validation of every returned report,
- graceful error envelopes instead of exploding the caller when a
  provider-specific implementation is absent or malformed.
raw docstring

com.blockether.vis.internal.provider.limits-format

Channel-neutral {:dynamic {:limits [...]}} row formatters.

Hoisted from the TUI extension (channel_tui/limits_fmt.clj) so every channel — TUI footer, TUI provider cards — renders the SAME compact account-quota summary from a provider's normalized limits report. The TUI namespace now aliases these vars; other channels consume them through vis.core.

The interesting account-level rows (:zai-coding-plan-5h, :codex-7d, :premium_interactions, ...) live under [:dynamic :limits]; static :rpm/:tpm are svar catalog defaults, identical for every provider, useful only as fallback.

Channel-neutral `{:dynamic {:limits [...]}}` row formatters.

Hoisted from the TUI extension (`channel_tui/limits_fmt.clj`) so
every channel — TUI footer, TUI provider cards — renders the SAME
compact account-quota summary from a provider's normalized limits
report. The TUI namespace now aliases these vars; other channels
consume them through `vis.core`.

The interesting account-level rows (`:zai-coding-plan-5h`,
`:codex-7d`, `:premium_interactions`, ...) live under
`[:dynamic :limits]`; static `:rpm`/`:tpm` are svar catalog
defaults, identical for every provider, useful only as fallback.
raw docstring

com.blockether.vis.internal.provider.oauth

Unified OAuth token-refresh facade, shared by every provider.

WHY THIS EXISTS — Providers whose token endpoint ROTATES the refresh_token on every exchange (Anthropic, OpenAI Codex) must never run two refresh exchanges at once: the second reuses an already-rotated refresh token and the server answers HTTP 400 invalid_grant. Under a 401 "storm" — the turn loop's per-iteration retry PLUS a usage/limits poll, all sharing one credential — that race killed whole turns (~10 refreshes/min: one lost the rotation race and the turn died). Providers that mint a short-lived token from a STABLE credential (GitHub Copilot) don't 400, but still benefit: concurrent 401s otherwise stampede the exchange endpoint with redundant calls.

THE MODEL — refresh is serialized PER CREDENTIAL STORE, never globally. Each make-file-refresher / refresher call mints its OWN lock, so a refresh to Anthropic and a refresh to Codex run fully in parallel; only two refreshes to the SAME store (e.g. two sessions both hitting Anthropic) serialize — which is the whole point. Once the lock is held, a caller REUSES a result another thread just produced (creds persisted within default-reuse-window-ms, or an already-valid cache) so a burst of N concurrent 401s collapses into ONE exchange.

USE — file-backed rotating stores (Anthropic, Codex): make-file-refresher. Cache-backed / bespoke stores (Copilot): refresher with custom reuse/refresh fns. Both return a 0-arg fn yielding the provider-token map, owning their own lock; drop them straight into :provider/get-token-fn / :provider/refresh-token-fn.

Unified OAuth token-refresh facade, shared by every provider.

WHY THIS EXISTS — Providers whose token endpoint ROTATES the
refresh_token on every exchange (Anthropic, OpenAI Codex) must never run
two refresh exchanges at once: the second reuses an already-rotated
refresh token and the server answers HTTP 400 `invalid_grant`. Under a
401 "storm" — the turn loop's per-iteration retry PLUS a usage/limits
poll, all sharing one credential — that race killed whole turns (~10 refreshes/min: one lost the
rotation race and the turn died). Providers that mint a short-lived token from a STABLE
credential (GitHub Copilot) don't 400, but still benefit: concurrent
401s otherwise stampede the exchange endpoint with redundant calls.

THE MODEL — refresh is serialized PER CREDENTIAL STORE, never globally.
Each `make-file-refresher` / `refresher` call mints its OWN lock, so a
refresh to Anthropic and a refresh to Codex run fully in parallel; only
two refreshes to the SAME store (e.g. two sessions both hitting
Anthropic) serialize — which is the whole point. Once the lock is held,
a caller REUSES a result another thread just produced (creds persisted
within `default-reuse-window-ms`, or an already-valid cache) so a burst
of N concurrent 401s collapses into ONE exchange.

USE — file-backed rotating stores (Anthropic, Codex): `make-file-refresher`.
Cache-backed / bespoke stores (Copilot): `refresher` with custom
reuse/refresh fns. Both return a 0-arg fn yielding the provider-token
map, owning their own lock; drop them straight into
`:provider/get-token-fn` / `:provider/refresh-token-fn`.
raw docstring

com.blockether.vis.internal.provider.service

Channel-neutral provider management service.

Everything a channel needs to render and mutate the provider fleet — status probing, account limits, live model catalogs, presets, and config persistence — WITHOUT any UI. Hoisted from the TUI extension (channel_tui/provider.clj) so any future surface manages the SAME fleet through the SAME primitives; the channels keep only their interaction layer (lanterna dialogs, ...).

Auth is classified, not implemented, here: auth-kind tells a channel whether a provider wants an API key, an interactive OAuth flow (owned by the provider extension + channel), or nothing (local). The registry's :provider/*-fn contract stays the single integration point for provider extensions, so a provider extension automatically works in every channel.

Channel-neutral provider management service.

Everything a channel needs to render and mutate the provider fleet
— status probing, account limits, live model catalogs, presets, and
config persistence — WITHOUT any UI. Hoisted from the TUI extension
(`channel_tui/provider.clj`) so any future surface manages the SAME
fleet through the SAME primitives; the channels keep only their
interaction layer (lanterna dialogs, ...).

Auth is classified, not implemented, here: `auth-kind` tells a
channel whether a provider wants an API key, an interactive OAuth
flow (owned by the provider extension + channel), or nothing
(local). The registry's `:provider/*-fn` contract stays the single
integration point for provider extensions, so a provider extension
automatically works in every channel.
raw docstring

com.blockether.vis.internal.provider.vendor.alibaba

Alibaba Model Studio static-API-key providers. Each plan is registered as its own extension:

:alibaba-coding-plan -> Coding Plan subscription (https://coding-intl.dashscope.aliyuncs.com/v1). Env var: ALIBABA_CODING_PLAN_API_KEY.

:alibaba-token-plan -> Token Plan prepaid token bundle (https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1). Env var: ALIBABA_TOKEN_PLAN_API_KEY.

Both endpoints speak the OpenAI-compatible chat wire (/chat/completions, SSE streaming with stream_options.include_usage, native tool_calls, reasoning_content for thinking models), so svar's default :openai-compatible-chat api-style drives them unchanged.

The plans are SEPARATE credentials: a Token Plan key is rejected by the Coding Plan endpoint and vice versa (HTTP 401 InvalidApiKey). Hence one provider id, one env var and one auth-file slice per plan - never a shared ALIBABA_API_KEY fallback that would silently authenticate as the wrong plan.

The provider ids match their models.dev slugs, which is what lets svar resolve pricing, context windows and capabilities for the catalog models listed in each preset; svar's KNOWN_PROVIDERS has no Alibaba entry, so the preset here owns :base-url (svar accepts an unknown provider id whenever a base URL is supplied).

Auth lifecycle:

  1. vis-agent providers auth alibaba-coding-plan (or vis-agent providers auth alibaba-token-plan) takes the API key once and persists it under ~/.vis/alibaba-auth.json, as canonical snake_case JSON - top-level plan tag, then api_key / saved_at (never kebab, never keyword keys).
  2. Subsequent runs read the configured provider key, env var, or persisted key. A TUI/config :api-key wins so status/limits match the key used for model calls; the env vars override the auth file when present so CI / scripted setups stay home-directory-free.
  3. vis-agent providers status alibaba-coding-plan reports the source (config / env / file) without exposing the full key.
  4. vis-agent providers logout alibaba-coding-plan clears the persisted key for that plan only; the other plan stays intact.
Alibaba Model Studio static-API-key providers. Each plan is registered as its own extension:

  :alibaba-coding-plan -> Coding Plan subscription
                (https://coding-intl.dashscope.aliyuncs.com/v1).
                Env var: `ALIBABA_CODING_PLAN_API_KEY`.

  :alibaba-token-plan  -> Token Plan prepaid token bundle
                (https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1).
                Env var: `ALIBABA_TOKEN_PLAN_API_KEY`.

Both endpoints speak the OpenAI-compatible chat wire (`/chat/completions`,
SSE streaming with `stream_options.include_usage`, native `tool_calls`,
`reasoning_content` for thinking models), so svar's default
`:openai-compatible-chat` api-style drives them unchanged.

The plans are SEPARATE credentials: a Token Plan key is rejected by the
Coding Plan endpoint and vice versa (HTTP 401 `InvalidApiKey`). Hence one
provider id, one env var and one auth-file slice per plan - never a shared
`ALIBABA_API_KEY` fallback that would silently authenticate as the wrong
plan.

The provider ids match their models.dev slugs, which is what lets svar
resolve pricing, context windows and capabilities for the catalog models
listed in each preset; svar's `KNOWN_PROVIDERS` has no Alibaba entry, so
the preset here owns `:base-url` (svar accepts an unknown provider id
whenever a base URL is supplied).

Auth lifecycle:
  1. `vis-agent providers auth alibaba-coding-plan` (or
     `vis-agent providers auth alibaba-token-plan`) takes the API key once
     and persists it under `~/.vis/alibaba-auth.json`, as canonical
     snake_case JSON - top-level plan tag, then `api_key` / `saved_at`
     (never kebab, never keyword keys).
  2. Subsequent runs read the configured provider key, env var, or
     persisted key. A TUI/config `:api-key` wins so status/limits match the
     key used for model calls; the env vars override the auth file when
     present so CI / scripted setups stay home-directory-free.
  3. `vis-agent providers status alibaba-coding-plan` reports the source
     (config / env / file) without exposing the full key.
  4. `vis-agent providers logout alibaba-coding-plan` clears the persisted
     key for that plan only; the other plan stays intact.
raw docstring

com.blockether.vis.internal.provider.vendor.anthropic

Anthropic providers.

Providers:

  • :anthropic - normal Anthropic API key provider. API key lives in Vis config.
  • :anthropic-coding-plan - Claude subscription OAuth provider. OAuth credentials live in ~/.vis/anthropic-auth.json.

Runtime calls hand the OAuth access token to svar; svar handles only the Anthropic Messages API wire differences for subscription tokens.

Anthropic providers.

Providers:
- `:anthropic` - normal Anthropic API key provider. API key lives in Vis config.
- `:anthropic-coding-plan` - Claude subscription OAuth provider. OAuth
  credentials live in `~/.vis/anthropic-auth.json`.

Runtime calls hand the OAuth access token to svar; svar handles only the
Anthropic Messages API wire differences for subscription tokens.
raw docstring

com.blockether.vis.internal.provider.vendor.github-copilot

GitHub Copilot OAuth provider - device flow authentication + token lifecycle.

Auth flow:

  1. Device flow -> user visits github.com/login/device, enters code
  2. Poll until authorized -> receive OAuth token (ghu_...)
  3. Exchange OAuth token for short-lived Copilot API token via api.github.com/copilot_internal/v2/token
  4. Auto-refresh the API token before expiry

Token detection priority (same as Copilot CLI):

  1. Persisted OAuth token in ~/.vis/github-copilot-auth.json
  2. COPILOT_GITHUB_TOKEN env var
  3. GH_TOKEN env var
  4. GITHUB_TOKEN env var
  5. macOS Keychain (copilot-cli service) - if security CLI available

Works with both Individual and Business/Enterprise plans. Enterprise users can pass :enterprise-domain for GHE.

GitHub Copilot OAuth provider - device flow authentication + token lifecycle.

Auth flow:
1. Device flow -> user visits github.com/login/device, enters code
2. Poll until authorized -> receive OAuth token (`ghu_...`)
3. Exchange OAuth token for short-lived Copilot API token
   via `api.github.com/copilot_internal/v2/token`
4. Auto-refresh the API token before expiry

Token detection priority (same as Copilot CLI):
1. Persisted OAuth token in `~/.vis/github-copilot-auth.json`
2. `COPILOT_GITHUB_TOKEN` env var
3. `GH_TOKEN` env var
4. `GITHUB_TOKEN` env var
5. macOS Keychain (`copilot-cli` service) - if `security` CLI available

Works with both Individual and Business/Enterprise plans.
Enterprise users can pass `:enterprise-domain` for GHE.
raw docstring

com.blockether.vis.internal.provider.vendor.lmstudio

LM Studio local provider preset extension.

LM Studio local provider preset extension.
raw docstring

com.blockether.vis.internal.provider.vendor.mistral

Mistral.ai provider preset extension. API keys are configured by channels.

Mistral.ai provider preset extension. API keys are configured by channels.
raw docstring

com.blockether.vis.internal.provider.vendor.ollama

Ollama local provider preset extension.

Ollama local provider preset extension.
raw docstring

com.blockether.vis.internal.provider.vendor.openai

OpenAI provider preset extension. API keys are configured by channels.

OpenAI provider preset extension. API keys are configured by channels.
raw docstring

com.blockether.vis.internal.provider.vendor.openai-codex

OpenAI Codex (ChatGPT OAuth) provider.

Headless clients use Codex's official device authorization flow, so the browser can be on a phone or desktop while the gateway stays behind NAT. The interactive CLI also retains the registered loopback PKCE flow. Neither path rewrites a provider-registered redirect URI.

Tokens are persisted at ~/.vis/openai-codex-auth.json. The access token is a JWT; Codex requests require the embedded ChatGPT account id, so this namespace validates/extracts it during login/refresh.

The dynamic quota report lives here too (dynamic-limits!): it fetches https://chatgpt.com/backend-api/wham/usage, selects the regular Codex bucket (or the nested Codex Spark bucket) and exposes the 5h and 7d percentage windows as normalized Vis limit rows.

OpenAI Codex (ChatGPT OAuth) provider.

Headless clients use Codex's official device authorization flow, so the
browser can be on a phone or desktop while the gateway stays behind NAT.
The interactive CLI also retains the registered loopback PKCE flow.
Neither path rewrites a provider-registered redirect URI.

Tokens are persisted at `~/.vis/openai-codex-auth.json`. The access
token is a JWT; Codex requests require the embedded ChatGPT account
id, so this namespace validates/extracts it during login/refresh.

The dynamic quota report lives here too (`dynamic-limits!`): it fetches
`https://chatgpt.com/backend-api/wham/usage`, selects the regular Codex
bucket (or the nested Codex Spark bucket) and exposes the 5h and 7d
percentage windows as normalized Vis limit rows.
raw docstring

com.blockether.vis.internal.provider.vendor.opencode-go

OpenCode Go (https://opencode.ai/go) static-API-key provider.

OpenCode Go is a flat-rate ($10/month) subscription gateway serving a curated set of open-source coding models from ONE endpoint (https://opencode.ai/zen/go/v1) over TWO wire dialects. Vis surfaces them as a SINGLE first-class provider — :opencode-go — that routes each model to the correct wire automatically:

OpenAI chat wire (/chat/completions, svar default): GLM, Kimi, DeepSeek, MiMo, Hy3.

Anthropic Messages wire (/messages, per-model :api-style :anthropic): MiniMax, Qwen.

svar reads (or (:api-style model-map) (:api-style provider)) at request build time, so a per-model :api-style override on the Anthropic models inside :default-models is all that is needed — one provider, one key, one endpoint, two wires.

Authentication is the shared static-API-key shape, owned by com.blockether.vis.internal.provider.key-store and declared by BOOK below: lookup order, status, logout, the token envelope and the interactive vis-agent providers auth opencode-go flow all come from there.

What stays here is what only OpenCode Go knows: which model rides which wire, and the live /usage quota report (dynamic-limits! below).

OpenCode Go (https://opencode.ai/go) static-API-key provider.

OpenCode Go is a flat-rate ($10/month) subscription gateway serving a curated
set of open-source coding models from ONE endpoint
(`https://opencode.ai/zen/go/v1`) over TWO wire dialects. Vis surfaces them
as a SINGLE first-class provider — `:opencode-go` — that routes each model to
the correct wire automatically:

  OpenAI chat wire (/chat/completions, svar default):
    GLM, Kimi, DeepSeek, MiMo, Hy3.

  Anthropic Messages wire (/messages, per-model `:api-style :anthropic`):
    MiniMax, Qwen.

svar reads `(or (:api-style model-map) (:api-style provider))` at request
build time, so a per-model `:api-style` override on the Anthropic models
inside `:default-models` is all that is needed — one provider, one key, one
endpoint, two wires.

Authentication is the shared static-API-key shape, owned by
`com.blockether.vis.internal.provider.key-store` and declared by `BOOK` below:
lookup order, status, logout, the token envelope and the interactive
`vis-agent providers auth opencode-go` flow all come from there.

What stays here is what only OpenCode Go knows: which model rides which wire,
and the live `/usage` quota report (`dynamic-limits!` below).
raw docstring

com.blockether.vis.internal.provider.vendor.openrouter

OpenRouter static-API-key provider (https://openrouter.ai/api/v1).

OpenRouter is a multi-provider gateway speaking the OpenAI chat wire, so no :api-style override is needed - svar's default OpenAI transport handles it. Model names are vendor/model slugs (anthropic/claude-sonnet-4.5, openai/gpt-5.1, ...).

Authentication is the shared static-API-key shape, owned by com.blockether.vis.internal.provider.key-store and declared by BOOK below: lookup order, status, logout, the token envelope and the interactive vis-agent providers auth openrouter flow all come from there.

What stays here is what only OpenRouter knows: the starter catalog, the live model enrichment, and the credit report from GET /api/v1/key - the credits this key has consumed and, for capped keys, its limit.

OpenRouter static-API-key provider (https://openrouter.ai/api/v1).

OpenRouter is a multi-provider gateway speaking the OpenAI chat wire, so no
`:api-style` override is needed - svar's default OpenAI transport handles it.
Model names are `vendor/model` slugs (`anthropic/claude-sonnet-4.5`,
`openai/gpt-5.1`, ...).

Authentication is the shared static-API-key shape, owned by
`com.blockether.vis.internal.provider.key-store` and declared by `BOOK` below:
lookup order, status, logout, the token envelope and the interactive
`vis-agent providers auth openrouter` flow all come from there.

What stays here is what only OpenRouter knows: the starter catalog, the live
model enrichment, and the credit report from `GET /api/v1/key` - the credits
this key has consumed and, for capped keys, its limit.
raw docstring

com.blockether.vis.internal.provider.vendor.zai

Z.ai (ZhipuAI) static-API-key provider helpers. Each plan is registered as its own extension:

:zai-coding-plan -> coding-plan subscription (https://api.z.ai/api/coding/paas/v4). Env var: ZAI_CODING_API_KEY.

:zai -> pay-as-you-go / Pass gateway (https://api.z.ai/api/paas/v4). Env var: ZAI_API_KEY.

Both endpoints serve the same GLM model family (glm-5.3-flash, glm-5.3, glm-5-turbo, glm-5.1, ...) with effort-based reasoning on GLM-5.3 models and binary thinking on older models (handled by svar). They share helper code, but the runtime extension registry sees one extension entry per provider id.

Auth lifecycle:

  1. vis-agent providers auth zai-coding (or vis-agent providers auth zai) prompts for the API key once and persists it under ~/.vis/zai-auth.json, as canonical snake_case JSON - top-level plan tag, then api_key / saved_at (never kebab, never keyword keys).
  2. Subsequent runs read the configured provider key, env var, or persisted key. A TUI/config :api-key wins so status/limits match the key used for model calls; env vars (ZAI_CODING_API_KEY, ZAI_API_KEY) override the auth file when present so CI / scripted setups stay home-directory-free.
  3. vis-agent providers status zai-coding reports the source (config / env / file) without exposing the full key.
  4. vis-agent providers logout zai-coding clears the persisted key for that plan only; the other plan stays intact.
Z.ai (ZhipuAI) static-API-key provider helpers. Each plan is registered as its own extension:

  :zai-coding-plan -> coding-plan subscription
                (https://api.z.ai/api/coding/paas/v4).
                Env var: `ZAI_CODING_API_KEY`.

  :zai        -> pay-as-you-go / `Pass` gateway
                (https://api.z.ai/api/paas/v4).
                Env var: `ZAI_API_KEY`.

Both endpoints serve the same GLM model family (`glm-5.3-flash`,
`glm-5.3`, `glm-5-turbo`, `glm-5.1`, ...) with effort-based reasoning
on GLM-5.3 models and binary thinking on older models (handled by svar). They
share helper code, but the runtime extension registry sees one
extension entry per provider id.

Auth lifecycle:
  1. `vis-agent providers auth zai-coding` (or `vis-agent providers auth zai`) prompts for the API
     key once and persists it under `~/.vis/zai-auth.json`,
     as canonical snake_case JSON - top-level plan tag, then
     `api_key` / `saved_at` (never kebab, never keyword keys).
  2. Subsequent runs read the configured provider key, env var, or
     persisted key. A TUI/config `:api-key` wins so status/limits
     match the key used for model calls; env vars
     (`ZAI_CODING_API_KEY`, `ZAI_API_KEY`) override the auth file when
     present so CI / scripted setups stay home-directory-free.
  3. `vis-agent providers status zai-coding` reports the source
     (config / env / file) without exposing the full key.
  4. `vis-agent providers logout zai-coding` clears the persisted key for
     that plan only; the other plan stays intact.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close