Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 15 additions & 1 deletion docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,21 @@ MiniMax model list is still the previous default receives it on the next start;
is left as it is; register the preview by hand with
`ocx models add minimax MiniMax-M3.1-Flash-Preview --context-window 1000000`.

**OpenCode Go** requires a stable session identifier for routing. OpenCodex sends
**OpenCode Go** routes `gemini-4-argon-contributor` through its Gemini API, including
streaming and tool-result continuations. The existing `opencode-go` preset keeps its
historical URL pinning: saved custom URLs, including `https://opencode.ai/inference/go/v1`,
route through the canonical `https://opencode.ai/zen/go/v1` endpoint and inherit Go defaults.
Gemini then uses Go's native model endpoint with key authentication. A differently named
custom provider keeps its configured URL and does not receive this Gemini pin.
Go selects protocols by exact model ID: GPT 6 Luna, GPT 5.6 Luna, Grok 4.6/4.7 and
Muse Spark Contributor use Responses; Claude Haiku 5.5, MiniMax M2.7/M3 and
Qwen3.7 Plus/3.8 Max/3.8 Flash use Anthropic Messages. Legacy MiniMax M2.5 and
Union Alpha also retain Messages. Models without a listed wire retain Chat.
New native pins apply to the pinned Go destination with key authentication.
Responses models still accept an explicit `modelAdapters` override.
Gemini uses `low`, `medium` and `high` thinking levels; higher requested efforts clamp to `high`.

OpenCode Go requires a stable session identifier for routing. OpenCodex sends
`claude-haiku-5-5`, like the MiniMax models, over the Anthropic Messages wire, matching
[OpenCode Go's endpoint table](https://opencode.ai/docs/go/#endpoints). OpenCodex derives
its Go session header from Codex thread/session headers, or from a client's
Expand Down
4 changes: 4 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@
"catalog-modelalias-unique-sync.test.ts": "codex-integration",
"catalog-oauth-observation.test.ts": "codex-integration",
"catalog-opencode-go-context-window.test.ts": "codex-integration",
"catalog-opencode-go-static.test.ts": "codex-integration",
"catalog-remote-pull.test.ts": "codex-integration",
"catalog-retain-models.test.ts": "codex-integration",
"catalog-routed-comp-hash.test.ts": "codex-integration",
Expand Down Expand Up @@ -1283,6 +1284,9 @@
"opencode-cli.test.ts": "providers",
"opencode-free-provider.test.ts": "providers",
"opencode-go-deepseek.test.ts": "providers",
"opencode-go-gemini-wire.test.ts": "providers",
"opencode-go-model-protocols.test.ts": "providers",
"registry-transport-alias.test.ts": "providers",
"opencode-go-grok46-responses.test.ts": "providers",
"opencode-go-luna-wire.test.ts": "providers",
"opencode-go-muse-context.test.ts": "providers",
Expand Down
21 changes: 17 additions & 4 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
import { createToolCallIdAllocator } from "./tool-call-id";
import { openCodeGoGeminiEndpoint } from "../types/opencode-go";
import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
import type {
AdapterEvent,
Expand Down Expand Up @@ -864,6 +865,7 @@ function invalidGoogleShapeEvent(
};
}

/** Build Gemini transport and response parsing for AI Studio, Vertex, Cloud Code Assist or Go. */
export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
const toolSchemaProfile = {
Expand Down Expand Up @@ -945,6 +947,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
}
: {}),

/** Compile an admitted request into the selected Gemini endpoint, body and authentication. */
async buildRequest(parsed: OcxParsedRequest) {
// Structured-output admission runs FIRST, before messagesToGeminiFormat writes
// lastInjectedCallIds/lastReasoningReplayScope: a refused request must not leave
Expand Down Expand Up @@ -1202,11 +1205,21 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
}

// ai-studio (default): Generative Language API + x-goog-api-key.
const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
// Go speaks Gemini on its own endpoint; ordinary AI Studio keeps /v1beta and its key header.
const goEndpoint = openCodeGoGeminiEndpoint(provider, routedModelId);
const endpoint = goEndpoint ?? `${provider.baseUrl}/v1beta/models/${routedModelId}`;
const url = `${endpoint}:${method}${streamParam}`;
const apiKey = provider.apiKey?.trim();
if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
headers["x-goog-api-key"] = apiKey;
if (!apiKey) throw new Error(`${goEndpoint ? "OpenCode Go" : "google (AI Studio)"} requires a non-empty API key`);
if (goEndpoint) {
// Console inference documents Bearer auth. Send only the selected Go key.
for (const name of Object.keys(headers)) {
if (["authorization", "x-goog-api-key", "x-api-key"].includes(name.toLowerCase())) delete headers[name];
}
headers["Authorization"] = `Bearer ${apiKey}`;
} else {
headers["x-goog-api-key"] = apiKey;
}

