From 3759c6cbb0ddee0a306bef9384ce90551bdd2848 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 7 Aug 2026 17:33:26 +0900 Subject: [PATCH 1/2] fix(cursor): preserve the Grok wire model prefix on requests (#1159) Adopted from PR #1159 by @jonathanli12, rebuilt on the current stack. Original closed in favor of this commit. Discovery normalizes Cursor's optional `cursor-` prefix so catalog matching can compare canonical ids. Requests then inherited that prefix-free form, so regular Grok 4.5 went out as `grok-4.5-{tier}` when Cursor's live discovery advertises `cursor-grok-4.5-{tier}`. The fix keeps the two paths separate rather than changing normalization: `cursorRequestWireModelIdWithEffort` composes the request-side id and leaves `cursorWireModelIdWithEffort` alone for discovery. Touching normalization would have fixed the request and broken catalog matching in the same edit. Grok Fast is untouched: it keeps the canonical `grok-4.5` model id with `effort` and `fast=true` as separate parameters. Scope note: only regular Grok 4.5 gets the prefix. This does not change Claude-family ids, and it does not address #1162 (Cursor Claude-family resource_exhausted), which has no code-level cause identified yet. Both tests confirmed to fail with the request-builder change reverted. --- .../src/content/docs/reference/adapters.md | 5 ++-- src/adapters/cursor/effort-map.ts | 11 +++++++ src/adapters/cursor/request-builder.ts | 4 +-- tests/cursor-effort-suffix.test.ts | 29 +++++++++++++++---- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 970557956d..dd6224d6c8 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -181,8 +181,9 @@ advertised effort control on those models as proof of upstream-native reasoning - Exposes Cursor Router as `cursor/auto` plus explicit `cursor/auto-cost`, `cursor/auto-balance`, and `cursor/auto-intelligence` entries. Explicit levels are encoded in `requested_model.parameters` while the legacy `cursor/auto` entry retains the account/team default. -- Keeps `cursor/grok-4.5-fast` as a selectable model while sending Cursor's canonical `grok-4.5` - model with separate `effort` and `fast=true` parameters. +- Sends regular `cursor/grok-4.5` tiers with Cursor's exact live-discovery wire ids + (`cursor-grok-4.5-low`, `-medium`, or `-high`). Keeps `cursor/grok-4.5-fast` selectable while + sending the canonical `grok-4.5` model with separate `effort` and `fast=true` parameters. - Cursor-native local filesystem/shell/network execution is denied by default. Explicit `mcpServers` and `desktopExecutor` integrations have separate opt-ins; `nativeLocalExec: "on"` enables the broader built-in executor and bypasses Codex approval/sandbox semantics, and legacy diff --git a/src/adapters/cursor/effort-map.ts b/src/adapters/cursor/effort-map.ts index a7c0b76e4b..1e937b3107 100644 --- a/src/adapters/cursor/effort-map.ts +++ b/src/adapters/cursor/effort-map.ts @@ -126,3 +126,14 @@ export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: s } return `${baseModelId}-${effortSuffix}`; } + +/** + * Compose the exact flattened id sent by AgentService/Run. Discovery normalizes Cursor's optional + * `cursor-` prefix only for catalog matching, but regular Grok 4.5 requests require that prefix on + * the wire. Keep this separate from {@link cursorWireModelIdWithEffort} so discovery can continue + * comparing canonical, prefix-free ids. Grok Fast uses requested_model parameters instead. + */ +export function cursorRequestWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string { + const flattened = cursorWireModelIdWithEffort(baseModelId, effortSuffix); + return baseModelId === "grok-4.5" ? `cursor-${flattened}` : flattened; +} diff --git a/src/adapters/cursor/request-builder.ts b/src/adapters/cursor/request-builder.ts index a28a32acb6..b60f49fffe 100644 --- a/src/adapters/cursor/request-builder.ts +++ b/src/adapters/cursor/request-builder.ts @@ -10,7 +10,7 @@ import type { import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types"; import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types"; import { cursorWireModelSelection, type CursorRoutingLevel } from "./discovery"; -import { cursorEffortSuffix, cursorWireModelIdWithEffort } from "./effort-map"; +import { cursorEffortSuffix, cursorRequestWireModelIdWithEffort } from "./effort-map"; import { cursorMcpToolEncodedSize, cursorMcpToolsEncodedSize, @@ -151,7 +151,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): { ], }; } - return { ...selection, modelId: suffix ? cursorWireModelIdWithEffort(id, suffix) : id }; + return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id }; } function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined { diff --git a/tests/cursor-effort-suffix.test.ts b/tests/cursor-effort-suffix.test.ts index 218a06eef7..2d4c627f0a 100644 --- a/tests/cursor-effort-suffix.test.ts +++ b/tests/cursor-effort-suffix.test.ts @@ -3,6 +3,15 @@ import { createCursorRequest } from "../src/adapters/cursor/request-builder"; import { cursorEffortSuffix, cursorModelEffortLadder } from "../src/adapters/cursor/effort-map"; import type { OcxParsedRequest } from "../src/types"; +// Static fixture recorded from Cursor GetUsableModels on 2026-08-06. This pins the +// exact wire ids observed during the incident; live availability normalization is +// covered separately in cursor-discovery.test.ts. +const RECORDED_CURSOR_GROK_45_DISCOVERY_IDS = [ + "cursor-grok-4.5-low", + "cursor-grok-4.5-medium", + "cursor-grok-4.5-high", +] as const; + function modelIdFor(modelId: string, reasoning?: string): string { const parsed: OcxParsedRequest = { modelId, @@ -84,13 +93,13 @@ describe("Cursor per-model reasoning-effort suffix", () => { }); test("grok-4.5 uses current tiers and sends Fast as a separate model parameter", () => { - expect(modelIdFor("cursor/grok-4.5", "low")).toBe("grok-4.5-low"); - expect(modelIdFor("cursor/grok-4.5", "medium")).toBe("grok-4.5-medium"); - expect(modelIdFor("cursor/grok-4.5", "high")).toBe("grok-4.5-high"); - expect(modelIdFor("cursor/grok-4.5", "xhigh")).toBe("grok-4.5-high"); - expect(modelIdFor("cursor/grok-4.5")).toBe("grok-4.5-high"); + expect(modelIdFor("cursor/grok-4.5", "low")).toBe("cursor-grok-4.5-low"); + expect(modelIdFor("cursor/grok-4.5", "medium")).toBe("cursor-grok-4.5-medium"); + expect(modelIdFor("cursor/grok-4.5", "high")).toBe("cursor-grok-4.5-high"); + expect(modelIdFor("cursor/grok-4.5", "xhigh")).toBe("cursor-grok-4.5-high"); + expect(modelIdFor("cursor/grok-4.5")).toBe("cursor-grok-4.5-high"); expect(selectionFor("cursor/grok-4.5", "high")).toEqual({ - modelId: "grok-4.5-high", + modelId: "cursor-grok-4.5-high", parameters: undefined, }); expect(selectionFor("cursor/grok-4.5-fast", "low")).toEqual({ @@ -118,6 +127,14 @@ describe("Cursor per-model reasoning-effort suffix", () => { expect(cursorModelEffortLadder("grok-4.5-fast")).toEqual(["low", "medium", "high"]); }); + test("regular grok-4.5 request ids match the recorded discovery fixture", () => { + for (const effort of ["low", "medium", "high"] as const) { + const requestModelId = modelIdFor("cursor/grok-4.5", effort); + expect(requestModelId).toBe(`cursor-grok-4.5-${effort}`); + expect(RECORDED_CURSOR_GROK_45_DISCOVERY_IDS).toContain(requestModelId); + } + }); + test("kimi-k3 maps to its live effort-suffixed variants", () => { expect(modelIdFor("cursor/kimi-k3", "low")).toBe("kimi-k3-low"); expect(modelIdFor("cursor/kimi-k3", "medium")).toBe("kimi-k3-high"); From aabee91d88b2f0eef8e74e991c16d1b46dad7845 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Fri, 7 Aug 2026 17:33:59 +0900 Subject: [PATCH 2/2] fix(quota): report unlimited A6API keys instead of hiding them (#1171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopted from PR #1171 by @byongshintv, rebuilt on the current stack. Original closed in favor of this commit. An unlimited A6API key reports zero finite credit totals. Finite-total validation then treated that as a terminal failure and returned before the key could be represented at all, so a perfectly working key looked dead in the dashboard. The unlimited branch now runs ahead of that validation and emits the generic `customWindows` row the GUI and CLI already consume, preserving expiry. It accepts `true`, `1`, and `"true"` for the upstream flag. Two known limitations, stated rather than discovered later: `creditsUsd` and its expiry are not yet surfaced by the GUI — visibility comes from `customWindows` — and neither changes existing behavior for finite keys. Confirmed to fail with the unlimited branch disabled. --- src/providers/quota.ts | 40 ++++++++++++++++++++++++++++++++++++ tests/provider-quota.test.ts | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 3ff9396c8f..01516340ab 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -64,6 +64,15 @@ export interface ProviderQuotaWindow { resetAt?: number; } +export interface ProviderQuotaCreditsUsd { + used: number; + limit: number; + remaining: number; + percent: number; + expiresAt?: number; + unlimited?: boolean; +} + export interface ProviderQuota { fiveHourPercent?: number; fiveHourResetAt?: number; @@ -72,6 +81,7 @@ export interface ProviderQuota { monthlyPercent?: number; monthlyResetAt?: number; customWindows?: ProviderQuotaWindow[]; + creditsUsd?: ProviderQuotaCreditsUsd; updatedAt: number; } @@ -203,6 +213,8 @@ function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is Provide return typeof quota.fiveHourPercent === "number" || typeof quota.weeklyPercent === "number" || typeof quota.monthlyPercent === "number" + || quota.creditsUsd?.unlimited === true + || typeof quota.creditsUsd?.percent === "number" || !!quota.customWindows?.some(window => typeof window.percent === "number"); } @@ -340,6 +352,27 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro } const subscription = a6apiPayload(await subscriptionResponse.json().catch(() => null)); const token = a6apiPayload(await tokenResponse.json().catch(() => null)); + const unlimited = token?.unlimited_quota === true + || token?.unlimited_quota === 1 + || token?.unlimited_quota === "true"; + const normalizedExpiry = normalizeResetAt(token?.expires_at); + const expiry = normalizedExpiry && normalizedExpiry > 0 + ? { expiresAt: normalizedExpiry } + : {}; + if (unlimited) { + return report(provider, "a6api:billing", { + creditsUsd: { + used: 0, + limit: 0, + remaining: 0, + percent: 0, + unlimited: true, + ...expiry, + }, + customWindows: [{ label: "Unlimited API credits", percent: 0 }], + updatedAt: Date.now(), + }); + } const limitUsd = firstFinite(subscription, ["hard_limit_usd"]); const grantedUnits = firstFinite(token, ["total_granted"]); const usedUnits = firstFinite(token, ["total_used"]); @@ -362,6 +395,13 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro if (percent === undefined) return TERMINAL_QUOTA_FAILURE; const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`; return report(provider, "a6api:billing", { + creditsUsd: { + used: usedUsd, + limit: limitUsd, + remaining: remainingUsd, + percent, + ...expiry, + }, customWindows: [{ label, percent }], updatedAt: Date.now(), }); diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 873a7af548..4a0ed3b1cb 100644 --- a/tests/provider-quota.test.ts +++ b/tests/provider-quota.test.ts @@ -282,6 +282,13 @@ describe("fetchProviderQuotaReports", () => { label: "API credits ($15.00 of $20.00 remaining)", percent: 25, }]); + expect(result.reports[0]?.quota.creditsUsd).toEqual({ + used: 5, + limit: 20, + remaining: 15, + percent: 25, + expiresAt: Date.parse("2026-08-01T00:00:00Z"), + }); expect(seen.map(row => row.url).sort()).toEqual([ "https://api.a6api.com/api/usage/token/", "https://api.a6api.com/dashboard/billing/subscription", @@ -290,6 +297,36 @@ describe("fetchProviderQuotaReports", () => { expect(seen.every(row => row.redirect === "error")).toBe(true); }); + test("A6API unlimited keys remain visible even when all finite credit totals are zero", async () => { + globalThis.fetch = (async (input: RequestInfo | URL) => new Response(JSON.stringify( + String(input).includes("subscription") + ? { data: { hard_limit_usd: 100_000_000 } } + : { data: { + total_granted: 0, + total_used: 0, + total_available: 0, + unlimited_quota: true, + expires_at: "2027-01-01T00:00:00Z", + } }, + ), { status: 200 })) as typeof fetch; + + const result = await fetchProviderQuotaReports(a6apiOnlyConfig(), true); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.quota.creditsUsd).toEqual({ + used: 0, + limit: 0, + remaining: 0, + percent: 0, + unlimited: true, + expiresAt: Date.parse("2027-01-01T00:00:00Z"), + }); + expect(result.reports[0]?.quota.customWindows).toEqual([{ + label: "Unlimited API credits", + percent: 0, + }]); + }); + test("A6API quota never sends API keys to a non-canonical base URL", async () => { const seen: string[] = []; globalThis.fetch = (async (input: RequestInfo | URL) => {