mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
fix(cursor): pin Auto to CLI --model auto instead of account default (#1819)
* fix(cursor): pin Auto to CLI --model auto instead of account default Selecting Auto was treated as unset/ACP default[] after the Default→Auto rename. Persist and spawn `auto` so the picker matches `agent --list-models`. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): keep live ACP model until Auto actually applies Pin session.model to auto for the next `--model auto` spawn, but do not claim the live backend switched when ACP only advertises default[]. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): reject live Auto unless ACP auto or spawn confirmed it Do not persist Auto when the running catalog has no auto option. Clear the spawn-auto flag after a concrete model apply so Auto → concrete → Auto cannot fake a live switch. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cursor): keep ACP wire ids spawn-safe and apply effort Cursor's parameterized picker (`clientCapabilities._meta.parameterizedModelPicker`) advertises bare model bases plus separate `fast` / `thought_level` config options. `buildCursorModelsSnapshotFromAcp` synthesized every base as `${base}[fast=${fast}]`, and Cursor rejects any bracket id whose parameter set is not that model's complete set, so 74 of the 76 published picker ids failed at ACP `session/new` with `Cannot use this model` (#1818). - Stop synthesizing wire ids: keep the advertised values verbatim (bare bases are accepted by `--model`, defaults applied). Parameter sets differ per model, so a synthesized id cannot be validated from the options alone. - Flag such catalogs as `parameterized` so variant CLI skus still attach to their base rows — the ACP apply path expresses them through config options. - Apply effort in `applyParameterizedCursorModel` (ids `effort`, `reasoning`, `reasoning_effort`), applied against the config options Cursor re-advertises for the newly selected model, and skipped on models without that axis. - Match CLI sku bases across Cursor's legacy `cursor-` family prefix (`cursor-grok-4.6-high` → ACP base `grok-4.6`) so those variant rows are not dropped. - Version the shared on-disk catalog cache (v2) so runners that already cached the rejected synthesized ids re-probe instead of serving them. Verified with live `cursor-agent 2026.09.10-fd3934a` ACP runs: the catalog no longer contains bracket ids, every advertised base and sampled variant sku spawns, and in-session `claude-opus-4-8-low` / `cursor-grok-4.6-low` set the matching `effort`/`reasoning` option. * test(cursor): cover aliased effort ids and thinking coexistence Covers the branches the HAPI bot review called out for #1818: the `xhigh` → `extra-high` alias on `gpt-5.5` (`reasoning`), the `reasoning_effort` id used by `gemini-3.8-flash`, and switching effort on a model that also advertises Claude's separate `thought_level` `thinking` toggle. * test(cursor): cover legacy sku base and post-switch effort option Adds the two apply-path cases the HAPI bot review called out for #1818: resolving `cursor-grok-4.6-high` onto an advertised `grok-4.6` base, and reading the effort option Cursor advertises for the newly selected model (`reasoning` for gpt-5.5 → `effort` for claude-opus-4-8) instead of the previous model's options. * test(cursor): create the cache dir in the migration test The version-envelope test wrote $HAPI_HOME/cache/cursor-models.json without creating the directory, so running it alone (or filtering with -t) failed with ENOENT before the assertion; it only passed because sibling tests in the same file had already created the path. Reported in the HAPI bot review of #1822. Verified: `vitest run ... -t 'ignores pre-versioned'` → 1 passed, whole file → 4 passed, and every other changed test file passes when run standalone. * Clarify Cursor Auto live-switch limitation in session pickers * Reconcile partial Cursor model changes before acknowledging Auto * Serialize Cursor model changes across clients with the existing async lock --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -877,7 +877,15 @@ export type ListAgyModelsResponse = AgyModelsResponse
|
||||
|
||||
export type CursorModelSummary = OpencodeModelSummary
|
||||
|
||||
export type CursorModelsResponse = OpencodeModelsResponse
|
||||
export type CursorModelsResponse = OpencodeModelsResponse & {
|
||||
/**
|
||||
* True when ACP advertised Cursor's parameterized model picker: bare model bases
|
||||
* plus separate `fast` / `thought_level` config options. The ACP apply path
|
||||
* expresses variant CLI skus through those options, so variant rows stay valid
|
||||
* even when the catalog itself carries no bracket wire ids.
|
||||
*/
|
||||
parameterized?: boolean
|
||||
}
|
||||
|
||||
export type ListCursorModelsResponse = CursorModelsResponse
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
CURSOR_AUTO_MODEL_ID,
|
||||
cursorCliSkuBaseId,
|
||||
cursorSpawnModelId,
|
||||
cursorModelBaseMatches,
|
||||
findBestCliSkuForAcpWire,
|
||||
isCursorAcpCatalogModelId,
|
||||
isCursorAcpWireModelId,
|
||||
isCursorAutoModelId,
|
||||
isCursorCliSkuVariantId,
|
||||
matchCliSkuToAcpWireId,
|
||||
parseCursorAvailableModelsFromRejection,
|
||||
parseCursorSkuParamHints,
|
||||
remapStaleCursorModelId
|
||||
} from './cursorCliSku';
|
||||
|
||||
@@ -19,6 +24,19 @@ const cursorGrokCatalog = [
|
||||
{ modelId: 'cursor-grok-4.5-high-fast' },
|
||||
];
|
||||
|
||||
describe('cursorSpawnModelId', () => {
|
||||
it('pins Auto as the CLI auto id and omits unset models', () => {
|
||||
expect(isCursorAutoModelId('auto')).toBe(true);
|
||||
expect(isCursorAutoModelId('default[]')).toBe(true);
|
||||
expect(isCursorAutoModelId('composer-2.5')).toBe(false);
|
||||
expect(cursorSpawnModelId('auto')).toBe(CURSOR_AUTO_MODEL_ID);
|
||||
expect(cursorSpawnModelId('default')).toBe(CURSOR_AUTO_MODEL_ID);
|
||||
expect(cursorSpawnModelId('default[]')).toBe(CURSOR_AUTO_MODEL_ID);
|
||||
expect(cursorSpawnModelId(null)).toBeNull();
|
||||
expect(cursorSpawnModelId('composer-2.5[fast=true]')).toBe('composer-2.5[fast=true]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursorCliSkuBaseId', () => {
|
||||
it('strips effort/speed suffixes from CLI skus', () => {
|
||||
expect(cursorCliSkuBaseId('gpt-5.5-high-fast')).toBe('gpt-5.5');
|
||||
@@ -273,6 +291,29 @@ describe('round-trip (regression for #883: "selected but no response")', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursorModelBaseMatches', () => {
|
||||
it('matches bases across the legacy cursor- CLI family prefix', () => {
|
||||
expect(cursorModelBaseMatches('cursor-grok-4.6', 'grok-4.6')).toBe(true);
|
||||
expect(cursorModelBaseMatches('grok-4.6', 'cursor-grok-4.6')).toBe(true);
|
||||
expect(cursorModelBaseMatches('cursor-grok-4.5', 'cursor-grok-4.5')).toBe(true);
|
||||
expect(cursorModelBaseMatches('composer-2.5', 'composer-2.5')).toBe(true);
|
||||
expect(cursorModelBaseMatches('grok-4.6', 'grok-4.5')).toBe(false);
|
||||
expect(cursorModelBaseMatches('claude-opus-4-8', 'claude-opus-5')).toBe(false);
|
||||
});
|
||||
|
||||
it('parses parameter hints from CLI sku suffixes', () => {
|
||||
expect(parseCursorSkuParamHints('cursor-grok-4.6-low')).toMatchObject({
|
||||
effort: 'low',
|
||||
fast: 'false'
|
||||
});
|
||||
expect(parseCursorSkuParamHints('gpt-5.5-extra-high-fast')).toMatchObject({
|
||||
reasoning: 'extra-high',
|
||||
fast: 'true'
|
||||
});
|
||||
expect(parseCursorSkuParamHints('composer-2.5')).toMatchObject({ fast: 'false' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCursorAcpWireModelId', () => {
|
||||
it('detects parameterized wire ids only', () => {
|
||||
expect(isCursorAcpWireModelId('gpt-5.5[fast=false]')).toBe(true);
|
||||
|
||||
@@ -8,18 +8,54 @@ export function resolveCursorLegacyModelBase(baseId: string): string {
|
||||
return CURSOR_LEGACY_MODEL_BASE_ALIASES[trimmed] ?? trimmed;
|
||||
}
|
||||
|
||||
/** CLI `agent --list-models` id for Cursor Auto (`auto - Auto`). */
|
||||
export const CURSOR_AUTO_MODEL_ID = 'auto';
|
||||
|
||||
/** ACP parameterized wire ids use bracket params; CLI `agent --list-models` slugs do not. */
|
||||
export function isCursorAcpWireModelId(modelId: string): boolean {
|
||||
const trimmed = modelId.trim();
|
||||
return trimmed === 'default[]' || trimmed.includes('[');
|
||||
}
|
||||
|
||||
/** True for CLI Auto and the legacy default aliases that must not stay as a Default option. */
|
||||
export function isCursorAutoModelId(modelId: string | null | undefined): boolean {
|
||||
const normalized = modelId?.trim().toLowerCase();
|
||||
return normalized === 'auto' || normalized === 'default' || normalized === 'default[]';
|
||||
}
|
||||
|
||||
/** Spawn `--model` value: pin Auto as `auto`. Empty/unset omits `--model`. */
|
||||
export function cursorSpawnModelId(model: string | null | undefined): string | null {
|
||||
const trimmed = model?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
return isCursorAutoModelId(trimmed) ? CURSOR_AUTO_MODEL_ID : trimmed;
|
||||
}
|
||||
|
||||
export function cursorModelBaseId(modelId: string): string {
|
||||
const trimmed = modelId.trim();
|
||||
const bracket = trimmed.indexOf('[');
|
||||
return bracket === -1 ? trimmed : trimmed.slice(0, bracket);
|
||||
}
|
||||
|
||||
function stripCursorFamilyPrefix(base: string): string {
|
||||
return base.startsWith('cursor-') ? base.slice('cursor-'.length) : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare an ACP wire base with a CLI sku base. Cursor's CLI keeps a legacy `cursor-`
|
||||
* family prefix for some models (`cursor-grok-4.6-high` vs ACP base `grok-4.6`), so a
|
||||
* plain equality check silently drops those variant rows.
|
||||
*/
|
||||
export function cursorModelBaseMatches(a: string, b: string): boolean {
|
||||
const left = a.trim();
|
||||
const right = b.trim();
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
return stripCursorFamilyPrefix(left) === stripCursorFamilyPrefix(right);
|
||||
}
|
||||
|
||||
/** Longest-first suffixes from Cursor CLI sku ids (e.g. `gpt-5.5-high-fast` → `gpt-5.5`). */
|
||||
const CLI_SKU_SUFFIXES = [
|
||||
'-extra-high-fast',
|
||||
@@ -116,6 +152,14 @@ export function parseCursorWireParams(modelId: string): Record<string, string> {
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter hints carried by a CLI `agent --list-models` sku
|
||||
* (effort/reasoning/thinking/fast suffixes).
|
||||
*/
|
||||
export function parseCursorSkuParamHints(slug: string): Record<string, string> {
|
||||
return inferSkuParamHints(slug);
|
||||
}
|
||||
|
||||
function inferSkuParamHints(slug: string): Record<string, string> {
|
||||
const lower = slug.toLowerCase();
|
||||
const hints: Record<string, string> = {};
|
||||
|
||||
Reference in New Issue
Block a user