diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 970557956..dd6224d6c 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 a7c0b76e4..1e937b310 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 a28a32acb..b60f49fff 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/src/providers/quota.ts b/src/providers/quota.ts index 3ff9396c8..01516340a 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/cursor-effort-suffix.test.ts b/tests/cursor-effort-suffix.test.ts index 218a06eef..2d4c627f0 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"); diff --git a/tests/provider-quota.test.ts b/tests/provider-quota.test.ts index 873a7af54..4a0ed3b1c 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) => {