mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 00:14:15 +00:00
fix(cursor): address fourth-round review feedback for protobuf module
This commit is contained in:
1 parent
66a93ee46f
commit
c5e8241393
3 files changed
+139
-91
No files matched your search
@@ -42,41 +42,19 @@ interface Http2Response {
|
|||||||
body: Buffer;
|
body: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detect cloud environment */
|
|
||||||
function isCloudEnv(): boolean {
|
|
||||||
if (
|
|
||||||
typeof globalThis !== 'undefined' &&
|
|
||||||
'caches' in globalThis &&
|
|
||||||
typeof (globalThis as { caches?: unknown }).caches === 'object'
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
try {
|
|
||||||
// Check for EdgeRuntime without causing compilation error
|
|
||||||
if (typeof (globalThis as { EdgeRuntime?: string }).EdgeRuntime !== 'undefined') return true;
|
|
||||||
} catch (err) {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error('[cursor] EdgeRuntime detection failed:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lazy import http2 */
|
/** Lazy import http2 */
|
||||||
let http2Module: typeof import('http2') | null = null;
|
let http2Module: typeof import('http2') | null = null;
|
||||||
async function getHttp2() {
|
async function getHttp2() {
|
||||||
if (http2Module) return http2Module;
|
if (http2Module) return http2Module;
|
||||||
if (!isCloudEnv()) {
|
try {
|
||||||
try {
|
http2Module = await import('http2');
|
||||||
http2Module = await import('http2');
|
return http2Module;
|
||||||
return http2Module;
|
} catch (err) {
|
||||||
} catch (err) {
|
if (process.env.CCS_DEBUG) {
|
||||||
if (process.env.CCS_DEBUG) {
|
console.error('[cursor] http2 module not available, falling back to fetch:', err);
|
||||||
console.error('[cursor] http2 import failed:', err);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,6 +192,10 @@ export class CursorExecutor {
|
|||||||
const delimIdx = accessToken.indexOf('::');
|
const delimIdx = accessToken.indexOf('::');
|
||||||
const cleanToken = delimIdx !== -1 ? accessToken.slice(delimIdx + 2) : accessToken;
|
const cleanToken = delimIdx !== -1 ? accessToken.slice(delimIdx + 2) : accessToken;
|
||||||
|
|
||||||
|
if (!cleanToken) {
|
||||||
|
throw new Error('Access token is empty after parsing');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authorization: `Bearer ${cleanToken}`,
|
authorization: `Bearer ${cleanToken}`,
|
||||||
'connect-accept-encoding': 'gzip',
|
'connect-accept-encoding': 'gzip',
|
||||||
@@ -318,7 +300,7 @@ export class CursorExecutor {
|
|||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
client.close();
|
client.close();
|
||||||
resolve({
|
resolve({
|
||||||
status: Number(responseHeaders[':status']),
|
status: Number(responseHeaders[':status']) || 500,
|
||||||
headers: responseHeaders,
|
headers: responseHeaders,
|
||||||
body: Buffer.concat(chunks),
|
body: Buffer.concat(chunks),
|
||||||
});
|
});
|
||||||
@@ -456,18 +438,21 @@ export class CursorExecutor {
|
|||||||
|
|
||||||
// Check for protobuf-decoded error
|
// Check for protobuf-decoded error
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
const isRateLimit =
|
||||||
|
result.error.toLowerCase().includes('rate') ||
|
||||||
|
result.error.toLowerCase().includes('limit');
|
||||||
yield {
|
yield {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
response: new Response(
|
response: new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: {
|
error: {
|
||||||
message: result.error,
|
message: result.error,
|
||||||
type: 'rate_limit_error',
|
type: isRateLimit ? 'rate_limit_error' : 'server_error',
|
||||||
code: 'rate_limited',
|
code: isRateLimit ? 'rate_limited' : 'cursor_error',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 429,
|
status: isRateLimit ? 429 : 400,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -39,9 +39,17 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
|||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
|
|
||||||
if (msg.role === 'system') {
|
if (msg.role === 'system') {
|
||||||
|
let content = '';
|
||||||
|
if (typeof msg.content === 'string') {
|
||||||
|
content = msg.content;
|
||||||
|
} else if (Array.isArray(msg.content)) {
|
||||||
|
for (const part of msg.content) {
|
||||||
|
if (part.type === 'text' && part.text) content += part.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
result.push({
|
result.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `[System Instructions]\n${msg.content}`,
|
content: `[System Instructions]\n${content}`,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
parseConnectRPCFrame,
|
parseConnectRPCFrame,
|
||||||
} from '../../../src/cursor/cursor-protobuf-decoder';
|
} from '../../../src/cursor/cursor-protobuf-decoder';
|
||||||
import { buildCursorRequest } from '../../../src/cursor/cursor-translator';
|
import { buildCursorRequest } from '../../../src/cursor/cursor-translator';
|
||||||
|
import { generateCursorBody } from '../../../src/cursor/cursor-protobuf';
|
||||||
import { CursorExecutor } from '../../../src/cursor/cursor-executor';
|
import { CursorExecutor } from '../../../src/cursor/cursor-executor';
|
||||||
import { WIRE_TYPE, FIELD } from '../../../src/cursor/cursor-protobuf-schema';
|
import { WIRE_TYPE, FIELD } from '../../../src/cursor/cursor-protobuf-schema';
|
||||||
|
|
||||||
@@ -277,6 +278,117 @@ describe('Message Translation', () => {
|
|||||||
expect(result.messages).toHaveLength(1);
|
expect(result.messages).toHaveLength(1);
|
||||||
expect(result.messages[0].content).toBe('Hello World');
|
expect(result.messages[0].content).toBe('Hello World');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle system message with array content format', () => {
|
||||||
|
const result = buildCursorRequest(
|
||||||
|
'gpt-4',
|
||||||
|
{
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: 'System instruction part 1' },
|
||||||
|
{ type: 'text', text: ' part 2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.messages).toHaveLength(1);
|
||||||
|
expect(result.messages[0].role).toBe('user');
|
||||||
|
expect(result.messages[0].content).toBe('[System Instructions]\nSystem instruction part 1 part 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Request Encoding', () => {
|
||||||
|
describe('generateCursorBody', () => {
|
||||||
|
it('should encode basic text message', () => {
|
||||||
|
const result = generateCursorBody([{ role: 'user', content: 'Hello' }], 'gpt-4', [], null);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode message with tools', () => {
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
type: 'function' as const,
|
||||||
|
function: {
|
||||||
|
name: 'get_weather',
|
||||||
|
description: 'Get weather data',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
city: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['city'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = generateCursorBody([{ role: 'user', content: 'What is the weather?' }], 'gpt-4', tools, null);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
it('should handle malformed frame gracefully', () => {
|
||||||
|
const executor = new CursorExecutor();
|
||||||
|
|
||||||
|
// Incomplete frame header (only 3 bytes instead of 5)
|
||||||
|
const incompleteFrame = Buffer.from([0x00, 0x00, 0x00]);
|
||||||
|
|
||||||
|
const result = executor.transformProtobufToJSON(incompleteFrame, 'gpt-4', {
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should return valid response even with malformed input
|
||||||
|
expect(result.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle truncated payload', () => {
|
||||||
|
const executor = new CursorExecutor();
|
||||||
|
|
||||||
|
// Frame header says payload is 100 bytes but only 5 bytes follow
|
||||||
|
const truncatedFrame = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05]);
|
||||||
|
|
||||||
|
const result = executor.transformProtobufToJSON(truncatedFrame, 'gpt-4', {
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should handle gracefully
|
||||||
|
expect(result.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multi-frame buffer', () => {
|
||||||
|
const executor = new CursorExecutor();
|
||||||
|
|
||||||
|
// Create two simple frames
|
||||||
|
const frame1 = wrapConnectRPCFrame(
|
||||||
|
encodeField(FIELD.RESPONSE_TEXT, WIRE_TYPE.LEN, 'Frame 1'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const frame2 = wrapConnectRPCFrame(
|
||||||
|
encodeField(FIELD.RESPONSE_TEXT, WIRE_TYPE.LEN, ' Frame 2'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Concatenate them
|
||||||
|
const multiFrame = Buffer.concat([Buffer.from(frame1), Buffer.from(frame2)]);
|
||||||
|
|
||||||
|
const result = executor.transformProtobufToJSON(multiFrame, 'gpt-4', {
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.status).toBe(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -482,63 +594,6 @@ describe('CursorExecutor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('transformProtobufToSSE', () => {
|
|
||||||
it('should output SSE format for simple text response', async () => {
|
|
||||||
const executor = new CursorExecutor();
|
|
||||||
|
|
||||||
// Minimal protobuf frame with text content
|
|
||||||
const textPayload = new Uint8Array([
|
|
||||||
(FIELD.MSG_CONTENT << 3) | WIRE_TYPE.LEN,
|
|
||||||
4,
|
|
||||||
...[116, 101, 115, 116], // "test"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const buffer = Buffer.from(
|
|
||||||
wrapConnectRPCFrame(textPayload, {
|
|
||||||
compress: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = executor.transformProtobufToSSE(buffer, 'test-model', {
|
|
||||||
messages: [],
|
|
||||||
stream: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.status).toBe(200);
|
|
||||||
expect(result.headers.get('Content-Type')).toBe('text/event-stream');
|
|
||||||
|
|
||||||
const body = await result.text();
|
|
||||||
expect(body).toContain('data: ');
|
|
||||||
expect(body).toContain('"object":"chat.completion.chunk"');
|
|
||||||
expect(body).toContain('data: [DONE]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle error responses in SSE format', () => {
|
|
||||||
const executor = new CursorExecutor();
|
|
||||||
|
|
||||||
// Protobuf frame with error
|
|
||||||
const errorPayload = new Uint8Array([
|
|
||||||
(FIELD.MSG_CONTENT << 3) | WIRE_TYPE.LEN,
|
|
||||||
17,
|
|
||||||
...[101, 114, 114, 111, 114, 58, 32, 116, 101, 115, 116, 32, 101, 114, 114, 111, 114], // "error: test error"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const buffer = Buffer.from(
|
|
||||||
wrapConnectRPCFrame(errorPayload, {
|
|
||||||
compress: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = executor.transformProtobufToSSE(buffer, 'test-model', {
|
|
||||||
messages: [],
|
|
||||||
stream: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error responses should still be valid
|
|
||||||
expect(result.status).toBeGreaterThanOrEqual(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
it('should return empty buffer on decompression failure', () => {
|
it('should return empty buffer on decompression failure', () => {
|
||||||
const executor = new CursorExecutor();
|
const executor = new CursorExecutor();
|
||||||
|
|||||||
Reference in new issue
Block a user