From dbaa73823e4cc4b491ee8ad5cd5bef6e1277aa5c Mon Sep 17 00:00:00 2001 From: hffmnnj <52758545+hffmnnj@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:10:57 -0700 Subject: [PATCH 1/6] feat(models): add Grok 4.6 to the fallback model catalog Discovery via GetUsableModels remains authoritative and unchanged. This fallback entry mirrors Cursor's facing 256K default context (not the xAI direct-API 500K), matching how sibling entries mirror Cursor limits, and keeps grok-code-fast-1 intact. normalizeSingleModel needs no Grok-specific branch: it derives reasoning from thinkingDetails presence and name from displayName/aliases generically, so a discovered grok-4.6 flows through the same path as every other model. --- src/models.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models.ts b/src/models.ts index 6926693..84c49b4 100644 --- a/src/models.ts +++ b/src/models.ts @@ -59,6 +59,7 @@ const FALLBACK_MODELS: CursorModel[] = [ { id: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark", reasoning: true, contextWindow: 128_000, maxTokens: 128_000 }, // Other models { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 64_000 }, + { id: "grok-4.6", name: "Grok 4.6", reasoning: true, contextWindow: 256_000, maxTokens: 64_000 }, { id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 }, ]; From 9aa12f26b6789090371e9641bfc80315f38907cb Mon Sep 17 00:00:00 2001 From: hffmnnj <52758545+hffmnnj@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:14:06 -0700 Subject: [PATCH 2/6] feat(cost): add explicit Grok 4.6 cost metadata MODEL_COST_TABLE gains an exact grok-4.6 key (input $2 / output $6 / cached-read $0.20) so estimateModelCost resolves it via the exact lookup before the generic /grok/i pattern fallback, which previously routed every grok id to the misleadingly-named grok-4.20 key (a real but different xAI model at $1.25/$2.50 with 1M context). The config and provider model hooks emit the entry generically from Task 2.1's fallback model with limits and reasoning; attachment/image stay false (vision out of scope per SPEC section 5). --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 368500b..f67b81e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -399,6 +399,7 @@ const MODEL_COST_TABLE: Record = { "gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } }, // xAI + "grok-4.6": { input: 2, output: 6, cache: { read: 0.2, write: 0 } }, "grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } }, // Moonshot From 6e099e0f4b3fffab068e3d397f501bb03f23e43c Mon Sep 17 00:00:00 2001 From: hffmnnj <52758545+hffmnnj@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:22:29 -0700 Subject: [PATCH 3/6] test(proxy): cover Grok 4.6 catalog, cost, and routing Prove MH-4 exposure and routing with deterministic fixtures only. The fallback catalog path exposes grok-4.6 with the Cursor-facing 256K context and reasoning flag while grok-code-fast-1 stays untouched; the v2 catalog and config hooks emit the complete entry with explicit $2/$6 cost and cached-read metadata rather than relying on the generic grok pattern; and both the streaming and non-streaming request paths accept the id and route it to Cursor's Run API through the model-agnostic proxy. The fake Cursor server now records the model id of every Run request so the routing assertion observes the wire instead of proxy internals. No new scenario was needed; the capture is additive to the existing stream handler. Smoke assertion call sites grow from 94 to 127 (+35); no quarantines exist on this branch. Vision flags remain asserted false. --- test/fixtures/fake-cursor-server.ts | 55 +++++++++++- test/smoke.ts | 134 ++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/test/fixtures/fake-cursor-server.ts b/test/fixtures/fake-cursor-server.ts index 962aa68..d6fdcc5 100644 --- a/test/fixtures/fake-cursor-server.ts +++ b/test/fixtures/fake-cursor-server.ts @@ -1,7 +1,8 @@ import http2 from "node:http2"; import type { AddressInfo } from "node:net"; -import { create, toBinary } from "@bufbuild/protobuf"; +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; import { + AgentClientMessageSchema, AgentServerMessageSchema, ConversationStateStructureSchema, ExecServerMessageSchema, @@ -74,6 +75,8 @@ export interface FakeCursorServer { setScenario: (scenario: CursorScenario) => void; assertClean: () => void; close: () => Promise; + /** Model ids observed on every Run request, in arrival order. */ + getRunRequestModelIds: () => string[]; /** Test-only resource accounting hooks for proving cleanup assertions fire. */ trackChild: (pid: number) => void; untrackChild: (pid: number) => void; @@ -176,6 +179,43 @@ function isScenario(value: string): value is CursorScenario { return (CURSOR_SCENARIOS as readonly string[]).includes(value); } +/** + * Extract the model id from every Run request frame in a Connect-framed + * client payload. The proxy sends each Run request as a framed + * AgentClientMessage; heartbeats and other client messages are skipped. + */ +function decodeRunRequestModelIds(bytes: Buffer): string[] { + const ids: string[] = []; + let offset = 0; + while (offset + 5 <= bytes.length) { + const flags = bytes[offset]!; + const view = new DataView( + bytes.buffer, + bytes.byteOffset + offset, + bytes.byteLength - offset, + ); + const messageLength = view.getUint32(1, false); + const frameEnd = offset + 5 + messageLength; + if (frameEnd > bytes.length) break; + if ((flags & 0b0000_0010) === 0) { + const payload = bytes.subarray(offset + 5, frameEnd); + if (payload.length > 0) { + try { + const decoded = fromBinary(AgentClientMessageSchema, payload); + if (decoded.message.case === "runRequest") { + const modelId = + decoded.message.value.requestedModel?.modelId ?? + decoded.message.value.modelDetails?.modelId; + if (modelId) ids.push(modelId); + } + } catch {} + } + } + offset = frameEnd; + } + return ids; +} + /** * A local Connect-over-H2 server with bounded failure sequences. The request header * x-fake-cursor-scenario selects a scenario; setScenario supplies the default. @@ -187,6 +227,7 @@ export async function createFakeCursorServer(options: { deadlineMs?: number } = const streams = new Set(); const childPids = new Set(); const bridgeEntries = new Set(); + const runRequestModelIds: string[] = []; let defaultScenario: CursorScenario = "clean-end-stream"; let toolPauseCount = 0; @@ -207,6 +248,17 @@ export async function createFakeCursorServer(options: { deadlineMs?: number } = return; } + // Record which model id the proxy routed to Cursor on every Run request. + const requestChunks: Buffer[] = []; + stream.on("data", (chunk) => requestChunks.push(Buffer.from(chunk))); + stream.once("close", () => { + if (requestChunks.length > 0) { + runRequestModelIds.push( + ...decodeRunRequestModelIds(Buffer.concat(requestChunks)), + ); + } + }); + const requested = String(headers["x-fake-cursor-scenario"] ?? defaultScenario); const scenario = isScenario(requested) ? requested : defaultScenario; let completed = false; @@ -379,6 +431,7 @@ export async function createFakeCursorServer(options: { deadlineMs?: number } = return { apiUrl: `http://127.0.0.1:${port}`, setScenario(scenario) { defaultScenario = scenario; }, + getRunRequestModelIds() { return [...runRequestModelIds]; }, trackChild(pid) { childPids.add(pid); }, untrackChild(pid) { childPids.delete(pid); }, trackBridgeEntry(key) { bridgeEntries.add(key); }, diff --git a/test/smoke.ts b/test/smoke.ts index 48afb39..f9fc5b9 100644 --- a/test/smoke.ts +++ b/test/smoke.ts @@ -1207,6 +1207,139 @@ async function testDiscoveryFallbackAndSuccess( console.log("[test] Discovery fallback and success OK"); } +async function testGrok46CatalogAndRouting( + modules: TestModules, + backend: TestCursorBackend, +) { + console.log("[test] Testing Grok 4.6 catalog exposure, cost, and routing..."); + + // Fallback discovery exposes grok-4.6 with the Cursor-facing metadata and + // leaves grok-code-fast-1 untouched. + modules.clearModelCache(); + backend.setDiscoveryMode("empty"); + const fallback = await modules.getCursorModels("test-token"); + const grok46 = fallback.find((model) => model.id === "grok-4.6"); + assert(grok46, "Fallback discovery must expose grok-4.6"); + assertEqual(grok46.name, "Grok 4.6", "grok-4.6 fallback display name"); + assertEqual(grok46.reasoning, true, "grok-4.6 fallback must be reasoning-enabled"); + assertEqual(grok46.contextWindow, 256_000, "grok-4.6 fallback must use the Cursor-facing 256K context"); + assertEqual(grok46.maxTokens, 64_000, "grok-4.6 fallback max tokens"); + const grokCodeFast = fallback.find((model) => model.id === "grok-code-fast-1"); + assert(grokCodeFast, "Fallback discovery must keep grok-code-fast-1"); + assertEqual(grokCodeFast.reasoning, false, "grok-code-fast-1 must stay non-reasoning"); + assertEqual(grokCodeFast.contextWindow, 128_000, "grok-code-fast-1 must keep its 128K context"); + + // The v2 catalog hook (auth.loader -> provider.models) emits a complete + // Grok 4.6 entry: limits, reasoning flag, explicit cost, text-only caps. + const authState = { + type: "oauth" as const, + access: makeJwt(Math.floor(Date.now() / 1000) + 3600), + refresh: "valid-refresh", + expires: Date.now() + 3_600_000, + }; + const hooks = await modules.CursorAuthPlugin({ + client: { auth: { set: async () => {} } }, + } as any); + const provider = { models: {} } as any; + modules.clearModelCache(); + backend.setDiscoveryMode("empty"); + const catalogConfig = await hooks.auth!.loader(async () => authState, provider); + assert(catalogConfig.baseURL, "Catalog hook must start the proxy"); + const catalogEntry = (provider.models as Record)["grok-4.6"]; + assert(catalogEntry, "Catalog hook must emit a grok-4.6 entry"); + assertEqual(catalogEntry.name, "Grok 4.6", "Catalog entry display name"); + assertEqual(catalogEntry.capabilities.reasoning, true, "Catalog entry must advertise reasoning"); + assertEqual(catalogEntry.limit.context, 256_000, "Catalog entry context limit"); + assertEqual(catalogEntry.limit.output, 64_000, "Catalog entry output limit"); + assertEqual(catalogEntry.cost.input, 2, "grok-4.6 input cost must be $2/M, not the generic grok pattern"); + assertEqual(catalogEntry.cost.output, 6, "grok-4.6 output cost must be $6/M, not the generic grok pattern"); + assertEqual(catalogEntry.cost.cache.read, 0.2, "grok-4.6 cached-read cost must be $0.20/M"); + assertEqual(catalogEntry.cost.cache.write, 0, "grok-4.6 cache-write cost must be $0/M"); + assertEqual(catalogEntry.capabilities.attachment, false, "grok-4.6 must stay text-only for attachments"); + assertEqual(catalogEntry.capabilities.input.image, false, "grok-4.6 must stay text-only for images"); + + // The config hook (opencode >= 1.18 config schema) emits the same entry. + modules.clearModelCache(); + backend.setDiscoveryMode("empty"); + process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({ + cursor: { + type: "oauth", + refresh: "valid-refresh", + access: makeJwt(Math.floor(Date.now() / 1000) + 3600), + expires: Date.now() + 3_600_000, + }, + }); + try { + const cfg: any = { provider: {} }; + await hooks.config!(cfg); + const configEntry = cfg.provider?.cursor?.models?.["grok-4.6"]; + assert(configEntry, "Config hook must emit a grok-4.6 entry"); + assertEqual(configEntry.name, "Grok 4.6", "Config entry display name"); + assertEqual(configEntry.reasoning, true, "Config entry must advertise reasoning"); + assertEqual(configEntry.limit.context, 256_000, "Config entry context limit"); + assertEqual(configEntry.limit.output, 64_000, "Config entry output limit"); + assertEqual(configEntry.cost.input, 2, "Config entry input cost must be $2/M"); + assertEqual(configEntry.cost.output, 6, "Config entry output cost must be $6/M"); + assertEqual(configEntry.cost.cache_read, 0.2, "Config entry cached-read cost must be $0.20/M"); + assertEqual(configEntry.cost.cache_write, 0, "Config entry cache-write cost must be $0/M"); + } finally { + delete process.env.OPENCODE_AUTH_CONTENT; + } + modules.stopProxy(); + + // Both request paths accept grok-4.6 and route the id to Cursor's Run API + // through the model-agnostic proxy, with no model-specific branch. The child + // process loads the proxy with the fixture as its Cursor endpoint. + const fixture = await createFakeCursorServer({ deadlineMs: 1_000 }); + try { + fixture.setScenario("clean-end-stream"); + const routing = await runScenarioSubprocess(fixture.apiUrl, ` + const { startProxy, stopProxy } = await import("./src/proxy.ts"); + const port = await startProxy(async () => "test-token"); + const streamRes = await fetch("http://localhost:" + port + "/v1/chat/completions", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model: "grok-4.6", stream: true, messages: [{ role: "user", content: "route grok-4.6 through streaming" }] }), + }); + const streamBody = await streamRes.text(); + const nonStreamRes = await fetch("http://localhost:" + port + "/v1/chat/completions", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model: "grok-4.6", stream: false, messages: [{ role: "user", content: "route grok-4.6 through non-streaming" }] }), + }); + const nonStreamBody = await nonStreamRes.json(); + stopProxy(); + console.log(JSON.stringify({ + streamStatus: streamRes.status, + streamDone: streamBody.includes("data: [DONE]"), + streamEcho: streamBody.includes('"model":"grok-4.6"'), + nonStreamStatus: nonStreamRes.status, + nonStreamModel: nonStreamBody.model, + })); + process.exit(0); + `); + assertEqual(routing.streamStatus, 200, "Streaming path must accept grok-4.6"); + assertEqual(routing.streamDone, true, "Streaming path must complete for grok-4.6"); + assertEqual(routing.streamEcho, true, "Streaming chunks must echo the grok-4.6 model id"); + assertEqual(routing.nonStreamStatus, 200, "Non-streaming path must accept grok-4.6"); + assertEqual(routing.nonStreamModel, "grok-4.6", "Non-streaming response must echo the grok-4.6 model id"); + + const deadline = Date.now() + 500; + while ( + Date.now() < deadline && + fixture.getRunRequestModelIds().filter((id) => id === "grok-4.6").length < 2 + ) { + await Bun.sleep(10); + } + assertEqual( + fixture.getRunRequestModelIds().filter((id) => id === "grok-4.6").length, + 2, + "Both streaming and non-streaming requests must route grok-4.6 to Cursor's Run API", + ); + } finally { + await fixture.close(); + } + console.log("[test] Grok 4.6 catalog exposure, cost, and routing OK"); +} + async function main() { const backend = await createTestCursorBackend(); process.env.CURSOR_API_URL = backend.apiUrl; @@ -1235,6 +1368,7 @@ async function main() { await testLifecycleDiagnostics(modules); await testExpiredTokenRefreshBeforeDiscovery(modules, backend); await testDiscoveryFallbackAndSuccess(modules, backend); + await testGrok46CatalogAndRouting(modules, backend); await Bun.sleep(0); assert(!unhandledRejection, `Unhandled rejection escaped the smoke harness: ${String(unhandledRejection)}`); console.log("\nāœ“ All smoke tests passed"); From 8217861deccf62c0425095787c30d2adc6f0b71f Mon Sep 17 00:00:00 2001 From: hffmnnj <52758545+hffmnnj@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:24:54 -0700 Subject: [PATCH 4/6] docs(readme): document Grok 4.6 support Add a Models section covering Grok 4.6: fallback catalog registration, Cursor-facing 256K default context, Agent + Thinking reasoning, $2/$6 short-context pricing, and text-in/text-out behavior (no image/vision input is forwarded for any model, unlike the xAI direct API). --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 1003269..0191637 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,21 @@ This opens Cursor OAuth in the browser. Tokens are stored in Start OpenCode and select any Cursor model. The plugin starts a local OpenAI-compatible proxy on demand and routes requests through Cursor's gRPC API. +## Models + +Grok 4.6 is supported. The plugin registers it as `grok-4.6` in the fallback +model catalog, and live discovery returns whatever models Cursor serves. + +- Default context: 256K tokens (Cursor-facing default) +- Reasoning: supported — Cursor lists Grok 4.6 with Agent + Thinking +- Short-context pricing: $2 input / $6 output per million tokens + +The plugin proxies Cursor's Connect API, so model metadata mirrors Cursor's +published surface. The xAI direct API advertises a 500K window and accepts +image input; neither applies to this plugin, which is text-in / text-out for +every model. Image and vision input are never forwarded, so Grok 4.6 is +text-only here. + ## How it works 1. OAuth — browser-based login to Cursor via PKCE. From 9202da94fcd24c0d183ca388eb8a56f0ea8ce8b7 Mon Sep 17 00:00:00 2001 From: hffmnnj <52758545+hffmnnj@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:28:48 -0700 Subject: [PATCH 5/6] fix(models): normalize live-discovered models to known fallback limits The GetUsableModels protobuf ModelDetails carries no limit fields, so every live-discovered model previously normalized to the generic 200K/64K defaults even when the fallback catalog knew its real limits. A live grok-4.6 would therefore have been reported with a 200K context, contradicting the Cursor-facing 256K catalog and docs surface. normalizeSingleModel now consults a data-driven FALLBACK_MODEL_BY_ID map: known ids reuse their fallback contextWindow/maxTokens (and fallback name and reasoning only when discovery omits them), while discovery fields such as displayName and thinkingDetails stay authoritative where present. Unknown models keep the generic defaults, so sibling and new models are unaffected and no one-off id branch was introduced. Fallback and proxy routing are unchanged. The smoke harness previously accepted a reasoning flag on synthetic discovered models but never serialized it into the response; it now emits a thinkingDetails object for truthy values (the protobuf expresses thinking support by presence), and the Grok catalog test asserts live grok-4.6 normalizes to 256K/64K/reasoning true, unknown models keep generic defaults, and discovery thinkingDetails overrides fallback reasoning. --- src/models.ts | 25 +++++++++++++++++++++---- test/smoke.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/models.ts b/src/models.ts index 84c49b4..7b779c0 100644 --- a/src/models.ts +++ b/src/models.ts @@ -63,6 +63,16 @@ const FALLBACK_MODELS: CursorModel[] = [ { id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 }, ]; +/** + * Known catalog metadata keyed by id. The GetUsableModels protobuf carries + * no limit fields, so live-discovered models reuse these context/max-token + * (and, when discovery omits them, display/reasoning) values while keeping + * discovery fields such as displayName and thinkingDetails authoritative. + */ +const FALLBACK_MODEL_BY_ID = new Map( + FALLBACK_MODELS.map((model) => [model.id, model] as const), +); + /** * Pseudo-model for Cursor's server-side Auto routing. Always exposed * alongside discovered models; the proxy maps it to Run modelId "default". @@ -185,12 +195,19 @@ function normalizeSingleModel(model: unknown): CursorModel | null { const id = details.modelId.trim(); if (!id) return null; + const known = FALLBACK_MODEL_BY_ID.get(id); + const hasThinkingDetails = details.thinkingDetails !== undefined; + return { id, - name: pickDisplayName(details, id), - reasoning: Boolean(details.thinkingDetails), - contextWindow: DEFAULT_CONTEXT_WINDOW, - maxTokens: DEFAULT_MAX_TOKENS, + name: pickDisplayName(details, known?.name ?? id), + // Discovery fields win when present; known fallback metadata fills the + // gaps left by the limit-less GetUsableModels protobuf. + reasoning: hasThinkingDetails + ? Boolean(details.thinkingDetails) + : (known?.reasoning ?? false), + contextWindow: known?.contextWindow ?? DEFAULT_CONTEXT_WINDOW, + maxTokens: known?.maxTokens ?? DEFAULT_MAX_TOKENS, }; } diff --git a/test/smoke.ts b/test/smoke.ts index f9fc5b9..0d29680 100644 --- a/test/smoke.ts +++ b/test/smoke.ts @@ -14,6 +14,7 @@ import { KvServerMessageSchema, ModelDetailsSchema, TextDeltaUpdateSchema, + ThinkingDetailsSchema, TurnEndedUpdateSchema, } from "../src/proto/agent_pb"; import { @@ -188,6 +189,12 @@ async function createTestCursorBackend(): Promise { displayName: model.name, displayNameShort: model.name, aliases: [], + // The protobuf only expresses thinking support by + // presence, so a truthy reasoning flag emits a + // thinkingDetails object; false/absent leaves it out. + ...(model.reasoning + ? { thinkingDetails: create(ThinkingDetailsSchema, {}) } + : {}), }), ), }), @@ -1229,6 +1236,45 @@ async function testGrok46CatalogAndRouting( assertEqual(grokCodeFast.reasoning, false, "grok-code-fast-1 must stay non-reasoning"); assertEqual(grokCodeFast.contextWindow, 128_000, "grok-code-fast-1 must keep its 128K context"); + // Live discovery normalizes known models to their fallback limits: the + // GetUsableModels protobuf carries no limit fields, so a live grok-4.6 must + // still report the Cursor-facing 256K/64K with reasoning true, while the + // discovery display name stays authoritative. Unknown models keep generic + // defaults. + modules.clearModelCache(); + backend.setDiscoveryMode("success"); + backend.setDiscoveredModels([ + { id: "grok-4.6", name: "Grok 4.6" }, + { id: "unknown-live-model", name: "Unknown Live Model" }, + ]); + const live = await modules.getCursorModels("test-token"); + const liveGrok46 = live.find((model) => model.id === "grok-4.6"); + assert(liveGrok46, "Live discovery must expose grok-4.6"); + assertEqual(liveGrok46.name, "Grok 4.6", "Live grok-4.6 must use the discovery display name"); + assertEqual(liveGrok46.reasoning, true, "Live grok-4.6 must reuse fallback reasoning when discovery omits thinking details"); + assertEqual(liveGrok46.contextWindow, 256_000, "Live grok-4.6 must normalize to the Cursor-facing 256K context"); + assertEqual(liveGrok46.maxTokens, 64_000, "Live grok-4.6 must normalize to 64K max tokens"); + const unknownLive = live.find((model) => model.id === "unknown-live-model"); + assert(unknownLive, "Live discovery must expose the unknown model"); + assertEqual(unknownLive.name, "Unknown Live Model", "Unknown live model keeps its discovery display name"); + assertEqual(unknownLive.reasoning, false, "Unknown live model keeps the generic non-reasoning default"); + assertEqual(unknownLive.contextWindow, 200_000, "Unknown live model keeps the generic 200K context default"); + assertEqual(unknownLive.maxTokens, 64_000, "Unknown live model keeps the generic 64K max-tokens default"); + + // Discovery fields stay authoritative where present: thinkingDetails emitted + // by Cursor overrides the fallback reasoning flag, while known limits still + // normalize from fallback metadata. + modules.clearModelCache(); + backend.setDiscoveredModels([ + { id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: true }, + ]); + const authoritative = await modules.getCursorModels("test-token"); + const authoritativeGcf = authoritative.find((model) => model.id === "grok-code-fast-1"); + assert(authoritativeGcf, "Live discovery must expose grok-code-fast-1"); + assertEqual(authoritativeGcf.reasoning, true, "Discovery thinkingDetails must override fallback reasoning"); + assertEqual(authoritativeGcf.contextWindow, 128_000, "Known limits must still normalize from fallback metadata"); + assertEqual(authoritativeGcf.maxTokens, 64_000, "Known max tokens must still normalize from fallback metadata"); + // The v2 catalog hook (auth.loader -> provider.models) emits a complete // Grok 4.6 entry: limits, reasoning flag, explicit cost, text-only caps. const authState = { From 7affbe4fb760b28f77110859e0ff953ef476cf77 Mon Sep 17 00:00:00 2001 From: hffmnnj <52758545+hffmnnj@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:33:39 -0700 Subject: [PATCH 6/6] fix(cost): correct grok-4.6 cached-read rate to $0.50/M xAI's short-context pricing below 200K prompt tokens is $2.00 input / $0.50 cached input / $6.00 output per 1M (docs.grok-4-6-provenance.md row 20, fn_20260812_vaamzb8z). The explicit grok-4.6 entry shipped in 9aa12f2 copied the stale generic grok metadata (cache read $0.20) from the grok-4.20 fallback, which is a different xAI model at $1.25/$2.50. Correct the explicit key to cache read $0.50 and align the two Grok 4.6 deterministic cost assertions (v2 catalog and config hooks). The grok-4.20 generic fallback, all other models, routing, and limits are unchanged. --- src/index.ts | 2 +- test/smoke.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index f67b81e..f87b098 100644 --- a/src/index.ts +++ b/src/index.ts @@ -399,7 +399,7 @@ const MODEL_COST_TABLE: Record = { "gpt-5.4-nano": { input: 0.2, output: 1.25, cache: { read: 0.02, write: 0 } }, // xAI - "grok-4.6": { input: 2, output: 6, cache: { read: 0.2, write: 0 } }, + "grok-4.6": { input: 2, output: 6, cache: { read: 0.5, write: 0 } }, "grok-4.20": { input: 2, output: 6, cache: { read: 0.2, write: 0 } }, // Moonshot diff --git a/test/smoke.ts b/test/smoke.ts index 0d29680..68658fa 100644 --- a/test/smoke.ts +++ b/test/smoke.ts @@ -1299,7 +1299,7 @@ async function testGrok46CatalogAndRouting( assertEqual(catalogEntry.limit.output, 64_000, "Catalog entry output limit"); assertEqual(catalogEntry.cost.input, 2, "grok-4.6 input cost must be $2/M, not the generic grok pattern"); assertEqual(catalogEntry.cost.output, 6, "grok-4.6 output cost must be $6/M, not the generic grok pattern"); - assertEqual(catalogEntry.cost.cache.read, 0.2, "grok-4.6 cached-read cost must be $0.20/M"); + assertEqual(catalogEntry.cost.cache.read, 0.5, "grok-4.6 cached-read cost must be $0.50/M"); assertEqual(catalogEntry.cost.cache.write, 0, "grok-4.6 cache-write cost must be $0/M"); assertEqual(catalogEntry.capabilities.attachment, false, "grok-4.6 must stay text-only for attachments"); assertEqual(catalogEntry.capabilities.input.image, false, "grok-4.6 must stay text-only for images"); @@ -1326,7 +1326,7 @@ async function testGrok46CatalogAndRouting( assertEqual(configEntry.limit.output, 64_000, "Config entry output limit"); assertEqual(configEntry.cost.input, 2, "Config entry input cost must be $2/M"); assertEqual(configEntry.cost.output, 6, "Config entry output cost must be $6/M"); - assertEqual(configEntry.cost.cache_read, 0.2, "Config entry cached-read cost must be $0.20/M"); + assertEqual(configEntry.cost.cache_read, 0.5, "Config entry cached-read cost must be $0.50/M"); assertEqual(configEntry.cost.cache_write, 0, "Config entry cache-write cost must be $0/M"); } finally { delete process.env.OPENCODE_AUTH_CONTENT;