Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Adds a new LLM provider implementing LLMProvider interface with call() and stream() methods. Integrates with provider factory in src/llm/index.ts, config detection in src/llm/config.ts, and error handling via tracking and recovery. Use when adding a new model backend, integrating a third-party LLM API, or extending LLM platform support. Do NOT use for fixing bugs in existing providers, modifying existing provider behavior, or changing the LLMProvider interface.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 4621% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 169% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 78% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 56% | 0% |
this.client = new YourSDK({ apiKey: config.apiKey }). Never lazy-initialize on first call — providers are instantiated once and cached in src/llm/index.ts.options.model || this.defaultModel. Never hardcode model names. Callers supply model overrides via LLMCallOptions.model.Verify directory exists: ls -la src/llm/. Create src/llm/your-provider.ts. Match existing provider patterns (src/llm/anthropic.ts, src/llm/openai-compat.ts).
Minimal structure:
typescriptimport type { LLMProvider, LLMCallOptions, LLMStreamOptions, LLMStreamCallbacks, LLMConfig, TokenUsage } from './types.js'; import { trackUsage } from './usage.js'; import { estimateTokens } from './utils.js'; export class YourProviderProvider implements LLMProvider { private client: YourSDKType; private defaultModel: string; constructor(config: LLMConfig) { if (!config.apiKey) throw new Error('API key required'); this.client = new YourSDK({ apiKey: config.apiKey, ...(config.baseUrl && { baseURL: config.baseUrl }) }); this.defaultModel = config.model; } async call(options: LLMCallOptions): Promise<string> { const model = options.model || this.defaultModel; const response = await this.client.messages.create({ model, max_tokens: options.maxTokens || 4096, system: options.system, messages: [{ role: 'user', content: options.prompt }] }); trackUsage(model, { inputTokens: response.usage?.input_tokens || 0, outputTokens: response.usage?.output_tokens || 0 }); return response.content?.[0]?.text || ''; } async stream(options: LLMStreamOptions, callbacks: LLMStreamCallbacks): Promise<void> { const model = options.model || this.defaultModel; const messages = [...(options.messages || []), { role: 'user' as const, content: options.prompt }]; try { const stream = await this.client.stream({ model, max_tokens: options.maxTokens || 10240, system: options.system, messages }); let stopReason: string | undefined, usage: TokenUsage | undefined; for await (const chunk of stream) { if (chunk.delta?.text) callbacks.onText(chunk.delta.text); if (chunk.delta?.stop_reason) stopReason = chunk.delta.stop_reason; if (chunk.usage) usage = { inputTokens: chunk.usage.input_tokens, outputTokens: chunk.usage.output_tokens }; } if (usage) trackUsage(model, usage); callbacks.onEnd({ stopReason, usage }); } catch (error) { callbacks.onError(error instanceof Error ? error : new Error(String(error))); } } }
Verify: File exports the class; imports match existing providers.
Edit src/llm/types.ts line 1. Add your provider in kebab-case:
typescriptexport type ProviderType = 'anthropic' | 'vertex' | 'openai' | 'cursor' | 'claude-cli' | 'your-provider';
Verify: npx tsc --noEmit shows no ProviderType errors.
If your provider needs fields beyond apiKey, model, baseUrl, extend LLMConfig in src/llm/types.ts:
typescriptexport interface LLMConfig { provider: ProviderType; model: string; fastModel?: string; apiKey?: string; baseUrl?: string; yourProviderSecret?: string; }
Edit src/llm/config.ts:
Line 9: Add to DEFAULT_MODELS:
typescriptexport const DEFAULT_MODELS: Record<ProviderType, string> = { anthropic: 'claude-sonnet-4-6', vertex: 'claude-sonnet-4-6', openai: 'gpt-5.4-mini', cursor: 'sonnet-4.6', 'claude-cli': 'default', 'your-provider': 'your-provider/default-model', };
Line 17: Add to MODEL_CONTEXT_WINDOWS if known:
typescriptexport const MODEL_CONTEXT_WINDOWS: Record<string, number> = { 'your-provider/model-name': 128_000, };
Line 59: In resolveFromEnv(), add env detection before final return null:
typescriptif (process.env.YOUR_PROVIDER_API_KEY) { return { provider: 'your-provider', apiKey: process.env.YOUR_PROVIDER_API_KEY, model: process.env.CALIBER_MODEL || DEFAULT_MODELS['your-provider'], baseUrl: process.env.YOUR_PROVIDER_BASE_URL, }; }
Line 115: In readConfigFile() validation, add 'your-provider' to includes list.
Verify: npm run test -- src/llm/__tests__/ -t config confirms env var detection works.
Edit src/llm/index.ts. Add import (line ~4):
typescriptimport { YourProviderProvider } from './your-provider.js';
In createProvider() switch (line ~24), add before default case:
typescriptcase 'your-provider': return new YourProviderProvider(config);
Verify: npx tsc --noEmit passes; no type errors on switch cases.
Create src/llm/__tests__/your-provider.test.ts:
typescriptimport { describe, it, expect, beforeEach } from 'vitest'; import { YourProviderProvider } from '../your-provider.js'; describe('YourProviderProvider', () => { let provider: YourProviderProvider; beforeEach(() => { provider = new YourProviderProvider({ provider: 'your-provider', model: 'test', apiKey: 'test' }); }); it('implements LLMProvider interface', () => { expect(typeof provider.call).toBe('function'); expect(typeof provider.stream).toBe('function'); }); it('call() returns string', async () => { const result = await provider.call({ system: 'helpful', prompt: 'hi' }); expect(typeof result).toBe('string'); }); it('stream() invokes callbacks', async () => { const texts: string[] = []; let ended = false; await provider.stream({ system: 'helpful', prompt: 'hi' }, { onText: (t) => texts.push(t), onEnd: () => { ended = true; }, onError: () => {}, }); expect(ended).toBe(true); }); });
Verify: npm run test -- src/llm/__tests__/your-provider.test.ts passes.
Run factory tests with your provider env var:
bashYOUR_PROVIDER_API_KEY=test npm run test -- src/llm/__tests__/index.test.ts
Verify: getProvider() instantiates your provider; llmCall() dispatches correctly.
User says: "I need caliber to use my local LM Studio instance."
Actions: Create src/llm/lm-studio.ts extending OpenAICompatProvider. Add 'lm-studio' to ProviderType. In config.ts:
typescriptif (process.env.LM_STUDIO_BASE_URL) { return { provider: 'lm-studio', apiKey: '', model: 'local', baseUrl: process.env.LM_STUDIO_BASE_URL }; }
Register in createProvider() case. User: export LM_STUDIO_BASE_URL=http://localhost:8000/v1. Result: caliber uses local LM Studio; tokens estimated via estimateTokens().
User says: "Ollama is auto-detected; no API key needed."
Actions: Create src/llm/ollama.ts extending OpenAICompatProvider. Add 'ollama' to ProviderType and SEAT_BASED_PROVIDERS. In config.ts:
typescriptif (process.env.OLLAMA_HOST) { return { provider: 'ollama', model: 'mistral', baseUrl: process.env.OLLAMA_HOST || 'http://localhost:11434/v1' }; }
Result: Offline per-machine LLM without API keys.
Unknown provider: your-provider
Cannot find module './your-provider.js'
API key is required for YourProvider
YOUR_PROVIDER_API_KEY=test npm run test -- src/llm/__tests__/index.test.ts.LLM response did not include usage tokens
trackUsage(model, { inputTokens: estimateTokens(options.system + options.prompt), outputTokens: estimateTokens(response.text) });Stream callbacks never fire; onEnd not called
for await (const chunk of stream) { } callbacks.onEnd({ stopReason, usage });My model parameter is ignored
options.model || this.defaultModel.const model = options.model || this.defaultModel; const response = await this.client.create({ model, ... });trackUsage() is never called
trackUsage(model, { inputTokens: ..., outputTokens: ... });Type error: Provider doesn't implement LLMProvider
Other measured skills in the registry, with their headline benchmark lift.