a8ca88797a
- Store/sanitize upstreamHeaders; shared forbidden header names (upstreamHeaders.ts) - chatCore: buildUpstreamHeadersForExecute; T5 recomputes; 401 retry uses translatedBody.model - Dashboard compat popover + i18n; Zod partialRecord + header value newline guard - Executors merge upstreamExtraHeaders; sanitize unit tests - Dev: bootstrap env in run-next, instrumentation-node import, credentialLoader dedupe Made-with: Cursor
907 lines
27 KiB
TypeScript
907 lines
27 KiB
TypeScript
declare const EdgeRuntime: string | undefined;
|
|
/**
|
|
* CursorExecutor — Handles communication with the Cursor IDE API.
|
|
*
|
|
* This executor is the most complex due to Cursor's non-standard protocol:
|
|
*
|
|
* SECTION 1: Authentication (generateChecksum)
|
|
* - SHA-256 based checksum using machine ID and timestamp
|
|
* - WorkOS token refresh for session management
|
|
*
|
|
* SECTION 2: Request Encoding (transformRequest, buildHeaders)
|
|
* - ConnectRPC Protobuf binary encoding via cursorProtobuf.js
|
|
* - Chat body construction with model routing
|
|
*
|
|
* SECTION 3: Response Parsing (executeStream, parseEvent*)
|
|
* - Binary EventStream → SSE text conversion
|
|
* - Gzip decompression of response frames
|
|
* - HTTP/2 support with h2 fallback to fetch
|
|
*
|
|
* @see cursorProtobuf.js for Protobuf encoding/decoding utilities
|
|
*/
|
|
|
|
import { BaseExecutor, mergeUpstreamExtraHeaders } from "./base.ts";
|
|
import { PROVIDERS, HTTP_STATUS } from "../config/constants.ts";
|
|
import {
|
|
generateCursorBody,
|
|
parseConnectRPCFrame,
|
|
extractTextFromResponse,
|
|
} from "../utils/cursorProtobuf.ts";
|
|
import { estimateUsage } from "../utils/usageTracking.ts";
|
|
import { FORMATS } from "../translator/formats.ts";
|
|
import crypto from "crypto";
|
|
import { v5 as uuidv5 } from "uuid";
|
|
import zlib from "zlib";
|
|
|
|
// Detect cloud environment
|
|
const isCloudEnv = () => {
|
|
if (typeof caches !== "undefined" && typeof caches === "object") return true;
|
|
if (typeof EdgeRuntime !== "undefined") return true;
|
|
return false;
|
|
};
|
|
|
|
// Lazy import http2 (only in Node.js environment)
|
|
let http2 = null;
|
|
if (!isCloudEnv()) {
|
|
try {
|
|
http2 = await import("http2");
|
|
} catch {
|
|
// http2 not available
|
|
}
|
|
}
|
|
|
|
// --- SECTION 1: Authentication Constants ---
|
|
const COMPRESS_FLAG = {
|
|
NONE: 0x00,
|
|
GZIP: 0x01,
|
|
GZIP_ALT: 0x02,
|
|
GZIP_BOTH: 0x03,
|
|
};
|
|
|
|
const CURSOR_STREAM_DEBUG = process.env.CURSOR_STREAM_DEBUG === "1";
|
|
const debugLog = (...args: unknown[]) => {
|
|
if (CURSOR_STREAM_DEBUG) console.log(...args);
|
|
};
|
|
|
|
function decompressPayload(payload, flags) {
|
|
// Check if payload is JSON error (starts with {"error")
|
|
if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) {
|
|
try {
|
|
const text = payload.toString("utf-8");
|
|
if (text.startsWith('{"error"')) {
|
|
debugLog(`[DECOMPRESS] Detected JSON error, skipping decompression`);
|
|
return payload;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if (
|
|
flags === COMPRESS_FLAG.GZIP ||
|
|
flags === COMPRESS_FLAG.GZIP_ALT ||
|
|
flags === COMPRESS_FLAG.GZIP_BOTH
|
|
) {
|
|
// Primary: try gzip decompression (standard gzip header 0x1f 0x8b)
|
|
try {
|
|
return zlib.gunzipSync(payload);
|
|
} catch (gzipErr) {
|
|
// Fallback: GZIP_ALT (0x02) and GZIP_BOTH (0x03) frames sometimes use
|
|
// raw zlib deflate format instead of gzip wrapping (#250)
|
|
try {
|
|
return zlib.inflateSync(payload);
|
|
} catch (deflateErr) {
|
|
// Last resort: try raw deflate (no zlib header)
|
|
try {
|
|
return zlib.inflateRawSync(payload);
|
|
} catch (rawErr) {
|
|
debugLog(
|
|
`[DECOMPRESS ERROR] flags=${flags}, payloadSize=${payload.length}, gzip=${gzipErr.message}, deflate=${deflateErr.message}, raw=${rawErr.message}`
|
|
);
|
|
debugLog(
|
|
`[DECOMPRESS ERROR] First 50 bytes (hex):`,
|
|
payload.slice(0, 50).toString("hex")
|
|
);
|
|
debugLog(
|
|
`[DECOMPRESS ERROR] First 50 bytes (utf8):`,
|
|
payload
|
|
.slice(0, 50)
|
|
.toString("utf8")
|
|
.replace(/[^\x20-\x7E]/g, ".")
|
|
);
|
|
// Try to use payload as-is if all decompression methods fail
|
|
return payload;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
function createErrorResponse(jsonError) {
|
|
const errorMsg =
|
|
jsonError?.error?.details?.[0]?.debug?.details?.title ||
|
|
jsonError?.error?.details?.[0]?.debug?.details?.detail ||
|
|
jsonError?.error?.message ||
|
|
"API Error";
|
|
|
|
const isRateLimit = jsonError?.error?.code === "resource_exhausted";
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: errorMsg,
|
|
type: isRateLimit ? "rate_limit_error" : "api_error",
|
|
code: jsonError?.error?.details?.[0]?.debug?.error || "unknown",
|
|
},
|
|
}),
|
|
{
|
|
status: isRateLimit ? HTTP_STATUS.RATE_LIMITED : HTTP_STATUS.BAD_REQUEST,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
function parseCursorJsonErrorFrame(text: string) {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isToolBoundaryAbort(jsonError: unknown, toolCallCount: number) {
|
|
if (!jsonError || toolCallCount <= 0) return false;
|
|
const e = jsonError as Record<string, unknown>;
|
|
const err = e?.error as Record<string, unknown> | undefined;
|
|
const details = (err?.details as Record<string, unknown>[] | undefined)?.[0];
|
|
const debug = details?.debug as Record<string, unknown> | undefined;
|
|
const debugDetails = debug?.details as Record<string, unknown> | undefined;
|
|
const code = (err?.code as string) || "";
|
|
const debugError = (debug?.error as string) || "";
|
|
const title = (debugDetails?.title as string) || "";
|
|
const detail = (debugDetails?.detail as string) || "";
|
|
const message = `${title} ${detail}`.toLowerCase();
|
|
const isAbortedCode = code === "aborted" || debugError === "ERROR_USER_ABORTED_REQUEST";
|
|
return isAbortedCode && message.includes("tool call ended before result was received");
|
|
}
|
|
|
|
function mergeToolCallDelta(existing, incoming) {
|
|
const mergedName = incoming?.function?.name || existing?.function?.name || "";
|
|
const existingArgs = existing?.function?.arguments || "";
|
|
const deltaArgs = incoming?.function?.arguments || "";
|
|
return {
|
|
id: incoming.id || existing.id,
|
|
type: "function",
|
|
function: {
|
|
name: mergedName,
|
|
arguments: `${existingArgs}${deltaArgs}`,
|
|
},
|
|
isLast: Boolean(existing?.isLast || incoming?.isLast),
|
|
index: existing?.index ?? incoming?.index ?? 0,
|
|
};
|
|
}
|
|
|
|
type CursorHttpResponse = {
|
|
status: number;
|
|
headers: Record<string, unknown>;
|
|
body: Buffer;
|
|
};
|
|
|
|
export class CursorExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("cursor", PROVIDERS.cursor);
|
|
}
|
|
|
|
buildUrl() {
|
|
return `${this.config.baseUrl}${this.config.chatPath || ""}`;
|
|
}
|
|
|
|
// Jyh cipher checksum for Cursor API authentication
|
|
generateChecksum(machineId) {
|
|
const timestamp = Math.floor(Date.now() / 1000000);
|
|
const byteArray = new Uint8Array([
|
|
(timestamp >> 40) & 0xff,
|
|
(timestamp >> 32) & 0xff,
|
|
(timestamp >> 24) & 0xff,
|
|
(timestamp >> 16) & 0xff,
|
|
(timestamp >> 8) & 0xff,
|
|
timestamp & 0xff,
|
|
]);
|
|
|
|
let t = 165;
|
|
for (let i = 0; i < byteArray.length; i++) {
|
|
byteArray[i] = ((byteArray[i] ^ t) + (i % 256)) & 0xff;
|
|
t = byteArray[i];
|
|
}
|
|
|
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
let encoded = "";
|
|
|
|
for (let i = 0; i < byteArray.length; i += 3) {
|
|
const a = byteArray[i];
|
|
const b = i + 1 < byteArray.length ? byteArray[i + 1] : 0;
|
|
const c = i + 2 < byteArray.length ? byteArray[i + 2] : 0;
|
|
|
|
encoded += alphabet[a >> 2];
|
|
encoded += alphabet[((a & 3) << 4) | (b >> 4)];
|
|
|
|
if (i + 1 < byteArray.length) {
|
|
encoded += alphabet[((b & 15) << 2) | (c >> 6)];
|
|
}
|
|
if (i + 2 < byteArray.length) {
|
|
encoded += alphabet[c & 63];
|
|
}
|
|
}
|
|
|
|
return `${encoded}${machineId}`;
|
|
}
|
|
|
|
buildHeaders(credentials) {
|
|
const accessToken = credentials.accessToken;
|
|
const machineId = credentials.providerSpecificData?.machineId;
|
|
const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
|
|
|
|
if (!machineId) {
|
|
throw new Error("Machine ID is required for Cursor API");
|
|
}
|
|
|
|
const cleanToken = accessToken.includes("::") ? accessToken.split("::")[1] : accessToken;
|
|
|
|
return {
|
|
authorization: `Bearer ${cleanToken}`,
|
|
"connect-accept-encoding": "gzip",
|
|
"connect-protocol-version": "1",
|
|
"content-type": "application/connect+proto",
|
|
"user-agent": "connect-es/1.6.1",
|
|
"x-amzn-trace-id": `Root=${crypto.randomUUID()}`,
|
|
"x-client-key": crypto.createHash("sha256").update(cleanToken).digest("hex"),
|
|
"x-cursor-checksum": this.generateChecksum(machineId),
|
|
"x-cursor-client-version": "2.3.41",
|
|
"x-cursor-client-type": "ide",
|
|
"x-cursor-client-os":
|
|
process.platform === "win32"
|
|
? "windows"
|
|
: process.platform === "darwin"
|
|
? "macos"
|
|
: "linux",
|
|
"x-cursor-client-arch": process.arch === "arm64" ? "aarch64" : "x64",
|
|
"x-cursor-client-device-type": "desktop",
|
|
"x-cursor-config-version": crypto.randomUUID(),
|
|
"x-cursor-timezone": Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
"x-ghost-mode": ghostMode ? "true" : "false",
|
|
"x-request-id": crypto.randomUUID(),
|
|
"x-session-id": uuidv5(cleanToken, uuidv5.DNS),
|
|
};
|
|
}
|
|
|
|
transformRequest(model, body, stream, credentials) {
|
|
// Messages are already translated by chatCore (claude→openai→cursor)
|
|
// Do NOT call buildCursorRequest again — double-translation drops tool_results
|
|
const messages = body.messages || [];
|
|
const tools = body.tools || [];
|
|
const reasoningEffort = body.reasoning_effort || null;
|
|
return generateCursorBody(messages, model, tools, reasoningEffort);
|
|
}
|
|
|
|
async makeFetchRequest(
|
|
url: string,
|
|
headers: Record<string, string>,
|
|
body: Uint8Array,
|
|
signal?: AbortSignal
|
|
): Promise<CursorHttpResponse> {
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers,
|
|
body: body as unknown as BodyInit,
|
|
signal,
|
|
});
|
|
|
|
return {
|
|
status: response.status,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
body: Buffer.from(await response.arrayBuffer()),
|
|
};
|
|
}
|
|
|
|
makeHttp2Request(
|
|
url: string,
|
|
headers: Record<string, string>,
|
|
body: Uint8Array,
|
|
signal?: AbortSignal
|
|
): Promise<CursorHttpResponse> {
|
|
if (!http2) {
|
|
throw new Error("http2 module not available");
|
|
}
|
|
|
|
return new Promise<CursorHttpResponse>((resolve, reject) => {
|
|
const urlObj = new URL(url);
|
|
const client = http2.connect(`https://${urlObj.host}`);
|
|
const chunks = [];
|
|
let responseHeaders = {};
|
|
|
|
client.on("error", reject);
|
|
|
|
const req = client.request({
|
|
":method": "POST",
|
|
":path": urlObj.pathname,
|
|
":authority": urlObj.host,
|
|
":scheme": "https",
|
|
...headers,
|
|
});
|
|
|
|
req.on("response", (hdrs) => {
|
|
responseHeaders = hdrs;
|
|
});
|
|
req.on("data", (chunk) => {
|
|
chunks.push(chunk);
|
|
});
|
|
req.on("end", () => {
|
|
client.close();
|
|
resolve({
|
|
status:
|
|
typeof responseHeaders[":status"] === "number"
|
|
? responseHeaders[":status"]
|
|
: Number(responseHeaders[":status"] || HTTP_STATUS.SERVER_ERROR),
|
|
headers: responseHeaders,
|
|
body: Buffer.concat(chunks),
|
|
});
|
|
});
|
|
req.on("error", (err) => {
|
|
client.close();
|
|
reject(err);
|
|
});
|
|
|
|
if (signal) {
|
|
signal.addEventListener("abort", () => {
|
|
req.close();
|
|
client.close();
|
|
reject(new Error("Request aborted"));
|
|
});
|
|
}
|
|
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async execute({ model, body, stream, credentials, signal, log, upstreamExtraHeaders }) {
|
|
const url = this.buildUrl();
|
|
const headers = this.buildHeaders(credentials);
|
|
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
|
|
const transformedBody = this.transformRequest(model, body, stream, credentials);
|
|
|
|
try {
|
|
const response: CursorHttpResponse = http2
|
|
? await this.makeHttp2Request(url, headers, transformedBody, signal)
|
|
: await this.makeFetchRequest(url, headers, transformedBody, signal);
|
|
|
|
if (response.status !== 200) {
|
|
const errorText = response.body?.toString() || "Unknown error";
|
|
const errorResponse = new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: `[${response.status}]: ${errorText}`,
|
|
type: "invalid_request_error",
|
|
code: "",
|
|
},
|
|
}),
|
|
{
|
|
status: response.status,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
return { response: errorResponse, url, headers, transformedBody: body };
|
|
}
|
|
|
|
const transformedResponse =
|
|
stream !== false
|
|
? this.transformProtobufToSSE(response.body, model, body)
|
|
: this.transformProtobufToJSON(response.body, model, body);
|
|
|
|
return { response: transformedResponse, url, headers, transformedBody: body };
|
|
} catch (error) {
|
|
const errorResponse = new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: error.message,
|
|
type: "connection_error",
|
|
code: "",
|
|
},
|
|
}),
|
|
{
|
|
status: HTTP_STATUS.SERVER_ERROR,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
return { response: errorResponse, url, headers, transformedBody: body };
|
|
}
|
|
}
|
|
|
|
transformProtobufToJSON(buffer, model, body) {
|
|
const responseId = `chatcmpl-cursor-${Date.now()}`;
|
|
const created = Math.floor(Date.now() / 1000);
|
|
|
|
let offset = 0;
|
|
let totalContent = "";
|
|
const toolCalls = [];
|
|
const toolCallsMap = new Map(); // Track streaming tool calls by ID
|
|
const finalizedIds = new Set<string>();
|
|
let frameCount = 0;
|
|
|
|
debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`);
|
|
|
|
while (offset < buffer.length) {
|
|
if (offset + 5 > buffer.length) {
|
|
debugLog(
|
|
`[CURSOR BUFFER] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
|
|
);
|
|
break;
|
|
}
|
|
|
|
const flags = buffer[offset];
|
|
const length = buffer.readUInt32BE(offset + 1);
|
|
|
|
debugLog(
|
|
`[CURSOR BUFFER] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`
|
|
);
|
|
|
|
if (offset + 5 + length > buffer.length) {
|
|
debugLog(
|
|
`[CURSOR BUFFER] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`
|
|
);
|
|
break;
|
|
}
|
|
|
|
let payload = buffer.slice(offset + 5, offset + 5 + length);
|
|
offset += 5 + length;
|
|
frameCount++;
|
|
|
|
payload = decompressPayload(payload, flags);
|
|
if (!payload) {
|
|
debugLog(`[CURSOR BUFFER] Frame ${frameCount}: decompression failed, skipping`);
|
|
continue;
|
|
}
|
|
|
|
// Check for JSON error frames (byte guard: skip toString on non-JSON frames)
|
|
if (payload.length > 0 && payload[0] === 0x7b) {
|
|
try {
|
|
const text = payload.toString("utf-8");
|
|
if (text.includes('"error"')) {
|
|
const hasContent = totalContent || toolCallsMap.size > 0;
|
|
debugLog(
|
|
`[CURSOR BUFFER] Error frame (hasContent=${hasContent}): ${text.slice(0, 500)}`
|
|
);
|
|
if (hasContent) {
|
|
break;
|
|
}
|
|
return createErrorResponse(JSON.parse(text));
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const result = extractTextFromResponse(new Uint8Array(payload));
|
|
debugLog(`[CURSOR DECODED] Frame ${frameCount}:`, result);
|
|
|
|
if (result.error) {
|
|
const hasContent = totalContent || toolCallsMap.size > 0;
|
|
debugLog(`[CURSOR BUFFER] Decoded error (hasContent=${hasContent}): ${result.error}`);
|
|
// If we already have content, treat error as stream termination
|
|
if (hasContent) {
|
|
break;
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: result.error,
|
|
type: "rate_limit_error",
|
|
code: "rate_limited",
|
|
},
|
|
}),
|
|
{
|
|
status: HTTP_STATUS.RATE_LIMITED,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
if (result.toolCall) {
|
|
const tc = result.toolCall;
|
|
|
|
if (toolCallsMap.has(tc.id)) {
|
|
// Accumulate arguments for existing tool call
|
|
const existing = toolCallsMap.get(tc.id);
|
|
existing.function.arguments += tc.function.arguments;
|
|
existing.isLast = tc.isLast;
|
|
} else {
|
|
// New tool call
|
|
toolCallsMap.set(tc.id, { ...tc });
|
|
}
|
|
|
|
// Push to final array when isLast is true
|
|
if (tc.isLast) {
|
|
const finalToolCall = toolCallsMap.get(tc.id);
|
|
finalizedIds.add(tc.id);
|
|
toolCalls.push({
|
|
id: finalToolCall.id,
|
|
type: finalToolCall.type,
|
|
function: {
|
|
name: finalToolCall.function.name,
|
|
arguments: finalToolCall.function.arguments,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (result.text) totalContent += result.text;
|
|
}
|
|
|
|
debugLog(
|
|
`[CURSOR BUFFER] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, finalized toolCalls: ${toolCalls.length}`
|
|
);
|
|
|
|
// Finalize all remaining tool calls in map (in case stream ended without isLast=true)
|
|
for (const [id, tc] of toolCallsMap.entries()) {
|
|
// Check if already in final array
|
|
if (!finalizedIds.has(id)) {
|
|
debugLog(`[CURSOR BUFFER] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`);
|
|
toolCalls.push({
|
|
id: tc.id,
|
|
type: tc.type,
|
|
function: {
|
|
name: tc.function.name,
|
|
arguments: tc.function.arguments,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
debugLog(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`);
|
|
|
|
const message: Record<string, unknown> = {
|
|
role: "assistant",
|
|
content: totalContent || null,
|
|
};
|
|
|
|
if (toolCalls.length > 0) {
|
|
message.tool_calls = toolCalls;
|
|
}
|
|
|
|
const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI);
|
|
|
|
const completion = {
|
|
id: responseId,
|
|
object: "chat.completion",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message,
|
|
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
},
|
|
],
|
|
usage,
|
|
};
|
|
|
|
return new Response(JSON.stringify(completion), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
transformProtobufToSSE(buffer, model, body) {
|
|
const responseId = `chatcmpl-cursor-${Date.now()}`;
|
|
const created = Math.floor(Date.now() / 1000);
|
|
|
|
const chunks = [];
|
|
let offset = 0;
|
|
let totalContent = "";
|
|
const toolCalls = [];
|
|
const toolCallsMap = new Map(); // Track streaming tool calls by ID
|
|
const finalizedIds = new Set<string>();
|
|
const emittedToolCallIds = new Set<string>();
|
|
let frameCount = 0;
|
|
|
|
debugLog(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`);
|
|
|
|
while (offset < buffer.length) {
|
|
if (offset + 5 > buffer.length) {
|
|
debugLog(
|
|
`[CURSOR BUFFER SSE] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
|
|
);
|
|
break;
|
|
}
|
|
|
|
const flags = buffer[offset];
|
|
const length = buffer.readUInt32BE(offset + 1);
|
|
|
|
debugLog(
|
|
`[CURSOR BUFFER SSE] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`
|
|
);
|
|
|
|
if (offset + 5 + length > buffer.length) {
|
|
debugLog(
|
|
`[CURSOR BUFFER SSE] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`
|
|
);
|
|
break;
|
|
}
|
|
|
|
let payload = buffer.slice(offset + 5, offset + 5 + length);
|
|
offset += 5 + length;
|
|
frameCount++;
|
|
|
|
payload = decompressPayload(payload, flags);
|
|
if (!payload) {
|
|
debugLog(`[CURSOR BUFFER SSE] Frame ${frameCount}: decompression failed, skipping`);
|
|
continue;
|
|
}
|
|
|
|
// Check for JSON error frames (byte-guard: only decode if starts with '{')
|
|
if (payload[0] === 0x7b) {
|
|
try {
|
|
const text = payload.toString("utf-8");
|
|
if (text.includes('"error"')) {
|
|
const hasContent = chunks.length > 0 || totalContent || toolCallsMap.size > 0;
|
|
debugLog(
|
|
`[CURSOR BUFFER SSE] Error frame (hasContent=${hasContent}): ${text.slice(0, 500)}`
|
|
);
|
|
if (hasContent) {
|
|
break;
|
|
}
|
|
return createErrorResponse(JSON.parse(text));
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
const result = extractTextFromResponse(new Uint8Array(payload));
|
|
debugLog(`[CURSOR DECODED SSE] Frame ${frameCount}:`, result);
|
|
|
|
if (result.error) {
|
|
const hasContent = chunks.length > 0 || totalContent || toolCallsMap.size > 0;
|
|
debugLog(`[CURSOR BUFFER SSE] Decoded error (hasContent=${hasContent}): ${result.error}`);
|
|
// If we already have content, treat error as stream termination
|
|
if (hasContent) {
|
|
break;
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: {
|
|
message: result.error,
|
|
type: "rate_limit_error",
|
|
code: "rate_limited",
|
|
},
|
|
}),
|
|
{
|
|
status: HTTP_STATUS.RATE_LIMITED,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
|
|
if (result.toolCall) {
|
|
const tc = result.toolCall;
|
|
|
|
if (chunks.length === 0) {
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: { role: "assistant", content: "" },
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
})}\n\n`
|
|
);
|
|
}
|
|
|
|
if (toolCallsMap.has(tc.id)) {
|
|
// Accumulate arguments for existing tool call
|
|
const existing = toolCallsMap.get(tc.id);
|
|
const oldArgsLen = existing.function.arguments.length;
|
|
existing.function.arguments += tc.function.arguments;
|
|
existing.isLast = tc.isLast;
|
|
|
|
// Stream the delta arguments
|
|
if (tc.function.arguments) {
|
|
emittedToolCallIds.add(tc.id);
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
index: existing.index,
|
|
id: tc.id,
|
|
type: "function",
|
|
function: {
|
|
name: tc.function.name,
|
|
arguments: tc.function.arguments,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
})}\n\n`
|
|
);
|
|
}
|
|
} else {
|
|
// New tool call - assign index and add to map
|
|
const toolCallIndex = toolCalls.length;
|
|
finalizedIds.add(tc.id);
|
|
toolCalls.push({ ...tc, index: toolCallIndex });
|
|
toolCallsMap.set(tc.id, { ...tc, index: toolCallIndex });
|
|
|
|
// Stream initial tool call with name
|
|
emittedToolCallIds.add(tc.id);
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
index: toolCallIndex,
|
|
id: tc.id,
|
|
type: "function",
|
|
function: {
|
|
name: tc.function.name,
|
|
arguments: tc.function.arguments,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
})}\n\n`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (result.text) {
|
|
totalContent += result.text;
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta:
|
|
chunks.length === 0 && toolCalls.length === 0
|
|
? { role: "assistant", content: result.text }
|
|
: { content: result.text },
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
})}\n\n`
|
|
);
|
|
}
|
|
}
|
|
|
|
debugLog(
|
|
`[CURSOR BUFFER SSE] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, toolCalls array: ${toolCalls.length}`
|
|
);
|
|
|
|
// Finalize all remaining tool calls in map (stream may have ended without isLast=true)
|
|
for (const [id, tc] of toolCallsMap.entries()) {
|
|
if (!finalizedIds.has(id)) {
|
|
debugLog(`[CURSOR BUFFER SSE] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`);
|
|
const toolCallIndex = toolCalls.length;
|
|
toolCalls.push({
|
|
id: tc.id,
|
|
type: tc.type,
|
|
index: toolCallIndex,
|
|
function: {
|
|
name: tc.function.name,
|
|
arguments: tc.function.arguments,
|
|
},
|
|
});
|
|
|
|
// Emit SSE chunk for the finalized tool call if not already emitted
|
|
if (!emittedToolCallIds.has(tc.id)) {
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
index: toolCallIndex,
|
|
id: tc.id,
|
|
type: "function",
|
|
function: {
|
|
name: tc.function.name,
|
|
arguments: tc.function.arguments,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
})}\n\n`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (chunks.length === 0 && toolCalls.length === 0) {
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: { role: "assistant", content: "" },
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
})}\n\n`
|
|
);
|
|
}
|
|
|
|
const usage = estimateUsage(body, totalContent.length, FORMATS.OPENAI);
|
|
|
|
chunks.push(
|
|
`data: ${JSON.stringify({
|
|
id: responseId,
|
|
object: "chat.completion.chunk",
|
|
created,
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {},
|
|
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
},
|
|
],
|
|
usage,
|
|
})}\n\n`
|
|
);
|
|
chunks.push("data: [DONE]\n\n");
|
|
|
|
return new Response(chunks.join(""), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-cache",
|
|
Connection: "keep-alive",
|
|
},
|
|
});
|
|
}
|
|
|
|
async refreshCredentials() {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default CursorExecutor;
|