Skip to main content

Add a provider

Start with the protocol

Does the endpoint speak the Anthropic API or OpenAI Responses? For your own installation, you need no plugin: use the form or a local backend JSON file in Add a provider. The corresponding CLI must be installed. OpenAI Chat Completions alone is not the Responses protocol.

To share endpoint and model configuration with other users, distribute a declarative provider pack. Add host code only when you need dynamic credentials or another CLI protocol. All paths use backends:register, since they choose where mission prompts are sent. Normal plugin installation, permission, signature and licence requirements still apply; see Testing and Publishing.

Share a declarative pack

A pack is an ESM npm package without main. Put providers in timon.backends. Here is a complete package.json; replace the example endpoint and model ID with values supported by your provider:

{
"name": "@acme/timon-provider",
"version": "0.1.0",
"type": "module",
"timon": {
"id": "acme-provider",
"apiVersion": 2,
"kind": "behavior",
"label": "Acme provider",
"permissions": ["backends:register"],
"settings": [
{ "key": "apiKey", "type": "secret", "label": "Provider API key" }
],
"backends": [{
"id": "acme-responses",
"dialect": "codex",
"label": "Acme Responses",
"env": { "ACME_API_KEY": { "setting": "apiKey" } },
"providerConfig": {
"id": "acme",
"name": "Acme",
"baseUrl": "https://responses.example.test/v1",
"envKey": "ACME_API_KEY",
"wireApi": "responses"
},
"defaultModel": "example-model",
"models": [{ "slug": "example-model", "label": "Example model" }]
}],
"license": { "required": false }
}
}

Install the prepared package with timon plugin add ./my-provider, then enter the key in its plugin settings. No JavaScript is loaded, even if an index.js file exists. API 2 is the original backend-facade floor; declarative packs need a Timon release supporting timon.backends (introduced on 18 September 2026).

For an Anthropic-compatible endpoint, use dialect: "claude" with ANTHROPIC_BASE_URL as a literal URL and ANTHROPIC_AUTH_TOKEN as the setting reference; omit providerConfig. The cookbook contains both complete profiles in one pack.

{ "setting": "apiKey" } references only a type: "secret" setting declared by this plugin. Its current value is read at each mission launch and its availability is checked when the catalogue is consulted. Saving, replacing or clearing it takes effect without reloading the plugin. A manifest secret default is not used. Keep actual keys out of the package.

Alternatively, { "secret": "ACME_API_KEY" } references a vault entry, subject to the mission's normal secret grants. See Secrets. TypeScript authors can import SettingRef, SecretRef and LocalBackendFile from the SDK. A setting reference is valid only in a plugin pack, not a standalone local file.

What is refused

Entries use the same strict schema as local backend files:

  • main together with timon.backends, or missing backends:register: the manifest is refused. Packs do not mix executable host code with declarations.
  • cmd, requiredBinary, envFrom, requiredEnv, and unknown fields: data cannot select a binary or read inherited daemon credentials.
  • An undeclared setting or one not typed secret: a pack cannot read arbitrary settings or another plugin's key.
  • Environment names outside provider-data naming rules, or reserved runtime, loader and home variables: declarations must not alter process execution. Names must use uppercase snake case with a provider prefix and end in _API_KEY, _AUTH_TOKEN, _TOKEN, _BASE_URL, _API_BASE, _API_VERSION, _ORGANIZATION, _PROJECT, _REGION or _MODEL; reserved names remain forbidden even with these suffixes. This also applies to providerConfig.envKey.
  • A duplicate backend ID: a pack cannot replace a built-in, local or plugin profile. Use a distinct lowercase letters/digits/dashes ID, at most 40 characters.

An invalid entry stays out of the registry with its reason in plugin status; valid neighbours still load. A missing key or unavailable dialect keeps a registered profile inactive. Disabling, removing or updating the pack removes its registrations.

Add code for dynamic credentials

Use a host entry point when a token must be refreshed. Declare main in the package, omit timon.backends, and keep backends:register, a secret setting, and the settings permission. For example, lib/index.ts:

import type { PluginContext, RegisteredProfile, EnvResolver } from '@timon-ai/sdk';

export function apply(ctx: PluginContext) {
const env: EnvResolver = () => {
const apiKey = ctx.settings.get('apiKey');
if (typeof apiKey !== 'string' || !apiKey) throw new Error('Missing provider key');
return { ANTHROPIC_AUTH_TOKEN: apiKey };
};
const profile: RegisteredProfile = {
id: 'acme-dynamic',
dialect: 'claude',
label: 'Acme dynamic credentials',
env: async () => ({
...await env(),
ANTHROPIC_BASE_URL: 'https://anthropic.example.test',
}),
defaultModel: 'example-model',
models: [{ slug: 'example-model', label: 'Example model' }],
};
ctx.backends.registerProfile(profile);
}

