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).
- Bring your own key (BYO). The user's key, on the user's machine. Generation runs inside the engine (pinned to
0.2.2-rc.1), which resolves the route and the key per request. - Managed. Picasso's key, held in the admin vault. The CLI and Mac send the signed-in account's session token to the API, and the API calls the provider through
@picasso/providers.
Defaults
One set of constants in packages/providers/src/defaults.ts, re-exported from @picasso/providers:
| Constant | Value | Meaning |
|---|---|---|
DEFAULT_DSH_PROVIDER_ROUTE | deepseek-official | BYO route used when nothing else is chosen |
DEFAULT_MANAGED_PROVIDER | deepseek | managed ProviderName for the same upstream |
DEFAULT_MODEL | deepseek-flash | default chat model (the pinned engine's DeepSeek catalogue lists deepseek-flash and deepseek-v4-pro) |
DEFAULT_PROVIDER_SECRET_ROTATION_DAYS | 90 | default 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
- Managed.
withRateLimitRetry(retry.ts,MANAGED_RETRY_POLICY) wraps the factory'sfetch. It retries status 429, 503 and 529 at most twice (3 attempts), with backoff from 500 ms to 8 s and 20% jitter, inside a 30 s budget. It honoursRetry-Afterup to 20 s; a longer wait is returned to the caller as-is. A retry only happens before any response byte is used, so streamed output is never billed twice. A provider 429 surfaces asprovider_rate_limitedwithretryAfterMs. - BYO. The engine's
llm-retryplugin keeps its defaults (5 retries, 500 ms to 10 s, including rate limits).
---
Operator setup sheet
1. Bring your own key (CLI, TUI, Mac)
- Install the CLI. It runs on the pinned engine;
picasso doctorshows whether the engine and the policy and isolation patches are loaded. - Add the default route:
picasso provider set deepseek-official <DEEPSEEK_API_KEY>. The default model isdeepseek-flash. This key also enables web search. - Add another route:
picasso provider set <route> <key>, thenpicasso provider use <route>, thenpicasso provider check <route>.picasso provider listshows every route and whether its key resolves. - Azure AI Foundry: run
picasso provider set azure-foundry <key> --base-url https://<resource>.services.ai.azure.com/openai/v1(orhttps://<resource>.openai.azure.com/openai/v1). This stores the endpoint in$DSH_HOME/settings.yaml. ExportingAZURE_FOUNDRY_BASE_URLin the environment Picasso starts from, thenpicasso provider set azure-foundry <key>, also works. Model ids are deployment names; the route declaresgpt-5.5,gpt-5.6-sol,gpt-5.6-terraandgpt-5.6-luna. Without a base URL the route loads with a diagnostic and is not served. - 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. - Picasso's free model:
picasso provider use picasso-free(signs in first). - 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:
| Command | Effect | |||
|---|---|---|---|---|
picasso models connections / list | The 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):
- Each source route replaces the target's route of the same id. A literal
apiKeyand credential headers never cross;apiKeyEnvreferences do. A key reference or credential header set in Picasso survives a source that has none. - Routes with the same display name and base URL are one route: the one the default model names is kept, else the first (the others are reported as skipped duplicates, for example
prismnext tokiro-prism). - A re-sync takes back only routes an earlier sync brought in that the source no longer has. A route the person removed in Picasso after a sync brought it in stays out (
excludedin the record). Routes added in Picasso are never touched. - The default model is taken only while the target names none of its own or still names the one a sync set.
- The record is
$DSH_HOME/picasso-providers-dsh.<profile>.json, shared by the Mac helper and the CLI.
| Surface | When it syncs | How |
|---|---|---|
| Mac | At 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. |
| CLI | picasso 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):
| Route | Key env | Default / smoke model | Notes |
|---|---|---|---|
deepseek-official (default) | DEEPSEEK_API_KEY | deepseek-flash | also deepseek-v4-pro |
google | GOOGLE_API_KEY | gemini-4-argon | pinned catalogue; picasso provider use google selects gemini-4-argon |
openai / anthropic / xai / openrouter | OPENAI_API_KEY / ANTHROPIC_API_KEY / XAI_API_KEY / OPENROUTER_API_KEY | pinned catalogue | |
moonshotai / zai / minimax | MOONSHOTAI_API_KEY / ZAI_API_KEY / MINIMAX_API_KEY | pinned catalogue | Z.AI facts from docs.z.ai (GLM 5.3: 1M in, 131K out, low/high/max) |
xiaomi | XIAOMI_API_KEY | mimo-v2.6-pro | catalogue route (https://api.xiaomimimo.com/v1) |
qwen | DASHSCOPE_API_KEY | qwen3.8-flash | international DashScope (dashscope-intl.aliyuncs.com/compatible-mode/v1); also qwen3.8-max, qwen3.7-plus, qwen3-coder-next |
azure-foundry | AZURE_OPENAI_API_KEY + AZURE_FOUNDRY_BASE_URL | your deployment | |
prism / kiro-prism | PRISM_API_KEY | claude-sonnet-5.5 (also claude-opus-5.5, claude-haiku-5.5, claude-fable-5.1) | |
| your own gateway | the key env you name | the models you add | Settings › Providers › Add provider or picasso provider add (OpenAI- or Anthropic-compatible) |
groq, mistral, together, fireworks, cerebras, nvidia, huggingface, cohere | <NAME>_API_KEY | catalogue / declared | |
amazon-bedrock, google-vertex, github-copilot | ambient (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.
- Custody (once). Key Vault
picasso-prod-kvin the production resource group onaz-auraone-gurbaksh, RBAC mode, purge protection on. Keyprovider-secret-wrap(RSA 3072, or RSA-HSM on Premium) with opswrapKeyandunwrapKey. 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 getssecrets/*.
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.
- API and worker env (names only; the identity is ambient):
| Env | Value |
|---|---|
PICASSO_ADMIN_PROVIDER_SECRET_CUSTODY_PROVIDER | azure-key-vault |
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_KEY_VAULT_URL | https://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_NAME | provider-secret-wrap |
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_KEY_VAULT_KEY_VERSION | optional; pins the wrap version (unwrap always uses the stored version) |
PICASSO_ADMIN_PROVIDER_SECRET_AZURE_MANAGED_IDENTITY_CLIENT_ID | only for a user-assigned identity |
PICASSO_ADMIN_PROVIDER_SECRET_ROTATION_DAYS | optional, integer 7 to 365, default 90 |
PICASSO_ADMIN_PROVIDER_SECRET_VALIDATION_RUNNER | leave unset (in-repo). webhook is opt-in and then needs …_VALIDATION_WEBHOOK_URL and …_TOKEN. |
PICASSO_AZURE_FOUNDRY_BASE_URL | required for managed azure-foundry |
PICASSO_<PROVIDER>_BASE_URL | optional 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).
- Check custody. Admin
/setupshows the KMS gate passed, and the alias health check on/providers/vaultreportsremoteand healthy. - Add a key (admin A).
/providers/vault→ Production Add Key: provider, environmentproduction, alias, secret, KMS key idazure-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.
- Approve (admin B). A different admin from the creator approves the stored approval request for
activateonprovider-secret(/v1/admin/approvals/:id/approve). - Promote.
POST /v1/admin/provider-secrets/:id/versions/:version/promotewith the approval id inx-admin-approval-id. It is refused withprovider_secret_validation_stalewhen the last validation is older than 24 hours. - 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, withlegalStatus: approved, and is idempotent. Before validation it returns 409provider_secret_not_validated. A/routingpolicy is optional. - Clients.
picasso auth login, thenpicasso run --credential-mode managed …. The Mac uses the signed-in account. - Rotation. A key stops serving 90 days after creation unless
rotation_due_atis set. The worker reportsrotation_due_soon14 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):
| Provider | Probe | Auth |
|---|---|---|
| openai, xai | GET /v1/models | Bearer |
| anthropic | GET /v1/models | x-api-key, anthropic-version: 2023-06-01 |
GET /v1beta/models?pageSize=1 | x-goog-api-key (never in the URL) | |
| openrouter | GET /api/v1/key (/models is public, so it proves nothing) | Bearer |
| azure-foundry | GET <base>/openai/v1/models; no base URL → base_url_missing | Bearer |
| deepseek | GET https://api.deepseek.com/models | Bearer |
| zhipu | GET /api/paas/v4/models | Bearer |
| qwen, xiaomi, kimi, minimax | GET /v1/models | Bearer |
| anything else | unsupported_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 keysKeys 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.
| Surface | Behavior | Owner |
|---|---|---|
CLI provider list | Every route shows missing with a set hint, plus the header line "No provider has a key yet." when all are missing. | CLI |
CLI run / -p | Preflight before the engine starts: the message on stderr and exit code 3. With --json: {"error":"no_provider_configured","hint":…}. | CLI |
CLI --credential-mode managed | Not 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 / /subagent | The same preflight; the run records subagent-failed with the code. | CLI |
CLI mcp-server | The tool call returns an MCP error result with the code; the server stays up. | CLI |
| TUI | Launch 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 composer | The 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 relay | A missing-credential engine error maps to HTTP 409 {"error":"no_provider_configured"} on the turn route. | Mac |
Admin /providers | missing-key badge, "No providers configured" and a link to the vault. | Providers |
Admin /models | The empty catalog links to "Seed managed models from a validated key". | Providers |
| API managed routes | No promoted, validated alias for the model: managed_provider_secret_not_ready (4xx). A provider 429: HTTP 429 with Retry-After. | Admin/API |