mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
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:
@@ -60,6 +60,7 @@ ccs cursor stop
|
||||
- `ghost_mode`: enabled
|
||||
- `auto_start`: disabled
|
||||
- Model list resolution: authenticated live fetch when available, with cached/default fallback.
|
||||
- Request model validation: if a requested model is not present in the available Cursor model catalog, daemon falls back to the resolved default model.
|
||||
|
||||
These values are managed in unified config and can be updated from CLI or dashboard.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
fetchModelsFromCursorApi,
|
||||
getModelsForDaemon,
|
||||
clearCursorModelsCache,
|
||||
resolveCursorRequestModel,
|
||||
} from '../../../src/cursor/cursor-models';
|
||||
|
||||
describe('DEFAULT_CURSOR_MODELS', () => {
|
||||
@@ -50,6 +51,26 @@ describe('getDefaultModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveCursorRequestModel', () => {
|
||||
it('keeps requested model when present in available models', () => {
|
||||
const resolved = resolveCursorRequestModel('claude-4.6-opus', DEFAULT_CURSOR_MODELS);
|
||||
expect(resolved).toBe('claude-4.6-opus');
|
||||
});
|
||||
|
||||
it('falls back to default when requested model is unavailable', () => {
|
||||
const resolved = resolveCursorRequestModel('non-existent-model', DEFAULT_CURSOR_MODELS);
|
||||
expect(resolved).toBe(DEFAULT_CURSOR_MODEL);
|
||||
});
|
||||
|
||||
it('falls back to first available model when default id is absent from available set', () => {
|
||||
const resolved = resolveCursorRequestModel('non-existent-model', [
|
||||
{ id: 'fallback-1', name: 'Fallback 1', provider: 'openai' },
|
||||
{ id: 'fallback-2', name: 'Fallback 2', provider: 'anthropic' },
|
||||
]);
|
||||
expect(resolved).toBe('fallback-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectProvider', () => {
|
||||
it('detects anthropic models', () => {
|
||||
expect(detectProvider('claude-4.5-sonnet')).toBe('anthropic');
|
||||
|
||||
Reference in New Issue
Block a user