mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 22:17:14 +00:00
fix: harden cursor daemon model fallback
This commit is contained in:
@@ -23,6 +23,8 @@ export { DEFAULT_CURSOR_MODEL, DEFAULT_CURSOR_MODELS, detectProvider, formatMode
|
|||||||
|
|
||||||
const CURSOR_MODELS_API_ENDPOINT = 'https://api2.cursor.sh/v1/models';
|
const CURSOR_MODELS_API_ENDPOINT = 'https://api2.cursor.sh/v1/models';
|
||||||
const CURSOR_MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
|
const CURSOR_MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
const CURSOR_MODELS_MAX_BODY_SIZE = 1024 * 1024; // 1MB limit
|
||||||
|
const CURSOR_DAEMON_MODELS_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
let liveModelsCache: {
|
let liveModelsCache: {
|
||||||
models: CursorModel[];
|
models: CursorModel[];
|
||||||
@@ -81,6 +83,14 @@ function parseApiModelsResponse(payload: unknown): CursorModel[] | null {
|
|||||||
return models.length > 0 ? models : null;
|
return models.length > 0 ? models : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseContentLength(value: string | string[] | undefined): number | null {
|
||||||
|
const rawValue = Array.isArray(value) ? value[0] : value;
|
||||||
|
if (!rawValue) return null;
|
||||||
|
|
||||||
|
const parsed = Number.parseInt(rawValue, 10);
|
||||||
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
function getCachedLiveModels(nowMs: number = Date.now()): CursorModel[] | null {
|
function getCachedLiveModels(nowMs: number = Date.now()): CursorModel[] | null {
|
||||||
if (!liveModelsCache) return null;
|
if (!liveModelsCache) return null;
|
||||||
if (liveModelsCache.expiresAtMs <= nowMs) {
|
if (liveModelsCache.expiresAtMs <= nowMs) {
|
||||||
@@ -195,24 +205,39 @@ export async function fetchModelsFromDaemon(port: number): Promise<CursorModel[]
|
|||||||
port,
|
port,
|
||||||
path: '/v1/models',
|
path: '/v1/models',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 5000,
|
timeout: CURSOR_DAEMON_MODELS_TIMEOUT_MS,
|
||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
const MAX_BODY_SIZE = 1024 * 1024; // 1MB limit
|
const contentLength = parseContentLength(res.headers['content-length']);
|
||||||
|
if (contentLength !== null && contentLength > CURSOR_MODELS_MAX_BODY_SIZE) {
|
||||||
|
debugLog(
|
||||||
|
'Cursor daemon /v1/models content-length exceeded 1MB; falling back to defaults'
|
||||||
|
);
|
||||||
|
res.destroy();
|
||||||
|
req.destroy();
|
||||||
|
safeResolve(DEFAULT_CURSOR_MODELS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let data = '';
|
let data = '';
|
||||||
|
let bodySize = 0;
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
data += chunk;
|
if (resolved) return;
|
||||||
if (data.length > MAX_BODY_SIZE) {
|
bodySize += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
|
||||||
|
if (bodySize > CURSOR_MODELS_MAX_BODY_SIZE) {
|
||||||
debugLog('Cursor daemon /v1/models body exceeded 1MB; falling back to defaults');
|
debugLog('Cursor daemon /v1/models body exceeded 1MB; falling back to defaults');
|
||||||
res.destroy();
|
res.destroy();
|
||||||
req.destroy();
|
req.destroy();
|
||||||
safeResolve(DEFAULT_CURSOR_MODELS);
|
safeResolve(DEFAULT_CURSOR_MODELS);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data += chunk;
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
|
if (resolved) return;
|
||||||
try {
|
try {
|
||||||
const response = JSON.parse(data) as { data?: Array<{ id?: unknown }> };
|
const response = JSON.parse(data) as { data?: Array<{ id?: unknown }> };
|
||||||
if (Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ describe('fetchModelsFromDaemon', () => {
|
|||||||
it(
|
it(
|
||||||
'falls back to defaults when daemon response exceeds max body size',
|
'falls back to defaults when daemon response exceeds max body size',
|
||||||
async () => {
|
async () => {
|
||||||
const oversizedPayload = 'x'.repeat(1024 * 1024 + 1024);
|
const oversizedPayload = Buffer.alloc(1024 * 1024 + 1024, 'x');
|
||||||
const server = http.createServer((_req, res) => {
|
const server = http.createServer((_req, res) => {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(oversizedPayload);
|
res.end(oversizedPayload);
|
||||||
@@ -187,11 +187,37 @@ describe('fetchModelsFromDaemon', () => {
|
|||||||
expect(models).toEqual(DEFAULT_CURSOR_MODELS);
|
expect(models).toEqual(DEFAULT_CURSOR_MODELS);
|
||||||
} finally {
|
} finally {
|
||||||
server.closeAllConnections?.();
|
server.closeAllConnections?.();
|
||||||
|
server.closeIdleConnections?.();
|
||||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
30000
|
30000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('falls back to defaults when daemon advertises an oversized response', async () => {
|
||||||
|
const server = http.createServer((_req, res) => {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': String(1024 * 1024 + 1),
|
||||||
|
});
|
||||||
|
res.flushHeaders();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
throw new Error('Unable to resolve test server port');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = await fetchModelsFromDaemon(address.port);
|
||||||
|
expect(models).toEqual(DEFAULT_CURSOR_MODELS);
|
||||||
|
} finally {
|
||||||
|
server.closeAllConnections?.();
|
||||||
|
server.closeIdleConnections?.();
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchModelsFromCursorApi', () => {
|
describe('fetchModelsFromCursorApi', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user