Skip to content
Open
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
29 changes: 29 additions & 0 deletions scripts/model-metadata.source.json
Original file line number Diff line number Diff line change
Expand Up @@ -81148,6 +81148,35 @@
"maxLevel": "high"
}
},
"grok-4.20-multi-agent-0309": {
"id": "grok-4.20-multi-agent-0309",
"name": "Grok 4.20 Multi-Agent (0309)",
"api": "openai-responses",
"provider": "xai",
"baseUrl": "https://api.x.ai/v1",
"reasoning": true,
"input": [
"text",
"image"
],
"cost": {
"input": 1.25,
"output": 2.5,
"cacheRead": 0.2,
"cacheWrite": 0
},
"contextWindow": 1000000,
"maxTokens": 30000,
"compat": {
"supportsImageDetailOriginal": false,
"includeEncryptedReasoning": false
Comment thread
olddonkey marked this conversation as resolved.
},
"thinking": {
"mode": "effort",
"minLevel": "minimal",
"maxLevel": "high"
}
},
"grok-4.20-multi-agent-beta-latest": {
"id": "grok-4.20-multi-agent-beta-latest",
"name": "Grok 4.20 Multi-Agent Beta",
Expand Down
4 changes: 4 additions & 0 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,7 @@ export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, R
]),
xai: new Set([
"grok-4.3",
"grok-4.20-multi-agent-0309",
"grok-4.20-0309-reasoning",
"grok-4.20-0309-non-reasoning",
"grok-build-0.1",
Expand Down Expand Up @@ -1564,6 +1565,9 @@ export async function fetchProviderModels(

export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
// xAI /models advertises both the dated deployment and this floating alias.
// Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/generated/model-metadata.ts

Large diffs are not rendered by default.

29 changes: 24 additions & 5 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ const XAI_MODELS = [
"grok-4.6",
"grok-4.5",
"grok-4.3",
"grok-4.20-multi-agent-0309",
"grok-4.20-0309-reasoning",
"grok-4.20-0309-non-reasoning",
"grok-build-0.1",
Expand Down Expand Up @@ -1148,8 +1149,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// 260709 refresh: lineup + metadata from official docs.x.ai (grok-4.5 announced 07-08);
// grok-composer-2.5-fast kept as account-verified (absent from public docs). Evidence:
// devlog/model_update/260709_model_refresh/001_xai_lineup.md.
// grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
// 260823: grok-4.20-multi-agent-0309 still returns 400 on Chat Completions, but works
// on Responses. The server reports this dated id for both it and the floating
// grok-4.20-multi-agent-beta-latest alias, so expose only the dated deployment id.
// 260813: grok-4.6 added per docs.x.ai/developers/grok-4-6. Context/vision still match
// grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
models: XAI_MODELS,
Expand All @@ -1165,9 +1167,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// than the seeded ones do.
supportsVerbosity: false,
defaultModel: "grok-4.5",
// Keep Codex Responses callers on the compatibility Chat wire until xAI can replay
// opaque reasoning continuation and compaction state across later turns. The scoped
// declaration also keeps caller-owned service tiers off the OAuth subscription route.
// Keep 4.6/4.5 Responses callers on the compatibility Chat wire until xAI can replay
// opaque reasoning continuation and compaction state across later turns. Multi-agent has
// no Chat wire, so Responses callers use its only working wire under both auth modes.
// Caller-owned service tiers stay off the unclassified OAuth subscription route; key-auth
// Fast remains proxy-owned and is still selected through keyAuthServiceTier above.
modelWireDefaults: {
"grok-4.6": {
wire: "openai-chat",
Expand All @@ -1181,6 +1185,19 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
authModes: ["oauth"],
forwardCallerServiceTier: false,
},
"grok-4.20-multi-agent-0309": {
// Even at high effort it emits no reasoning-summary deltas or encrypted replay
// material. Do not encode that as modelSupportsReasoningSummaries:false: through
// Codex #1100 that suppresses the entire reasoning object, including the effort
// that controls this model's agent count. An empty summary pane is harmless.
// Chat Completions returns 400 for this model, so every inbound uses Responses —
// `anthropic` included. Omitting it left providerModelWireDefault returning undefined
// for the Claude Messages lane, so resolveWireProtocolOverride kept xAI's provider-wide
// openai-chat adapter and sent this model to the wire it 400s on.
wire: "openai-responses",
inbound: ["responses", "chat", "anthropic"],
forwardCallerServiceTier: false,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
Expand All @@ -1192,6 +1209,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
"grok-4.6": ["text", "image"],
"grok-4.5": ["text", "image"],
"grok-4.3": ["text", "image"],
"grok-4.20-multi-agent-0309": ["text", "image"],
"grok-4.20-0309-reasoning": ["text", "image"],
"grok-4.20-0309-non-reasoning": ["text", "image"],
},
Expand All @@ -1210,6 +1228,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
"grok-4.6": 500_000,
"grok-4.5": 500_000,
"grok-4.3": 1_000_000,
"grok-4.20-multi-agent-0309": 1_000_000,
"grok-4.20-0309-reasoning": 1_000_000,
"grok-4.20-0309-non-reasoning": 1_000_000,
"grok-build-0.1": 256_000,
Expand Down
17 changes: 16 additions & 1 deletion tests/codex-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3732,13 +3732,28 @@ describe("Codex catalog routed normalization", () => {
"kimi/kimi-k2.7-code-highspeed",
"xai/grok-4.20-0309-non-reasoning",
"xai/grok-4.20-0309-reasoning",
"xai/grok-4.20-multi-agent-0309",
"xai/grok-4.3",
"xai/grok-4.5",
"xai/grok-build-0.1",
"xai/grok-composer-2.5-fast",
]);
expect(models.find(model => model.provider === "kimi" && model.id === "k3[1m]")?.contextWindow).toBe(1_048_576);
expect(models.some(model => model.id === "grok-4.20-multi-agent-0309")).toBe(false);
expect(models.find(model => model.provider === "xai" && model.id === "grok-4.20-multi-agent-0309"))
.toMatchObject({
contextWindow: 1_000_000,
inputModalities: ["text", "image"],
});
expect(models.find(model => model.provider === "xai" && model.id === "grok-4.20-multi-agent-0309")?.supportsReasoningSummaries)
.toBeUndefined();
expect(getModelMetadata("xai", "grok-4.20-multi-agent-0309")).toMatchObject({
contextWindow: 1_000_000,
maxTokens: 30_000,
input: ["text", "image"],
reasoning: true,
cost: { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
});
expect(getModelMetadata("xai", "grok-4.20-multi-agent-0309")).not.toHaveProperty("supportsReasoningSummaries");
expect(models.some(model => model.id === "configured-ghost")).toBe(false);
expect(warning.mock.calls.flat().join(" ")).not.toContain("omitted configured model ids");
} finally {
Expand Down
29 changes: 29 additions & 0 deletions tests/provider-live-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ afterEach(() => {
clearModelCache(PROVIDER);
clearModelCache(HY3_PROVIDER);
clearModelCache(HY3_CONTROL_PROVIDER);
clearModelCache("xai");
});

describe("live provider model discovery (authority + fallback)", () => {
Expand Down Expand Up @@ -79,6 +80,34 @@ describe("live provider model discovery (authority + fallback)", () => {
}
});

test("xAI live discovery hides the multi-agent beta alias and keeps the dated deployment", async () => {
globalThis.fetch = (async () => new Response(JSON.stringify({
data: [
{ id: "grok-4.20-multi-agent-0309" },
{ id: "grok-4.20-multi-agent-beta-latest" },
{ id: "grok-4.6" },
],
}), { status: 200, headers: { "content-type": "application/json" } })) as typeof fetch;

const models = await gatherRoutedModels({
providers: {
xai: withTestFetch({
baseUrl: "https://api.x.ai/v1",
adapter: "openai-chat",
authMode: "key",
apiKey: "sk-test",
liveModels: true,
models: ["grok-4.6"],
}),
},
} as unknown as OcxConfig);
const ids = models.filter(model => model.provider === "xai").map(model => model.id);

expect(ids).toContain("grok-4.20-multi-agent-0309");
expect(ids).toContain("grok-4.6");
expect(ids).not.toContain("grok-4.20-multi-agent-beta-latest");
});

test("HY3 compatibility guard hides only opencode-go/hy3-preview from live discovery", async () => {
globalThis.fetch = (async (url: string | URL | Request) => {
const isOpenCodeGo = String(url).startsWith("https://opencode-go.test/");
Expand Down
49 changes: 47 additions & 2 deletions tests/xai-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
XAI_GROK_CLIENT_VERSION,
} from "../src/providers/xai-transport";
import { getProviderRegistryEntry } from "../src/providers/registry";
import { XAI_RESPONSES_OPT_IN_MODELS } from "../src/providers/xai-responses-opt-in";
import { resolveWireProtocolOverride } from "../src/server/adapter-resolve";
import type { OcxAssistantMessage, OcxParsedRequest, OcxProviderConfig } from "../src/types";

const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
Expand Down Expand Up @@ -604,15 +606,58 @@ describe("xAI outbound compatibility headers", () => {
});

describe("xAI reasoning_content cache preservation", () => {
test("registry preset replays reasoning_content for grok reasoning models only", () => {
test("registry preset exposes multi-agent only on Responses without claiming replay material", () => {
const entry = getProviderRegistryEntry("xai");
expect(entry?.preserveReasoningContentModels).toEqual([
"grok-4.6",
"grok-4.5",
"grok-4.3",
"grok-4.20-0309-reasoning",
]);
expect(entry?.models).not.toContain("grok-4.20-multi-agent-0309");
expect(entry?.models).toContain("grok-4.20-multi-agent-0309");
expect(entry?.preserveReasoningContentModels).not.toContain("grok-4.20-multi-agent-0309");
expect(entry?.modelSupportsReasoningSummaries?.["grok-4.20-multi-agent-0309"]).toBeUndefined();
expect(resolveWireProtocolOverride(
"xai",
"grok-4.20-multi-agent-0309",
provider("oauth"),
"responses",
).adapter).toBe("openai-responses");
expect(resolveWireProtocolOverride(
"xai",
"grok-4.20-multi-agent-0309",
provider("key"),
"responses",
).adapter).toBe("openai-responses");
expect(resolveWireProtocolOverride(
"xai",
"grok-4.20-multi-agent-0309",
provider("oauth"),
"chat",
).adapter).toBe("openai-responses");
expect(resolveWireProtocolOverride(
"xai",
"grok-4.20-multi-agent-0309",
provider("key"),
"chat",
).adapter).toBe("openai-responses");
// The Claude Messages lane resolves with inbound "anthropic" (src/server/claude-messages.ts).
// It is not a spelling of "responses": an inbound missing from the allow-list makes
// providerModelWireDefault return undefined, which silently keeps xAI's provider-wide
// openai-chat adapter — the one wire this model answers with a 400.
expect(resolveWireProtocolOverride(
"xai",
"grok-4.20-multi-agent-0309",
provider("oauth"),
"anthropic",
).adapter).toBe("openai-responses");
expect(resolveWireProtocolOverride(
"xai",
"grok-4.20-multi-agent-0309",
provider("key"),
"anthropic",
).adapter).toBe("openai-responses");
expect(XAI_RESPONSES_OPT_IN_MODELS).not.toContain("grok-4.20-multi-agent-0309");
expect(entry?.models).toContain("grok-build-0.1");
for (const noReasoning of entry?.noReasoningModels ?? []) {
expect(entry?.preserveReasoningContentModels).not.toContain(noReasoning);
Expand Down
Loading