Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,13 @@ Most use the `openai-chat` adapter with a bearer key; Anthropic-compatible prese
preset, `xiaomi-mimo`, and a token-plan preset, `mimo`. All three default to MiMo V2.6 (`mimo-v2.6-pro`,
or `mimo-v2.6-flash` on `xiaomi-mimo`). Xiaomi retires `mimo-v2.5` and `mimo-v2.5-pro` on 2026-10-21
with no redirect, so switch a saved V2.5 default before then; opencodex does not rewrite it for you.

**Reasoning capabilities.** Model metadata describes three independent capabilities: `supportsReasoning`, `supportsThinkingToggle`, and `supportsReasoningEffort`. An omitted value is unknown; `false` is an explicit declaration. `defaultThinkingEnabled` records the default switch state, while `reasoningEfforts` lists actual strength choices. Per-model declarations live in the provider's `modelCapabilities` map; reasoning axes merge over registry defaults using case-insensitive model IDs, with operator values taking precedence. `/v1/models` exposes the corresponding `supports_reasoning`, `supports_thinking_toggle`, `supports_reasoning_effort`, and `default_thinking_enabled` fields under `capabilities`, without inventing an effort list for fixed-strength models.

Legacy configurations with an explicit empty effort list and no reasoning capability declarations retain `capabilities.supports_reasoning: false` in `/v1/models` discovery. An explicit `supportsReasoning: true` or `supportsThinkingToggle: true` still advertises thinking when the effort list is empty.

Public MiMo V2.6 is one instance: its three models support reasoning and an on/off switch, but no adjustable intensity; thinking defaults to enabled. The wire adapter sends `thinking.type: enabled/disabled`. OCX's Responses ingress also accepts `reasoning.enabled` independently of `reasoning.effort`. For clients that only offer effort enums, Codex catalogs and OpenCode/Kilo exports represent this switch as `none/high`; those values are client compatibility aliases, not MiMo strength levels. Client-owned labels may still say “High.” Fixed reasoning models without a switch get no fabricated control. Xiaomi's [deep-thinking guide](https://mimo.mi.com/docs/en-US/quick-start/usage-guide/other/deep-thinking) describes the public MiMo switch; free, token-plan, third-party routes and older MiMo models keep their existing declarations.

Volcengine Coding Plan and Agent Plan use their native Responses endpoints through `openai-responses`.
During validated Ark Coding Plan tool continuations, replaying the returned Responses `reasoning` item
answered `400 InvalidParameter`, so the Coding Plan preset drops replayed reasoning items before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `unsupportedHostedTools?` | `string[]` | Hosted tool declarations this Responses destination rejects, so they are stripped from `tools`, from client-loaded `additional_tools`, and from `tool_choice` instead of being forwarded and rejected upstream. Use it for an OpenAI-compatible gateway with a narrower capability set — one that accepts plain Responses requests and `function` tools but returns HTTP 400 for hosted `web_search` — so a text-only prompt is not failed by a capability it never needed. Accepts only hosted tool type names (`web_search`, `web_search_preview`, `file_search`, `computer_use_preview`, `computer_use`, `code_interpreter`, `image_generation`, `image_gen`, `mcp`, `tool_search`, `local_shell`, `x_search`); an unrecognized name is rejected rather than silently ignored. Spelling variants of one capability are aliased, so `["web_search"]` also denies `web_search_preview`. A provider cannot both deny a hosted tool here and prefer it in `modelPreferHostedTools`. Xiaomi MiMo Responses destinations (`xiaomimimo.com` and its subdomains, including `api.xiaomimimo.com` and `token-plan-cn.xiaomimimo.com`) automatically strip `web_search` and `web_search_preview` while preserving function tools; no declaration is needed for these hosts. This is independent of `supportsResponsesCustomTools`; set both for a gateway that also rejects native custom tools. `PATCH /api/providers?name=<provider>` accepts an array or `null` to clear it. |
| `reasoningEffortMap?` | `Record<string, string>` | Provider-wide wire aliases for reasoning labels. Map a label to `"__omit__"` to drop the reasoning field from the upstream request entirely: `reasoning_effort` on an OpenAI-compatible wire, and Ollama's native `think` field on the Ollama native adapter (#2356). |
| `modelReasoningEffortMap?` | `Record<string, Record<string, string>>` | Per-model wire aliases for reasoning labels. Map a label to `"__omit__"` to drop the reasoning field from the upstream request entirely. |
| `reasoningWireFormat?` | `"gateway-object"` | For OpenAI-compatible gateways that accept `reasoning: { enabled, effort }` instead of `reasoning_effort`. The ClinePass preset sets this automatically. A provider save that keeps the destination keeps it; see [What a provider save keeps](#what-a-provider-save-keeps). `PATCH` accepts `"gateway-object"` or `null` to clear it. |
| `reasoningWireFormat?` | `"gateway-object"` | For OpenAI-compatible gateways that accept `reasoning: { enabled, effort }` instead of `reasoning_effort`. For custom Responses destinations, this also explicitly permits the OCX `reasoning.enabled` extension; other Responses destinations strip that internal field. Model thinking capability alone does not opt into this wire format. The ClinePass preset sets this automatically. A provider save that keeps the destination keeps it; see [What a provider save keeps](#what-a-provider-save-keeps). `PATCH` accepts `"gateway-object"` or `null` to clear it. |
| `noReasoningModels?` | `string[]` | Models that reject reasoning/thinking parameters. |
| `noTemperatureModels?` | `string[]` | Models that reject caller-specified `temperature`. |
| `noTopPModels?` | `string[]` | Models that reject caller-specified `top_p`. |
Expand Down
30 changes: 25 additions & 5 deletions src/adapters/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import { decodeServerSentEvents } from "../lib/sse-decoder";
import { isLikelyRealAnthropicThinkingSignature } from "../responses/reasoning-envelope";
import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget";
import { isReasoningEffortOmitted, modelRecordValue } from "../reasoning-effort";
import { resolveThinkingEnabled } from "../clients/reasoning-controls";
import { resolveReasoningCapabilities } from "../providers/reasoning-capabilities";
import { declaresThinkingToggleWire, reasoningControlsAreUnsupported } from "./reasoning-controls";
import { applyAgentRouterLanguageFraming, isAgentRouterEndpoint } from "./agentrouter";
import { anthropicCacheWrite1hTokens, resolveAnthropicCacheRetention, type AnthropicCacheRetention } from "./anthropic/cache-retention";
import { claudeFamilyVersion, rejectsCombinedSampling, rejectsForcedToolChoice, rejectsSamplingParameters, supportsExplicitThinkingDisable, usesAdaptiveThinking, usesBetweenToolsFloor } from "./anthropic-model-contract";
Expand Down Expand Up @@ -1082,16 +1085,33 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
// model both defaults to thinking and accepts being told not to.
const family = claudeFamilyVersion(parsed.modelId);
progressUpdates = usesNativeAnthropicEndpoint(provider) && family?.family === "opus" && family.major === 5 && family.minor === 5;
const effectiveReasoning = parsed.options.reasoning ?? defaultReasoningEffort(provider, parsed.modelId);
if (effectiveReasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
const thinkingToggleWire = declaresThinkingToggleWire(provider, parsed.modelId);
const reasoningControlsUnsupported = reasoningControlsAreUnsupported(provider, parsed.modelId);
const capabilities = resolveReasoningCapabilities(provider, parsed.modelId);
const thinkingEnabled = thinkingToggleWire
? resolveThinkingEnabled(provider, parsed.modelId, parsed.options)
: undefined;
const nativeThinkingContract = usesAdaptiveThinking(parsed.modelId)
|| family?.family === "haiku" || family?.family === "sonnet" || family?.family === "opus";
const supportsReasoningEffort = capabilities.supportsReasoningEffort === true;
const requestedReasoning = parsed.options.reasoning ?? defaultReasoningEffort(provider, parsed.modelId);
// Native Claude wire constraints outrank a generic toggle declaration. Decode the boolean
// into native effort intent, then let the family contract choose budget/adaptive/off shapes.
const effectiveReasoning = nativeThinkingContract && thinkingEnabled !== undefined
? thinkingEnabled ? (requestedReasoning && requestedReasoning !== "none" ? requestedReasoning : "high") : "none"
: requestedReasoning;
if (!reasoningControlsUnsupported && thinkingEnabled !== undefined && !nativeThinkingContract) {
body.thinking = { type: thinkingEnabled ? "enabled" : "disabled" };
}
if (!reasoningControlsUnsupported && (thinkingEnabled === undefined || nativeThinkingContract) && effectiveReasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) {
body.thinking = { type: "disabled" };
} else if (effectiveReasoning === "none" && usesBetweenToolsFloor(parsed.modelId)) {
} else if (!reasoningControlsUnsupported && (thinkingEnabled === undefined || nativeThinkingContract) && effectiveReasoning === "none" && usesBetweenToolsFloor(parsed.modelId)) {
// Sonnet 5.5 rejects `disabled`; `between_tools` is its lowest setting and turns off up-front
// thinking. No effort is sent: the API default (high) is inside the range it accepts.
body.thinking = { type: "between_tools" };
delete body.temperature;
delete body.top_p;
} else if (typeof effectiveReasoning === "string" && effectiveReasoning !== "none") {
} else if (!reasoningControlsUnsupported && thinkingEnabled !== false && (thinkingEnabled === undefined || supportsReasoningEffort || nativeThinkingContract) && typeof effectiveReasoning === "string" && effectiveReasoning !== "none") {
if (usesAdaptiveThinking(parsed.modelId)) {
// Adaptive-thinking models replace the token budget with an effort knob and reject
// `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible
Expand Down Expand Up @@ -1148,7 +1168,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
delete body.top_p;
}

if (progressUpdates) body.thinking = { type: "adaptive", display: "updates" };
if (progressUpdates && !reasoningControlsUnsupported && thinkingEnabled !== false) body.thinking = { type: "adaptive", display: "updates" };

if (rejectsSamplingParameters(parsed.modelId)) {
// Opus 4.7+, Sonnet 5+, Haiku 5.5+ and Fable 400 on non-default sampling, with or without
Expand Down
9 changes: 4 additions & 5 deletions src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { withRequiredChatToolResult } from "./openai-chat/required-tool-result";
import { hasShrinkableOpenAIChatImages, normalizeOpenAIChatImages } from "./openai-chat-images";
import { protectGlmSummaryBudget, resolveMaxTokens } from "./openai-chat/summary-budget";
import { chatParallelToolCallsWireValue } from "./openai-chat/parallel-tool-calls";
import { applyExplicitChatReasoningWirePolicy } from "./openai-chat/reasoning-wire";
import { applyExplicitChatReasoningWirePolicy, declaresIndependentChatReasoningEffort } from "./openai-chat/reasoning-wire";
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../types";
import { modelInList } from "../types";
import { modelInList, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage } from "../types";
import { mapReasoningEffort } from "../reasoning-effort";
import { debugProviderDiagnostic } from "../lib/debug";
import { sseFieldValue } from "../lib/sse-decoder";
Expand Down Expand Up @@ -153,7 +152,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
const explicitReasoning = applyExplicitChatReasoningWirePolicy({
provider,
modelId: parsed.modelId,
hasTools: !!tools,
hasTools: !!tools, thinkingEnabled: parsed.options.thinkingEnabled,
requestedEffort,
wireEffort: reasoningEffort,
reasoningDisabled,
Expand All @@ -173,7 +172,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
wireValue: budget,
};
}
} else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
} else if (modelInList(provider.thinkingToggleModels, parsed.modelId) && !declaresIndependentChatReasoningEffort(provider, parsed.modelId)) {
if (reasoningEffort === "enabled" || reasoningEffort === "disabled" || reasoningEffort === "adaptive") {
body.thinking = { type: reasoningEffort };
reasoningLog = {
Expand Down
35 changes: 29 additions & 6 deletions src/adapters/openai-chat/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { applyGithubCopilotContextTier } from "../../providers/github-copilot-co
import { canonicalFastTierMarker, decideTier, type ResolvedFastPolicy } from "../../providers/fastwire";
import { debugProviderDiagnostic } from "../../lib/debug";
import { isDebugEnabled } from "../../lib/debug-settings";
import { modelRecordValue } from "../../reasoning-effort";
import { modelInList, type OcxProviderConfig } from "../../types";
import { mapReasoningEffort, modelRecordValue } from "../../reasoning-effort";
import { resolveReasoningCapabilities } from "../../providers/reasoning-capabilities";
import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
import { applyExplicitChatReasoningWirePolicy, declaresIndependentChatReasoningControls } from "./reasoning-wire";
import { declaresThinkingToggleWire, reasoningControlsAreUnsupported } from "../reasoning-controls";
import { planNativeChatReasoningReplay } from "./reasoning-replay";
import type { OcxReasoningReplayScopeRef } from "../../types";
import { jsonUtf8Bytes } from "../../lib/json-byte-size";
Expand Down Expand Up @@ -87,22 +89,40 @@ export function buildOpenAIChatPassthroughRequest(
const replayPlan = planNativeChatReasoningReplay(body.messages, provider, modelId, replayScope,
Array.isArray(rawBody.tools) && rawBody.tools.length > 0, maxSerializedBytes);
if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
const toggleWire = declaresThinkingToggleWire(provider, modelId);
const independentControls = declaresIndependentChatReasoningControls(provider, modelId);
const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
const reasoningDisabled = modelInList(provider.noReasoningModels, modelId)
|| (!toggleWire && rawEfforts?.length === 0) || reasoningControlsAreUnsupported(provider, modelId);
if (reasoningDisabled) {
delete body.reasoning_effort;
}
const hasTools = Array.isArray(rawBody.tools) && rawBody.tools.length > 0;
const requestedEffort = typeof body.reasoning_effort === "string" ? body.reasoning_effort : undefined;
applyExplicitChatReasoningWirePolicy({
const explicitThinkingEnabled = rawBody.reasoning !== null && typeof rawBody.reasoning === "object"
&& typeof (rawBody.reasoning as { enabled?: unknown }).enabled === "boolean"
? (rawBody.reasoning as { enabled: boolean }).enabled
: undefined;
const explicitReasoning = applyExplicitChatReasoningWirePolicy({
provider,
modelId,
hasTools,
thinkingEnabled: explicitThinkingEnabled,
requestedEffort,
wireEffort: requestedEffort,
wireEffort: toggleWire && independentControls ? mapReasoningEffort(provider, modelId, requestedEffort) : requestedEffort,
reasoningDisabled,
body,
});
let reasoningLog = explicitReasoning.handled ? explicitReasoning.reasoningLog : undefined;
// A native caller's own thinking object is already in the upstream wire shape. Forward it only
// on a provider-declared toggle wire, below an explicit reasoning.enabled but above a default.
if (!reasoningDisabled && toggleWire && independentControls
&& resolveReasoningCapabilities(provider, modelId).supportsThinkingToggle !== false
&& explicitThinkingEnabled === undefined
&& rawBody.thinking !== undefined) {
body.thinking = rawBody.thinking;
reasoningLog = undefined;
}

const openRouterRouting = resolveOpenRouterRouting(provider, modelId);
if (openRouterRouting) body.provider = openRouterProviderPayload(openRouterRouting);
Expand Down Expand Up @@ -208,5 +228,8 @@ export function buildOpenAIChatPassthroughRequest(
});
}

return { url, method: "POST", headers, body: bodyJson };
return {
url, method: "POST", headers, body: bodyJson,
...(reasoningLog ? { reasoningLog } : {}),
};
}
Loading
Loading