fix(cursor): fallback when requested model is unavailable

- resolve request model against current available catalog

- choose safe default when configured default id is not present

- add tests and docs for daemon request-model fallback behavior
This commit is contained in:
Tam Nhu Tran committed 2026-02-17 20:53:03 +07:00
1 parent 9031e5a085
commit fae8716900
4 files changed
+82 -11

No files matched your search

+25 -10
View File
@@ -8,7 +8,7 @@ import * as http from 'http';
import { Readable } from 'stream';
import { CursorExecutor } from './cursor-executor';
import { checkAuthStatus } from './cursor-auth';
import { DEFAULT_CURSOR_MODEL, getModelsForDaemon } from './cursor-models';
import { getModelsForDaemon, resolveCursorRequestModel } from './cursor-models';
import type { CursorTool } from './cursor-protobuf-schema';
interface DaemonRuntimeOptions {
@@ -229,10 +229,10 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
const parsedBody = (await readJsonBody(req)) as OpenAIChatRequest;
const messages = normalizeMessages(parsedBody.messages);
const model =
typeof parsedBody.model === 'string' && parsedBody.model
? parsedBody.model
: DEFAULT_CURSOR_MODEL;
const requestedModel =
typeof parsedBody.model === 'string' && parsedBody.model.trim().length > 0
? parsedBody.model.trim()
: undefined;
const stream = parsedBody.stream === true;
const authStatus = checkAuthStatus();
@@ -256,6 +256,25 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
return;
}
const daemonCredentials = {
accessToken: authStatus.credentials.accessToken,
machineId: authStatus.credentials.machineId,
ghostMode: options.ghostMode,
};
const availableModels = await getModelsForDaemon({
credentials: daemonCredentials,
});
const model = resolveCursorRequestModel(requestedModel, availableModels);
if (
requestedModel &&
requestedModel !== model &&
(process.env.CCS_DEBUG === '1' || process.env.CCS_DEBUG === 'true')
) {
console.error(
`[cursor] Requested model "${requestedModel}" is unavailable; falling back to "${model}".`
);
}
const abortController = new AbortController();
const abortOnDisconnect = () => {
if (!abortController.signal.aborted && !res.writableEnded) {
@@ -271,11 +290,7 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
model,
stream,
signal: abortController.signal,
credentials: {
accessToken: authStatus.credentials.accessToken,
machineId: authStatus.credentials.machineId,
ghostMode: options.ghostMode,
},
credentials: daemonCredentials,
body: {
messages,
tools: Array.isArray(parsedBody.tools) ? parsedBody.tools : undefined,
+35 -1
View File
@@ -264,10 +264,44 @@ export async function getAvailableModels(port: number): Promise<CursorModel[]> {
return fetchModelsFromDaemon(port);
}
function getCatalogDefaultModelId(availableModels: CursorModel[]): string {
if (availableModels.some((model) => model.id === DEFAULT_CURSOR_MODEL)) {
return DEFAULT_CURSOR_MODEL;
}
const explicitDefault = availableModels.find((model) => model.isDefault)?.id;
if (explicitDefault) {
return explicitDefault;
}
const firstAvailable = availableModels.find(
(model) => typeof model.id === 'string' && model.id.trim().length > 0
)?.id;
return firstAvailable || DEFAULT_CURSOR_MODEL;
}
export function resolveCursorRequestModel(
requestedModel: string | null | undefined,
availableModels: CursorModel[]
): string {
const fallbackModel = getCatalogDefaultModelId(availableModels);
const normalizedRequested = typeof requestedModel === 'string' ? requestedModel.trim() : '';
if (!normalizedRequested) {
return fallbackModel;
}
if (availableModels.some((model) => model.id === normalizedRequested)) {
return normalizedRequested;
}
return fallbackModel;
}
/**
* Get the default model.
* Uses GPT-5.3 Codex as default.
*/
export function getDefaultModel(): string {
return DEFAULT_CURSOR_MODEL;
return getCatalogDefaultModelId(DEFAULT_CURSOR_MODELS);
}