mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 06:18:55 +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
|
- `ghost_mode`: enabled
|
||||||
- `auto_start`: disabled
|
- `auto_start`: disabled
|
||||||
- Model list resolution: authenticated live fetch when available, with cached/default fallback.
|
- 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.
|
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 { Readable } from 'stream';
|
||||||
import { CursorExecutor } from './cursor-executor';
|
import { CursorExecutor } from './cursor-executor';
|
||||||
import { checkAuthStatus } from './cursor-auth';
|
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';
|
import type { CursorTool } from './cursor-protobuf-schema';
|
||||||
|
|
||||||
interface DaemonRuntimeOptions {
|
interface DaemonRuntimeOptions {
|
||||||
@@ -229,10 +229,10 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
|
|||||||
|
|
||||||
const parsedBody = (await readJsonBody(req)) as OpenAIChatRequest;
|
const parsedBody = (await readJsonBody(req)) as OpenAIChatRequest;
|
||||||
const messages = normalizeMessages(parsedBody.messages);
|
const messages = normalizeMessages(parsedBody.messages);
|
||||||
const model =
|
const requestedModel =
|
||||||
typeof parsedBody.model === 'string' && parsedBody.model
|
typeof parsedBody.model === 'string' && parsedBody.model.trim().length > 0
|
||||||
? parsedBody.model
|
? parsedBody.model.trim()
|
||||||
: DEFAULT_CURSOR_MODEL;
|
: undefined;
|
||||||
const stream = parsedBody.stream === true;
|
const stream = parsedBody.stream === true;
|
||||||
|
|
||||||
const authStatus = checkAuthStatus();
|
const authStatus = checkAuthStatus();
|
||||||
@@ -256,6 +256,25 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
|
|||||||
return;
|
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 abortController = new AbortController();
|
||||||
const abortOnDisconnect = () => {
|
const abortOnDisconnect = () => {
|
||||||
if (!abortController.signal.aborted && !res.writableEnded) {
|
if (!abortController.signal.aborted && !res.writableEnded) {
|
||||||
@@ -271,11 +290,7 @@ export function startCursorDaemonServer(options: DaemonRuntimeOptions): http.Ser
|
|||||||
model,
|
model,
|
||||||
stream,
|
stream,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
credentials: {
|
credentials: daemonCredentials,
|
||||||
accessToken: authStatus.credentials.accessToken,
|
|
||||||
machineId: authStatus.credentials.machineId,
|
|
||||||
ghostMode: options.ghostMode,
|
|
||||||
},
|
|
||||||
body: {
|
body: {
|
||||||
messages,
|
messages,
|
||||||
tools: Array.isArray(parsedBody.tools) ? parsedBody.tools : undefined,
|
tools: Array.isArray(parsedBody.tools) ? parsedBody.tools : undefined,
|
||||||
|
|||||||
@@ -264,10 +264,44 @@ export async function getAvailableModels(port: number): Promise<CursorModel[]> {
|
|||||||
return fetchModelsFromDaemon(port);
|
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.
|
* Get the default model.
|
||||||
* Uses GPT-5.3 Codex as default.
|
* Uses GPT-5.3 Codex as default.
|
||||||
*/
|
*/
|
||||||
export function getDefaultModel(): string {
|
export function getDefaultModel(): string {
|
||||||
return DEFAULT_CURSOR_MODEL;
|
return getCatalogDefaultModelId(DEFAULT_CURSOR_MODELS);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
fetchModelsFromCursorApi,
|
fetchModelsFromCursorApi,
|
||||||
getModelsForDaemon,
|
getModelsForDaemon,
|
||||||
clearCursorModelsCache,
|
clearCursorModelsCache,
|
||||||
|
resolveCursorRequestModel,
|
||||||
} from '../../../src/cursor/cursor-models';
|
} from '../../../src/cursor/cursor-models';
|
||||||
|
|
||||||
describe('DEFAULT_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', () => {
|
describe('detectProvider', () => {
|
||||||
it('detects anthropic models', () => {
|
it('detects anthropic models', () => {
|
||||||
expect(detectProvider('claude-4.5-sonnet')).toBe('anthropic');
|
expect(detectProvider('claude-4.5-sonnet')).toBe('anthropic');
|
||||||
|
|||||||
Reference in New Issue
Block a user