Merge PR #8988: fix(catalog): collapse Cursor Grok 4.5/4.6 effort siblings (@roboomp)

This commit is contained in:
can1357
2026-08-19 11:59:55 +02:00
5 changed files with 168 additions and 35 deletions
+3
View File
@@ -4,6 +4,8 @@
### Added
- Added a Cursor variant-collapse table folding the per-effort Grok siblings (`cursor-grok-4.5` low/medium/high and `cursor-grok-4.6` low/medium/high/xhigh, plus their `-fast` service-tier lanes) into one logical model per lane with effort routing onto the live wire ids, matching Devin's `grok-4-5` collapse ([#8803](https://github.com/can1357/oh-my-pi/issues/8803)).
- Regenerated the Cursor agent protobufs to model hosted WebFetch permission queries (`interaction_query` / `interaction_response` field 9) and the matching `ToolCall` variant (field 37).
### Fixed
@@ -17,6 +19,7 @@
- ChatGPT Codex discovery that advertises only worker `-wm` SKUs now also registers the plain model route, so a configured `openai-codex/<model>` keeps resolving instead of fuzzy-falling-back to the `-wm` SKU some accounts reject.
- Fixed `opencode-go/muse-spark-1.2` (and `muse-spark-1.2-contributor`) failing every tool-call turn with `OpenAI completions stream closed before a finish_reason was received`. The Go gateway serves these ids only at `/zen/go/v1/responses`, but the `/zen/go/v1/models` discovery omits the `provider.npm` hint, so the resolver fell through to `openai-completions`; both ids are now pinned to `openai-responses` like `deepseek-v4-flash` ([#8957](https://github.com/can1357/oh-my-pi/issues/8957)).
- Fixed GitHub Copilot `grok-4.6` / `grok-4.6-1m` failing with HTTP 400 `unsupported_api_for_model` by routing them through the OpenAI Responses API (`/responses`) instead of `/chat/completions`, matching `grok-4.5`. Stale cached completion routes are invalidated on refresh ([#8807](https://github.com/can1357/oh-my-pi/issues/8807)).
- Fixed Cursor Grok 4.5/4.6 discovery classifying the versioned ids as non-reasoning: `GetUsableModels` ships no `thinkingDetails` and the bundled references read `reasoning: false`, so the picker hid the effort ladder. Discovery now marks `cursor-grok-<version>` ids as reasoning models (the non-reasoning `grok-code-*` ids stay out) ([#8803](https://github.com/can1357/oh-my-pi/issues/8803)).
## [17.3.6] - 2026-08-17
+14 -1
View File
@@ -28,6 +28,15 @@ const CURSOR_MAX_MODE_1M_ID_PATTERN = /claude|gemini/;
/** Kimi's official bare K3 id (`k3`, `kimi/k3`); `k3-256k` is the 256k SKU and stays out. */
const CURSOR_KIMI_K3_BARE_ID_PATTERN = /(^|\/)k3$/i;
/**
* Versioned Cursor Grok ids (`cursor-grok-4.5`, `cursor-grok-4.6-high`) are
* reasoning models whose effort is carried in the per-tier sibling id.
* `GetUsableModels` ships no `thinkingDetails` and the bundled references read
* `reasoning: false`, so classification falls back to the id. The non-reasoning
* `grok-code-*` coding models lack the version digit and stay out.
*/
const CURSOR_GROK_REASONING_ID_PATTERN = /^cursor-grok-\d/i;
/**
* Model-id families whose native catalogs (anthropic, openai/openai-codex,
* google) are multimodal. Cursor-only or text-only families (`composer-*`,
@@ -297,7 +306,11 @@ function normalizeCursorModel(
const name = pickModelDisplayName(details, id);
const reference = references.get(id);
const reasoning = isKimiK3ModelId(id) || Boolean(details.thinkingDetails) || reference?.reasoning === true;
const reasoning =
isKimiK3ModelId(id) ||
CURSOR_GROK_REASONING_ID_PATTERN.test(id) ||
Boolean(details.thinkingDetails) ||
reference?.reasoning === true;
if (reference) {
return {
+67 -34
View File
@@ -111,19 +111,20 @@ function thinkingPair(baseId: string, name: string): EffortVariantFamily {
};
}
type DevinTierRoutes = Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string>>;
type TierRoutes = Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string>>;
/** Devin families with a `-max` sibling: five wire tiers, `low` floor. */
const DEVIN_FIVE_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
/** Pre-5.6 Devin GPT families top out at `-xhigh`: four wire tiers, `low` floor. */
const DEVIN_FOUR_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
function devinTierFamily(
id: string,
name: string,
routes: DevinTierRoutes,
efforts: readonly Effort[],
): EffortVariantFamily {
/**
* Build one effort-tier family from a tier→wire-id map: routing keeps only the
* listed efforts, `members` dedupes the targets in tier order, and thinking is
* `mode: "effort"` (mandatory when the family has no `off` route). Shared by the
* Devin and Cursor tables, whose per-effort siblings follow the same shape.
*/
function tierFamily(id: string, name: string, routes: TierRoutes, efforts: readonly Effort[]): EffortVariantFamily {
const routing: Partial<Record<Effort | "off", string>> = {};
if (routes.off) routing.off = routes.off;
for (const effort of efforts) {
@@ -177,7 +178,7 @@ function devinTierFamily(
function devinGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] {
const base = `gpt-5-6-${variant}`;
return [
devinTierFamily(
tierFamily(
base,
name,
{
@@ -190,7 +191,7 @@ function devinGpt56Families(variant: "luna" | "sol" | "terra", name: string): re
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
`${base}-fast`,
`${name} Fast`,
{
@@ -395,7 +396,7 @@ export const GEMINI_CLI_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
};
export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
families: [
devinTierFamily(
tierFamily(
"claude-opus-5",
"Claude Opus 5",
{
@@ -407,7 +408,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-opus-5-fast",
"Claude Opus 5 Fast",
{
@@ -419,7 +420,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-fable-5",
"Claude Fable 5",
{
@@ -431,7 +432,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-sonnet-5",
"Claude Sonnet 5",
{
@@ -443,7 +444,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-opus-4-7",
"Claude Opus 4.7",
{
@@ -455,7 +456,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-opus-4-7-fast",
"Claude Opus 4.7 Fast",
{
@@ -467,7 +468,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-opus-4-8",
"Claude Opus 4.8",
{
@@ -479,7 +480,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"claude-opus-4-8-fast",
"Claude Opus 4.8 Fast",
{
@@ -491,7 +492,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-2",
"GPT-5.2",
{
@@ -503,7 +504,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-3-codex",
"GPT-5.3 Codex",
{
@@ -514,7 +515,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-3-codex-fast",
"GPT-5.3 Codex Fast",
{
@@ -525,7 +526,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-4",
"GPT-5.4",
{
@@ -537,7 +538,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-4-fast",
"GPT-5.4 Fast",
{
@@ -549,7 +550,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-4-mini",
"GPT-5.4 Mini",
{
@@ -560,7 +561,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-5",
"GPT-5.5",
{
@@ -572,7 +573,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FOUR_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gpt-5-5-fast",
"GPT-5.5 Fast",
{
@@ -587,7 +588,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
...devinGpt56Families("luna", "GPT-5.6 Luna"),
...devinGpt56Families("sol", "GPT-5.6 Sol"),
...devinGpt56Families("terra", "GPT-5.6 Terra"),
devinTierFamily(
tierFamily(
"kimi-k3",
"Kimi K3",
{
@@ -597,7 +598,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
[Effort.Low, Effort.High, Effort.Max],
),
devinTierFamily(
tierFamily(
"swe-1-7",
"SWE-1.7",
{
@@ -606,7 +607,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
[Effort.Medium, Effort.Max],
),
devinTierFamily(
tierFamily(
"grok-4-5",
"Grok 4.5",
{
@@ -616,7 +617,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
[Effort.Low, Effort.Medium, Effort.High],
),
devinTierFamily(
tierFamily(
"inkling",
"Inkling",
{
@@ -629,7 +630,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
DEVIN_FIVE_TIER_EFFORTS,
),
devinTierFamily(
tierFamily(
"gemini-3-1-pro",
"Gemini 3.1 Pro",
{
@@ -638,7 +639,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
[Effort.Low, Effort.High],
),
devinTierFamily(
tierFamily(
"gemini-3-5-flash",
"Gemini 3.5 Flash",
{
@@ -649,7 +650,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
),
devinTierFamily(
tierFamily(
"gemini-3-6-flash",
"Gemini 3.6 Flash",
{
@@ -660,7 +661,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
[Effort.Minimal, Effort.Low, Effort.Medium, Effort.High],
),
devinTierFamily(
tierFamily(
"gemini-3-flash",
"Gemini 3 Flash",
{
@@ -694,7 +695,7 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
},
// GLM-5.2 1M — paid variants that consume weekly quota. Collapse the
// three 1M-context variants into one entry with proper effort routing.
devinTierFamily(
tierFamily(
"glm-5-2-1m",
"GLM-5.2 1M",
{
@@ -707,11 +708,43 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
],
};
/** Cursor's Grok tier tokens equal the effort id, so routing is a direct map. */
const CURSOR_GROK_45_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
const CURSOR_GROK_46_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
/**
* Cursor serves Grok 4.5/4.6 as per-effort sibling ids
* (`cursor-grok-4.6-low|-medium|-high|-xhigh`) alongside a parallel `-fast`
* service-tier lane (`-low-fast`, …). Each lane collapses into its own logical
* model — the `-fast` axis is a sibling family, never a second routing
* dimension — with effort routing onto the live sibling wire ids.
*/
function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]): readonly EffortVariantFamily[] {
const build = (fast: boolean): EffortVariantFamily => {
const suffix = fast ? "-fast" : "";
const routes: TierRoutes = {};
for (const effort of efforts) {
routes[effort] = `cursor-grok-${version}-${effort}${suffix}`;
}
return tierFamily(`cursor-grok-${version}${suffix}`, `Grok ${version}${fast ? " Fast" : ""}`, routes, efforts);
};
return [build(false), build(true)];
}
/** `cursor` Grok families: per-effort siblings collapsed per service-tier lane. */
export const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
families: [
...cursorGrokFamilies("4.5", CURSOR_GROK_45_EFFORTS),
...cursorGrokFamilies("4.6", CURSOR_GROK_46_EFFORTS),
],
};
/** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */
export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = {
"google-antigravity": ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
"google-gemini-cli": GEMINI_CLI_VARIANT_COLLAPSE_TABLE,
devin: DEVIN_VARIANT_COLLAPSE_TABLE,
cursor: CURSOR_VARIANT_COLLAPSE_TABLE,
};
/**
@@ -25,6 +25,10 @@ const FIXTURE_MODEL_IDS = [
"kimi-k3-low",
"kimi-k3-max",
"grok-code-fast-2",
// Versioned Cursor Grok siblings: bundled references read reasoning:false,
// but the id marks them reasoning.
"cursor-grok-4.5-high",
"cursor-grok-4.6-xhigh",
// Bundled-reference ids: the reference stays authoritative.
"claude-4.5-opus-high",
"claude-4.6-opus-high",
@@ -92,6 +96,14 @@ describe("cursor discovery input modalities (issue #4726)", () => {
expect(byId.get("kimi-k3-max")?.reasoning).toBe(true);
});
it("marks versioned Cursor Grok ids as reasoning despite reasoning:false references (issue #8803)", async () => {
const byId = await discover();
expect(byId.get("cursor-grok-4.5-high")?.reasoning).toBe(true);
expect(byId.get("cursor-grok-4.6-xhigh")?.reasoning).toBe(true);
// grok-code-* coding models lack the version digit and stay non-reasoning.
expect(byId.get("grok-code-fast-2")?.reasoning).toBe(false);
});
it("keeps bundled references authoritative for input modalities", async () => {
const byId = await discover();
// Bundled cursor references carry their own input classification; the
@@ -15,6 +15,7 @@ import { googleGeminiCliModelManagerOptions } from "@oh-my-pi/pi-catalog/provide
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
import {
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
CURSOR_VARIANT_COLLAPSE_TABLE,
collapseEffortVariants,
collapseEffortVariantsAcrossProviders,
DEVIN_VARIANT_COLLAPSE_TABLE,
@@ -656,6 +657,77 @@ describe("Devin tier routing", () => {
});
});
describe("Cursor Grok tier routing (issue #8803)", () => {
function cursorMemberSpec(id: string): ModelSpec<"cursor-agent"> {
return {
id,
name: id,
api: "cursor-agent",
provider: "cursor",
baseUrl: "https://api2.cursor.sh",
// Live GetUsableModels + bundled references mark these reasoning:false;
// collapse must still recognize the effort ladder.
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 64_000,
};
}
const RAW_SIBLINGS = [
"cursor-grok-4.5-high",
"cursor-grok-4.5-high-fast",
"cursor-grok-4.5-low",
"cursor-grok-4.5-low-fast",
"cursor-grok-4.5-medium",
"cursor-grok-4.5-medium-fast",
"cursor-grok-4.6-high",
"cursor-grok-4.6-high-fast",
"cursor-grok-4.6-low",
"cursor-grok-4.6-low-fast",
"cursor-grok-4.6-medium",
"cursor-grok-4.6-medium-fast",
"cursor-grok-4.6-xhigh",
"cursor-grok-4.6-xhigh-fast",
];
it("collapses the 14 effort siblings into four logical models, split by the -fast lane", () => {
const collapsed = collapseEffortVariants(RAW_SIBLINGS.map(cursorMemberSpec), CURSOR_VARIANT_COLLAPSE_TABLE);
expect(collapsed.map(model => model.id).sort()).toEqual([
"cursor-grok-4.5",
"cursor-grok-4.5-fast",
"cursor-grok-4.6",
"cursor-grok-4.6-fast",
]);
const g46 = collapsed.find(model => model.id === "cursor-grok-4.6");
if (!g46) throw new Error("cursor-grok-4.6 did not collapse");
expect(g46.name).toBe("Grok 4.6");
// Effort route forces reasoning even though every member said false.
expect(g46.reasoning).toBe(true);
expect(g46.thinking?.mode).toBe("effort");
expect(g46.thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High, Effort.XHigh]);
expect(g46.thinking?.requiresEffort).toBe(true);
});
it("routes each user effort onto the live sibling wire id per service-tier lane", () => {
const collapsed = collapseEffortVariants(RAW_SIBLINGS.map(cursorMemberSpec), CURSOR_VARIANT_COLLAPSE_TABLE);
const model = (id: string) => {
const found = collapsed.find(m => m.id === id);
if (!found) throw new Error(`${id} did not collapse`);
return buildModel(found as ModelSpec<"cursor-agent">);
};
expect(resolveWireModelId(model("cursor-grok-4.6"), Effort.XHigh)).toBe("cursor-grok-4.6-xhigh");
expect(resolveWireModelId(model("cursor-grok-4.6"), Effort.Low)).toBe("cursor-grok-4.6-low");
expect(resolveWireModelId(model("cursor-grok-4.6-fast"), Effort.High)).toBe("cursor-grok-4.6-high-fast");
expect(resolveWireModelId(model("cursor-grok-4.5"), Effort.Medium)).toBe("cursor-grok-4.5-medium");
// 4.5 has no xhigh sibling: the ceiling stays at high.
expect(model("cursor-grok-4.5").thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High]);
});
});
describe("variant aliases", () => {
it("resolves members and recycled ids per provider", () => {
expect(resolveVariantAlias("google-antigravity", "gemini-3.5-flash-low")).toBe("gemini-3.5-flash");