Contributing a Provider
Jan Agent ships no inference engine, so every model runs through a provider. Most providers are OpenAI-compatible
endpoints and need no code at all - the user configures them with jan config set. This guide covers the three
levels of integration, from least to most code:
- A plain OpenAI-compatible endpoint - no Rust changes.
- A provider you want in the
/loginpicker - one entry in a catalog. - A provider with a nonstandard wire API (not openai
chat/completions) - a converter.
1. A plain OpenAI-compatible provider (no code)
If the provider exposes an OpenAI- or Anthropic-compatible chat/completions endpoint, users configure it directly:
jan config set \ --provider myprovider \ --api-key sk-... \ --base-url https://api.myprovider.com/v1 \ --model my-model-1 \ --model my-model-2
That is the entire change - --api-type openai (the default) and anthropic are both supported without code. The
provider appears in /model and /settings, and the agent resolves it like any other.
The config-file path is ~/.jan/config.toml. Read the Providers page for the full
config surface, including per-project overrides and environment variables.
2. Adding it to the /login picker (one catalog entry)
To let users sign in from /login (key entry + automatic GET /models discovery) instead of hand-writing config,
add a ProviderDefinition to the catalog. One entry covers jan /login, model discovery on sign-in, and the
provider appearing in /model.
Where: src-tauri/src/core/cli/auth/mod.rs, in provider_catalog().
ProviderDefinition { id: "myprovider", // id in config.toml and in `myprovider/model` ids name: "MyProvider", // display name in pickers default_base_url: "https://api.myprovider.com/v1".to_string(), transport: Transport::OpenAi, // or Transport::Anthropic api_key: ApiKeyMetadata { keys_url: "https://myprovider.com/account/api-keys", hint: "get a key at myprovider.com/account/api-keys", open_in_browser: true, }, oauth: None, // see the OAuth note below}
Notes:
Transportis a simple OpenAI/Anthropic flag (core/cli/auth/mod.rs). If your wire API is neither, you need a converter - see step 3.oauthmetadata alone does not turn on account login. Account login is driven separately byaccount::AccountProvider(core/cli/auth/account.rs); add supporting code there only if you are wiring a Jan-owned OAuth flow.- Sign-in validates the key against
GET {base_url}/modelsand persists the discovered ids, so a fresh entry becomes selectable immediately.
3. A nonstandard wire API (a converter)
If the provider does not speak OpenAI chat/completions (for example Anthropic /v1/messages, OpenAI /v1/responses,
Google generateContent), the agent translates between the internal chat shape and the provider's native protocol
with an UpstreamConverter.
Where: src-tauri/src/core/server/converters.rs.
- Implement
UpstreamConverter(build the native request body from the chat request, and translate the native stream back to chat events). - Register it in
converter_for(api_type, oauth)- theapi_typestring is what a config entry declares:
match api_type { Some("openai-responses") => Some(Box::new(OpenAIResponsesConverter::new())), Some("google") => Some(Box::new(GoogleGenerateContentConverter::new())), Some("anthropic") => Some(Box::new(AnthropicMessagesConverter::new())), _ => None, // verbatim chat/completions passthrough}
- Users then set the provider with that wire type:
jan config set \ --provider myprovider \ --api-key sk-... \ --base-url https://api.myprovider.com/v1 \ --api-type google \ # matches the converter_for key --model my-model
The converter is selected per-request via resolve_api_type_for_model (in
src-tauri/src/core/agent/upstream.rs), mirroring how the upstream URL and credential are resolved.
How model ids resolve to a provider
The agent must pick one provider when a model id could be offered by several (e.g. mimo-v2.5 served both by
minimax directly and by an opencode gateway). The rules, in resolve_upstream_for_model
(src-tauri/src/core/agent/upstream.rs):
- An exact match in a provider's
modelslist. - Otherwise a
<provider>/<model>prefix naming a configured provider. - When several providers offer the same id, a credentialed one wins over a keyless twin (deterministic, so the same model always routes the same way).
The request body always receives the bare model id - a <provider>/ qualifier is stripped by
strip_provider_prefix before the request is sent, because upstreams reject a provider-qualified id
(OpenCode GO returns model not supported for opencode/gpt-5.6-luna, for instance).
Checklist
- OpenAI/Anthropic compatible? Ship the config surface only, or add the catalog entry for
/login. - Nonstandard wire API? Implement + register an
UpstreamConverterand document the--api-typevalue. - The base URL is
https://(orhttp://for a loopback/localhost endpoint). - The
/loginsign-in discovers models, so the ids fetch correctly on a fresh key. - A model id offered by several providers prefers the credentialed one.
- Tests:
cargo test --lib --no-default-features --features clifor the CLI build.