Spaces:
Paused
Paused
| 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 { getCursorUserAgent } from "../config/providerHeaderProfiles.ts"; | |
| import { PROVIDERS, HTTP_STATUS } from "../config/constants.ts"; | |
| import { | |
| generateCursorBody, | |
| parseConnectRPCFrame, | |
| extractTextFromResponse, | |
| } from "../utils/cursorProtobuf.ts"; | |
| import { estimateUsage } from "../utils/usageTracking.ts"; | |
| import { getCursorVersion } from "../utils/cursorVersionDetector.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 ghostMode = credentials.providerSpecificData?.ghostMode !== false; | |
| // Use stored machineId, or derive a stable one from the access token | |
| // (cursor-agent imports don't provide a machineId) | |
| const machineId = | |
| credentials.providerSpecificData?.machineId || | |
| crypto.createHash("sha256").update(accessToken).digest("hex"); | |
| 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": getCursorUserAgent(getCursorVersion()), | |
| "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": getCursorVersion(), | |
| "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-user-agent": getCursorUserAgent(getCursorVersion()), | |
| "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 = await 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; | |