Compile TypeScript to JavaScript and point main at that output before packaging. Obtain @timon-ai/sdk with the developer distribution; public npm availability is not assumed. The code example shows an entire JavaScript host template.

RegisteredProfile requires id, dialect, label, models (possibly an empty array) and defaultModel (possibly null). Optional display fields include vendor, brand and custom; a Responses profile also supplies providerConfig. The type reference links to all inherited profile fields; envFrom, requiredEnv and the base env shape are specifically excluded from code profiles.

The env resolver is called at every mission launch, and also for availability probes. It may return a promise. Refresh a short-lived token inside that function, reading its current inputs from settings or ctx.secrets (with the corresponding permission and grants). Make repeated or concurrent calls safe; a probe is not evidence that a mission will launch. Do not capture a key once in apply, and do not conditionally skip registration when it is absent.

Throwing or rejecting makes the profile inactive. The daemon never relays the error's message; keep secrets out of errors and your own logs anyway. The next successful resolution can restore availability. Code profiles accept literal non-secret strings or an EnvResolver, not { secret } / { setting } objects, envFrom or requiredEnv. Read credentials through the plugin context inside the function instead.

Add a dialect only for another CLI protocol

A dialect describes how to speak to a CLI. It is justified when the CLI's launch arguments, input or output protocol differ from existing dialects; a different API endpoint or model list alone does not require one.

Import Dialect from the SDK. Register it with ctx.backends.registerDialect('acme-cli', dialect), then register a profile whose dialect is acme-cli. The returned disposers allow early cleanup; registrations are also removed automatically when the plugin unloads.

Set dialect.id to the identifier passed to registerDialect. For backward compatibility, existing JavaScript plugins without id still register and work, but emit a deprecation warning. A mismatched id also emits a warning while registration continues under the identifier passed to registerDialect; Timon does not rewrite the dialect object. Omission is deprecated; new plugins should always supply matching identifiers.

The daemon launches the process. A dialect returns a spawn description; it does not start its own process. A plugin-selected executable is checked against the operator's pluginPolicy.exec.allow. Reusing a built-in dialect's binary does not add an executable to that policy.

The required Dialect members are:

MemberContract
id, defaultCmdDialect identifier and default executable.
persistentProcess, finishMissionOnExitProcess lifetime and whether exit ends the mission. A one-shot process must leave the mission alive.
sessionResumable, sessionStoreResume support and global or per-mission session storage.
permissionModelmcp-bridge, sandbox or none, matching the CLI's actual tool controls.
rateLimitReportingstructured, textual or none, matching observable limit signals.
hostedConnectorsWhether the CLI supplies hosted account connectors.
buildSpawn(opts, profile)Async function returning cmd, args, initialStdin and optional env/cwd. Use the resolved profile environment.
writeUserInput(proc, text, images?)Write another turn to persistent stdin; a one-shot implementation does nothing.
parseLine(line)Return normalized events; unusable lines produce [], never an exception.
extractSessionId(event)Return a non-empty session ID from system:init, otherwise null.

Optional hooks include usage reporting, trace spans, model switching, session error detection and disposal; see Dialect. Set maxPromptBytes if the CLI has a prompt size limit. The echo template illustrates the protocol, not a working AI provider or the operating system's echo command.

Test your dialect without the product repository

The product's conformance suite is internal and is not shipped in the SDK. Implement these checks in your own test runner with recorded CLI JSON-lines fixtures, a temporary directory, and fake stdin; no real API key is needed:

  1. Verify capability fields and enum values. Persistent processes and global session stores must declare sessionResumable: true. Build a fresh spawn and assert the selected command, string arguments, working directory and resolved env. Confirm the model, system prompt and user message reach argv, stdin or the generated CLI files. Exercise the advertised prompt limit without oversized single arguments (in particular Linux's 131,072-byte argument ceiling), including multibyte text. If no maxPromptBytes is declared, the prompt must not travel in argv.
  2. Build a resumed spawn and verify the session ID is passed, while fresh spawns do not resume. Keep the initialStdin strategy consistent across turns.
  3. A persistent process must receive initial stdin and subsequent turns through writeUserInput. A one-shot process must write nothing on subsequent stdin and declare finishMissionOnExit: false.
  4. Parse a complete fixture into JSON-serializable system, assistant, user, result or assistant_delta events. Exactly one system:init opens the turn; if it names a backend, it names the profile. Results carry boolean is_error and subtype: "success" or "error". Test both outcomes.
  5. Empty lines, whitespace, banners, malformed JSON and null return []. Only the init event yields a session ID; other events, non-init system events and empty IDs return null.
  6. If disposal exists, calling it for a mission with no on-disk state is safe. Separately exercise unload, missing credentials and executable-policy refusal on a test installation before distributing the plugin.