Skip to content
Merged
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
5 changes: 3 additions & 2 deletions docs-site/src/content/docs/reference/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/adapters/cursor/effort-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions src/adapters/cursor/request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions src/providers/quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -72,6 +81,7 @@ export interface ProviderQuota {
monthlyPercent?: number;
monthlyResetAt?: number;
customWindows?: ProviderQuotaWindow[];
creditsUsd?: ProviderQuotaCreditsUsd;
updatedAt: number;
}

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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"]);
Expand All @@ -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(),
});
Expand Down
29 changes: 23 additions & 6 deletions tests/cursor-effort-suffix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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");
Expand Down
37 changes: 37 additions & 0 deletions tests/provider-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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) => {
Expand Down
Loading