Connect-provider drawer
The connect-provider drawer is the right-side surface that slides over the providers list when a user clicks Connect provider. It replaces the legacy multi-stepconnect/ wizard with a single in-place form that validates credentials, discovers models, and submits the provider end-to-end. The drawer mounts whenever the providers-list URL carries ?connect=open — the three CTAs on the list page (header button, empty-state primary, “connect another” strip) all flip that query param. PR #1078 shipped the visual layer; PR #1086 wired the real submit, test, and discovery paths described here.
The drawer is the only supported entry point for creating providers from the UI. The previous
providers/connect route now 308s to ?connect=open so inbound links from emails and docs continue to work.Test-strategy buckets
Every catalog entry declares atestStrategy that picks one of five sub-handlers on the API. The dispatch enum lives on ProviderCatalogEntry in apps/web/src/lib/provider-catalog.ts and is mirrored on the API side in apps/api/src/domains/llm-routing/model/provider-catalog.ts. A drift-detector test keeps the two in lockstep.
| Bucket | Strategy | Providers | User-facing behaviour |
|---|---|---|---|
| A | list-models | 20 — openai, mistral, groq, cerebras, together, deepseek, deepinfra, xai, openrouter, perplexity, nvidia-nim, nebius, novita, anyscale, moonshot, vllm, tgi, lmstudio, litellm, custom | Real GET {baseUrl}{modelListPath} with Authorization: Bearer. Discovered models hydrate the models section. |
| B | list-models-quirk | 7 — anthropic, gemini, cohere, ollama, qwen, huggingface, ai21 | Same as A but the sub-handler swaps the path or auth header (e.g. anthropic’s x-api-key, gemini’s ?key=, ollama’s /api/tags). |
| C | list-models-templated | 6 — cloudflare, fireworks, databricks, ibm-watsonx, snowflake, azure-openai | The base URL contains <placeholder> segments filled from drawer-collected templateFields before the request fires. |
| D | cloud-sdk | 2 — bedrock, vertex | In-drawer Test is disabled with the tooltip “Test connection available in a follow-up — your credentials will be validated on first inference.” Submission still persists the provider. |
| E | probe-completion | 4 — sambanova, voyage, replicate, zai | No /models endpoint exists, so the handler issues a tiny chat / embedding / account call to confirm the credential works. |
apps/api/src/domains/llm-routing/use-cases/test-strategy.ts.
Adding a new provider
The drawer auto-renders any provider with a complete catalog entry. To add one:-
Append the entry to
apps/web/src/lib/provider-catalog.tswithid,name,tier,auth,baseUrl,description,logoSlug, and — required —testStrategy. The existing fields are documented inline inProviderCatalogEntry. -
Mirror the entry in
apps/api/src/domains/llm-routing/model/provider-catalog.ts. This is the API-side dispatch catalog the test handler reads. Both files must agree onidandtestStrategy. -
Run the drift-detector tests to confirm the two catalogs agree:
-
If
testStrategyislist-models-quirk, add a sub-handler branch inapps/api/src/domains/llm-routing/use-cases/test-strategy.ts. Anchor on an existing quirk (anthropic’sx-api-keyis the simplest reference). -
If
testStrategyislist-models-templated, populatetemplateFieldswith the placeholder inputs the drawer must collect (accountId,workspace,resourceName,project, orregion). The drawer’s identity section renders one input per field. -
If
testStrategyiscloud-sdk(bedrock, vertex), no test handler is wired. Submission still works; the in-drawer Test button is disabled with the deferred-validation tooltip. The credential is validated on the first real inference call.
modelListPath defaults to /v1/models. Only override it when the upstream exposes models at a non-standard path (ollama’s /api/tags, groq’s /openai/v1/models).
Audit log
Every test call — both the pre-create route (POST /providers/test-credential) and the existing per-credential route (POST /providers/:id/credentials/:credId/test) — writes a row to provider_test_audit. Columns:
| Column | Notes |
|---|---|
id | uuid PK |
project_id | FK to projects, ON DELETE CASCADE |
user_id | FK to users, ON DELETE CASCADE |
provider_id | Catalog id; not a FK (the provider row may not exist yet) |
credential_id | Nullable FK to llm_provider_credentials, ON DELETE SET NULL |
ok | Boolean — whether the upstream call succeeded |
test_strategy | The bucket letter / strategy enum that ran |
upstream_status | Nullable HTTP status from the upstream |
error_kind | auth | network | rate-limit | not-found | upstream | test-deferred | unknown |
duration_ms | Wall-clock duration |
created_at | timestamptz |
Rate limiting
The pre-create test route is per-user rate-limited: 10 calls / minute / user and 60 calls / hour / user. Bursts return429 with a Retry-After header, which the drawer surfaces as error: "rate-limit" (“Provider rate limit hit, try again”). The current implementation uses an in-memory sliding window; a Redis-backed counter is on the follow-up list so the limit holds across multi-instance API rollouts.
OAuth — deferred
OAuth is in the catalog (auth: ["api-key", "oauth"]) for the providers that support it, but the drawer hides the OAuth option from the auth segmented control until the redirect-callback flow lands. Providers that will gain an OAuth tab in the follow-up:
- Google Gemini (
gemini) - Azure OpenAI (
azure-openai) - Google Vertex AI (
vertex) — also supportsservice-account - Databricks (
databricks) - Snowflake Cortex (
snowflake) - IBM watsonx (
ibm-watsonx)
See also
Providers
What a provider is and how credential resolution works at the gateway layer.
Supported providers
Per-provider format, streaming, and tool-call matrix.
- Visual-layer plan —
specs/2026-05-10-create-provider-drawer-plan.md - Wiring plan —
specs/2026-05-11-connect-drawer-wiring-plan.md