@wrongstack/providers
v0.318.0
Published
WrongStack LLM provider adapters (Anthropic, OpenAI, Google) built on declarative WireFormatConfig.
Readme
@wrongstack/providers
LLM provider adapters for WrongStack: Anthropic, OpenAI, Google, OpenAI-compatible (Mistral, DeepSeek, xAI/Grok, Groq, Together, Fireworks, OpenRouter, …), plus OAuth-based variants (Claude Pro/Max via "Sign in with Claude", ChatGPT via "Sign in with ChatGPT", GitHub Copilot).
All providers ride a single declarative WireFormatConfig adapter — even the majors (Anthropic / OpenAI / Google) are thin wrappers around their presets. Adding a new OpenAI-compatible provider is usually a 20-line preset, not a new file.
Install
pnpm add @wrongstack/providers @wrongstack/core@wrongstack/core provides the shared Provider interface, message types, capabilities model, and tool format.
Provider definition authority
Product identity, endpoint, environment-variable, model, usage, compatibility-policy, local-server, and setup-card metadata live in the typed ProviderDefinition registry. Runtime presets, factory tuning, CLI/WebUI local presets, the WebUI setup catalog, and the generated provider catalog are projections of that registry.
After changing a definition, run pnpm providers:catalog:write. Release checks fail when either generated snapshot drifts.
Supported wire families
| Family | Class | Preset | Used by |
|--------|-------|--------|---------|
| anthropic | AnthropicProvider | anthropicWireFormat | Claude API key |
| anthropic-oauth | AnthropicOAuthProvider | anthropicWireFormat | Claude Pro/Max (OAuth) |
| openai | OpenAIProvider | openaiWireFormat | OpenAI API key |
| openai-codex | OpenAICodexProvider | — | ChatGPT (Responses API, OAuth) |
| github-copilot | GitHubCopilotProvider | — | GitHub Copilot (OAuth) |
| openai-compatible | OpenAICompatibleProvider | generic | Any /v1/chat/completions endpoint |
| google | GoogleProvider | googleWireFormat | Gemini API key |
Tuned presets (auto-registered for matching provider ids)
| Preset | Family | Base URL | Notes |
|--------|--------|----------|-------|
| mistralWireFormat | openai-compatible | https://api.mistral.ai/v1 | JSON mode, 128K context |
| ollamaWireFormat | openai-compatible | http://localhost:11434/v1 | No auth, keep-alive |
| vllmWireFormat | openai-compatible | http://localhost:8000/v1 | 32K context |
| lmstudioWireFormat | openai-compatible | http://localhost:1234/v1 | 8K context |
What's in here
src/
anthropic.ts thin WireFormatProvider wrapper (anthropic preset)
openai.ts thin WireFormatProvider wrapper (openai preset)
google.ts thin WireFormatProvider wrapper (google preset)
openai-compatible.ts generic /v1/chat/completions adapter
anthropic-oauth.ts Claude Pro/Max OAuth (same wire as anthropic)
openai-codex.ts ChatGPT Responses API (OAuth)
github-copilot.ts GitHub Copilot (OAuth)
wire-adapter.ts base HTTP + SSE streaming for all providers
wire-format.ts declarative WireFormatConfig → WireFormatProvider
presets/ tuned configs for anthropic / openai / google / mistral / local-llm
capabilities.ts resolve capabilities for (provider, model) pairs
family-capabilities.ts per-family capability defaults
sse.ts SSE parser with 256 KB buffer cap
aggregate.ts tool_use stream-event aggregator
tool-format/ tools ↔ Anthropic / OpenAI / Responses converters
stop-reason.ts normalize provider stop_reason → canonical
error-parse.ts parse provider HTTP error envelopes
index.ts factory builder + preset registrationRequest parameters
Every provider accepts the canonical Request type from @wrongstack/core. Parameters not supported by a provider are silently ignored at the wire level:
| Parameter | Anthropic | OpenAI | Gemini | Compatible |
|-----------|-----------|--------|--------|------------|
| model | ✅ | ✅ | ✅ | ✅ |
| messages | ✅ | ✅ | ✅ | ✅ |
| maxTokens | ✅ max_tokens | ✅ max_completion_tokens | ✅ maxOutputTokens | ✅ max_tokens |
| temperature | ✅ temperature | ✅ temperature | ✅ temperature | ✅ temperature |
| topP | ✅ top_p | ✅ top_p | ✅ topP | ✅ top_p |
| topK | ✅ top_k | ❌ | ✅ topK | ✅ top_k |
| frequencyPenalty | ❌ | ✅ frequency_penalty | ✅ frequencyPenalty | ✅ frequency_penalty |
| presencePenalty | ❌ | ✅ presence_penalty | ✅ presencePenalty | ✅ presence_penalty |
| seed | ❌ | ✅ seed | ✅ seed | ✅ seed |
| stopSequences | ✅ stop_sequences | ✅ stop | ✅ stopSequences | ✅ stop |
| toolChoice | ✅ | ✅ | ✅ | ✅ |
| user | ✅ (metadata.user_id) | ✅ user | ❌ | ✅ user |
| logprobs | ❌ | ✅ logprobs + top_logprobs | ✅ logprobs | ✅ logprobs |
| reasoning | ✅ thinking | ✅ reasoning_effort | ✅ thinkingConfig | via quirks |
| responseFormat | ❌ | ✅ response_format | ✅ responseMimeType+responseSchema | Provider-specific |
| cache | ✅ cache_control | ❌ | ❌ | ❌ |
| safetySettings | ❌ | ❌ | ✅ safetySettings array | ❌ |
| candidateCount | ❌ | ❌ | ✅ candidateCount | ❌ |
Quick example
import { AnthropicProvider } from '@wrongstack/providers';
const provider = new AnthropicProvider({
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const stream = provider.stream(
{
model: 'anthropic-test-model',
messages: [{ role: 'user', content: 'hello' }],
maxTokens: 512,
temperature: 0.7,
topK: 40,
user: 'user-abc',
reasoning: { enabled: true, effort: 'high' },
},
{ signal: new AbortController().signal },
);
for await (const event of stream) {
if (event.type === 'text_delta') process.stdout.write(event.text);
}Using a preset (OpenAI-compatible service)
Presets are automatically used when the models.dev registry returns a matching provider id. You can also use them directly:
import { createWireFormatFactory, mistralWireFormat } from '@wrongstack/providers';
// Build a factory from the Mistral preset
const factory = createWireFormatFactory(mistralWireFormat, {
apiKey: process.env.MISTRAL_API_KEY!,
});
const provider = factory.create({ apiKey: process.env.MISTRAL_API_KEY! });
const result = await provider.complete(
{
model: 'mistral-large-2506',
messages: [{ role: 'user', content: 'hello' }],
maxTokens: 512,
seed: 42,
},
{ signal: new AbortController().signal },
);Wire-format adapter (declarative)
For a new provider that doesn't fit one of the existing presets, write a WireFormatConfig and plug it into WireFormatProvider. See docs/provider-author-guide.md for the full spec.
import { WireFormatProvider, type WireFormatConfig, defineWireFormat } from '@wrongstack/providers';
const myWire = defineWireFormat({
id: 'myprovider',
family: 'openai-compatible',
capabilities: { tools: true, parallelTools: true, vision: false, streaming: true, promptCache: false, systemPrompt: true, jsonMode: false, maxContext: 32_000, cacheControl: 'none' },
defaultBaseUrl: 'https://api.myprovider.com/v1',
buildUrl: (baseUrl) => `${baseUrl.replace(/\/+$/, '')}/chat/completions`,
buildHeaders: (apiKey) => ({ authorization: `Bearer ${apiKey}` }),
buildBody: (req, { capabilities }) => ({
model: req.model,
messages: req.messages,
max_tokens: req.maxTokens ?? capabilities.maxOutput ?? 8192,
stream: true,
}),
createStreamState: (fallbackModel) => ({ model: fallbackModel, started: false }),
parseStreamEvent: () => [],
finalizeStream: () => [{ type: 'message_stop', stopReason: 'end_turn', usage: { input: 0, output: 0 } }],
});
const provider = new WireFormatProvider(myWire, { apiKey: '…' });Capabilities model
Each provider family has a baseline capability profile in CAPABILITIES_BY_FAMILY. Per-model overrides from the models.dev catalog are AND-ed with the family baseline. The agent uses capabilities to gate vision, reasoning, tool-use, and parameter support.
Capability flags
| Flag | anthropic | openai | google | compatible |
|------|-----------|--------|--------|------------|
| tools / parallelTools | ✅ | ✅ | ✅ | ✅ |
| vision | ✅ | ✅ | ✅ | ❌ |
| streaming | ✅ | ✅ | ✅ | ✅ |
| systemPrompt | ✅ | ✅ | ✅ | ✅ |
| promptCache | ✅ (native) | ❌ | ❌ | ❌ |
| jsonMode | ❌ | ✅ | ✅ | ❌ |
| reasoning | ❌ (via thinking) | ✅ | via thinkingConfig | via quirks |
| topK | ✅ | ❌ | ✅ | ❌ |
| frequencyPenalty | ❌ | ✅ | ✅ | ❌ |
| presencePenalty | ❌ | ✅ | ✅ | ❌ |
| seed | ❌ | ✅ | ✅ | ❌ |
| structuredOutput | ❌ | ✅ | ✅ | ❌ |
| logprobs | ❌ | ✅ | ✅ | ❌ |
| audio | ❌ | ✅ (gpt-audio-*) | ❌ | ❌ |
User-defined model overrides via CustomModelDefinition.capabilities skip the AND gate and take full effect.
Thinking / reasoning
Extended thinking (chain-of-thought) is managed through the Request.reasoning field and works across providers:
Anthropic:
reasoning.enabled→thinking: { type: "enabled", budget_tokens }. The budget is derived automatically frommaxTokensandreasoning.effort. Thinking is streamed asthinking_start,thinking_delta,thinking_signature, andthinking_stopevents. The signature must be echoed back on the next request — the stream parser handles this automatically.OpenAI (o-series models):
reasoning.effort→reasoning_effort: "low"|"medium"|"high". Valuesminimal,xhigh, andmaxare filtered to avoid provider 400s. Reasoning content from the response (delta.reasoning_content) is echoed back asmessage.reasoning_contenton subsequent turns.Gemini:
reasoning.enabled→generationConfig.thinkingConfig: { type: "enabled"|"disabled" }.OpenAI-compatible: The
thinkingParamquirk controls how reasoning is mapped —'zai-glm'(ZAI reasoning),'kimi-toggle'(Kimi thinking mode), or'always-on'(models that always think and reject disable). SeeCompatibilityQuirks.
Structured output
Set Request.responseFormat to constrain the model's output:
// JSON Schema (OpenAI, Gemini)
{
type: 'json_schema',
jsonSchema: {
name: 'person',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
},
required: ['name', 'age']
}
}
}
// Free-form JSON (OpenAI, Gemini)
{ type: 'json_object' }
// Plain text (default)
{ type: 'text' }- OpenAI →
response_formatwithjson_schemaorjson_objecttype - Gemini →
responseMimeType: "application/json"+responseSchema(when json_schema)
Safety settings
Gemini supports per-category safety thresholds via Request.safetySettings:
{
safetySettings: [
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH' },
{ category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_LOW_AND_ABOVE' },
],
}Supported categories: HARM_CATEGORY_HARASSMENT, HARM_CATEGORY_HATE_SPEECH, HARM_CATEGORY_SEXUALLY_EXPLICIT, HARM_CATEGORY_DANGEROUS_CONTENT.
Thresholds: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE.
Structured error responses
Every provider HTTP error is normalized through parseProviderHttpError() into a ProviderError with:
status— HTTP status codeproviderId— which provider returned the errorretryable— whether the agent loop should retry (true for 429/5xx)describe()— one-line human-readable description for the CLI/TUI status line
Tool input parsing (parseToolInput)
All providers run tool-call JSON through _tool-input.ts. It guarantees the agent always receives a Record<string, unknown> for tool_use.input, never a parse-error or null. Invalid or non-object inputs are wrapped under { __raw: ... } instead of crashing the provider runner.
Local LLM presets
Presets for Ollama, vLLM, and LM Studio are tuned for local servers:
import { ollamaWireFormat } from '@wrongstack/providers';
import { WireFormatProvider } from '@wrongstack/providers';
const ollama = new WireFormatProvider(ollamaWireFormat, {
apiKey: '', // Ollama doesn't use auth
baseUrl: 'http://localhost:11434/v1',
});The createLocalLlmPreset() factory lets you create custom local presets with bodyExtras for provider-specific fields (e.g., Ollama's keep_alive, vLLM's repetition_penalty).
OAuth providers
Three providers use OAuth token refresh patterns:
AnthropicOAuthProvider — Claude Pro/Max "Sign in with Claude". Same wire as the API-key Anthropic family but with
Authorization: Bearerand Claude Code identity headers. Tokens self-refresh near-expiry and on 401.OpenAICodexProvider — ChatGPT "Sign in with ChatGPT" (Responses API). Uses the OpenAI Responses wire format (not chat/completions). Reasoning effort is configurable via
reasoningEffort.GitHubCopilotProvider — GitHub Copilot subscription. Wraps the OpenAI-compatible chat endpoint with Copilot editor headers and token-based URL discovery.
All three persist refreshed tokens through the setOAuthTokenPersister hook so new tokens survive session restarts.
License
MIT