const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
reportToolSchemaLoss(compiled.toolSchemaLossReport);
Expand Down
3 changes: 2 additions & 1 deletion src/protocols/provider-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ function usableModelId(model: unknown): model is string {
function candidateModelIds(name: string, provider: Readonly<OcxProviderConfig>): string[] {
const registry = PROVIDER_REGISTRY.find(entry => entry.id === name);
const ids = new Set<string>();
/** Collect valid candidate IDs without exceeding the protocol-summary scan limit. */
const add = (model: unknown) => {
if (ids.size < MODEL_SCAN_LIMIT && usableModelId(model)) ids.add(model);
};
for (const model of Object.keys(provider.modelAdapters ?? {})) add(model);
for (const model of Object.keys(registry?.modelWireDefaults ?? {})) add(model);
for (const model of Object.keys(captureWireAdapterHardPins(name))) add(model);
for (const model of Object.keys(captureWireAdapterHardPins(name, provider))) add(model);
add(provider.defaultModel);
for (const model of provider.models ?? []) add(model);
return [...ids].sort((left, right) => left.localeCompare(right));
Expand Down
4 changes: 3 additions & 1 deletion src/providers/opencode-go-transport.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import type { OcxProviderConfig } from "../types";
import { registryEntryForProviderDestination } from "./registry";
import { isOpenCodeGoGeminiDestination } from "../types/opencode-go";

export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";

Expand Down Expand Up @@ -44,7 +45,8 @@ export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
Pick<OcxProviderConfig, "baseUrl" | "adapter">
& Partial<Pick<OcxProviderConfig, "authMode">>,
): T {
if (registryEntryForProviderDestination(destinationProvider)?.id !== "opencode-go") return provider;
if (registryEntryForProviderDestination(destinationProvider)?.id !== "opencode-go"
&& !(provider.adapter === "google" && isOpenCodeGoGeminiDestination(provider))) return provider;
if (!sessionLane) return provider;
if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;

Expand Down
15 changes: 7 additions & 8 deletions src/providers/registry/entries-core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OPENCODE_GO_BASE_URL, OPENCODE_GO_RESPONSES_DEFAULTS } from "../../types/opencode-go";
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "../kiro-models";
import { DEVIN_MODEL_CONTEXT_WINDOWS, DEVIN_MODEL_EFFORTS, DEVIN_DEFAULT_EFFORTS } from "../../adapters/devin/live-models";
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "../antigravity-models";
Expand Down Expand Up @@ -824,7 +825,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
escapeBuiltinToolNames: true,
},
{
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: OPENCODE_GO_BASE_URL,
destinationAliases: [{ adapter: "openai-chat", baseUrl: "https://opencode.ai/inference/go/v1" }],
authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
// Zen Go can close a Chat stream after a fully assembled function call without sending
Expand All @@ -846,13 +848,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
- 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence.
- 장점, 단점 및 영향: Each listed model reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update.
*/
modelWireDefaults: {
"gpt-5.6-luna": "openai-responses",
"grok-4.6": "openai-responses",
"grok-4.7": "openai-responses",
"muse-spark-1.3-contributor": "openai-responses",
"muse-spark-1.2-contributor": "openai-responses",
},
modelWireDefaults: OPENCODE_GO_RESPONSES_DEFAULTS,
modelContextWindows: {
"kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
// Zen Go discovers only the gateway id, so carry DeepSeek's official 1M V4.1
Expand Down Expand Up @@ -909,6 +905,9 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
},
modelReasoningEfforts: {
"gpt-5.6-luna": OPENAI_API_GPT56_REASONING_EFFORTS,
// Gemini's native thinkingLevel ladder; higher Codex efforts clamp to high.
// https://ai.google.dev/gemini-api/docs/models/gemini-4-argon
"gemini-4-argon-contributor": ["low", "medium", "high"],
"grok-4.6": ["low", "medium", "high", "xhigh"],
"grok-4.7": ["low", "medium", "high", "xhigh"],
"glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
Expand Down
2 changes: 1 addition & 1 deletion src/providers/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export interface ProviderRegistryEntry {
*/
chatCompletionsPath?: string;
/**
* Endpoints this entry used to live at, kept so a saved custom provider that still points
* Canonical or historical endpoints, kept so a saved custom provider that still points
* at one keeps receiving this row's metadata through `registryEntryForProviderDestination`.
* Destination matching is by adapter plus normalized base URL, so moving a row's wire or
* prefix would otherwise orphan every config a user wrote against the old address.
Expand Down
3 changes: 2 additions & 1 deletion src/providers/service-tier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ServiceTierCapabilityProvider = Pick<
| "modelAdapters"
| "baseUrl"
| "authMode"
| "googleMode"
| "apiKeyTransport"
| "chatServiceTier"
| "fastWire"
Expand Down Expand Up @@ -119,7 +120,7 @@ function buildFastPolicyAuthority(
: {}),
}),
modelAdapters: Object.freeze({ ...(provider.modelAdapters ?? {}) }),
hardPins: captureWireAdapterHardPins(providerName),
hardPins: captureWireAdapterHardPins(providerName, provider),
hardPinPrefixes: captureWireAdapterHardPinPrefixes(providerName, provider),
registryWireDefaults: cloneRegistryWireDefaults(registry?.modelWireDefaults),
});
Expand Down
61 changes: 61 additions & 0 deletions src/types/opencode-go.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** Exact-model protocols from https://opencode.ai/v2/docs/console/go/#endpoints (2026-10-15).
* Unknown models retain Chat. Responses entries are overridable registry defaults;
* the native wires are hard pins because modelAdapters cannot select those adapters.
*/
export const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";

export const OPENCODE_GO_MODEL_WIRES: Readonly<Record<string, "google" | "anthropic" | "openai-responses">> = Object.freeze({
"gemini-4-argon-contributor": "google",
"claude-haiku-5-5": "anthropic",
"minimax-m2.7": "anthropic",
"minimax-m3": "anthropic",
"qwen3.7-plus": "anthropic",
"qwen3.8-max": "anthropic",
"qwen3.8-flash": "anthropic",
"gpt-6-luna": "openai-responses",
"gpt-5.6-luna": "openai-responses",
"grok-4.6": "openai-responses",
"grok-4.7": "openai-responses",
"muse-spark-1.2-contributor": "openai-responses",
"muse-spark-1.3-contributor": "openai-responses",
// Retain previously supported Go models absent from the current endpoint table (#4847).
"minimax-m2.5": "anthropic",
"union-alpha": "anthropic",
});

/** Compatibility: these five pins predate destination scoping. New native pins require Go. */
export const OPENCODE_GO_LEGACY_NAME_PINS: ReadonlySet<string> = new Set([
"claude-haiku-5-5", "minimax-m2.5", "minimax-m2.7", "minimax-m3", "union-alpha",
]);

export const OPENCODE_GO_RESPONSES_DEFAULTS = Object.freeze(Object.fromEntries(
Object.entries(OPENCODE_GO_MODEL_WIRES).filter(([, wire]) => wire === "openai-responses"),
));

export interface OpenCodeGoDestination {
readonly baseUrl?: unknown;
readonly authMode?: unknown;
readonly googleMode?: unknown;
}

/** A provider name alone must never authorize redirecting a credential to OpenCode. */
export function isOpenCodeGoGeminiDestination(provider: OpenCodeGoDestination | undefined): boolean {
if (!provider || typeof provider.baseUrl !== "string") return false;
if (provider.authMode !== undefined && provider.authMode !== "key") return false;
if (provider.googleMode !== undefined && provider.googleMode !== "ai-studio") return false;
try {
const url = new URL(provider.baseUrl.trim());
return url.origin === "https://opencode.ai"
&& !url.username && !url.password && !url.search && !url.hash
&& ["/zen/go/v1", "/inference/go/v1"].includes(url.pathname.replace(/\/+$/, ""));
} catch {
return false;
}
}

/** Return the fixed Gemini endpoint only for the exact model and an eligible Go destination. */
export function openCodeGoGeminiEndpoint(provider: OpenCodeGoDestination, modelId: string): string | undefined {
return Object.hasOwn(OPENCODE_GO_MODEL_WIRES, modelId) && OPENCODE_GO_MODEL_WIRES[modelId] === "google" && isOpenCodeGoGeminiDestination(provider)
? `https://opencode.ai/inference/go/v1/models/${encodeURIComponent(modelId)}`
: undefined;
}
53 changes: 16 additions & 37 deletions src/types/wire.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isOpenCodeGoGeminiDestination, OPENCODE_GO_BASE_URL, OPENCODE_GO_MODEL_WIRES, OPENCODE_GO_LEGACY_NAME_PINS, type OpenCodeGoDestination } from "./opencode-go";

/**
* 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
Expand Down Expand Up @@ -40,35 +42,16 @@ export const MODEL_ADAPTER_OVERRIDE_ALLOWED: ReadonlySet<string> = new Set([
"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<string, ReadonlySet<string>> = {
"opencode-go": new Set([
"claude-haiku-5-5",
"minimax-m2.5",
"minimax-m2.7",
"minimax-m3",
// OpenCode's catalog identifies Union Alpha as @ai-sdk/anthropic while the
// provider defaults to OpenAI-compatible; direct Chat returns 500 and direct
// Messages reaches the session check (#4847).
"union-alpha",
]),
};

function anthropicWireModelsForProvider(providerName: string): ReadonlySet<string> | 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<Record<string, string>> {
const models = anthropicWireModelsForProvider(providerName);
if (!models) return Object.freeze({});
return Object.freeze(Object.fromEntries([...models].map(modelId => [modelId, "anthropic"])));
/** Native Go wire pins share the exact-model table with registry Responses defaults. */
export function captureWireAdapterHardPins(providerName: string, provider?: WirePinProvider): Readonly<Record<string, string>> {
if (providerName !== "opencode-go") return Object.freeze({});
// The existing Go preset pins saved URLs to this endpoint before dispatch. Pure policy
// captures must use that same destination even when handed an unnormalized config row.
const destination = provider && { ...provider, baseUrl: OPENCODE_GO_BASE_URL };
return Object.freeze(Object.fromEntries(Object.entries(OPENCODE_GO_MODEL_WIRES).filter(([modelId, wire]) =>
(wire === "anthropic" && OPENCODE_GO_LEGACY_NAME_PINS.has(modelId))
|| (wire !== "openai-responses" && isOpenCodeGoGeminiDestination(destination)),
)));
}

interface WirePinPrefixRule {
Expand Down Expand Up @@ -98,9 +81,7 @@ const WIRE_ADAPTER_PIN_PREFIXES: Readonly<Record<string, WirePinPrefixRule>> = O
});

/** Just enough of a provider config to tell whether it still points at the pinned endpoint. */
export interface WirePinProvider {
readonly baseUrl?: unknown;
}
export interface WirePinProvider extends OpenCodeGoDestination {}

function normalizedEndpoint(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
Expand Down Expand Up @@ -148,13 +129,11 @@ function prefixedWireAdapter(providerName: string, modelId: string, provider: Wi
* adapter" would pass on the first pass and then let the override win on the second.
*/
export function isWirePinnedModel(providerName: string, modelId: string, provider?: WirePinProvider): boolean {
return (anthropicWireModelsForProvider(providerName)?.has(modelId) ?? false)
|| prefixedWireAdapter(providerName, modelId, provider) !== undefined;
return pinnedWireAdapter(providerName, modelId, provider) !== undefined;
}

/** The wire a pinned model must use, or undefined when the model is not pinned. */
export function pinnedWireAdapter(providerName: string, modelId: string, provider?: WirePinProvider): string | undefined {
return anthropicWireModelsForProvider(providerName)?.has(modelId)
? "anthropic"
: prefixedWireAdapter(providerName, modelId, provider);
const pins = captureWireAdapterHardPins(providerName, provider);
return Object.hasOwn(pins, modelId) ? pins[modelId] : prefixedWireAdapter(providerName, modelId, provider);
}
2 changes: 1 addition & 1 deletion structure/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ A Devin live row spreads its measured `inputModalities` before
value survives only when none of them applies. Devin live rows collapse by catalog family (so `swe-1-6-fast` stays its own row), read their ladder from the family effort axis (including `minimal`), and carry the effective enabled default member's effort as `defaultReasoningEffort` when the family marks a default; `swe-1-6` is marked text-only because it drops images without an error.

For `liveModels: false`, a static provider publishes the ordered union of `models` and `retainModels`. When `models` is absent or empty, its configured `defaultModel` seeds that
union before retained ids; a nonempty explicit list does not import a different default.
union before retained ids; a nonempty explicit list does not import a different default. The existing `opencode-go` preset retains name-based registry ownership: saved custom URLs are pinned to its canonical endpoint at routing, and omitted/empty models inherit its default unless explicitly overridden (`tests/codex-integration/catalog-opencode-go-static.test.ts`).
Kiro keeps this static union as its floor and merges cached account model IDs and input limits; gathering neither refreshes tokens nor calls management, and runtime `/models` discovery remains disabled.
Without any default or configured/retained ids, the static result stays empty. The existing
forward-auth native path remains separate. Static gathering does not refresh OAuth or call
Expand Down
Loading
Loading