Help us shape the future of Go! Take the 2026 Go Developer Survey

provider

package
v0.57.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: AGPL-3.0 Imports: 30 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// EffortNone turns reasoning off.
	EffortNone = "none"
	// EffortOn turns reasoning on at the model's own depth. It is the only
	// level besides EffortNone for models that switch thinking on or off but
	// take no depth.
	EffortOn      = "on"
	EffortMinimal = "minimal"
	EffortLow     = "low"
	EffortMedium  = "medium"
	EffortHigh    = "high"
	// EffortXHigh sits between high and max (Claude Opus 4.7 and later, GPT-5.2
	// and later).
	EffortXHigh = "xhigh"
	EffortMax   = "max"
)

The levels, in ogcode's vocabulary. Lowest first.

View Source
const (
	CacheWriteMultiplier = 1.25
	CacheReadMultiplier  = 0.10
)

Cache-token prices as a multiple of the model's base input price, used only when a model publishes no cache prices of its own. Anthropic charges 1.25x to write an entry and 0.1x to read one; providers that never bill a write report a cache write count of 0, so the write multiplier costs them nothing.

View Source
const DefaultOGXGatewayURL = "https://ogx.ogcode.in/v1"

DefaultOGXGatewayURL is OG Lab's gateway, the endpoint the OGX token is valid against. OGX_GATEWAY_URL overrides it — development and staging point this at a local gateway.

View Source
const DefaultOllamaBaseURL = "http://localhost:11434/v1"

DefaultOllamaBaseURL is the default local Ollama OpenAI-compatible endpoint.

View Source
const OGXAppID = "ogcode"

OGXAppID is the first-party identity ogcode presents to the gateway when it asserts its requests. It matches the `app` the connect flow registers with, and it is the key the gateway's app-secret table looks the shared secret up under.

View Source
const OGXProviderID = "ogx"

OGX is the subscription plan sold by OG Lab. Inference runs through OG Lab's gateway, which speaks the OpenAI Chat Completions API, so the provider is an ordinary *OpenAIProvider pointed at the gateway with the token the connect flow stored — no protocol of its own.

Two things make it different from every other OpenAI-compatible endpoint:

  • The gateway's /v1/models IS the plan. It returns exactly the models the account's plan grants, and an empty list when the plan grants none. So Models() has no static fallback: a catalogue that comes back empty means the plan carries nothing, and offering a fallback would present models the account cannot reach.

  • The token is durable and per-install, and the gateway reads the prompt_cache_key field as the session identity for token spend, so every request carries it.

  • The plan meters the tokens it serves at its own rates: the gateway publishes them on /v1/models, and nothing else prices ogx usage — the vendor's catalogue price is a comparison yardstick, never the bill (see meteredByOwnListing in pricing.go).

Registration is gated on the stored link carrying a plan (session.OGXAccount .HasPlan). A planless link contributes no provider at all, which is what keeps it from becoming the default. One residual case is left unguarded: a plan whose catalogue fetches empty registers a provider that can serve nothing, and ProviderPriority would place it ahead of every other provider. That only happens for a plan with no granted models, which the settings screen surfaces as an empty, explained list rather than a failure.

Variables

