docs/07-providers.md

07 — Providers

Picasso runs every model call through one of two credential modes. Subscription sign-in is not available at launch (17 is post-launch).

Defaults

One set of constants in packages/providers/src/defaults.ts, re-exported from @picasso/providers:

ConstantValueMeaning
DEFAULT_DSH_PROVIDER_ROUTEdeepseek-officialBYO route used when nothing else is chosen
DEFAULT_MANAGED_PROVIDERdeepseekmanaged ProviderName for the same upstream
DEFAULT_MODELdeepseek-flashdefault chat model (the pinned engine's DeepSeek catalogue lists deepseek-flash and deepseek-v4-pro)
DEFAULT_PROVIDER_SECRET_ROTATION_DAYS90default managed key lifetime

The copies that cannot import the constants (packages/picasso-llm-adapter/index.js, the agent-default-model row of the shipped runtime config, and the isolation overlay) are pinned to them by packages/providers/test/default-constants.test.ts, which also fails if deepseek-v4-flash reappears as a default.

Provider interface

Managed adapters implement Provider (packages/providers/src/types.ts):

type Provider = {
  name: string;
  models(): Promise<ModelInfo[]>;
  generate(req: GenerateRequest): AsyncIterable<GenerateEvent>;
  generateText?(req: GenerateRequest): Promise<string>;
  estimateCost(req: GenerateRequest): CostEstimate;
  countTokens(text: string, model: string): number;
};

createManagedProvider({provider, secret, env?, fetch?}) (managed-factory.ts) is the only factory. The API's managed proxy and the validation probe both use it, so a probe hits the same base URL with the same headers as a real call. managedProviderEndpoint(provider, env) returns that base URL, the chat and models paths, and the auth headers.

Managed providers: anthropic, openai, azure-foundry, google, xai, openrouter, qwen, xiaomi, kimi, deepseek, zhipu, minimax, plus Picasso's free model (picasso-free, served through an openai-compatible alias). PICASSO_<PROVIDER>_BASE_URL overrides the base URL of the OpenAI-compatible ones (qwen, xiaomi, kimi, deepseek, zhipu, minimax). PICASSO_AZURE_FOUNDRY_BASE_URL is required for azure-foundry.

Retry

---

Operator setup sheet

1. Bring your own key (CLI, TUI, Mac)

  1. Install the CLI. It runs on the pinned engine; picasso doctor shows whether the engine and the policy and isolation patches are loaded.
  2. Add the default route: picasso provider set deepseek-official <DEEPSEEK_API_KEY>. The default model is deepseek-flash. This key also enables web search.
  3. Add another route: picasso provider set <route> <key>, then picasso provider use <route>, then picasso provider check <route>. picasso provider list shows every route and whether its key resolves.
  4. Azure AI Foundry: run picasso provider set azure-foundry <key> --base-url https://<resource>.services.ai.azure.com/openai/v1 (or https://<resource>.openai.azure.com/openai/v1). This stores the endpoint in $DSH_HOME/settings.yaml. Exporting AZURE_FOUNDRY_BASE_URL in the environment Picasso starts from, then picasso provider set azure-foundry <key>, also works. Model ids are deployment names; the route declares gpt-5.5, gpt-5.6-sol, gpt-5.6-terra and gpt-5.6-luna. Without a base URL the route loads with a diagnostic and is not served.
  5. A custom OpenAI- or Anthropic-compatible endpoint (any gateway, or a local server such as Ollama): picasso provider add ollama --api openai-completions --base-url http://localhost:11434/v1 --model-id <id> (interactive when flags are omitted). picasso run --provider <route> takes a route you added as well as the built-in families (anthropic, openai, azure-foundry, google, xai, qwen, xiaomi, kimi, deepseek, zhipu, minimax, openrouter, ollama, vllm), and --model <provider>/<id> drops the run's own provider prefix.
  6. Picasso's free model: picasso provider use picasso-free (signs in first).
  7. Mac: Settings › Providers. Add the same keys there. Choosing an API-key provider opens that provider's API-key page in your browser ("Opened <host> in your browser. Create a key there and paste it here.") for the eleven providers with a verified key page: Z.AI, DeepSeek, Google AI Studio, Anthropic, OpenAI, OpenRouter, xAI, MiniMax, Moonshot/Kimi, Groq and Cloudflare; for the others the sheet says "Paste your <Provider> API key." Added or edited providers show at once in Connections and Connected. You can add a provider's models, and Edit lists every model the endpoint reports.

Models, limits and connections

picasso models [--json] [--runtime-config <path>] lists every model of every configured route with its context window, output limit and reasoning levels. The facts come from real sources, in order: the endpoint's own model listing, the provider's documentation (for example docs.z.ai for Z.AI's GLM models), a vendored models.dev snapshot, then your saved settings. A fact no source reports stays unknown; it is never guessed. picasso provider check <route> prints the same per model (name, window, output, levels and their sources). The Mac shows them in a model's details ("1M in · 131K out · levels", or "Context unknown · Output unknown"), and asks before switching a thread to a model whose window is smaller than the thread's context.

picasso models connections manages the ordered list of model connections Picasso falls back through on quota, auth or outage errors:

CommandEffect
picasso models connections / listThe connections in order, with their state.
`picasso models connections move <id> up\down\top\<n>`Reorder.
`picasso models connections policy ask\ask-managed\auto`Ask before falling back, ask only before a managed (paid by Picasso) connection, or fall back automatically.
picasso models connections check <id> [model]Check access now.

The TUI has /model connections; the Mac has the same card in Settings › Providers (drag from the ≡ handle to reorder, Check access). /model <id> @<effort> sets the reasoning effort for any built-in model.

Provider and model names follow one display rule on every surface ("Cloudflare Workers AI", "GLM 5.2"). A selected route that was renamed or went stale follows its model to the current route instead of failing; when no route serves the model, the error is a plain 422 naming the model and provider. Keys are redacted from provider errors and logs.

Syncing providers from your own engine settings

If you also run the engine on its own, the providers, default model and web search you keep in its web settings can be brought into Picasso and kept in step. Picasso's engine runs on its own home, ~/.picasso/dsh-home ($DSH_HOME), with the Mac engine on profile picasso and the CLI on profile sdk. The source is the standalone engine's home: <!-- docs-freshness-ignore: legacy-dsh-home --> ~/.dsh/profiles/web/cordis.patch.yml (plus the home-level ~/.dsh/cordis.patch.yml), with keys in ~/.dsh/.credentials.yaml (the source home of the sync, not Picasso's own). Only key references are shared; a key the target already resolves is never copied or overwritten.

One merge serves both surfaces (planDshProviders in the shared-home package):

SurfaceWhen it syncsHow
MacAt launch when any of the three source files changed since the last sync (or never synced), and live while the app runs: the files are watched (kernel file events on the files and their folders, atomic-rename saves included, ~2 s debounce, no polling). Settings › Providers › "Use providers from the Picasso CLI" shows the last sync, counts, routes that need a key and errors, with Sync now and a Sync automatically switch (remembered).The app reads the source files directly (it is not sandboxed) and posts them to the relay's provider-import route, which writes through the running engine. Another folder can still be chosen; it is re-read at launch through its bookmark.
CLIpicasso provider sync [--from <home>] [--profile <name>] [--dry-run] on demand (run it once with --dry-run first: the first sync can replace a route of the same id, models included); after that, automatically at CLI start, before any session starts, when the source's files changed since the last sync (three stats otherwise).Writes $DSH_HOME/profiles/<profile>/cordis.patch.yml (default sdk, the CLI's engine) and copies only refs the target lacks when --from is another home. Web search is not synced by the CLI (it would install executable code). --profile picasso is refused while the Mac app runs (the app keeps that profile in step itself). picasso provider remove <route> keeps a synced route out of later syncs.

The CLI syncs only before an engine starts, never mid-session, so a running session's tools → system → Tier 1 prefix cannot change underneath it. The Mac's live sync writes provider settings only (routes, default model, keys) through the engine's settings API; none of them is part of that prefix. Turn it off with picasso provider sync --auto off (or PICASSO_PROVIDER_SYNC=off) on the CLI, and the Sync automatically switch on the Mac.

Key lookup order: process environment, then $DSH_HOME/.credentials.yaml, then <cwd>/.env, then $DSH_HOME/.env ($DSH_HOME defaults to ~/.picasso/dsh-home). Route order: --provider, then $DSH_HOME/active-provider, then the env override, then deepseek-official. The env override is PICASSO_PROVIDER, or the older PICASSO_DSH_PROVIDER when PICASSO_PROVIDER is unset. picasso provider list names only PICASSO_PROVIDER in its header; both variables are honored.

BYO routes (from the shipped runtime config and its providers overlay):

RouteKey envDefault / smoke modelNotes
deepseek-official (default)DEEPSEEK_API_KEYdeepseek-flashalso deepseek-v4-pro
googleGOOGLE_API_KEYgemini-4-argonpinned catalogue; picasso provider use google selects gemini-4-argon
openai / anthropic / xai / openrouterOPENAI_API_KEY / ANTHROPIC_API_KEY / XAI_API_KEY / OPENROUTER_API_KEYpinned catalogue
moonshotai / zai / minimaxMOONSHOTAI_API_KEY / ZAI_API_KEY / MINIMAX_API_KEYpinned catalogueZ.AI facts from docs.z.ai (GLM 5.3: 1M in, 131K out, low/high/max)
xiaomiXIAOMI_API_KEYmimo-v2.6-procatalogue route (https://api.xiaomimimo.com/v1)
qwenDASHSCOPE_API_KEYqwen3.8-flashinternational DashScope (dashscope-intl.aliyuncs.com/compatible-mode/v1); also qwen3.8-max, qwen3.7-plus, qwen3-coder-next
azure-foundryAZURE_OPENAI_API_KEY + AZURE_FOUNDRY_BASE_URLyour deployment
prism / kiro-prismPRISM_API_KEYclaude-sonnet-5.5 (also claude-opus-5.5, claude-haiku-5.5, claude-fable-5.1)
your own gatewaythe key env you namethe models you addSettings › Providers › Add provider or picasso provider add (OpenAI- or Anthropic-compatible)
groq, mistral, together, fireworks, cerebras, nvidia, huggingface, cohere<NAME>_API_KEYcatalogue / declared
amazon-bedrock, google-vertex, github-copilotambient (AWS chain, ADC, OAuth)catalogue

2. Managed (Picasso pays)

Preconditions: the production environment is live, a second admin with TOTP exists, and the provider-validation, audit and managed-usage worker jobs are deployed.

  1. Custody (once). Key Vault picasso-prod-kv in the production resource group on az-auraone-gurbaksh, RBAC mode, purge protection on. Key provider-secret-wrap (RSA 3072, or RSA-HSM on Premium) with ops wrapKey and unwrapKey. Assign Key Vault Crypto Service Encryption User, scoped to that key, to the API app identity and to each provider worker job identity. No identity gets secrets/*.

Each provider secret is encrypted with its own AES-256-GCM data key, and the data key is wrapped by the vault key (RSA-OAEP-256, Key Vault REST 7.4). The vault key never leaves Key Vault, and the custody mode is remote.

  1. API and worker env (names only; the identity is ambient):
EnvValue
PICASSO_ADMIN_PROVIDER_SECRET_CUSTODY_PROVIDERazure-key-vault
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_KEY_VAULT_URLhttps://picasso-prod-kv.vault.azure.net (https; host ends in .vault.azure.net or .managedhsm.azure.net)
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_KEY_VAULT_KEY_NAMEprovider-secret-wrap
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_KEY_VAULT_KEY_VERSIONoptional; pins the wrap version (unwrap always uses the stored version)
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_MANAGED_IDENTITY_CLIENT_IDonly for a user-assigned identity
PICASSO_ADMIN_PROVIDER_SECRET_ROTATION_DAYSoptional, integer 7 to 365, default 90
PICASSO_ADMIN_PROVIDER_SECRET_VALIDATION_RUNNERleave unset (in-repo). webhook is opt-in and then needs …_VALIDATION_WEBHOOK_URL and …_TOKEN.
PICASSO_AZURE_FOUNDRY_BASE_URLrequired for managed azure-foundry
PICASSO_<PROVIDER>_BASE_URLoptional for qwen, xiaomi, kimi, deepseek, zhipu, minimax

IDENTITY_ENDPOINT and IDENTITY_HEADER are injected by Container Apps. No *_QUEUE_URL is needed: the workers lease work from Postgres (queue_binding: postgres-lease).

  1. Check custody. Admin /setup shows the KMS gate passed, and the alias health check on /providers/vault reports remote and healthy.
  2. Add a key (admin A). /providers/vault → Production Add Key: provider, environment production, alias, secret, KMS key id azure-key-vault/provider-secret-wrap, legal approval ref, model families JSON (for example ["deepseek-flash"] or ["*"]), spend ceiling in USD, and an audit reason.

Validation is queued automatically (POST /v1/admin/provider-secrets returns the validation_job). Within one poll the provider-validation worker probes the key (below) and the alias shows passed or the failure code.

  1. Approve (admin B). A different admin from the creator approves the stored approval request for activate on provider-secret (/v1/admin/approvals/:id/approve).
  2. Promote. POST /v1/admin/provider-secrets/:id/versions/:version/promote with the approval id in x-admin-approval-id. It is refused with provider_secret_validation_stale when the last validation is older than 24 hours.
  3. Seed the catalog. Seed managed models on the validated alias (POST …/versions/:version/seed-models, reason required). It creates or enables one catalog entry per managed model in the alias families, with legalStatus: approved, and is idempotent. Before validation it returns 409 provider_secret_not_validated. A /routing policy is optional.
  4. Clients. picasso auth login, then picasso run --credential-mode managed …. The Mac uses the signed-in account.
  5. Rotation. A key stops serving 90 days after creation unless rotation_due_at is set. The worker reports rotation_due_soon 14 days ahead. Rotate on /providers/vault: the new version validates itself, then approve and promote it.

Validation runner

In-repo by default. The worker decrypts the secret inside the API or worker process and sends it only to the provider's own endpoint, never to a webhook. probeProviderKey (key-probe.ts):

ProviderProbeAuth
openai, xaiGET /v1/modelsBearer
anthropicGET /v1/modelsx-api-key, anthropic-version: 2023-06-01
googleGET /v1beta/models?pageSize=1x-goog-api-key (never in the URL)
openrouterGET /api/v1/key (/models is public, so it proves nothing)Bearer
azure-foundryGET <base>/openai/v1/models; no base URL → base_url_missingBearer
deepseekGET https://api.deepseek.com/modelsBearer
zhipuGET /api/paas/v4/modelsBearer
qwen, xiaomi, kimi, minimaxGET /v1/modelsBearer
anything elseunsupported_provider—

A 404 or 405 falls back to one chat call with max_tokens: 1. 2xx is ok; 401 and 403 fail for good; 429, 5xx, timeouts and network errors are retryable and the worker retries them up to 3 attempts. The job's result message is <code> <method> <endpoint> <status> <latencyMs>ms and never contains the key.

Live key smoke

scripts/providers/live-key-smoke.mjs checks every launch route with its own key: (a) a BYO turn through the pinned engine, (b) the managed probe, (c) a 1-token managed generate, and (d) whether api.deepseek.com/models lists deepseek-flash. It writes one evidence JSON per route plus summary.json, and refuses to write evidence that contains a key value.

bash scripts/providers/run-live-smoke.sh --check-keys  # which key fields OpenBao holds (names only)
bash scripts/providers/run-live-smoke.sh               # live, on ACR (picassostagingacr)
bash scripts/providers/run-live-smoke.sh --dry-run     # fake provider endpoint, no keys

Keys come from OpenBao lanes/shared/picasso-provider-smoke, one field per env name: DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY, XAI_API_KEY, OPENROUTER_API_KEY, MOONSHOTAI_API_KEY, ZAI_API_KEY, MINIMAX_API_KEY, DASHSCOPE_API_KEY, XIAOMI_API_KEY, AZURE_OPENAI_API_KEY, AZURE_FOUNDRY_BASE_URL (optional AZURE_FOUNDRY_DEPLOYMENT), PRISM_API_KEY. The owner writes them; lane AppRoles can only read. A field missing there falls back to lanes/shared/prism-key, lanes/shared/gemini-key or lanes/shared/legacy-provider-keys.openrouter for Prism, Google and OpenRouter. The wrapper hands the keys to the ACR task as one secret value through a 0600 file, so no key is on argv. Exit code 2 means only keys were missing.

---

Zero-provider behavior

Rule: never fall back to another provider and never synthesize output. The canonical message is `No provider configured. Run picasso provider set <route> <key> (default route: deepseek-official). with code no_provider_configured`.

"Configured" means the resolved route (by the route order above, including the implicit deepseek-official) has a credential under the key lookup order, or ambient auth for an ambient route.

SurfaceBehaviorOwner
CLI provider listEvery route shows missing with a set hint, plus the header line "No provider has a key yet." when all are missing.CLI
CLI run / -pPreflight before the engine starts: the message on stderr and exit code 3. With --json: {"error":"no_provider_configured","hint":…}.CLI
CLI --credential-mode managedNot signed in: exit 3, "Sign in first: picasso auth login". API managed_provider_secret_not_ready or managed_provider_unavailable: "Managed inference is not available for <model> yet." No fallback.CLI
CLI subagent run / /subagentThe same preflight; the run records subagent-failed with the code.CLI
CLI mcp-serverThe tool call returns an MCP error result with the code; the server stays up.CLI
TUILaunch banner and a blocked send when the resolved route has no key, not only when no provider is selected. The /provider suggestion comes first.CLI/TUI
Mac composerThe send is disabled when there is no active route and the default route has no key. The Mac words it as "No provider configured. Add a provider key in Settings › Providers to start." next to an Open Settings › Providers button. First run and Settings › Providers keep their copy.Mac
Desktop helper relayA missing-credential engine error maps to HTTP 409 {"error":"no_provider_configured"} on the turn route.Mac
Admin /providersmissing-key badge, "No providers configured" and a link to the vault.Providers
Admin /modelsThe empty catalog links to "Seed managed models from a validated key".Providers
API managed routesNo promoted, validated alias for the model: managed_provider_secret_not_ready (4xx). A provider 429: HTTP 429 with Retry-After.Admin/API