Skip to content

Adding a Vendor

Tokensor ships with OpenAI, Anthropic, and OpenCode provider integrations. The platform is designed so adding another key-based vendor takes a single registry entry in the backend and one in the frontend — no per-vendor routing logic, endpoints, or UI code.

This guide is for the Tokensor team (or contributors) extending the product.

How provider integrations work

A "provider" integration has two parts:

  1. A stored key — the user saves a provider API key on their account in onboarding or Settings. The key is stored (encrypted for opencode) and surfaced back as connected: true/false.
  2. A proxy/ingest path — calls made through the transparent proxies or the ingest API get attributed to that account. This part is vendor-agnostic; the same llm_calls pipeline handles every model.

Because the ingest pipeline is shared, adding a vendor is only about admitting its key — which is why both registries are small lists.

1. Backend registry

Add an entry to SUPPORTED_KEY_PROVIDERS in backend/api/settings.py, mapping the provider id to the account column that stores its key:

SUPPORTED_KEY_PROVIDERS: dict[str, dict[str, str]] = {
    "openai": {"attr": "openai_api_key"},
    "anthropic": {"attr": "anthropic_api_key"},
    "opencode": {"attr": "opencode_api_key"},
    # "grok": {"attr": "grok_api_key"},   # your vendor
}

That immediately enables:

  • PUT /v1/settings/integrations/{provider} — save the key
  • DELETE /v1/settings/integrations/{provider} — clear the key
  • GET /v1/settings/integrations — the authoritative supported list for API consumers

There is no per-provider handler: the routes are consolidated and validate against the registry, so an unknown provider returns 404. (GET on a provider's key endpoint is 405 — it is not a read route; key status comes from GET /v1/settings/account.)

If the new vendor's key should be encrypted at rest, define the account column as EncryptedSecret the way opencode_api_key is in backend/models/account.py (otherwise it is stored in plaintext like openai/anthropic).

2. Frontend registry

Add a matching entry to KEY_PROVIDERS in frontend/src/providers.ts:

export const KEY_PROVIDERS: ProviderConfig[] = [
  {
    id: 'openai',
    label: 'OpenAI',
    apiKeyLabel: 'OpenAI API Key',
    placeholder: 'sk-proj-...',
    snippet: ingestSnippet('gpt-4'),
  },
  // ... your vendor
]

Each entry carries the metadata the UI needs:

  • id — must match the backend registry id
  • label — shown on the onboarding tab and the Settings section
  • apiKeyLabel / placeholder — the key input field
  • snippet — the integration example shown in onboarding Step 2

That's it. The onboarding tabs, the Settings provider sections, and the save flow all render by mapping over KEY_PROVIDERS — nothing else changes.

What you get automatically

  • Onboarding shows a new tab for the vendor, including its key field and integration snippet.
  • Settings renders a new "Provider API Keys" section with save + status.
  • The vendor's llm_calls appear in the dashboard, analytics, and connection status with no further work.

Test both registries stay in sync

  • Backend: tests/api/test_settings_provider_keys.py asserts the consolidated routes and the supported list.
  • Frontend: Onboarding.test.tsx and Settings.test.tsx assert the tabs, labels, placeholders, and save calls are driven by the registry.

If you add a vendor to one registry and not the other, the backend 404s the missing provider at save time, so drift fails loudly rather than silently.