View Source
var AnthropicModels = []CatalogModel{

	{ID: "claude-fable-5-1", Name: "Claude Fable 5.1", ActiveByDefault: true, InputPricePerM: 10, OutputPricePerM: 50, CacheWritePricePerM: 12.5, CacheReadPricePerM: 0.25, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-opus-5-5", Name: "Claude Opus 5.5", ActiveByDefault: true, InputPricePerM: 4, OutputPricePerM: 20, CacheWritePricePerM: 5, CacheReadPricePerM: 0.2, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortMedium},
	{ID: "claude-sonnet-5-5", Name: "Claude Sonnet 5.5", ActiveByDefault: true, InputPricePerM: 2, OutputPricePerM: 10, CacheWritePricePerM: 2.5, CacheReadPricePerM: 0.2, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-haiku-4-5-20251001", Aliases: []string{"claude-haiku-4-5"}, Name: "Claude Haiku 4.5", ActiveByDefault: true, InputPricePerM: 1, OutputPricePerM: 5, CacheWritePricePerM: 1.25, CacheReadPricePerM: 0.1, SupportsImages: true, ContextWindow: 200_000, MaxOutputTokens: 64_000},

	{ID: "claude-mythos-5-1", Name: "Claude Mythos 5.1", InputPricePerM: 10, OutputPricePerM: 50, CacheWritePricePerM: 12.5, CacheReadPricePerM: 0.25, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},

	{ID: "claude-fable-5", Name: "Claude Fable 5", InputPricePerM: 10, OutputPricePerM: 50, CacheWritePricePerM: 12.5, CacheReadPricePerM: 1, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-mythos-5", Name: "Claude Mythos 5", InputPricePerM: 10, OutputPricePerM: 50, CacheWritePricePerM: 12.5, CacheReadPricePerM: 1, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-sonnet-5", Name: "Claude Sonnet 5", InputPricePerM: 2, OutputPricePerM: 10, CacheWritePricePerM: 2.5, CacheReadPricePerM: 0.2, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-opus-5", Name: "Claude Opus 5", InputPricePerM: 5, OutputPricePerM: 25, CacheWritePricePerM: 6.25, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-opus-4-8", Name: "Claude Opus 4.8", InputPricePerM: 5, OutputPricePerM: 25, CacheWritePricePerM: 6.25, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-opus-4-7", Name: "Claude Opus 4.7", InputPricePerM: 5, OutputPricePerM: 25, CacheWritePricePerM: 6.25, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", RejectsSampling: true, Efforts: claudeEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-opus-4-6", Name: "Claude Opus 4.6", InputPricePerM: 5, OutputPricePerM: 25, CacheWritePricePerM: 6.25, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", Efforts: claude46Efforts, DefaultEffort: EffortHigh},
	{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6", InputPricePerM: 3, OutputPricePerM: 15, CacheWritePricePerM: 3.75, CacheReadPricePerM: 0.3, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 128_000, Thinking: "adaptive", Efforts: claude46Efforts, DefaultEffort: EffortHigh},

	{ID: "claude-opus-4-5-20251101", Aliases: []string{"claude-opus-4-5"}, Name: "Claude Opus 4.5", InputPricePerM: 5, OutputPricePerM: 25, CacheWritePricePerM: 6.25, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 200_000, MaxOutputTokens: 64_000, Efforts: lowToHighEfforts, DefaultEffort: EffortHigh},
	{ID: "claude-sonnet-4-5-20250929", Aliases: []string{"claude-sonnet-4-5"}, Name: "Claude Sonnet 4.5", InputPricePerM: 3, OutputPricePerM: 15, CacheWritePricePerM: 3.75, CacheReadPricePerM: 0.3, SupportsImages: true, ContextWindow: 200_000, MaxOutputTokens: 64_000},
}

AnthropicModels is the authoritative list of models the Claude API serves (current and legacy-but-active; retired models are in LegacyModels). Maintained by contributors — see file header for instructions.

Source: platform.claude.com/docs models overview, per-model pages, pricing and model-deprecations pages, verified 2026-09-28; Sonnet 5.5 and the effort ladders (the effort guide's per-model levels and defaults) 2026-10-07. All are multimodal. From the 4.6 generation on, the dateless ID is the pinned snapshot, the default window is 1M tokens (generally available, no beta header, no long-context premium) and output is capped at 128K; the 4.5 models are 200K/64K. Cache prices are the 5-minute write and the read; Fable 5.1 and Opus 5.5 discount reads below the usual 0.1x. Effort defaults to high on every model that takes it except Opus 5.5, whose default is medium; no Claude model is given EffortNone, because turning thinking off is a 400 on the newest models and invites tool calls written as prose on the rest — low effort is the cheap setting instead.

View Source
var ErrOGXTokenRevoked = errors.New("ogx: the gateway no longer accepts this install token")

ErrOGXTokenRevoked is CheckOGXPlan's answer when the gateway no longer accepts the stored install token — the link was revoked on the web side, so only connecting again can restore it.

View Source
var ErrRequestTooLarge = errors.New("request body too large")

ErrRequestTooLarge marks a request refused before it was sent because its body exceeds the endpoint's limit. Resending the same conversation would produce the same oversized body, so it is never worth a retry.

View Source
var ErrUpstreamSyncDisabled = errors.New("model-catalog sync is disabled (OGCODE_CATALOG_SYNC=false)")

ErrUpstreamSyncDisabled is returned by SyncUpstreamCatalog when the OGCODE_CATALOG_SYNC=false kill switch is set.

View Source
var GoogleModels = []CatalogModel{

	{ID: "gemini-4-argon", Aliases: []string{"gemini-pro-latest"}, Name: "Gemini 4 Argon", InputPricePerM: 2, OutputPricePerM: 10, CacheReadPricePerM: 0.1, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 32_768, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortHigh},

	{ID: "gemini-3.8-flash", Aliases: []string{"gemini-flash-latest"}, Name: "Gemini 3.8 Flash", InputPricePerM: 0.75, OutputPricePerM: 3.75, CacheReadPricePerM: 0.075, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gemini-3.7-flash", Name: "Gemini 3.7 Flash", InputPricePerM: 0.75, OutputPricePerM: 3.75, CacheReadPricePerM: 0.075, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gemini-3.6-flash", Name: "Gemini 3.6 Flash", InputPricePerM: 0.75, OutputPricePerM: 3.75, CacheReadPricePerM: 0.075, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gemini-3.5-flash", Name: "Gemini 3.5 Flash", InputPricePerM: 1.5, OutputPricePerM: 9, CacheReadPricePerM: 0.15, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gemini-3.5-flash-lite", Aliases: []string{"gemini-flash-lite-latest"}, Name: "Gemini 3.5 Flash-Lite", InputPricePerM: 0.3, OutputPricePerM: 2.5, CacheReadPricePerM: 0.03, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: minimalToHighEfforts, DefaultEffort: EffortMinimal},
	{ID: "gemini-3.1-flash-lite", Name: "Gemini 3.1 Flash-Lite", InputPricePerM: 0.25, OutputPricePerM: 1.5, CacheReadPricePerM: 0.025, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: minimalToHighEfforts, DefaultEffort: EffortMinimal},

	{ID: "gemini-3.1-pro-preview", Aliases: []string{"gemini-3.1-pro-preview-customtools", "gemini-3-pro-preview"}, Name: "Gemini 3.1 Pro (preview)", InputPricePerM: 2, OutputPricePerM: 12, CacheReadPricePerM: 0.2, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortHigh},
	{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash (preview)", InputPricePerM: 0.5, OutputPricePerM: 3, CacheReadPricePerM: 0.05, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, RejectsSampling: true, Efforts: lowToHighEfforts, DefaultEffort: EffortHigh},

	{ID: "gemini-2.5-pro", Name: "Gemini 2.5 Pro", InputPricePerM: 1.25, OutputPricePerM: 10, CacheReadPricePerM: 0.125, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, Efforts: lowToHighEfforts},
	{ID: "gemini-2.5-flash", Name: "Gemini 2.5 Flash", InputPricePerM: 0.3, OutputPricePerM: 2.5, CacheReadPricePerM: 0.03, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, Efforts: noneToHighEfforts},
	{ID: "gemini-2.5-flash-lite", Name: "Gemini 2.5 Flash-Lite", InputPricePerM: 0.1, OutputPricePerM: 0.4, CacheReadPricePerM: 0.01, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 65_536, Efforts: noneToHighEfforts, DefaultEffort: EffortNone},
}

GoogleModels are Google's Gemini models. No provider lists them on its own: they are reached through the Gemini API's OpenAI-compatible endpoint (the OpenAI slot pointed at generativelanguage.googleapis.com) or OpenRouter.

Source: ai.google.dev Gemini API docs — models and per-model pages, pricing, the thinking guide, the OpenAI compatibility page, deprecations and the changelog — verified 2026-10-16. Every model takes text, images, audio and video in a 1,048,576-token window; all but the flagship write up to 65,536 tokens, thinking included. Gemini 4 Argon, the flagship reasoning model, writes up to 32,768 (its own API reports the lower cap; OpenRouter advertises 65,536) and caches implicitly only — the explicit cached-content API is not offered for it. Prices are the Standard paid tier for prompts up to 200K tokens; 3.6–3.8 Flash are at an introductory price through 2026-12-31 ($1.50 in / $7.50 out / $0.15 cached after), and Gemini 4 Argon rises to $4 in / $20 out / $0.20 cached on 2026-12-01. The 2.5 models are limited to projects that used them before; new projects should start on 3.5 Flash-Lite or 3.8 Flash. Gemini 3.7 Flash now answers as 3.8 Flash and 3.5 Flash as 3.6 Flash, and 3.1 Flash-Lite shuts down 2027-05-07. 3.x and later deprecated sampling parameters, so they are dropped as for the models that reject them.

Effort is reasoning_effort on the compatible endpoint, mapped onto the model's thinking level. 4 Argon and the 3.x models cannot turn thinking off, and "minimal" is left off their ladders except where it is a model's own default: Google rejects it outright on several of them, and elsewhere it needs the thought signatures ogcode does not send back. 2.5 Pro cannot turn thinking off; 2.5 Flash and Flash-Lite can ("none"). Gemini answers a level a model lacks with a 400, which is why every level here is per model.

View Source
var LegacyModels = []CatalogModel{

	{ID: "claude-opus-4-1-20250805", Aliases: []string{"claude-opus-4-1"}, Name: "Claude Opus 4.1", InputPricePerM: 15, OutputPricePerM: 75, CacheWritePricePerM: 18.75, CacheReadPricePerM: 1.5, SupportsImages: true, ContextWindow: 200_000},
	{ID: "claude-opus-4-20250514", Aliases: []string{"claude-opus-4"}, Name: "Claude Opus 4", InputPricePerM: 15, OutputPricePerM: 75, CacheWritePricePerM: 18.75, CacheReadPricePerM: 1.5, SupportsImages: true, ContextWindow: 200_000},
	{ID: "claude-sonnet-4-20250514", Aliases: []string{"claude-sonnet-4"}, Name: "Claude Sonnet 4", InputPricePerM: 3, OutputPricePerM: 15, CacheWritePricePerM: 3.75, CacheReadPricePerM: 0.3, SupportsImages: true, ContextWindow: 200_000},
	{ID: "claude-3-5-haiku-20241022", Aliases: []string{"claude-3-5-haiku"}, Name: "Claude Haiku 3.5", InputPricePerM: 0.8, OutputPricePerM: 4, CacheWritePricePerM: 1, CacheReadPricePerM: 0.08, SupportsImages: true, ContextWindow: 200_000},

	{ID: "gpt-5.2-codex", Name: "GPT-5.2 Codex", InputPricePerM: 1.75, OutputPricePerM: 14, CacheWritePricePerM: 1.75, CacheReadPricePerM: 0.175, SupportsImages: true, ContextWindow: 272_000},
	{ID: "gpt-5.1-codex", Name: "GPT-5.1 Codex", InputPricePerM: 1.25, OutputPricePerM: 10, CacheWritePricePerM: 1.25, CacheReadPricePerM: 0.125, SupportsImages: true, ContextWindow: 272_000},
	{ID: "gpt-5.1-codex-max", Name: "GPT-5.1 Codex Max", InputPricePerM: 1.25, OutputPricePerM: 10, CacheWritePricePerM: 1.25, CacheReadPricePerM: 0.125, SupportsImages: true, ContextWindow: 272_000},
	{ID: "gpt-5.1-codex-mini", Name: "GPT-5.1 Codex Mini", InputPricePerM: 0.25, OutputPricePerM: 2, CacheWritePricePerM: 0.25, CacheReadPricePerM: 0.025, SupportsImages: true, ContextWindow: 272_000},
}

LegacyModels are models retired from their vendor's own API that other hosts still serve (OpenRouter, Bedrock, Vertex). They are catalogue-only: no provider lists them, so nobody picks a model the first-party API would reject, but a host that does serve one still gets its window and prices. Output ceilings are not restated on current official pages, so they are 0.

View Source
var OGXAppSecret = ""

OGXAppSecret is the shared secret ogcode signs its gateway requests with, so the gateway admits this first-party client and not a bearer token copied out of it. It is baked in at build time via ldflags (see the Makefile) and reads from the OGX_APP_SECRET environment variable there, so a release binary carries the identity while a local build leaves it empty and unasserted.

View Source
var OpenAIModels = []CatalogModel{

	{ID: "gpt-6-sol", Aliases: []string{"gpt-6-sol-pro"}, Name: "GPT-6 Sol", ActiveByDefault: true, InputPricePerM: 2, OutputPricePerM: 10, CacheWritePricePerM: 2.5, CacheReadPricePerM: 0.2, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-6-luna", Aliases: []string{"gpt-6-luna-pro"}, Name: "GPT-6 Luna", ActiveByDefault: true, InputPricePerM: 0.1, OutputPricePerM: 0.5, CacheWritePricePerM: 0.125, CacheReadPricePerM: 0.01, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortMedium},

	{ID: "gpt-6-astra", Aliases: []string{"gpt-6-astra-pro"}, Name: "GPT-6 Astra", InputPricePerM: 10, OutputPricePerM: 50, CacheWritePricePerM: 12.5, CacheReadPricePerM: 1, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "low", ResponsesOnly: true, Efforts: lowToXHighEfforts},

	{ID: "gpt-5.6-sol", Aliases: []string{"gpt-5.6", "gpt-5.6-sol-pro"}, Name: "GPT-5.6 Sol", ActiveByDefault: true, InputPricePerM: 4, OutputPricePerM: 20, CacheWritePricePerM: 5, CacheReadPricePerM: 0.4, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5.6-terra", Aliases: []string{"gpt-5.6-terra-pro"}, Name: "GPT-5.6 Terra", ActiveByDefault: true, InputPricePerM: 2, OutputPricePerM: 12, CacheWritePricePerM: 2.5, CacheReadPricePerM: 0.2, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5.6-luna", Aliases: []string{"gpt-5.6-luna-pro"}, Name: "GPT-5.6 Luna", InputPricePerM: 0.2, OutputPricePerM: 1.2, CacheWritePricePerM: 0.25, CacheReadPricePerM: 0.02, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortMedium},

	{ID: "gpt-5.5", Name: "GPT-5.5", InputPricePerM: 5, OutputPricePerM: 30, CacheWritePricePerM: 5, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5.5-pro", Name: "GPT-5.5 Pro", InputPricePerM: 30, OutputPricePerM: 180, CacheWritePricePerM: 30, CacheReadPricePerM: 30, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "medium", ResponsesOnly: true, Efforts: mediumToXHighEfforts, DefaultEffort: EffortHigh},
	{ID: "gpt-5.4", Name: "GPT-5.4", InputPricePerM: 2.5, OutputPricePerM: 15, CacheWritePricePerM: 2.5, CacheReadPricePerM: 0.25, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortNone},
	{ID: "gpt-5.4-mini", Name: "GPT-5.4 Mini", InputPricePerM: 0.75, OutputPricePerM: 4.5, CacheWritePricePerM: 0.75, CacheReadPricePerM: 0.075, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortNone},
	{ID: "gpt-5.4-nano", Name: "GPT-5.4 Nano", InputPricePerM: 0.2, OutputPricePerM: 1.25, CacheWritePricePerM: 0.2, CacheReadPricePerM: 0.02, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "none", ToolsNeedNoReasoning: true, Efforts: noneToXHighEfforts, DefaultEffort: EffortNone},
	{ID: "gpt-5.4-pro", Name: "GPT-5.4 Pro", InputPricePerM: 30, OutputPricePerM: 180, CacheWritePricePerM: 30, CacheReadPricePerM: 30, SupportsImages: true, ContextWindow: 922_000, EffortFloor: "medium", ResponsesOnly: true, Efforts: mediumToXHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5.3-codex", Name: "GPT-5.3 Codex", InputPricePerM: 1.75, OutputPricePerM: 14, CacheWritePricePerM: 1.75, CacheReadPricePerM: 0.175, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "low", ResponsesOnly: true, Efforts: lowToXHighEfforts},

	{ID: "gpt-5.2", Name: "GPT-5.2", InputPricePerM: 1.75, OutputPricePerM: 14, CacheWritePricePerM: 1.75, CacheReadPricePerM: 0.175, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "none", Efforts: noneToXHighEfforts, DefaultEffort: EffortNone},
	{ID: "gpt-5.2-pro", Name: "GPT-5.2 Pro", InputPricePerM: 21, OutputPricePerM: 168, CacheWritePricePerM: 21, CacheReadPricePerM: 21, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "medium", ResponsesOnly: true, Efforts: mediumToXHighEfforts},
	{ID: "gpt-5.1", Name: "GPT-5.1", InputPricePerM: 1.25, OutputPricePerM: 10, CacheWritePricePerM: 1.25, CacheReadPricePerM: 0.125, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "none", Efforts: noneToHighEfforts, DefaultEffort: EffortNone},

	{ID: "gpt-5", Name: "GPT-5", InputPricePerM: 1.25, OutputPricePerM: 10, CacheWritePricePerM: 1.25, CacheReadPricePerM: 0.125, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "minimal", Efforts: minimalToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5-mini", Name: "GPT-5 Mini", InputPricePerM: 0.25, OutputPricePerM: 2, CacheWritePricePerM: 0.25, CacheReadPricePerM: 0.025, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "minimal", Efforts: minimalToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5-nano", Name: "GPT-5 Nano", InputPricePerM: 0.05, OutputPricePerM: 0.4, CacheWritePricePerM: 0.05, CacheReadPricePerM: 0.005, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "minimal", Efforts: minimalToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-5-pro", Name: "GPT-5 Pro", InputPricePerM: 15, OutputPricePerM: 120, CacheWritePricePerM: 15, CacheReadPricePerM: 15, SupportsImages: true, ContextWindow: 272_000, EffortFloor: "high", ResponsesOnly: true},

	{ID: "gpt-4.1", Name: "GPT-4.1", InputPricePerM: 2, OutputPricePerM: 8, CacheWritePricePerM: 2, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 1_047_576},
	{ID: "gpt-4.1-mini", Name: "GPT-4.1 Mini", InputPricePerM: 0.4, OutputPricePerM: 1.6, CacheWritePricePerM: 0.4, CacheReadPricePerM: 0.1, SupportsImages: true, ContextWindow: 1_047_576},
	{ID: "gpt-4.1-nano", Name: "GPT-4.1 Nano", InputPricePerM: 0.1, OutputPricePerM: 0.4, CacheWritePricePerM: 0.1, CacheReadPricePerM: 0.025, SupportsImages: true, ContextWindow: 1_047_576},
	{ID: "gpt-4o", Name: "GPT-4o", InputPricePerM: 2.5, OutputPricePerM: 10, CacheWritePricePerM: 2.5, CacheReadPricePerM: 1.25, SupportsImages: true, ContextWindow: 128_000},
	{ID: "gpt-4o-mini", Name: "GPT-4o Mini", InputPricePerM: 0.15, OutputPricePerM: 0.6, CacheWritePricePerM: 0.15, CacheReadPricePerM: 0.075, SupportsImages: true, ContextWindow: 128_000},

	{ID: "o3", Name: "o3", InputPricePerM: 2, OutputPricePerM: 8, CacheWritePricePerM: 2, CacheReadPricePerM: 0.5, SupportsImages: true, ContextWindow: 200_000, EffortFloor: "low", Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "o4-mini", Aliases: []string{"o4-mini-high"}, Name: "o4 Mini", InputPricePerM: 1.1, OutputPricePerM: 4.4, CacheWritePricePerM: 1.1, CacheReadPricePerM: 0.275, SupportsImages: true, ContextWindow: 200_000, EffortFloor: "low", Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "o3-mini", Aliases: []string{"o3-mini-high"}, Name: "o3 Mini", InputPricePerM: 1.1, OutputPricePerM: 4.4, CacheWritePricePerM: 1.1, CacheReadPricePerM: 0.55, ContextWindow: 200_000, EffortFloor: "low", Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "o1", Name: "o1", InputPricePerM: 15, OutputPricePerM: 60, CacheWritePricePerM: 15, CacheReadPricePerM: 7.5, SupportsImages: true, ContextWindow: 200_000, EffortFloor: "low", Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "o3-pro", Name: "o3 Pro", InputPricePerM: 20, OutputPricePerM: 80, CacheWritePricePerM: 20, CacheReadPricePerM: 20, SupportsImages: true, ContextWindow: 200_000, EffortFloor: "low", ResponsesOnly: true},
	{ID: "o1-pro", Name: "o1 Pro", InputPricePerM: 150, OutputPricePerM: 600, CacheWritePricePerM: 150, CacheReadPricePerM: 150, SupportsImages: true, ContextWindow: 200_000, EffortFloor: "low", ResponsesOnly: true},
}

OpenAIModels is the list of OpenAI models the OpenAI provider offers, minus the ResponsesOnly ones, which stay catalogued for hosts that bridge them. Maintained by contributors — see file header for instructions.

Source: developers.openai.com model pages, pricing, deprecations, changelog and the GPT-6 and Responses migration guides, verified 2026-09-28; effort ladders and defaults (the reasoning guide and model pages) 2026-10-07. The ladders are what Chat Completions accepts: "max" is Responses-only on GPT-5.6 and GPT-6, so it is left off. GPT-5.5 and later default to medium even with tools, where Chat Completions takes only "none" — see ToolsNeedNoReasoning. Prices are the Standard tier for prompts under 272K tokens. Already-retired models are left out (the retired Codex models OpenRouter still serves are in LegacyModels), as are gpt-oss (in OpenModels: OpenAI does not serve it) and the chat-latest aliases, which OpenAI does not recommend for API use.

View Source
var OpenModels = []CatalogModel{

	{ID: "deepseek-flash", Aliases: []string{"deepseek-v4.1-flash", "ogx-deepseek-v4.1-flash", "deepseek-ai/DeepSeek-V4.1-Flash", "deepseek-v4p1-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"}, Name: "DeepSeek V4.1 Flash", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheReadPricePerM: 0.006, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 393_216, Efforts: noneLowHighMaxEfforts, DefaultEffort: EffortHigh},
	{ID: "deepseek-v4-pro", Aliases: []string{"deepseek-v4-pro-0813", "deepseek-ai/DeepSeek-V4-Pro-0813", "deepseek-ai/DeepSeek-V4-Pro"}, Name: "DeepSeek V4 Pro", InputPricePerM: 1.32, OutputPricePerM: 3.96, CacheReadPricePerM: 0.044, ContextWindow: 1_048_576, MaxOutputTokens: 393_216, Efforts: noneLowHighMaxEfforts, DefaultEffort: EffortHigh},

	{ID: "deepseek-ai/DeepSeek-V4-Flash-0731", Name: "DeepSeek V4 Flash 0731", InputPricePerM: 0.021, OutputPricePerM: 0.32, CacheReadPricePerM: 0.016, ContextWindow: 1_048_576, Efforts: noneHighMaxEfforts},
	{ID: "deepseek-ai/DeepSeek-V3.2", Aliases: []string{"deepseek-v3.2-exp"}, Name: "DeepSeek V3.2", InputPricePerM: 0.28, OutputPricePerM: 0.42, CacheReadPricePerM: 0.028, ContextWindow: 163_840, Efforts: onOffEfforts},
	{ID: "deepseek-ai/DeepSeek-V3.1-Terminus", Aliases: []string{"deepseek-v3.1:671b-terminus"}, Name: "DeepSeek V3.1 Terminus", InputPricePerM: 0.27, OutputPricePerM: 1, CacheReadPricePerM: 0.135, ContextWindow: 163_840, Efforts: onOffEfforts},
	{ID: "deepseek-ai/DeepSeek-V3.1", Aliases: []string{"deepseek-chat-v3.1", "deepseek-v3.1:671b"}, Name: "DeepSeek V3.1", InputPricePerM: 0.25, OutputPricePerM: 0.95, CacheReadPricePerM: 0.13, ContextWindow: 163_840, Efforts: onOffEfforts},

	{ID: "deepseek-ai/DeepSeek-R1-0528", Aliases: []string{"deepseek-r1:671b", "deepseek-r1:671b-0528"}, Name: "DeepSeek R1 0528", InputPricePerM: 0.5, OutputPricePerM: 2.15, CacheReadPricePerM: 0.35, ContextWindow: 163_840},

	{ID: "qwen3.8-2.4t-a95b", Aliases: []string{"Qwen/Qwen3.8-2.4T-A95B-FP8"}, Name: "Qwen3.8 2.4T-A95B", InputPricePerM: 2, OutputPricePerM: 6, CacheReadPricePerM: 0.25, ContextWindow: 983_000, MaxOutputTokens: 131_072, Efforts: qwen38ThinkingEfforts, DefaultEffort: EffortXHigh},
	{ID: "qwen3.8-27b", Aliases: []string{"Qwen/Qwen3.8-27B-FP8", "qwen3.8"}, Name: "Qwen3.8 27B", InputPricePerM: 0.5, OutputPricePerM: 3, CacheReadPricePerM: 0.1, SupportsImages: true, ContextWindow: 983_000, MaxOutputTokens: 131_072, Efforts: qwen38Efforts, DefaultEffort: EffortXHigh},
	{ID: "Qwen/Qwen3.8-Flash-Next", Aliases: []string{"qwen3.8-flash", "qwen3.8-flash-next:125b-a6b"}, Name: "Qwen3.8 Flash", InputPricePerM: 0.15, OutputPricePerM: 0.47, CacheReadPricePerM: 0.016, SupportsImages: true, ContextWindow: 983_000, MaxOutputTokens: 131_072, Efforts: qwen38Efforts, DefaultEffort: EffortXHigh},
	{ID: "qwen3.6-35b-a3b", Aliases: []string{"qwen3.6:35b", "qwen3.6"}, Name: "Qwen3.6 35B-A3B", InputPricePerM: 0.375, OutputPricePerM: 2.25, SupportsImages: true, ContextWindow: 258_000, MaxOutputTokens: 65_536, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "qwen3.6-27b", Name: "Qwen3.6 27B", InputPricePerM: 0.6, OutputPricePerM: 3.6, SupportsImages: true, ContextWindow: 258_000, MaxOutputTokens: 65_536, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "qwen3.5-397b-a17b", Name: "Qwen3.5 397B-A17B", InputPricePerM: 0.6, OutputPricePerM: 3.6, SupportsImages: true, ContextWindow: 258_000, MaxOutputTokens: 65_536, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "qwen3.5-122b-a10b", Aliases: []string{"qwen3.5:122b"}, Name: "Qwen3.5 122B-A10B", InputPricePerM: 0.4, OutputPricePerM: 3.2, SupportsImages: true, ContextWindow: 258_000, MaxOutputTokens: 65_536, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "qwen3.5-35b-a3b", Aliases: []string{"qwen3.5:35b"}, Name: "Qwen3.5 35B-A3B", InputPricePerM: 0.25, OutputPricePerM: 2, SupportsImages: true, ContextWindow: 258_000, MaxOutputTokens: 65_536, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "qwen3.5-27b", Name: "Qwen3.5 27B", InputPricePerM: 0.3, OutputPricePerM: 2.4, SupportsImages: true, ContextWindow: 258_000, MaxOutputTokens: 65_536, Efforts: onOffEfforts, DefaultEffort: EffortOn},

	{ID: "qwen3-coder-next", Name: "Qwen3 Coder Next", InputPricePerM: 0.3, OutputPricePerM: 1.5, ContextWindow: 204_800, MaxOutputTokens: 65_536},

	{ID: "qwen3-coder-480b-a35b-instruct", Aliases: []string{"qwen3-coder", "qwen3-coder:480b"}, Name: "Qwen3 Coder 480B-A35B", InputPricePerM: 1.5, OutputPricePerM: 7.5, ContextWindow: 204_800, MaxOutputTokens: 65_536},
	{ID: "qwen3-coder-30b-a3b-instruct", Aliases: []string{"qwen3-coder:30b", "qwen3-coder:30b-a3b"}, Name: "Qwen3 Coder 30B-A3B", InputPricePerM: 0.45, OutputPricePerM: 2.25, ContextWindow: 204_800, MaxOutputTokens: 65_536},
	{ID: "qwen3-235b-a22b-instruct-2507", Aliases: []string{"qwen3-235b-a22b-2507", "qwen3:235b-instruct"}, Name: "Qwen3 235B-A22B Instruct 2507", InputPricePerM: 0.23, OutputPricePerM: 0.92, ContextWindow: 129_000, MaxOutputTokens: 32_768},
	{ID: "qwen3-235b-a22b-thinking-2507", Aliases: []string{"qwen3:235b-thinking"}, Name: "Qwen3 235B-A22B Thinking 2507", InputPricePerM: 0.23, OutputPricePerM: 2.3, ContextWindow: 126_000, MaxOutputTokens: 32_768},
	{ID: "qwen3-vl-235b-a22b-instruct", Aliases: []string{"qwen3-vl:235b"}, Name: "Qwen3 VL 235B-A22B", InputPricePerM: 0.4, OutputPricePerM: 1.6, SupportsImages: true, ContextWindow: 129_000, MaxOutputTokens: 32_768},

	{ID: "kimi-k3", Name: "Kimi K3", InputPricePerM: 3, OutputPricePerM: 15, CacheWritePricePerM: 3, CacheReadPricePerM: 0.3, SupportsImages: true, ContextWindow: 1_048_576, MaxOutputTokens: 131_072, RejectsSampling: true, Efforts: lowHighMaxEfforts, DefaultEffort: EffortMax},
	{ID: "kimi-k2.7-code", Aliases: []string{"kimi-k2p7-code"}, Name: "Kimi K2.7 Code", InputPricePerM: 0.95, OutputPricePerM: 4, CacheReadPricePerM: 0.19, SupportsImages: true, ContextWindow: 262_144, RejectsSampling: true},
	{ID: "kimi-k2.7-code-highspeed", Name: "Kimi K2.7 Code Highspeed", InputPricePerM: 1.9, OutputPricePerM: 8, CacheReadPricePerM: 0.38, SupportsImages: true, ContextWindow: 262_144, RejectsSampling: true},
	{ID: "kimi-k2.6", Aliases: []string{"kimi-k2p6"}, Name: "Kimi K2.6", InputPricePerM: 0.95, OutputPricePerM: 4, CacheReadPricePerM: 0.16, SupportsImages: true, ContextWindow: 262_144, RejectsSampling: true, Efforts: onOffEfforts, DefaultEffort: EffortOn},

	{ID: "moonshotai/Kimi-K2.5", Name: "Kimi K2.5", InputPricePerM: 0.45, OutputPricePerM: 2.25, CacheReadPricePerM: 0.07, SupportsImages: true, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "moonshotai/Kimi-K2-Thinking", Name: "Kimi K2 Thinking", InputPricePerM: 0.6, OutputPricePerM: 2.5, CacheReadPricePerM: 0.15, ContextWindow: 262_144},
	{ID: "moonshotai/Kimi-K2-Instruct-0905", Aliases: []string{"kimi-k2-0905", "kimi-k2-0905-preview"}, Name: "Kimi K2 0905", InputPricePerM: 0.6, OutputPricePerM: 2.5, ContextWindow: 262_144},
	{ID: "moonshotai/Kimi-K2-Instruct", Aliases: []string{"kimi-k2"}, Name: "Kimi K2", InputPricePerM: 0.57, OutputPricePerM: 2.3, ContextWindow: 131_072},

	{ID: "glm-5.3", Aliases: []string{"glm-5p3"}, Name: "GLM-5.3", InputPricePerM: 1.4, OutputPricePerM: 4.4, CacheReadPricePerM: 0.26, ContextWindow: 1_000_000, MaxOutputTokens: 131_072, Efforts: lowHighMaxEfforts, DefaultEffort: EffortMax},
	{ID: "glm-5.3-flash", Aliases: []string{"glm-5p3-flash", "ogx-glm-5.3-flash"}, Name: "GLM-5.3 Flash", InputPricePerM: 0.15, OutputPricePerM: 0.5, CacheReadPricePerM: 0.03, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 131_072, Efforts: lowHighMaxEfforts, DefaultEffort: EffortMax},
	{ID: "glm-5.3-flashx", Name: "GLM-5.3 FlashX", InputPricePerM: 0.37, OutputPricePerM: 1.25, CacheReadPricePerM: 0.075, SupportsImages: true, ContextWindow: 1_000_000, MaxOutputTokens: 131_072, Efforts: lowHighMaxEfforts, DefaultEffort: EffortMax},
	{ID: "glm-5.2", Aliases: []string{"glm-5p2"}, Name: "GLM-5.2", InputPricePerM: 1.4, OutputPricePerM: 4.4, CacheReadPricePerM: 0.26, ContextWindow: 1_000_000, MaxOutputTokens: 131_072, Efforts: noneHighMaxEfforts, DefaultEffort: EffortMax},
	{ID: "glm-5.1", Name: "GLM-5.1", InputPricePerM: 1.4, OutputPricePerM: 4.4, CacheReadPricePerM: 0.26, ContextWindow: 202_752, MaxOutputTokens: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "glm-5", Name: "GLM-5", InputPricePerM: 1, OutputPricePerM: 3.2, CacheReadPricePerM: 0.2, ContextWindow: 202_752, MaxOutputTokens: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "glm-4.7", Name: "GLM-4.7", InputPricePerM: 0.6, OutputPricePerM: 2.2, CacheReadPricePerM: 0.11, ContextWindow: 202_752, MaxOutputTokens: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortOn},

	{ID: "glm-4.7-flash", Name: "GLM-4.7 Flash", ContextWindow: 200_000, MaxOutputTokens: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "glm-4.6", Name: "GLM-4.6", InputPricePerM: 0.6, OutputPricePerM: 2.2, CacheReadPricePerM: 0.11, ContextWindow: 202_752, MaxOutputTokens: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "glm-4.5", Name: "GLM-4.5", InputPricePerM: 0.6, OutputPricePerM: 2.2, CacheReadPricePerM: 0.11, ContextWindow: 131_072, MaxOutputTokens: 98_304, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "glm-4.5-air", Name: "GLM-4.5 Air", InputPricePerM: 0.2, OutputPricePerM: 1.1, CacheReadPricePerM: 0.03, ContextWindow: 131_072, MaxOutputTokens: 98_304, Efforts: onOffEfforts, DefaultEffort: EffortOn},

	{ID: "MiniMax-M3", Name: "MiniMax M3", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheReadPricePerM: 0.06, SupportsImages: true, ContextWindow: 524_288, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "MiniMax-M2.7", Name: "MiniMax M2.7", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheWritePricePerM: 0.375, CacheReadPricePerM: 0.06, ContextWindow: 196_608},
	{ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 Highspeed", InputPricePerM: 0.6, OutputPricePerM: 2.4, CacheWritePricePerM: 0.375, CacheReadPricePerM: 0.06, ContextWindow: 196_608},
	{ID: "MiniMax-M2.5", Name: "MiniMax M2.5", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheWritePricePerM: 0.375, CacheReadPricePerM: 0.03, ContextWindow: 196_608},
	{ID: "MiniMax-M2.1", Name: "MiniMax M2.1", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheWritePricePerM: 0.375, CacheReadPricePerM: 0.03, ContextWindow: 196_608},
	{ID: "MiniMax-M2", Name: "MiniMax M2", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheWritePricePerM: 0.375, CacheReadPricePerM: 0.03, ContextWindow: 196_608, MaxOutputTokens: 131_072},
	{ID: "MiniMaxAI/MiniMax-M1-80k", Aliases: []string{"minimax-m1", "MiniMaxAI/MiniMax-M1-40k"}, Name: "MiniMax M1", InputPricePerM: 0.4, OutputPricePerM: 2.2, ContextWindow: 1_000_000},

	{ID: "meta-models/Muse-Glimmer-30B", Aliases: []string{"muse-glimmer"}, Name: "Muse Glimmer 30B", InputPricePerM: 0.3, OutputPricePerM: 1.2, CacheReadPricePerM: 0.04, SupportsImages: true, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-4-Maverick-17B-128E-Instruct", Aliases: []string{"llama-4-maverick", "llama4:maverick", "llama4:128x17b", "llama4-maverick-instruct-basic"}, Name: "Llama 4 Maverick", InputPricePerM: 0.1875, OutputPricePerM: 0.6525, SupportsImages: true, ContextWindow: 1_048_576},
	{ID: "meta-llama/Llama-4-Scout-17B-16E-Instruct", Aliases: []string{"llama-4-scout", "llama4:scout", "llama4:16x17b", "llama4", "llama4-scout-instruct-basic"}, Name: "Llama 4 Scout", InputPricePerM: 0.1, OutputPricePerM: 0.3, SupportsImages: true, ContextWindow: 1_048_576},
	{ID: "meta-llama/Llama-3.3-70B-Instruct", Aliases: []string{"llama3.3:70b", "llama3.3", "llama-3.3-70b-versatile", "meta-llama/Llama-3.3-70B-Instruct-Turbo", "llama-v3p3-70b-instruct"}, Name: "Llama 3.3 70B", InputPricePerM: 0.1, OutputPricePerM: 0.32, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.1-405B-Instruct", Aliases: []string{"llama3.1:405b", "llama-v3p1-405b-instruct"}, Name: "Llama 3.1 405B", ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.1-70B-Instruct", Aliases: []string{"Meta-Llama-3.1-70B-Instruct", "llama3.1:70b", "llama-v3p1-70b-instruct"}, Name: "Llama 3.1 70B", InputPricePerM: 0.4, OutputPricePerM: 0.4, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.1-8B-Instruct", Aliases: []string{"Meta-Llama-3.1-8B-Instruct", "llama3.1:8b", "llama3.1", "llama-3.1-8b-instant", "llama-v3p1-8b-instruct"}, Name: "Llama 3.1 8B", InputPricePerM: 0.05, OutputPricePerM: 0.08, CacheReadPricePerM: 0.025, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.2-3B-Instruct", Aliases: []string{"llama3.2:3b", "llama3.2"}, Name: "Llama 3.2 3B", InputPricePerM: 0.05, OutputPricePerM: 0.33, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.2-1B-Instruct", Aliases: []string{"llama3.2:1b"}, Name: "Llama 3.2 1B", InputPricePerM: 0.027, OutputPricePerM: 0.201, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.2-11B-Vision-Instruct", Aliases: []string{"llama3.2-vision:11b", "llama3.2-vision"}, Name: "Llama 3.2 11B Vision", SupportsImages: true, ContextWindow: 131_072},
	{ID: "meta-llama/Llama-3.2-90B-Vision-Instruct", Aliases: []string{"llama3.2-vision:90b"}, Name: "Llama 3.2 90B Vision", SupportsImages: true, ContextWindow: 131_072},

	{ID: "mistral-medium-3-5", Aliases: []string{"mistralai/Mistral-Medium-3.5-128B", "mistral-medium-3.5:128b"}, Name: "Mistral Medium 3.5", InputPricePerM: 1.5, OutputPricePerM: 7.5, CacheReadPricePerM: 0.15, SupportsImages: true, ContextWindow: 262_144, Efforts: onOffEfforts},
	{ID: "mistral-small-2603", Aliases: []string{"mistralai/Mistral-Small-4-119B-2603"}, Name: "Mistral Small 4", InputPricePerM: 0.15, OutputPricePerM: 0.6, CacheReadPricePerM: 0.015, SupportsImages: true, ContextWindow: 262_144, Efforts: onOffEfforts},
	{ID: "mistral-large-2512", Aliases: []string{"mistralai/Mistral-Large-3-675B-Instruct-2512", "mistral-large-3:675b"}, Name: "Mistral Large 3", InputPricePerM: 0.5, OutputPricePerM: 1.5, CacheReadPricePerM: 0.05, SupportsImages: true, ContextWindow: 262_144},
	{ID: "ministral-14b-2512", Aliases: []string{"mistralai/Ministral-3-14B-Instruct-2512", "ministral-3:14b"}, Name: "Ministral 3 14B", InputPricePerM: 0.2, OutputPricePerM: 0.2, CacheReadPricePerM: 0.02, SupportsImages: true, ContextWindow: 262_144},
	{ID: "ministral-8b-2512", Aliases: []string{"mistralai/Ministral-3-8B-Instruct-2512", "ministral-3:8b", "ministral-3"}, Name: "Ministral 3 8B", InputPricePerM: 0.15, OutputPricePerM: 0.15, CacheReadPricePerM: 0.015, SupportsImages: true, ContextWindow: 262_144},

	{ID: "ministral-3b-2512", Aliases: []string{"mistralai/Ministral-3-3B-Instruct-2512", "ministral-3:3b"}, Name: "Ministral 3 3B", InputPricePerM: 0.1, OutputPricePerM: 0.1, CacheReadPricePerM: 0.01, SupportsImages: true, ContextWindow: 131_072},
	{ID: "devstral-2512", Aliases: []string{"mistralai/Devstral-2-123B-Instruct-2512", "devstral-2:123b", "devstral-2"}, Name: "Devstral 2", InputPricePerM: 0.4, OutputPricePerM: 2, CacheReadPricePerM: 0.04, ContextWindow: 262_144},
	{ID: "mistralai/Devstral-Small-2-24B-Instruct-2512", Aliases: []string{"labs-devstral-small-2512", "devstral-small-2:24b", "devstral-small-2"}, Name: "Devstral Small 2", SupportsImages: true, ContextWindow: 262_144},
	{ID: "mistralai/Devstral-Small-2507", Aliases: []string{"devstral:24b", "devstral"}, Name: "Devstral Small 1.1", ContextWindow: 131_072},

	{ID: "mistralai/Magistral-Small-2509", Name: "Magistral Small 1.2", SupportsImages: true, ContextWindow: 131_072},
	{ID: "mistralai/Mistral-Small-3.2-24B-Instruct-2506", Aliases: []string{"mistral-small-2506", "mistral-small-3.2-24b-instruct", "mistral-small3.2:24b", "mistral-small3.2"}, Name: "Mistral Small 3.2", InputPricePerM: 0.09375, OutputPricePerM: 0.25, SupportsImages: true, ContextWindow: 131_072},

	{ID: "gemma-4-31b-it", Aliases: []string{"gemma4:31b"}, Name: "Gemma 4 31B", InputPricePerM: 0.09, OutputPricePerM: 0.34, CacheReadPricePerM: 0.05, SupportsImages: true, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortNone},
	{ID: "gemma-4-26b-a4b-it", Aliases: []string{"gemma4:26b"}, Name: "Gemma 4 26B-A4B", InputPricePerM: 0.09, OutputPricePerM: 0.3, CacheReadPricePerM: 0.05, SupportsImages: true, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortNone},
	{ID: "google/gemma-4-12B-it", Aliases: []string{"gemma4:12b"}, Name: "Gemma 4 12B", SupportsImages: true, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortNone},
	{ID: "google/gemma-4-E4B-it", Aliases: []string{"gemma4:e4b", "gemma4"}, Name: "Gemma 4 E4B", SupportsImages: true, ContextWindow: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortNone},
	{ID: "google/gemma-4-E2B-it", Aliases: []string{"gemma4:e2b"}, Name: "Gemma 4 E2B", SupportsImages: true, ContextWindow: 131_072, Efforts: onOffEfforts, DefaultEffort: EffortNone},
	{ID: "google/gemma-3-27b-it", Aliases: []string{"gemma3:27b"}, Name: "Gemma 3 27B", InputPricePerM: 0.08, OutputPricePerM: 0.45, CacheReadPricePerM: 0.04, SupportsImages: true, ContextWindow: 131_072},
	{ID: "google/gemma-3-12b-it", Aliases: []string{"gemma3:12b"}, Name: "Gemma 3 12B", InputPricePerM: 0.05, OutputPricePerM: 0.15, SupportsImages: true, ContextWindow: 131_072},
	{ID: "google/gemma-3-4b-it", Aliases: []string{"gemma3:4b", "gemma3"}, Name: "Gemma 3 4B", InputPricePerM: 0.05, OutputPricePerM: 0.1, SupportsImages: true, ContextWindow: 131_072},
	{ID: "google/gemma-3-1b-it", Aliases: []string{"gemma3:1b"}, Name: "Gemma 3 1B", ContextWindow: 32_768},

	{ID: "google/gemma-3n-E4B-it", Aliases: []string{"gemma3n:e4b", "gemma3n"}, Name: "Gemma 3n E4B", ContextWindow: 32_768},
	{ID: "google/gemma-3n-E2B-it", Aliases: []string{"gemma3n:e2b"}, Name: "Gemma 3n E2B", ContextWindow: 32_768},

	{ID: "gpt-oss-120b", Aliases: []string{"gpt-oss:120b"}, Name: "gpt-oss-120b", InputPricePerM: 0.15, OutputPricePerM: 0.6, CacheReadPricePerM: 0.075, ContextWindow: 131_072, MaxOutputTokens: 131_072, Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},
	{ID: "gpt-oss-20b", Aliases: []string{"gpt-oss:20b", "gpt-oss"}, Name: "gpt-oss-20b", InputPricePerM: 0.018, OutputPricePerM: 0.09, ContextWindow: 131_072, MaxOutputTokens: 131_072, Efforts: lowToHighEfforts, DefaultEffort: EffortMedium},

	{ID: "nvidia/nemotron-3-ultra-550b-a55b", Aliases: []string{"nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", "nemotron-3-ultra"}, Name: "Nemotron 3 Ultra", InputPricePerM: 0.6, OutputPricePerM: 2.4, CacheReadPricePerM: 0.12, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "nvidia/nemotron-3-super-120b-a12b", Aliases: []string{"nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16", "nemotron-3-super:120b", "nemotron-3-super"}, Name: "Nemotron 3 Super", InputPricePerM: 0.08, OutputPricePerM: 0.45, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "nvidia/nemotron-3-nano-30b-a3b", Aliases: []string{"nvidia/nemotron-nano-3-30b-a3b", "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16", "nemotron-3-nano:30b", "nemotron-3-nano"}, Name: "Nemotron 3 Nano 30B", InputPricePerM: 0.05, OutputPricePerM: 0.2, CacheReadPricePerM: 0.03, ContextWindow: 262_144, Efforts: onOffEfforts, DefaultEffort: EffortOn},
	{ID: "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16", Aliases: []string{"nemotron-3-nano:4b"}, Name: "Nemotron 3 Nano 4B", ContextWindow: 262_144},
	{ID: "nvidia/nemotron-3.5-lightning-30b-a3b", Aliases: []string{"nemotron-3.5-lightning", "nemotron-3.5-lightning:30b"}, Name: "Nemotron 3.5 Lightning", InputPricePerM: 0.08, OutputPricePerM: 0.2, CacheReadPricePerM: 0.04, ContextWindow: 262_144},
	{ID: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", Aliases: []string{"nemotron3:33b"}, Name: "Nemotron 3 Nano Omni", SupportsImages: true, ContextWindow: 131_072},

	{ID: "microsoft/phi-4", Aliases: []string{"phi4:14b", "phi4"}, Name: "Phi-4", InputPricePerM: 0.125, OutputPricePerM: 0.5, ContextWindow: 16_384, MaxOutputTokens: 16_384},
	{ID: "microsoft/Phi-4-reasoning", Aliases: []string{"phi4-reasoning:14b", "phi4-reasoning"}, Name: "Phi-4 Reasoning", InputPricePerM: 0.125, OutputPricePerM: 0.5, ContextWindow: 32_768, MaxOutputTokens: 32_768},
	{ID: "microsoft/Phi-4-reasoning-plus", Aliases: []string{"phi4-reasoning:plus"}, Name: "Phi-4 Reasoning Plus", InputPricePerM: 0.125, OutputPricePerM: 0.5, ContextWindow: 32_768},
	{ID: "microsoft/Phi-4-mini-instruct", Aliases: []string{"phi4-mini:3.8b", "phi4-mini"}, Name: "Phi-4 Mini", InputPricePerM: 0.075, OutputPricePerM: 0.3, ContextWindow: 131_072},
	{ID: "microsoft/Phi-4-multimodal-instruct", Name: "Phi-4 Multimodal", InputPricePerM: 0.08, OutputPricePerM: 0.32, SupportsImages: true, ContextWindow: 131_072},
	{ID: "microsoft/Phi-4-mini-reasoning", Aliases: []string{"phi4-mini-reasoning:3.8b", "phi4-mini-reasoning"}, Name: "Phi-4 Mini Reasoning", InputPricePerM: 0.075, OutputPricePerM: 0.3, ContextWindow: 128_000, MaxOutputTokens: 128_000},
	{ID: "microsoft/Phi-4-reasoning-vision-15B", Name: "Phi-4 Reasoning Vision", SupportsImages: true, ContextWindow: 16_384},
}

OpenModels is the catalogue of major open-weight model families. No provider lists these on its own; they are reached through whichever host serves them (OpenRouter, Ollama, Groq, OGX, Together, Fireworks, a custom endpoint), so each entry carries the ids those hosts use as Aliases — Hugging Face repos, OpenRouter slugs, Ollama tags (":latest" names the size Ollama defaults to), Groq and Fireworks ids. An alias that means different models on different hosts is left out rather than guessed.

Sources: each vendor's API docs and pricing pages, Hugging Face model cards and configs, OpenRouter's /models and /endpoints, Ollama's library and ollama.com/api/tags, Groq, Together and Fireworks docs — verified 2026-09-28. Effort ladders: the vendors' thinking guides (DeepSeek, Kimi, Z.ai, MiniMax, Qwen Cloud, Mistral), the model cards, and ollama.com/api/show's thinking metadata — verified 2026-10-07. They record what the model takes; a host can name the levels differently (Z.ai defaults GLM-5.2 to max, Ollama's cloud to high), which is why hosts that report their own levels override them. A model that always thinks at one depth (MiniMax M2, Kimi K2.7 Code, R1) or never thinks has no ladder: there is nothing to choose.

Prices are the vendor's own API where it serves the model (DeepSeek, Alibaba Qwen Cloud, Moonshot, Z.ai, MiniMax, Mistral, Azure for Phi), else the model's OpenRouter price; 0 where neither prices it. A host that lists its own price (OpenRouter) is billed at that, not at these (see PriceOn).

ContextWindow is the most a request can hold where the model is commonly served, biased low where hosts disagree: a larger host still works, just compacts early, and a host that serves less corrects it on its first overflow (see EffectiveContextWindow). MaxOutputTokens is the vendor's published ceiling, informational only — no provider lists these entries.

View Source
var PrimaryOllamaBaseURL = DefaultOllamaBaseURL

PrimaryOllamaBaseURL is the first candidate probed. A package-level var rather than a constant so tests can point it at a dead address and exercise the fallback path deterministically.

View Source
var ProviderPriority = []string{
	"anthropic", "openai", "openrouter", "ollama", "ogx",
}

ProviderPriority is the stable order used to choose a default provider when a session does not specify a model.

A user-configured first-party provider always wins.

"ogx" sits with the first-party providers: a connected subscription is a deliberate user choice. It ranks below ollama because a local daemon costs the user nothing per token, while OGX draws on a metered plan.

Functions

func AsIndexSession added in v0.40.0

func AsIndexSession(ctx context.Context) context.Context

AsIndexSession marks ctx as belonging to a background project-index run. Every provider request made under it takes the index's lane of the in-flight budget, which yields to the user's own work. The indexer applies it to the context it runs each batch under; no other caller should.

func BeginTurn added in v0.41.0

func BeginTurn(ctx context.Context) func()

BeginTurn tells the in-flight budget that an agent turn is running, so the background index keeps to its narrow share until the returned function is called and the quiet period after it has passed. A turn run under AsIndexSession is the index's own work and is not counted. A nested turn — a sub-agent inside a turn — simply counts twice.

func BillsPerToken added in v0.40.0

func BillsPerToken(providerID string) bool

BillsPerToken is billsPerToken for callers that must tell an unmetered provider (nothing to bill) from a metered one whose model has no known price.

func CheckOGXPlan added in v0.41.0

func CheckOGXPlan(ctx context.Context, token string) (int, error)

CheckOGXPlan asks the gateway, now, how many models the linked plan grants. It is the one live answer to "does this account hold a plan": the connect flow records the plan once, and a plan bought, lapsed or cancelled after that is only visible here. Unlike RefreshCatalog it keeps "the plan grants nothing" (0, nil) apart from "the gateway could not be asked" (an error), because the settings screen acts on the first and must not on the second.

A 401 is the token's fault only when the gateway says so: it also answers 401 to a build that cannot sign its requests, and that is not something a reconnect would fix.

func CollectionForBaseURL added in v0.53.1

func CollectionForBaseURL(baseURL string) string

CollectionForBaseURL exposes collectionFromBaseURL to callers outside the package — the DB-driven vendor slots want the same grouping label this package would derive for an endpoint, so the models they register carry the name the URL implies rather than a second spelling of it.

func EchoesReasoning added in v0.46.0

func EchoesReasoning(p Provider) bool

EchoesReasoning reports whether p sends an assistant message's reasoning back. The caller fills ModelMessage.ReasoningText only then, so text a provider would drop is neither carried nor counted toward the request's size.

func EchoesToolExtra added in v0.47.2

func EchoesToolExtra(p Provider) bool

EchoesToolExtra reports whether p needs an assistant tool call's own payload replayed. The stream reader captures it only then, so an opaque blob a provider would ignore is neither carried nor stored.

func EndpointHost added in v0.40.0

func EndpointHost(p Provider) string

EndpointHost is the host a provider sends its requests to when that says more than the provider's name — the OpenAI slot pointed at Z.ai, the Anthropic slot at DeepSeek's compatible API — and "" when it does not: a provider on its default endpoint, OGX, and an Ollama on this machine (or on ollama.com), whose name already says where it runs.

func IsContextLengthMessage added in v0.21.0

func IsContextLengthMessage(s string) bool

IsContextLengthMessage reports whether a message/body indicates the prompt exceeded the model's context window. Shared by APIError.IsContextLength and the loop's string-matching fallback so the two never drift. The hints must stay a superset of the phrasings contextWindowPatterns matches — the loop learns a window from exactly the bodies this function classifies, so a pattern whose sample body is not classified is dead code (pinned by TestParseContextWindowFromBody_PhrasingsClassifyAsOverflow).

func IsEffort added in v0.46.0

func IsEffort(level string) bool

IsEffort reports whether level is one of ogcode's effort levels.

func IsImageRejectionMessage added in v0.29.0

func IsImageRejectionMessage(s string) bool

IsImageRejectionMessage reports whether a message/body indicates the model does not accept image input. Shared by APIError.IsImageRejection, the probe's classifyProbeError, and the loop's string-matching fallback so they never drift.

func LoadPersistedUpstreamCatalog added in v0.55.0

func LoadPersistedUpstreamCatalog() error

LoadPersistedUpstreamCatalog re-installs the last synced upstream catalogue from the persisted copy under the ogcode config dir — the boot path. A missing file is a clean no-op (nothing was ever synced); a corrupt one is an error and installs nothing, so garbage never reaches the catalogue.

func OGXGatewayURL added in v0.38.0

func OGXGatewayURL() string

OGXGatewayURL returns the gateway base URL to use, honouring OGX_GATEWAY_URL.

func OllamaBinaryInstalled added in v0.16.0

func OllamaBinaryInstalled() bool

OllamaBinaryInstalled reports whether the `ollama` executable is on $PATH. Uses exec.LookPath (cross-platform) rather than probing a fixed set of install directories.

func OllamaRunning added in v0.16.0

func OllamaRunning(baseURL string) bool

OllamaRunning probes the Ollama server at the given base URL (or the default localhost endpoint when empty) with a short timeout. Returns true when the server responds with HTTP 200. The probe is best-effort: any transport error or non-200 status is treated as "not running".

func ParseContextWindowFromBody added in v0.35.0

func ParseContextWindowFromBody(body string) int

ParseContextWindowFromBody extracts the model's context window from a context-overflow error body — the one message a provider reliably states the figure in ("maximum context length is 8192 tokens", "195000 tokens > 200000 maximum"). Returns 0 when nothing plausible is found; the caller knows the request that just overflowed, so the sanity check that the window exceeds the prompt size lives at the call site, not here. Shared seam: the loop calls this on exactly the errors isContextLengthError classified, so the phrasings below must stay a superset of IsContextLengthMessage's hints.

func PlacesGuidance added in v0.37.0

func PlacesGuidance(p Provider) bool

PlacesGuidance reports whether p positions guidance itself.

func PreferLiveOllamaEndpoint added in v0.26.0

func PreferLiveOllamaEndpoint(configured string, st OllamaStatus) string

PreferLiveOllamaEndpoint resolves which endpoint to actually use when a base URL was configured (a persisted config row) and detection found a different live one.

A configured endpoint wins while it is still answering. A configured endpoint that has gone dead yields to whatever detection found, so a row persisted from an earlier launch cannot permanently shadow a working endpoint — the "local Ollama was uninstalled, but a router is up" case.

An explicit OLLAMA_BASE_URL must never be passed here: it is authoritative by definition and callers should use it directly.

func ProbeImageSupport added in v0.6.0

func ProbeImageSupport(ctx context.Context, p Provider, modelID string) (supports bool, definitive bool, err error)

ProbeImageSupport sends a single minimal image to the model and reports whether it was accepted. Return values:

  • (true, true, nil): the model accepted the image and responded.
  • (false, true, nil): the provider rejected the request for an image/modality reason.
  • (false, false, err): inconclusive (network/auth/rate-limit/etc.) — do NOT cache; retry later.

func ResolveEffort added in v0.46.0

func ResolveEffort(p Provider, model, chosen string) string

ResolveEffort returns the level a run should request: the session's choice when the model takes it on this provider, otherwise "" — the vendor's default. A stored choice can outlive what made it valid (the session moved to a model with other levels, or the endpoint changed under it), and a level the model does not take fails the whole request on most hosts, so it is dropped here rather than sent.

func UpstreamCatalogInfo added in v0.55.0

func UpstreamCatalogInfo() (syncedAt time.Time, source string, modelCount int, ok bool)

UpstreamCatalogInfo reads the persisted copy's meta without touching the live merged layer: when the last sync landed (the stamped sync time, else the file's generatedAt), where it came from, and how many models it carries. ok is false when nothing is persisted, no home is known, or the file does not parse.

func UpstreamCatalogURL added in v0.55.0

func UpstreamCatalogURL() string

UpstreamCatalogURL is the URL SyncUpstreamCatalog fetches the catalogue from: OGCODE_CATALOG_URL when set, else the tracked data file's raw URL.

func ValidateCredentials added in v0.10.0

func ValidateCredentials(ctx context.Context, providerID, apiKey, baseURL string) error

ValidateCredentials makes a minimal chat request with the given credentials to confirm the provider accepts them. It returns nil when the credentials work, or an error describing the failure. Used by the settings/onboarding "test key" flow. The caller is responsible for any timeout via ctx.

Types

type APIError added in v0.21.0

type APIError struct {
	Provider   string
	StatusCode int
	RetryAfter time.Duration // parsed from the Retry-After header; 0 if absent
	Body       string
}

APIError is a structured error returned by a provider when the LLM API responds with a non-2xx status. It carries the HTTP status code and any Retry-After hint so the agent loop can classify the failure and back off precisely instead of sniffing the error string. Error() preserves the historical "<provider> API error <code>: <body>" format, so existing logs and the loop's string-matching fallbacks keep working for non-HTTP (stream/network) errors.

func NewAPIError added in v0.21.0

func NewAPIError(providerID string, resp *http.Response, body string) *APIError

NewAPIError builds an APIError from a non-2xx HTTP response, parsing the Retry-After header. The caller supplies the already-read body.

func (*APIError) Error added in v0.21.0

func (e *APIError) Error() string

func (*APIError) IsContextLength added in v0.21.0

func (e *APIError) IsContextLength() bool

IsContextLength reports whether the error is a context-window overflow. These arrive as 400s whose body mentions the context length. A bare 400 with an empty body counts too, but only from Ollama, which is the one endpoint that answers an overflowing prompt that way. Every other provider explains its 400s, so treating a body-less one as overflow there just mislabels an unrelated rejection — and sends the user off compacting a conversation that was never too big.

func (*APIError) IsImageRejection added in v0.29.0

func (e *APIError) IsImageRejection() bool

IsImageRejection reports whether the error is a 400 caused by the model not accepting image input. The body of such a response mentions image/modality/ vision support. Used by the agent loop to make these failures resumable (resume strips the offending images) instead of fatal, since a non-vision model producing a tool image is a capability mismatch, not a malformed request — switching models or retrying without images fixes it.

func (*APIError) IsMalformedBody added in v0.49.0

func (e *APIError) IsMalformedBody() bool

IsMalformedBody reports the gateway's terse 400 for a body it could not read: over its size cap, or not valid JSON. The body it sends back is exactly {"error":"invalid request body"}, which says nothing actionable on its own.

func (*APIError) IsTransient added in v0.21.0

func (e *APIError) IsTransient() bool

IsTransient reports whether the status is worth retrying: 429 (rate limited), 529 (Anthropic overloaded), or any 5xx.

type AnthropicProvider

type AnthropicProvider struct {
	// contains filtered or unexported fields
}

AnthropicProvider implements Provider for the Anthropic Messages API.

func NewAnthropicProvider

func NewAnthropicProvider() *AnthropicProvider

func (*AnthropicProvider) BaseURL added in v0.40.0

func (p *AnthropicProvider) BaseURL() string

BaseURL is the endpoint requests go to: Anthropic's own API, or any Anthropic-compatible host the slot was pointed at.

func (*AnthropicProvider) EffortSpec added in v0.46.0

func (p *AnthropicProvider) EffortSpec(model string) EffortSpec

EffortSpec reports the effort levels a model takes on this endpoint: the catalogue's ladder for a Claude model, nothing for anything else. An Anthropic-compatible host serving another vendor's model gets no effort, since its levels — if it has any — are not Claude's.

func (*AnthropicProvider) ID

func (p *AnthropicProvider) ID() string

func (*AnthropicProvider) Models

func (p *AnthropicProvider) Models() []ModelInfo

func (*AnthropicProvider) PlacesGuidance added in v0.37.0

func (p *AnthropicProvider) PlacesGuidance() bool

PlacesGuidance reports that this provider positions mid-loop guidance itself.

Anthropic is the clean case for it. Tool results already travel as blocks INSIDE a user message here, so the guidance can be one more block appended to that same message — no new message, so nothing about message roles changes and there is no alternation question to get wrong. The OpenAI-shaped providers carry a tool result as its own role:"tool" message, which cannot hold unrelated text, so they keep the caller's fallback until that is done separately.

func (*AnthropicProvider) StreamChat

func (p *AnthropicProvider) StreamChat(ctx context.Context, req StreamRequest) (<-chan StreamEvent, error)

type CatalogModel added in v0.2.3

type CatalogModel struct {
	ID string `json:"id"`
	// Aliases are other ids for this model on other hosts; see the header.
	Aliases         []string `json:"aliases,omitempty"`
	Name            string   `json:"name,omitempty"`
	ActiveByDefault bool     `json:"activeByDefault"`
	InputPricePerM  float64  `json:"inputPricePerM"`  // USD per 1M input tokens (0 = unknown)
	OutputPricePerM float64  `json:"outputPricePerM"` // USD per 1M output tokens (0 = unknown)
	// CacheReadPricePerM and CacheWritePricePerM price cached input (0 = not
	// published). Anthropic bills both; OpenAI bills writes only on GPT-5.6 and
	// later (earlier models write at the plain input price, recorded as such),
	// and most open-model APIs bill only reads. A model with no cache discount
	// records its input price as the read price.
	CacheReadPricePerM  float64 `json:"cacheReadPricePerM"`
	CacheWritePricePerM float64 `json:"cacheWritePricePerM"`
	SupportsImages      bool    `json:"supportsImages,omitempty"` // whether the model accepts image input
	// ContextWindow is the most a request can hold, in tokens (0 = unknown →
	// byte-size fallback). Where a vendor caps input below the total window —
	// OpenAI's 1.05M models take at most 922K in, its 400K models 272K — the
	// cap is recorded, since that is where a request starts to be rejected.
	ContextWindow int `json:"contextWindow"`
	// MaxOutputTokens is the most output the model produces in one response.
	// 0 means unknown: no explicit limit is sent and the provider's own default
	// applies. NEVER guess this upward — a value above the model's real ceiling
	// makes every request fail, so understate it when a published figure is not
	// at hand. Only Anthropic entries carry a value that is sent: the OpenAI
	// entries stay at 0 (no cap is needed there, and the published figure is in
	// a comment), and no provider lists OpenModels, so their figure is only
	// informational.
	MaxOutputTokens int `json:"maxOutputTokens"`
	// Thinking names the reasoning mode to request for this model, for providers
	// that must ask for it explicitly. "adaptive" is what Claude 4.6 and later
	// accept: the model decides when and how deeply to think, and reasons
	// between tool calls on its own with no beta header.
	//
	// Empty means no thinking configuration is sent. Claude 4.5 and earlier
	// accept only a fixed `budget_tokens` that has to fit inside `max_tokens` —
	// a tradeoff the agent loop does not currently make, since it leaves
	// `max_tokens` at the provider default — and Haiku 4.5 cannot reason
	// between tool calls at all, which is where an agent loop would spend it.
	Thinking string `json:"thinking,omitempty"`
	// RejectsSampling marks models that refuse temperature (and other sampling
	// parameters) outright, thinking or not — Claude Opus 4.7 and later, Kimi's
	// current models. Every provider drops the parameter for them rather than
	// fail the request.
	RejectsSampling bool `json:"rejectsSampling,omitempty"`

	// OpenAI reasoning, which shapes a Chat Completions request (see
	// shapeForOpenAIModel in openai.go). Unset for every other vendor.
	//
	// EffortFloor is the lowest reasoning_effort the model accepts — "none",
	// "minimal" or "low" — and empty for a model that does not reason. A call
	// that does not ask for thinking (every utility call) sends it, so a small
	// output budget is not spent on reasoning. Any effort but "none" means the
	// model is reasoning, and a reasoning model rejects temperature.
	EffortFloor string `json:"effortFloor,omitempty"`
	// ToolsNeedNoReasoning marks a model that takes tools on Chat Completions
	// only with reasoning_effort "none" — GPT-5.4 and later. Tool calls with
	// reasoning need the Responses API, which ogcode does not speak.
	ToolsNeedNoReasoning bool `json:"toolsNeedNoReasoning,omitempty"`
	// ResponsesOnly marks a model whose tool calling needs the Responses API
	// (GPT-6 Astra, the pro and Codex models). The agent always sends tools
	// over Chat Completions, so the OpenAI provider does not offer it; the
	// catalogue still knows its window and price for a host that bridges the
	// two (OpenRouter).
	ResponsesOnly bool `json:"responsesOnly,omitempty"`

	// Efforts are the reasoning-effort levels the model itself accepts, lowest
	// first, in ogcode's vocabulary (see effort.go): graded levels for a model
	// that takes a depth, EffortNone and EffortOn for one that only switches
	// thinking on or off. Empty means the catalogue knows no effort control for
	// it — it may still reason, just not on request. DefaultEffort is the level
	// its vendor applies when none is sent.
	//
	// These are facts about the model. Whether a given endpoint can carry the
	// level is the provider's call (see EffortSpecifier), so a model listed
	// here can still have no picker behind a host that takes no effort field.
	Efforts       []string `json:"efforts,omitempty"`
	DefaultEffort string   `json:"defaultEffort,omitempty"`

	// Collection places a model the upstream layer ADDS (one no compiled-in
	// entry claims) with a provider that lists its own models: "anthropic"
	// extends the Anthropic provider's picker, "openai" the OpenAI one,
	// "google" the Gemini lookups, "open" the cross-provider open-weight
	// catalogue, "legacy" the retired list. Anything else, including empty, is
	// lookup-only: the entry enriches a user-declared model but no picker gains
	// it. Compiled-in rows carry no collection; the upstream data file sets it
	// when it is generated.
	Collection string `json:"collection,omitempty"`
}

CatalogModel is what is known about one model, whichever provider serves it.

func CatalogModelByID added in v0.31.0

func CatalogModelByID(id string) (CatalogModel, bool)

CatalogModelByID finds a model across every catalogue list under any of its names (see LookupCatalogModel). Callers must treat a false return as "unknown", not "free".

func LookupCatalogModel added in v0.40.0

func LookupCatalogModel(id string) (CatalogModel, bool)

LookupCatalogModel finds a model in the catalogue under any of its names, whichever provider serves it — the compiled-in tables, plus the upstream layer's replacements and additions once one is installed. The catalogue is the authority on a known model's context window, pricing and image support; a false return means "not catalogued" — unknown, never free or zero.

type CatalogRefresher added in v0.39.0

type CatalogRefresher interface {
	RefreshCatalog(ctx context.Context) []ModelInfo
}

CatalogRefresher is implemented by providers whose model catalogue is fetched from their endpoint rather than compiled in. RefreshCatalog performs that fetch and installs the result as the provider's cached catalogue, returning the models so the caller can persist them. It is the ONLY path that reaches the network for models — Models() is a pure read, so a refresh can run in the background without blocking anyone reading the picker.

A nil return means there was no live catalogue to install (the fetch failed, or this provider serves a compiled-in list); the caller persists nothing and the provider keeps whatever it already had.

type CatalogSetter added in v0.39.0

type CatalogSetter interface {
	SetCatalog(models []ModelInfo)
}

CatalogSetter is implemented by providers whose in-memory catalogue can be seeded from a persisted copy, so the picker is populated on startup before the first background refresh completes. SeedCatalog is a pure in-memory write and never touches the network.

type ContentPart

type ContentPart struct {
	Type string `json:"type"`
	Text string `json:"text,omitempty"`
}

type CustomModelInfo added in v0.55.0

type CustomModelInfo struct {
	ProviderID      string
	ContextWindow   int
	MaxOutputTokens int
	InputPricePerM  float64
	OutputPricePerM float64
	// Absolute per-million cache-token prices the user records by hand — the
	// gateway's published cache rate on a self-metered plan, for instance. A
	// positive figure wins over the listing's and the catalogue's; 0 means
	// "not configured" and falls back as today.
	CacheReadPricePerM  float64
	CacheWritePricePerM float64
	SupportsImages      bool
}

CustomModelInfo is what the user told us about a model no catalogue knows. Zero values mean "not configured" and fall back to catalogue/provider facts: a window, output ceiling or price of 0 invents nothing, and SupportsImages can only ever force image support ON (a bool cannot tell "no" from "unconfigured", and a catalogue fact outranks silence).

type EffortSpec added in v0.46.0

type EffortSpec struct {
	// Levels are the selectable levels, lowest first. Empty means the model
	// takes no effort here, and the picker is not offered.
	Levels []string `json:"levels,omitempty"`
	// Default is the level the endpoint applies when none is sent — what the
	// picker shows before the user chooses. Empty when it is not known.
	Default string `json:"default,omitempty"`
	// Note says why there is no choice, for a model the user would expect to
	// have one (it reasons, but not on request through this endpoint). Empty
	// when Levels is set or there is nothing worth saying.
	Note string `json:"note,omitempty"`
}

EffortSpec is what a session may pick for one model on one endpoint.

func EffortSpecFor added in v0.46.0

func EffortSpecFor(p Provider, model string) EffortSpec

EffortSpecFor reports what a session may pick for the model on the given provider. A provider that carries no effort, or a model it cannot carry one to, yields an empty spec.

func (EffortSpec) Allows added in v0.46.0

func (s EffortSpec) Allows(level string) bool

Allows reports whether level is one the spec offers.

type EffortSpecifier added in v0.46.0

type EffortSpecifier interface {
	// EffortSpec reports the levels the model takes through this provider.
	EffortSpec(model string) EffortSpec
}

EffortSpecifier is implemented by providers that can carry a reasoning effort to at least some of the models they serve.

type GuidancePlacer added in v0.37.0

type GuidancePlacer interface {
	// PlacesGuidance reports whether this provider consumes
	// StreamRequest.Guidance.
	PlacesGuidance() bool
}

GuidancePlacer is implemented by providers that position StreamRequest.Guidance themselves, at the end of the conversation where it costs no cached prefix. The caller checks for it before falling back to mutating the message history.

type MessageImage added in v0.6.0

type MessageImage struct {
	MediaType string `json:"mediaType"`
	Data      string `json:"data"`
}

MessageImage is an image attached to a message, carried provider-neutrally. Data is base64-encoded image bytes; MediaType is e.g. "image/jpeg".

type ModelInfo

type ModelInfo struct {
	ID              string  `json:"id"`
	Name            string  `json:"name"`
	ProviderID      string  `json:"providerId"`
	Default         bool    `json:"default"`
	ActiveByDefault bool    `json:"activeByDefault"`
	InputPricePerM  float64 `json:"inputPricePerM"`
	OutputPricePerM float64 `json:"outputPricePerM"`
	// CacheReadPricePerM / CacheWritePricePerM are the endpoint's own cached-
	// input prices in USD per 1M tokens (0 = not published, the biller then
	// derives them from the input price).
	CacheReadPricePerM  float64 `json:"cacheReadPricePerM,omitempty"`
	CacheWritePricePerM float64 `json:"cacheWritePricePerM,omitempty"`
	SupportsImages      bool    `json:"supportsImages"`
	// ContextWindow is the model's total context length in tokens (0 = unknown).
	// Used to size the compaction trigger; when 0 the loop falls back to a fixed
	// byte-size heuristic.
	ContextWindow int `json:"contextWindow,omitempty"`
	// MaxOutputTokens is the most output the model will produce in one response
	// (0 = unknown, leave the request's limit to the provider's own default).
	MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
	// Collection is an optional grouping label for dynamically-fetched models
	// from OpenAI-compatible providers (e.g. "DeepSeek", "Gemini") so the UI can
	// group them instead of collapsing everything under the OpenAI provider id.
	Collection string `json:"collection,omitempty"`
	// Efforts and DefaultEffort are the reasoning levels the serving host itself
	// reports for the model, lowest first, in ogcode's vocabulary — Ollama's
	// /api/show thinking metadata, OpenRouter's reasoning object. Empty when the
	// host reports none; the catalogue answers then (see EffortSpecifier).
	Efforts       []string `json:"efforts,omitempty"`
	DefaultEffort string   `json:"defaultEffort,omitempty"`
}

type ModelMessage

type ModelMessage struct {
	Role       string          `json:"role"`
	Content    json.RawMessage `json:"content,omitempty"`
	ToolCalls  json.RawMessage `json:"tool_calls,omitempty"`
	ToolCallID string          `json:"tool_call_id,omitempty"`
	Name       string          `json:"name,omitempty"`
	// Images carries image attachments for a tool-result message. Providers
	// render these per their API: Anthropic embeds them in the tool_result
	// content block; OpenAI-family inject a follow-up user message.
	Images []MessageImage `json:"images,omitempty"`
	// ReasoningParts carries thinking/reasoning blocks from a previous assistant
	// turn. Anthropic requires these to be forwarded back as "thinking" content
	// blocks with their signatures intact; OpenAI-family providers ignore this
	// field.
	ReasoningParts []ReasoningPart `json:"reasoningParts,omitempty"`
	// ReasoningText is the plain text of the reasoning the model produced for
	// this assistant message, for the OpenAI-compatible hosts that take it back
	// unsigned — DeepSeek's reasoning_content, Ollama's reasoning. The agent loop
	// fills it only for a provider that sends it (see ReasoningEchoer).
	ReasoningText string `json:"reasoningText,omitempty"`
}

type ModelPrice added in v0.40.0

type ModelPrice struct {
	Input, Output         float64
	CacheRead, CacheWrite float64
}

ModelPrice is what a model costs per million tokens on one provider. A zero cache price means "not published": callers derive it from the input price.

func ListPrice added in v0.40.0

func ListPrice(modelID string) (ModelPrice, bool)

ListPrice is the model's first-party price from the catalogue, whatever served it: what the same tokens would cost on the vendor's own API. It is the yardstick for a flat plan or a local run, which bill nothing per token, and false when the catalogue does not price the model.

func (ModelPrice) Cost added in v0.40.0

func (p ModelPrice) Cost(input, output, cacheRead, cacheWrite int) float64

Cost prices one set of token counts in USD. Cache traffic is billed at the published cache prices, or at the standard multiples of the input price when the model publishes none. Reasoning is not a parameter: providers bill it inside the output count.

type OllamaStatus added in v0.16.0

type OllamaStatus struct {
	// Installed reports whether the `ollama` binary is found on $PATH (via
	// exec.LookPath — cross-platform, unlike hardcoded install paths).
	Installed bool `json:"installed"`
	// Running reports whether the Ollama server responded to a health probe
	// (GET http://localhost:11434 with a short timeout). This is the reliable
	// signal that the endpoint is actually usable right now.
	Running bool `json:"running"`
	// BaseURL is the detected/expected Ollama base URL. It honours
	// OLLAMA_BASE_URL when set, otherwise defaults to the localhost endpoint.
	BaseURL string `json:"baseUrl"`
}

OllamaStatus describes the runtime detection state of a local Ollama install. It is computed by DetectOllama and surfaced to the frontend so the onboarding gate can treat a running instance as already configured.

func DetectOllama added in v0.16.0

func DetectOllama() OllamaStatus

DetectOllama performs a combined detection: binary presence + liveness probe. This is the single source of truth used by both the server (loadProviderMap / provider config endpoint) and the CLI (index command) so detection logic is never duplicated.

Resolution order:

  1. OLLAMA_BASE_URL, when set, is authoritative — we never probe elsewhere when the user has named a target.
  2. The default local endpoint (localhost:11434).
  3. The fallback candidates, so a machine with no local Ollama install still finds a router or proxy serving remote instances.

type OpenAIProvider

type OpenAIProvider struct {
	// contains filtered or unexported fields
}

OpenAIProvider implements Provider for the OpenAI Chat Completions API. Also used for OpenRouter and Ollama (same API format, different base URL). When configured for an OpenAI-compatible third party (DeepSeek, Gemini, Groq, …) via a custom base URL, the `collection` field tags dynamically-fetched models so the UI can group them instead of collapsing them under "openai".

func NewOGXProvider added in v0.38.0

func NewOGXProvider(token string) (*OpenAIProvider, error)

NewOGXProvider creates the provider for a connected OGX account. The model is left empty on purpose: unlike the other OpenAI-compatible providers there is no useful guess to make, because the gateway is the only thing that knows what the plan grants — Models() resolves a default from the fetched catalogue. The collection is empty too, so the UI groups the models under the provider id ("ogx") rather than inventing a label.

func NewOllamaProvider

func NewOllamaProvider() *OpenAIProvider

NewOllamaProvider creates an OpenAI-compatible provider for Ollama. When OLLAMA_BASE_URL points to a cloud endpoint (not localhost), the model list is fetched dynamically from /v1/models. For local Ollama, a static fallback list is used.

func NewOpenAIProvider

func NewOpenAIProvider() *OpenAIProvider

NewOpenAIProvider creates an OpenAIProvider from the OPENAI_* env vars.

func NewOpenRouterProvider

func NewOpenRouterProvider() *OpenAIProvider

NewOpenRouterProvider creates an OpenAI-compatible provider for OpenRouter.

func (*OpenAIProvider) BaseURL added in v0.16.0

func (p *OpenAIProvider) BaseURL() string

BaseURL returns the API base URL the provider is configured to use. Exposed so callers can compare endpoints without reaching into the unexported field directly.

func (*OpenAIProvider) EchoesReasoning added in v0.46.0

func (p *OpenAIProvider) EchoesReasoning() bool

EchoesReasoning reports whether this endpoint takes reasoning back, so the agent loop attaches it only where it will be sent.

func (*OpenAIProvider) EchoesToolExtra added in v0.47.2

func (p *OpenAIProvider) EchoesToolExtra() bool

EchoesToolExtra reports whether this endpoint attaches a per-tool-call extra that must be replayed, so the stream reader captures it only where it will be sent back and the request builder attaches it only there too.

func (*OpenAIProvider) EffortSpec added in v0.46.0

func (p *OpenAIProvider) EffortSpec(model string) EffortSpec

EffortSpec reports the effort levels a model takes through this endpoint.

func (*OpenAIProvider) ID

func (p *OpenAIProvider) ID() string

func (*OpenAIProvider) Models

func (p *OpenAIProvider) Models() []ModelInfo

Models returns the provider's model catalogue. It is a PURE READ: it never touches the network. The catalogue is populated by RefreshCatalog (the background refresh) or SetCatalog (the persisted copy seeded at startup), and callers get whatever is currently known — the compiled-in fallback until the first refresh lands.

This used to fetch synchronously behind a sync.Once, which made every read block on a 10s network call — the source of the UI freeze. Nothing on a read path may block on a fetch now.

func (*OpenAIProvider) RefreshCatalog added in v0.39.0

func (p *OpenAIProvider) RefreshCatalog(ctx context.Context) []ModelInfo

RefreshCatalog fetches the endpoint's live model list, installs it as the cached catalogue, and returns it so the caller can persist it. It is the ONLY path that talks to the network for models — Models() never does.

A nil return means "no live catalogue": the fetch failed, or this provider has a compiled-in list that needs no fetch. The cached catalogue is left untouched in that case, so a transient failure never wipes a good list — Models() falls back to the compiled-in one. A non-nil (possibly empty) return is a real answer to persist, including an empty plan that grants nothing.

func (*OpenAIProvider) RunsLocally added in v0.40.0

func (p *OpenAIProvider) RunsLocally(modelID string) bool

RunsLocally reports whether modelID runs on this machine: any model a local Ollama instance serves, except its ":cloud" / "-cloud" tags, which the instance forwards to ollama.com (see cloudModelID).

func (*OpenAIProvider) SetCatalog added in v0.39.0

func (p *OpenAIProvider) SetCatalog(models []ModelInfo)

SetCatalog seeds the in-memory catalogue from a persisted copy, so Models() answers with the last known list immediately on startup instead of waiting for a fetch that has not run yet. An empty list is ignored — it must never wipe a live catalogue a refresh has already installed.

func (*OpenAIProvider) StreamChat

func (p *OpenAIProvider) StreamChat(ctx context.Context, req StreamRequest) (<-chan StreamEvent, error)

type Provider

type Provider interface {
	ID() string
	Models() []ModelInfo
	StreamChat(ctx context.Context, req StreamRequest) (<-chan StreamEvent, error)
}

func NewOpenAICompatibleProvider added in v0.51.0

func NewOpenAICompatibleProvider(id, apiKey, baseURL, model, label string) (Provider, error)

NewOpenAICompatibleProvider builds an OpenAI-protocol provider under the id an ogcode.json named it, pointed at an endpoint that speaks that protocol (DeepSeek, Groq, Together, a self-hosted vLLM, …). It is how the config file's extra named providers reach the registry — the four first-party slots keep NewProviderWithConfig. label, when set, is the group the provider's models carry in the picker (the collection); empty falls back to the vendor collectionFromBaseURL recognises from the host, else "".

func NewProviderWithConfig added in v0.2.1

func NewProviderWithConfig(providerID, apiKey, baseURL string) (Provider, error)

NewProviderWithConfig creates a Provider with explicit credentials, used when credentials come from the DB rather than environment variables. providerID must be "anthropic", "openai", "openrouter", or "ollama". Env-var values are used as the base; apiKey and baseURL override them when non-empty.

type ReasoningEchoer added in v0.46.0

type ReasoningEchoer interface {
	// EchoesReasoning reports whether StreamChat sends ReasoningText.
	EchoesReasoning() bool
}

ReasoningEchoer is implemented by providers whose endpoint takes each assistant message's own reasoning back as plain text, from ModelMessage.ReasoningText.

type ReasoningPart added in v0.17.1

type ReasoningPart struct {
	Text      string `json:"text"`
	Signature string `json:"signature,omitempty"`
	// RedactedData is the opaque payload of a redacted_thinking block. When
	// set, the block carries no readable text and must be re-sent as a
	// redacted_thinking block rather than a thinking block.
	RedactedData string `json:"redactedData,omitempty"`
}

ReasoningPart represents a thinking/reasoning block from a model's response. Anthropic models return these with a cryptographic signature that must be forwarded back unchanged on subsequent turns.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func NewRegistry

func NewRegistry() *Registry

func (*Registry) CatalogModel added in v0.40.0

func (r *Registry) CatalogModel(modelID string) (CatalogModel, bool)

CatalogModel returns the built-in catalogue's entry for modelID where the catalogue speaks for it — everywhere but a model running on this machine, whose window and image support are the runtime's, found by asking it (a listing, a probe, an overflow) rather than by reading the model's datasheet.

func (*Registry) ContextWindow added in v0.21.0

func (r *Registry) ContextWindow(modelID string) int

ContextWindow returns the most a request to the model can hold, in tokens, or 0 when unknown. Three sources know it: the window the user configured for the model (one they added by hand or a built-in/fetched row they edited), the built-in catalogue, under any id a host gives the model — including one a user added by hand that no provider lists — and the serving host's own listing (OpenRouter's context_length, Ollama Cloud's /api/show).

A window the user configured is authoritative and returned as given: they are correcting a claim, and listings are frequently heuristic (OpenRouter's 1M for a 200K model, a host serving 512K of a 1M first-party model), so letting one silently cap the edit would make raising it do nothing. Only when they configured nothing do the catalogue and the listing answer, smaller winning. A model running on this machine gets only its listing's figure (see CatalogModel). A window that still does not fit the host self-corrects from observation — agent.EffectiveContextWindow caps with a learned capability. Callers treat 0 as "fall back to a size heuristic".

func (*Registry) CustomModel added in v0.55.0

func (r *Registry) CustomModel(modelID string) (CustomModelInfo, bool)

CustomModel returns what the user configured for a custom model, and false when modelID is not one.

func (*Registry) Default added in v0.10.0

func (r *Registry) Default() Provider

Default returns the highest-priority registered provider, or nil if the registry has no providers.

func (*Registry) DefaultUsable added in v0.33.0

func (r *Registry) DefaultUsable() Provider

DefaultUsable returns the provider a fresh prompt should run on, applying the same priority as Default but refusing to hand back an installed-but-stopped Ollama when anything else is available. A registered ollama provider only means "the binary exists (or a base URL was saved)" — if the daemon is down and OLLAMA_API_KEY is unset, the first prompt would die with connection refused, and ollama's presence in ProviderPriority would otherwise shadow every usable provider behind it.

An ollama provider is considered usable when OLLAMA_API_KEY is set or the daemon answers a probe. Non-ollama providers are always considered usable; unknown ollama implementations are assumed usable, while the concrete *OpenAIProvider is probed. When ollama is the only registered provider it is returned even if unreachable — ollama-only users otherwise lose their only option, and the provider surfaces the real connection error itself.

func (*Registry) EffortSpec added in v0.46.0

func (r *Registry) EffortSpec(providerID, modelID string) EffortSpec

EffortSpec reports what a session may pick for modelID on providerID, resolving the provider the way a run does (see ResolveProviderFor).

func (*Registry) EndpointHost added in v0.40.0

func (r *Registry) EndpointHost(providerID string) string

EndpointHost is EndpointHost for the provider registered as providerID.

func (*Registry) Get

func (r *Registry) Get(id string) Provider

func (*Registry) IsCustomModel added in v0.32.0

func (r *Registry) IsCustomModel(modelID string) bool

IsCustomModel reports whether modelID was registered as a user-added custom model (rather than a built-in catalog or dynamically-fetched model). Custom models are not present in any provider's curated catalog, so capability lookups that trust the catalog must treat them as unknown and probe instead.

func (*Registry) List

func (r *Registry) List() []string

func (*Registry) ListModels

func (r *Registry) ListModels() []ModelInfo

func (*Registry) MaxOutputTokens added in v0.26.1

func (r *Registry) MaxOutputTokens(modelID string) int

MaxOutputTokens returns the model's output ceiling in tokens, or 0 when unknown. Callers treat 0 as "send no explicit limit and let the provider apply its own default" — overstating a ceiling makes every request fail, so unknown must never be guessed upward.

What the user configured for the model wins when set.

func (*Registry) ModelOverride added in v0.55.0

func (r *Registry) ModelOverride(modelID, providerID string) (CustomModelInfo, bool)

ModelOverride returns what the user configured about a listed model on a provider, and false when they configured nothing there.

func (*Registry) ModelSupportsImages added in v0.6.0

func (r *Registry) ModelSupportsImages(modelID string) bool

ModelSupportsImages reports whether the given model accepts image input. A user who told us a model takes images is believed outright — the flag decides whether images are sent at all — and otherwise the built-in catalogue answers for any model it knows, whichever provider serves it (see CatalogModel); failing that the provider's own listing does. Unknown models default to false.

func (*Registry) PriceOn added in v0.40.0

func (r *Registry) PriceOn(providerID, modelID string) (ModelPrice, bool)

PriceOn returns what modelID costs on providerID, and false when there is no per-token price to quote — an unmetered provider, or a model nobody prices.

A price the provider lists itself (OpenRouter's, or Anthropic's and OpenAI's catalogue-backed listings) is what the user pays there and wins. Otherwise the catalogue's first-party price stands in: it is what an OpenAI-compatible endpoint of the model's own vendor charges, and a fair estimate elsewhere. An OpenRouter ":free" variant is free whatever the model costs anywhere else — and a plan that prices its models itself (OGX) takes no stand-in at all: the gateway's published rate, or the user's edit of it, is the only real quote.

A price the user configured for the model wins over all of that, field by field — whether they added the model themselves or edited one the provider or catalogue already prices: a zero input or output price falls back to the lookup below, so a user who knows only one of the two fills in the other. Cache rates behave the same: a positive CacheReadPricePerM or CacheWritePricePerM the user recorded (the gateway's published cache rate on a self-metered plan, say) quotes instead of whatever stood for it before; 0 keeps today's fallback exactly. Nothing is billed on an unmetered provider regardless — billsPerToken and the ":free" rule decide before any user price is read.

func (*Registry) RefreshCatalogs added in v0.39.0

func (r *Registry) RefreshCatalogs(ctx context.Context) map[string][]ModelInfo

RefreshCatalogs fetches the live catalogue from every provider that has one, returning the models keyed by provider id. Providers without a live catalogue (a compiled-in list, or one whose fetch failed) are absent from the result.

This is the single network path for model lists. It is expected to be called from a background goroutine (or the explicit /models/refresh handler), never from a read path — an unreachable endpoint costs up to the fetch timeout here, and nothing reading Models() should pay that.

func (*Registry) Register

func (r *Registry) Register(p Provider)

func (*Registry) RegisterCustomModel

func (r *Registry) RegisterCustomModel(modelID, providerID string)

RegisterCustomModel marks modelID as a user-added model on providerID, with nothing else configured about it — the shape every caller predating CustomModelInfo uses.

func (*Registry) RegisterCustomModelInfo added in v0.55.0

func (r *Registry) RegisterCustomModelInfo(modelID string, info CustomModelInfo)

RegisterCustomModelInfo records a user-added model and what the user told us about it. A registration replaces the whole record: the caller sends the full preference, so a field the user cleared must not survive a stale value.

func (*Registry) ReplaceProviders added in v0.10.0

func (r *Registry) ReplaceProviders(providers map[string]Provider)

ReplaceProviders atomically swaps the set of registered providers. Custom model routing (RegisterCustomModel) is preserved. Used to apply provider credential changes from the settings/onboarding UI without a server restart.

func (*Registry) ResolveProvider

func (r *Registry) ResolveProvider(modelID string) Provider

ResolveProvider resolves a model id with no stored provider preference, the shape every legacy session, plan and task row has (provider is empty there). It is the deterministic fallback described on ResolveProviderFor.

func (*Registry) ResolveProviderFor added in v0.39.0

func (r *Registry) ResolveProviderFor(modelID, providerID string) Provider

ResolveProviderFor resolves which provider serves a model, in this order:

  1. A custom-model registration for the id, which is an explicit user routing decision and therefore wins outright.
  2. The stored providerID, when a session, plan or task recorded one and it is still registered. A recorded provider is what a user chose for this work, so it beats any inference from the model id — and it is the fix for a model served by two providers, where the id alone is ambiguous.
  3. The model id itself, preferring a provider that marks it ActiveByDefault and otherwise the highest-priority provider that lists it (see lookupModel).
  4. The highest-priority registered provider, when the model is unknown.

A providerID that is no longer registered falls through to the model-id walk rather than failing: a removed provider must not make every stored session unresolvable.

func (*Registry) ServingProvider added in v0.40.0

func (r *Registry) ServingProvider(modelID string) Provider

ServingProvider is the provider that serves modelID by the registry's own routing — a custom-model registration, or a provider that lists the model — and nil when none does. Unlike ResolveProviderFor it never falls back to the highest-priority provider: it answers "where did this model run", for work whose provider was never recorded, and a guess there would misstate a bill.

func (*Registry) SetModelOverride added in v0.55.0

func (r *Registry) SetModelOverride(modelID string, info CustomModelInfo)

SetModelOverride records what the user configured about a model a catalogue or a provider already lists — its window, output ceiling, prices, image support — and leaves the model itself alone. Unlike RegisterCustomModelInfo it does not make the model custom: routing, the catalogue's name and the provider's listing keep answering exactly as they did, and a field left at zero falls back to them. The record is keyed by the provider the row was edited on as well as the id (see overrideKey). A record that configures nothing is no override at all, so clearing every field clears the entry.

func (*Registry) UnregisterCustomModel

func (r *Registry) UnregisterCustomModel(modelID string)

type StreamEvent

type StreamEvent struct {
	Type         StreamEventType `json:"type"`
	Text         string          `json:"text,omitempty"`
	Signature    string          `json:"signature,omitempty"`
	RedactedData string          `json:"redactedData,omitempty"`
	ToolCallID   string          `json:"toolCallId,omitempty"`
	ToolName     string          `json:"toolName,omitempty"`
	ToolInput    json.RawMessage `json:"toolInput,omitempty"`
	// ToolExtra is an opaque, per-tool-call payload the host attached to a
	// tool call and requires replayed verbatim when that call is resent from
	// history — Gemini's extra_content (its thought_signature). Captured only
	// for a host that needs it; see openai_tool_extra.go.
	ToolExtra    json.RawMessage `json:"toolExtra,omitempty"`
	FinishReason *string         `json:"finishReason,omitempty"`
	Usage        *TokenUsage     `json:"usage,omitempty"`
	Error        string          `json:"error,omitempty"`
	// Err is the Go error behind Error, carried in-process so a consumer can
	// match on the error's identity (errors.Is/As, down to the kernel errno)
	// instead of on its rendered text. Nil when the failure arrived as a
	// message from the provider rather than as a Go error. Never serialized:
	// the stream reader and the agent loop share a process, and the string is
	// what crosses any boundary beyond it.
	Err error `json:"-"`
}

type StreamEventType

type StreamEventType string
const (
	EventTextDelta     StreamEventType = "text-delta"
	EventToolCallStart StreamEventType = "tool-call-start"
	EventToolCallDelta StreamEventType = "tool-call-delta"
	EventToolCallEnd   StreamEventType = "tool-call-end"
	// EventReasoningStart opens a thinking block. It carries no content: it
	// marks the boundary between one block and the next, so blocks are stored
	// and replayed separately rather than concatenated.
	EventReasoningStart     StreamEventType = "reasoning-start"
	EventReasoning          StreamEventType = "reasoning"
	EventReasoningSignature StreamEventType = "reasoning-signature"
	// EventReasoningRedacted carries a safety-redacted thinking block. It has
	// no readable text — only an opaque payload that must be round-tripped
	// verbatim as a redacted_thinking block, so it is its own event rather
	// than a signature on an empty reasoning block.
	EventReasoningRedacted StreamEventType = "reasoning-redacted"
	EventFinish            StreamEventType = "finish"
	EventUsage             StreamEventType = "usage"
	EventError             StreamEventType = "error"
)

type StreamRequest

type StreamRequest struct {
	Model       string           `json:"model"`
	System      []string         `json:"system"`
	Messages    []ModelMessage   `json:"messages"`
	Tools       []ToolDefinition `json:"tools"`
	Temperature float64          `json:"temperature,omitempty"`
	MaxTokens   int              `json:"maxTokens,omitempty"`
	// Thinking asks for the model's reasoning mode, where the provider and the
	// model support one. Only the agent loop's main request sets it; the short
	// utility calls — titles, compaction, the auto-mode risk gate — leave it
	// off, because none of them is the kind of work reasoning improves.
	//
	// Leaving it off does NOT mean no reasoning happens: a reasoning model may
	// emit its chain of thought anyway, into the same output budget, before it
	// writes an answer. A utility call must therefore size MaxTokens for the
	// thinking plus the answer, or it gets empty content and finish=length. (The
	// auto-mode risk gate was sent 8 tokens on this assumption and silently
	// failed to judge any command; see riskLLMMaxTokens.)
	Thinking bool `json:"thinking,omitempty"`
	// Effort is the reasoning effort to ask for, in ogcode's vocabulary (see
	// effort.go), or "" for the model's default. It applies only with Thinking:
	// the agent loop sets both, from the session's choice, after checking the
	// model takes the level (ResolveEffort). Each provider translates it into
	// its own field and drops a level the model would reject.
	Effort string `json:"effort,omitempty"`
	// StepMechanical marks this request as a mechanical continuation — the
	// previous step of the turn produced tool calls and no final text, so the
	// model already spent its plan and this step only carries the results
	// forward. Only the agent loop sets it. Where a model with no session
	// effort would otherwise run at its endpoint default (flash-class models
	// reason deeply by default), applyEffort holds a mechanical step to the
	// lightest level the model takes.
	StepMechanical bool `json:"stepMechanical,omitempty"`
	// CacheKey identifies the conversation this request belongs to, for
	// providers whose prompt cache is shared across machines and needs a routing
	// hint to land a session's requests on the node holding its prefix. Only the
	// agent loop sets it (to the session id); a provider that has no use for it
	// ignores it. It is a routing hint, never an identity — it must not carry
	// anything about the user.
	CacheKey string `json:"cacheKey,omitempty"`
	// Guidance is text the user sent mid-turn, already formatted and labelled by
	// the caller. It is carried beside Messages rather than folded into them
	// because the best place to put it differs by provider, and the wrong place
	// is expensive: the caller's fallback appends it to the turn's FIRST user
	// message, which is the earliest possible byte to change and therefore
	// discards the whole turn's cached history on the step right after the user
	// steers — the step they are waiting on.
	//
	// A provider that implements GuidancePlacer positions this itself and the
	// caller leaves Messages alone. Everyone else ignores the field and the
	// caller folds the same text into Messages as before.
	Guidance string `json:"guidance,omitempty"`
	// Notices is board traffic pushed into this step — a peer's report, a
	// collision warning — already rendered and labelled by the caller. It rides
	// beside Messages for the same reason Guidance does, and lands after it: the
	// guidance is the user steering, the notices are what arrived since. A
	// provider that places guidance places these too; everyone else gets them
	// appended at the tail of the conversation by the caller.
	Notices string `json:"notices,omitempty"`
}

type TokenUsage

type TokenUsage struct {
	InputTokens      int `json:"inputTokens,omitempty"`
	OutputTokens     int `json:"outputTokens,omitempty"`
	ReasoningTokens  int `json:"reasoningTokens,omitempty"`
	CacheReadTokens  int `json:"cacheReadTokens,omitempty"`
	CacheWriteTokens int `json:"cacheWriteTokens,omitempty"`
}

TokenUsage carries per-message token accounting from a provider. Fields are non-zero where the provider reports them.

type ToolDefinition

type ToolDefinition struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Parameters  json.RawMessage `json:"parameters"`
}

type ToolExtraEchoer added in v0.47.2

type ToolExtraEchoer interface {
	// EchoesToolExtra reports whether StreamChat resends each tool call's extra.
	EchoesToolExtra() bool
}

ToolExtraEchoer is implemented by providers whose endpoint attaches an opaque payload to each tool call of an assistant message and requires that exact payload back when the message is replayed from history.

type UpstreamSyncResult added in v0.55.0

type UpstreamSyncResult struct {
	Installed int
	Updated   int
	Skipped   int
	Source    string
	FetchedAt time.Time
}

UpstreamSyncResult reports what one sync or apply did: Installed is the count of models added to the catalogue, Updated the count of compiled-in entries whose facts were replaced, and Skipped the entries refused at the plausibility guard or as duplicates within the file. Source is where the file came from — the fetch URL for a sync, or the file's own stamped source for an applied persisted file — and FetchedAt is when the file landed here (the persisted sync time, or the file's generatedAt for a raw file).

func ApplyUpstreamCatalog added in v0.55.0

func ApplyUpstreamCatalog(data []byte) (UpstreamSyncResult, error)

ApplyUpstreamCatalog parses, validates, and merges an upstream catalogue file without network or persistence — the test seam, and the path both SyncUpstreamCatalog and LoadPersistedUpstreamCatalog run through.

func SyncUpstreamCatalog added in v0.55.0

func SyncUpstreamCatalog(ctx context.Context) (UpstreamSyncResult, error)

SyncUpstreamCatalog fetches the upstream catalogue and merges it over the compiled-in tables, persisting a stamped copy for the next boot. A disabled kill switch returns ErrUpstreamSyncDisabled without touching anything; a fetch, parse, or persist failure is also a no-op for the merged state — what was merged before stays merged.

Directories

Path Synopsis
Command datagen regenerates internal/provider/data/catalog.json from the compiled-in catalog tables.
Command datagen regenerates internal/provider/data/catalog.json from the compiled-in catalog tables.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL