diff --git a/devlog/_plan/260818_megafile_split_program/010_wp1_types_value_leaves.md b/devlog/_plan/260818_megafile_split_program/010_wp1_types_value_leaves.md
new file mode 100644
index 0000000000..653f95d991
--- /dev/null
+++ b/devlog/_plan/260818_megafile_split_program/010_wp1_types_value_leaves.md
@@ -0,0 +1,104 @@
+# WP1 — types.ts value-leaf extraction (stacked PR 1 of the split program)
+
+Unit: devlog/_plan/260818_megafile_split_program. Risk basis: 000_risk_assessment.md.
+Branch: codex/split-wp1-types on dev @ b04cd26e7 (post FastWire B0/B1 merge).
+Class: C2 (mechanical move, shared-runtime file, full-suite gate).
+
+## Loop spec
+
+- Archetype: pure-move refactor, zero behavior change.
+- Trigger: split program WP1, lowest-risk opener.
+- Goal: src/types.ts stops carrying runtime value code; values live in leaves;
+ every existing import keeps working via re-export.
+- Non-goals: NO type-cluster split yet (OcxConfig/OcxProviderConfig stay),
+ NO consumer retargeting to leaf paths, NO behavior or signature change.
+- Verifier: bun run typecheck && bun run test (full — shared runtime file).
+- Stop: both green + core-lab-boundary green; PR opened against dev.
+- Memory artifact: this doc + ledger attests.
+
+## Scope (IN)
+
+Extract the two VALUE clusters from src/types.ts (1867 lines) into leaves:
+
+1. src/types/tools.ts — lines ~236-292:
+ namespacedToolName, toolChoiceAliases, toolAllowedByChoice,
+ resolveToolChoiceWireName, modelInList, OcxToolChoice (type),
+ isAllowedToolChoice, toolChoiceToolPredicate.
+ Needs `import type { OcxTool } from "../types"` — type-only, erased at
+ runtime, so the types.ts -> tools.ts re-export is NOT a runtime cycle.
+2. src/types/wire.ts — lines ~1760-1839:
+ UPSTREAM_HTTP_VERSION_VALUES, UpstreamHttpVersion,
+ REASONING_SUMMARY_DELIVERY_VALUES, ReasoningSummaryDelivery,
+ CodexAccountMode, OPENAI_PROVIDER_TIER_VERSION,
+ MODEL_ADAPTER_OVERRIDE_ALLOWED, ANTHROPIC_WIRE_MODELS (internal),
+ anthropicWireModelsForProvider (internal), captureWireAdapterHardPins,
+ isWirePinnedModel, pinnedWireAdapter. Self-contained, no imports.
+
+src/types.ts keeps every current export via `export ... from "./types/..."`;
+type-only names re-exported with `export type`.
+
+## Scope (OUT)
+
+- All interface/type clusters stay in types.ts this PR.
+- No import-path changes anywhere else in src/ or tests/.
+- No lab imports anywhere new (types is on the protected graph as a value
+ import from responses/core.ts: modelInList, namespacedToolName).
+
+## File change map
+
+- ADD src/types/tools.ts (~60 lines incl. docs)
+- ADD src/types/wire.ts (~85 lines incl. docs)
+- EDIT src/types.ts: delete moved bodies, add two re-export blocks at the
+ same positions; net -120 lines.
+
+## Accept criteria
+
+1. bun run typecheck exit 0.
+2. bun run test full suite: same pass count as base (13k+), 0 fail.
+3. tests/core-lab-boundary.test.ts green (covers the new static edges
+ types.ts -> types/tools.ts, types/wire.ts on the protected walk).
+4. rg confirms no consumer file changed: git diff --stat touches exactly 3
+ files.
+5. Value identity preserved: MODEL_ADAPTER_OVERRIDE_ALLOWED still a single
+ ReadonlySet instance (only one declaration site, re-export not re-create).
+
+Activation grounding: criterion 3's scenario is the existing boundary test
+run; criterion 5's scenario is the full suite (service-tier tests compare
+set membership through both import paths).
+
+## Verifier reality (PLAN-VERIFIER-REAL-01)
+
+- bun run typecheck: exists in package.json, reads src/ via tsconfig
+ include ["src"] — observes both new files. To be run in C.
+- bun run test: tests/ suite imports ../src/types in 400 files — observes
+ the barrel; core-lab-boundary walks the import graph from the three
+ protected roots which reach types.ts — observes the new edges.
+
+## Stacked-PR plan (DEV-STACK-01)
+
+PR 1 (this): value leaves + barrel. Target: dev.
+PR 2 (next cycle): type-cluster split (request/config/provider/accounts)
+stacked on PR 1's head branch.
+Later cycles per 000_risk_assessment.md order (config leaves, registry, ...).
+
+## Audit amendments (A-phase, 2 auditors: grok-4.6 NEAR-PASS / gpt-5.6-sol FAIL->fixed)
+
+1. CYCLE FIX (sol blocker): OcxTool (lines 211-232) moves INTO types/tools.ts.
+ tools.ts imports NOTHING from ../types — dependency is strictly one-way
+ (types.ts -> types/tools.ts). types.ts re-exports OcxTool as a type.
+2. RECIPE FIX (grok finding 7): `export type { X } from` does not BIND X in
+ the barrel. types.ts still uses OcxTool (line 106), OcxToolChoice (299),
+ UpstreamHttpVersion (1455), CodexAccountMode (1470),
+ ReasoningSummaryDelivery (1574) — so the barrel adds a local
+ `import type { OcxTool, OcxToolChoice } from "./types/tools"` and
+ `import type { UpstreamHttpVersion, ReasoningSummaryDelivery,
+ CodexAccountMode } from "./types/wire"` next to the Kiro import.
+3. OcxToolChoice + its guards travel with tools.ts (they are one cluster).
+4. Extensionless specifiers only (lab walker resolves `${base}.ts`).
+5. AC4 corrected: scope proof = `git diff --stat ..HEAD -- src tests`
+ showing exactly 3 src files; devlog/plan files are committed separately.
+6. AC5 proof corrected: identity is preserved by ESM re-export semantics
+ (single declaration site); drop the false 'both import paths' claim.
+7. Protected-roots note corrected: PROTECTED has 4 files; only
+ responses/core.ts puts types.ts on the runtime graph (core.ts:63).
+
diff --git a/src/types.ts b/src/types.ts
index c3e4a4eaca..e341ea6366 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,6 @@
import type { KiroOAuthMetadata } from "./oauth/types";
+import type { OcxTool, OcxToolChoice } from "./types/tools";
+import type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire";
/** Exact provider/credential namespace for process-local reasoning replay. */
export interface OcxReasoningReplayIdentity {
@@ -214,137 +216,17 @@ export interface OcxProviderOpaqueToolCallMetadata {
export type OcxAssistantContentPart = OcxTextContent | OcxThinkingContent | OcxToolCall;
-export interface OcxTool {
- name: string;
- description: string;
- parameters: Record;
- strict?: boolean;
- /** MCP namespace (e.g. "mcp__context7") for tools flattened out of a Responses "namespace" tool. */
- namespace?: string;
- /** Freeform/custom tool (e.g. apply_patch): the model's call must be relayed as a custom_tool_call. */
- freeform?: boolean;
- /** Client-executed tool discovery (tool_search): the model's call must be relayed as a tool_search_call. */
- toolSearch?: boolean;
- /** Tool definition restored from a prior tool_search output; transports may prioritize it when catalogs are bounded. */
- loadedFromToolSearch?: boolean;
- /** Cursor-only synthetic exact-match edit tool; never inferred from the wire name. */
- cursorStructuredEdit?: true;
- /** Synthetic web_search tool: the model's call is executed by the gpt-5.4-mini sidecar, not relayed to Codex. */
- webSearch?: boolean;
- /** Synthetic image_gen tool: the model's call is executed by the xAI image bridge sidecar, not relayed to Codex. */
- imageGeneration?: boolean;
- /** Synthetic video_gen tool: executed by the xAI video bridge sidecar. */
- videoGeneration?: boolean;
-}
-
-/**
- * Wire name a chat model sees for a tool. Namespaced (MCP) tools are flattened to
- * "__" so they survive the chat-completions function-tool format;
- * the proxy maps this back to {namespace, name} on the return trip (Codex routes MCP
- * calls by an explicit `namespace` field, not by parsing the name).
- */
-export function namespacedToolName(namespace: string | undefined, name: string): string {
- return namespace ? `${namespace}__${name}` : name;
-}
-
-export function toolChoiceAliases(tool: Pick): string[] {
- const wireName = namespacedToolName(tool.namespace, tool.name);
- return tool.namespace ? [wireName, `${tool.namespace}.${tool.name}`] : [wireName];
-}
-
-function sameToolIdentity(
- left: Pick,
- right: Pick,
-): boolean {
- return left.namespace === right.namespace && left.name === right.name;
-}
-
-/**
- * All tools that could be selected by one client-facing name. Bare logical names are included
- * here because they are a compatibility selector for namespaced tools, while wire and dotted
- * aliases come from `toolChoiceAliases`. A selector with more than one candidate is invalid.
- */
-export function toolChoiceCandidates(
- tools: readonly Pick[] | undefined,
- name: string,
-): Pick[] {
- if (!tools) return [];
- const candidates: Pick[] = [];
- for (const tool of tools) {
- if (tool.name !== name && !toolChoiceAliases(tool).includes(name)) continue;
- if (!candidates.some(candidate => sameToolIdentity(candidate, tool))) candidates.push(tool);
- }
- return candidates;
-}
-
-/**
- * Newer Codex clients can select a tool nested in a namespace by its bare name. Resolve that
- * shorthand only when the request contains one tool with the logical name, so an ambiguous name
- * cannot authorize a tool from an unintended namespace.
- */
-export function toolAllowedByChoice(
- tool: Pick,
- allowedTools: ReadonlySet,
- tools?: readonly Pick[],
-): boolean {
- if (!tools) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
- for (const name of [...toolChoiceAliases(tool), tool.name]) {
- if (!allowedTools.has(name)) continue;
- const candidates = toolChoiceCandidates(tools, name);
- if (candidates.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
- }
- return false;
-}
-
-export function resolveToolChoiceWireName(tools: readonly Pick[] | undefined, name: string): string {
- const candidates = toolChoiceCandidates(tools, name);
- if (candidates.length === 1) {
- const match = candidates[0];
- return namespacedToolName(match.namespace, match.name);
- }
- // Keep unknown/ambiguous names unchanged for callers that only serialize a selector. The
- // catalog-aware predicate rejects them, and parseRequest rejects ambiguous request selectors.
- return name;
-}
-
-/**
- * Whether `modelId` is in a per-provider classification list (e.g. `noVisionModels`). Matches the full
- * id, OR — for Ollama-style ids — the family before the ":size" tag, so a `gpt-oss` entry covers
- * `gpt-oss:120b`/`gpt-oss:20b`. Colon-less ids (e.g. `grok-build-0.1`) still match exactly only.
- */
-export function modelInList(list: string[] | undefined, modelId: string): boolean {
- if (!list || list.length === 0) return false;
- if (list.includes(modelId)) return true;
- const colon = modelId.indexOf(":");
- return colon > 0 && list.includes(modelId.slice(0, colon));
-}
-
-export type OcxToolChoice =
- | "auto"
- | "none"
- | "required"
- | { name: string }
- | { allowedTools: string[]; mode: "auto" | "required" };
-
-export function isAllowedToolChoice(value: OcxToolChoice | undefined): value is { allowedTools: string[]; mode: "auto" | "required" } {
- return typeof value === "object" && value !== null && "allowedTools" in value;
-}
-
-/** Compile the request's tool-choice policy into a reusable advertisement/restoration predicate. */
-export function toolChoiceToolPredicate(
- choice: OcxToolChoice | undefined,
- tools?: readonly Pick[],
-): (tool: Pick) => boolean {
- if (!choice || choice === "auto" || choice === "required") return () => true;
- if (choice === "none") return () => false;
- if (isAllowedToolChoice(choice)) {
- const allowed = new Set(choice.allowedTools);
- return tool => toolAllowedByChoice(tool, allowed, tools);
- }
- if (!tools) return tool => toolChoiceAliases(tool).includes(choice.name);
- const candidates = toolChoiceCandidates(tools, choice.name);
- return tool => candidates.length === 1 && sameToolIdentity(candidates[0], tool);
-}
+export type { OcxTool, OcxToolChoice } from "./types/tools";
+export {
+ namespacedToolName,
+ toolChoiceAliases,
+ toolChoiceCandidates,
+ toolAllowedByChoice,
+ resolveToolChoiceWireName,
+ modelInList,
+ isAllowedToolChoice,
+ toolChoiceToolPredicate,
+} from "./types/tools";
export interface OcxRequestOptions {
maxOutputTokens?: number;
@@ -1825,86 +1707,16 @@ export interface OcxProviderConfig {
nativeLocalExec?: "off" | "codex-sandbox" | "on";
}
-/**
- * Accepted values for the per-provider upstream HTTP-version pin (#1668). Shared by the
- * zod load schema, the management write boundary (POST/PATCH), and the fetch runtime, so
- * a value that one boundary accepts can never be rejected by another.
- */
-export const UPSTREAM_HTTP_VERSION_VALUES = [
- "auto",
- "http1.1",
- "h1",
- "http2",
- "h2",
-] as const;
-
-export type UpstreamHttpVersion = (typeof UPSTREAM_HTTP_VERSION_VALUES)[number];
-
-export const REASONING_SUMMARY_DELIVERY_VALUES = [
- "sequential",
- "sequential_cutoff",
- "concurrent",
- "concurrent_cutoff",
-] as const;
-
-export type ReasoningSummaryDelivery = typeof REASONING_SUMMARY_DELIVERY_VALUES[number];
-
-/** Trusted runtime ownership for Codex-account credentials. Never persisted per provider. */
-export type CodexAccountMode = "direct" | "pool";
-
-export const OPENAI_PROVIDER_TIER_VERSION = 2 as const;
-
-/**
- * Wires that a per-model `modelAdapters` override may select.
- *
- * Deliberately narrow: provider-specific adapters (cursor, kiro, google, ...) carry
- * their own credential and base-URL semantics, so exposing them here would widen the
- * auth boundary rather than pick a wire. Widening this set needs a per-adapter
- * credential threat model first (#404).
- */
-export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet = new Set([
- "openai-chat",
- "openai-responses",
-]);
-
-/**
- * Providers whose listed model ids must be driven over the Anthropic wire even when
- * the provider's configured adapter says otherwise — the upstream only speaks
- * Anthropic for these models.
- */
-const ANTHROPIC_WIRE_MODELS: Record> = {
- "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3"]),
-};
-
-function anthropicWireModelsForProvider(providerName: string): ReadonlySet | undefined {
- return Object.hasOwn(ANTHROPIC_WIRE_MODELS, providerName)
- ? ANTHROPIC_WIRE_MODELS[providerName]
- : undefined;
-}
-
-/** Detached provider-local hard-pin table for pure wire-policy resolution. */
-export function captureWireAdapterHardPins(providerName: string): Readonly> {
- const models = anthropicWireModelsForProvider(providerName);
- if (!models) return Object.freeze({});
- return Object.freeze(Object.fromEntries([...models].map(modelId => [modelId, "anthropic"])));
-}
-
-/**
- * True when the upstream speaks exactly one wire for this model, so a configured
- * override must not apply.
- *
- * Deliberately independent of the provider's current adapter: the wire resolver runs
- * more than once per request, and a check phrased as "pin differs from the current
- * adapter" would pass on the first pass and then let the override win on the second.
- */
-export function isWirePinnedModel(providerName: string, modelId: string): boolean {
- return anthropicWireModelsForProvider(providerName)?.has(modelId) ?? false;
-}
-
-/** The wire a pinned model must use, or undefined when the model is not pinned. */
-export function pinnedWireAdapter(providerName: string, modelId: string): string | undefined {
- return isWirePinnedModel(providerName, modelId) ? "anthropic" : undefined;
-}
+export type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire";
+export {
+ UPSTREAM_HTTP_VERSION_VALUES,
+ REASONING_SUMMARY_DELIVERY_VALUES,
+ OPENAI_PROVIDER_TIER_VERSION,
+ MODEL_ADAPTER_OVERRIDE_ALLOWED,
+ captureWireAdapterHardPins,
+ isWirePinnedModel,
+ pinnedWireAdapter,
+} from "./types/wire";
export interface CodexAccount {
id: string;
diff --git a/src/types/tools.ts b/src/types/tools.ts
new file mode 100644
index 0000000000..9e3dc37fd0
--- /dev/null
+++ b/src/types/tools.ts
@@ -0,0 +1,131 @@
+export interface OcxTool {
+ name: string;
+ description: string;
+ parameters: Record;
+ strict?: boolean;
+ /** MCP namespace (e.g. "mcp__context7") for tools flattened out of a Responses "namespace" tool. */
+ namespace?: string;
+ /** Freeform/custom tool (e.g. apply_patch): the model's call must be relayed as a custom_tool_call. */
+ freeform?: boolean;
+ /** Client-executed tool discovery (tool_search): the model's call must be relayed as a tool_search_call. */
+ toolSearch?: boolean;
+ /** Tool definition restored from a prior tool_search output; transports may prioritize it when catalogs are bounded. */
+ loadedFromToolSearch?: boolean;
+ /** Cursor-only synthetic exact-match edit tool; never inferred from the wire name. */
+ cursorStructuredEdit?: true;
+ /** Synthetic web_search tool: the model's call is executed by the gpt-5.4-mini sidecar, not relayed to Codex. */
+ webSearch?: boolean;
+ /** Synthetic image_gen tool: the model's call is executed by the xAI image bridge sidecar, not relayed to Codex. */
+ imageGeneration?: boolean;
+ /** Synthetic video_gen tool: executed by the xAI video bridge sidecar. */
+ videoGeneration?: boolean;
+}
+
+/**
+ * Wire name a chat model sees for a tool. Namespaced (MCP) tools are flattened to
+ * "__" so they survive the chat-completions function-tool format;
+ * the proxy maps this back to {namespace, name} on the return trip (Codex routes MCP
+ * calls by an explicit `namespace` field, not by parsing the name).
+ */
+export function namespacedToolName(namespace: string | undefined, name: string): string {
+ return namespace ? `${namespace}__${name}` : name;
+}
+
+export function toolChoiceAliases(tool: Pick): string[] {
+ const wireName = namespacedToolName(tool.namespace, tool.name);
+ return tool.namespace ? [wireName, `${tool.namespace}.${tool.name}`] : [wireName];
+}
+
+function sameToolIdentity(
+ left: Pick,
+ right: Pick,
+): boolean {
+ return left.namespace === right.namespace && left.name === right.name;
+}
+
+/**
+ * All tools that could be selected by one client-facing name. Bare logical names are included
+ * here because they are a compatibility selector for namespaced tools, while wire and dotted
+ * aliases come from `toolChoiceAliases`. A selector with more than one candidate is invalid.
+ */
+export function toolChoiceCandidates(
+ tools: readonly Pick[] | undefined,
+ name: string,
+): Pick[] {
+ if (!tools) return [];
+ const candidates: Pick[] = [];
+ for (const tool of tools) {
+ if (tool.name !== name && !toolChoiceAliases(tool).includes(name)) continue;
+ if (!candidates.some(candidate => sameToolIdentity(candidate, tool))) candidates.push(tool);
+ }
+ return candidates;
+}
+
+/**
+ * Newer Codex clients can select a tool nested in a namespace by its bare name. Resolve that
+ * shorthand only when the request contains one tool with the logical name, so an ambiguous name
+ * cannot authorize a tool from an unintended namespace.
+ */
+export function toolAllowedByChoice(
+ tool: Pick,
+ allowedTools: ReadonlySet,
+ tools?: readonly Pick[],
+): boolean {
+ if (!tools) return toolChoiceAliases(tool).some(name => allowedTools.has(name));
+ for (const name of [...toolChoiceAliases(tool), tool.name]) {
+ if (!allowedTools.has(name)) continue;
+ const candidates = toolChoiceCandidates(tools, name);
+ if (candidates.length === 1 && sameToolIdentity(candidates[0], tool)) return true;
+ }
+ return false;
+}
+
+export function resolveToolChoiceWireName(tools: readonly Pick[] | undefined, name: string): string {
+ const candidates = toolChoiceCandidates(tools, name);
+ if (candidates.length === 1) {
+ const match = candidates[0];
+ return namespacedToolName(match.namespace, match.name);
+ }
+ // Keep unknown/ambiguous names unchanged for callers that only serialize a selector. The
+ // catalog-aware predicate rejects them, and parseRequest rejects ambiguous request selectors.
+ return name;
+}
+
+/**
+ * Whether `modelId` is in a per-provider classification list (e.g. `noVisionModels`). Matches the full
+ * id, OR — for Ollama-style ids — the family before the ":size" tag, so a `gpt-oss` entry covers
+ * `gpt-oss:120b`/`gpt-oss:20b`. Colon-less ids (e.g. `grok-build-0.1`) still match exactly only.
+ */
+export function modelInList(list: string[] | undefined, modelId: string): boolean {
+ if (!list || list.length === 0) return false;
+ if (list.includes(modelId)) return true;
+ const colon = modelId.indexOf(":");
+ return colon > 0 && list.includes(modelId.slice(0, colon));
+}
+
+export type OcxToolChoice =
+ | "auto"
+ | "none"
+ | "required"
+ | { name: string }
+ | { allowedTools: string[]; mode: "auto" | "required" };
+
+export function isAllowedToolChoice(value: OcxToolChoice | undefined): value is { allowedTools: string[]; mode: "auto" | "required" } {
+ return typeof value === "object" && value !== null && "allowedTools" in value;
+}
+
+/** Compile the request's tool-choice policy into a reusable advertisement/restoration predicate. */
+export function toolChoiceToolPredicate(
+ choice: OcxToolChoice | undefined,
+ tools?: readonly Pick[],
+): (tool: Pick) => boolean {
+ if (!choice || choice === "auto" || choice === "required") return () => true;
+ if (choice === "none") return () => false;
+ if (isAllowedToolChoice(choice)) {
+ const allowed = new Set(choice.allowedTools);
+ return tool => toolAllowedByChoice(tool, allowed, tools);
+ }
+ if (!tools) return tool => toolChoiceAliases(tool).includes(choice.name);
+ const candidates = toolChoiceCandidates(tools, choice.name);
+ return tool => candidates.length === 1 && sameToolIdentity(candidates[0], tool);
+}
diff --git a/src/types/wire.ts b/src/types/wire.ts
new file mode 100644
index 0000000000..0800bca428
--- /dev/null
+++ b/src/types/wire.ts
@@ -0,0 +1,80 @@
+/**
+ * Accepted values for the per-provider upstream HTTP-version pin (#1668). Shared by the
+ * zod load schema, the management write boundary (POST/PATCH), and the fetch runtime, so
+ * a value that one boundary accepts can never be rejected by another.
+ */
+export const UPSTREAM_HTTP_VERSION_VALUES = [
+ "auto",
+ "http1.1",
+ "h1",
+ "http2",
+ "h2",
+] as const;
+
+export type UpstreamHttpVersion = (typeof UPSTREAM_HTTP_VERSION_VALUES)[number];
+
+export const REASONING_SUMMARY_DELIVERY_VALUES = [
+ "sequential",
+ "sequential_cutoff",
+ "concurrent",
+ "concurrent_cutoff",
+] as const;
+
+export type ReasoningSummaryDelivery = typeof REASONING_SUMMARY_DELIVERY_VALUES[number];
+
+/** Trusted runtime ownership for Codex-account credentials. Never persisted per provider. */
+export type CodexAccountMode = "direct" | "pool";
+
+export const OPENAI_PROVIDER_TIER_VERSION = 2 as const;
+
+/**
+ * Wires that a per-model `modelAdapters` override may select.
+ *
+ * Deliberately narrow: provider-specific adapters (cursor, kiro, google, ...) carry
+ * their own credential and base-URL semantics, so exposing them here would widen the
+ * auth boundary rather than pick a wire. Widening this set needs a per-adapter
+ * credential threat model first (#404).
+ */
+export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet = new Set([
+ "openai-chat",
+ "openai-responses",
+]);
+
+/**
+ * Providers whose listed model ids must be driven over the Anthropic wire even when
+ * the provider's configured adapter says otherwise — the upstream only speaks
+ * Anthropic for these models.
+ */
+const ANTHROPIC_WIRE_MODELS: Record> = {
+ "opencode-go": new Set(["minimax-m2.5", "minimax-m2.7", "minimax-m3"]),
+};
+
+function anthropicWireModelsForProvider(providerName: string): ReadonlySet | undefined {
+ return Object.hasOwn(ANTHROPIC_WIRE_MODELS, providerName)
+ ? ANTHROPIC_WIRE_MODELS[providerName]
+ : undefined;
+}
+
+/** Detached provider-local hard-pin table for pure wire-policy resolution. */
+export function captureWireAdapterHardPins(providerName: string): Readonly> {
+ const models = anthropicWireModelsForProvider(providerName);
+ if (!models) return Object.freeze({});
+ return Object.freeze(Object.fromEntries([...models].map(modelId => [modelId, "anthropic"])));
+}
+
+/**
+ * True when the upstream speaks exactly one wire for this model, so a configured
+ * override must not apply.
+ *
+ * Deliberately independent of the provider's current adapter: the wire resolver runs
+ * more than once per request, and a check phrased as "pin differs from the current
+ * adapter" would pass on the first pass and then let the override win on the second.
+ */
+export function isWirePinnedModel(providerName: string, modelId: string): boolean {
+ return anthropicWireModelsForProvider(providerName)?.has(modelId) ?? false;
+}
+
+/** The wire a pinned model must use, or undefined when the model is not pinned. */
+export function pinnedWireAdapter(providerName: string, modelId: string): string | undefined {
+ return isWirePinnedModel(providerName, modelId) ? "anthropic" : undefined;
+}