| import { BaseExecutor } from "./base.js"; |
| import { PROVIDERS } from "../config/providers.js"; |
| import { HTTP_STATUS } from "../config/runtimeConfig.js"; |
| import { |
| generateCursorBody, |
| parseConnectRPCFrame, |
| extractTextFromResponse |
| } from "../utils/cursorProtobuf.js"; |
| import { buildCursorHeaders } from "../utils/cursorChecksum.js"; |
| import { estimateUsage } from "../utils/usageTracking.js"; |
| import { SSE_DONE, SSE_HEADERS } from "../utils/sseConstants.js"; |
| import { chatChunkSse } from "../utils/sse.js"; |
| import { FORMATS } from "../translator/formats.js"; |
| import { proxyAwareFetch } from "../utils/proxyFetch.js"; |
| import zlib from "zlib"; |
|
|
| |
| const isCloudEnv = () => { |
| if (typeof caches !== "undefined" && typeof caches === "object") return true; |
| if (typeof EdgeRuntime !== "undefined") return true; |
| return false; |
| }; |
|
|
| |
| let http2 = null; |
| if (!isCloudEnv()) { |
| try { |
| http2 = await import("http2"); |
| } catch { |
| |
| } |
| } |
|
|
| const COMPRESS_FLAG = { |
| NONE: 0x00, |
| GZIP: 0x01, |
| TRAILER: 0x02, |
| GZIP_TRAILER: 0x03 |
| }; |
|
|
| const CURSOR_STREAM_DEBUG = process.env.CURSOR_STREAM_DEBUG === "1"; |
| const debugLog = (...args) => { |
| if (CURSOR_STREAM_DEBUG) console.log(...args); |
| }; |
|
|
| function isComposerModel(model) { |
| const modelId = String(model || "").split("/").pop(); |
| return /^composer(?:-|$)/i.test(modelId); |
| } |
|
|
| function visibleComposerContentFromThinking(thinking) { |
| if (!thinking) return ""; |
| const endTag = "</think>"; |
| const endIdx = thinking.lastIndexOf(endTag); |
| if (endIdx < 0) return ""; |
| return thinking.slice(endIdx + endTag.length).trimStart(); |
| } |
|
|
| function decompressPayload(payload, flags) { |
| |
| 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.TRAILER || |
| flags === COMPRESS_FLAG.GZIP_TRAILER |
| ) { |
| |
| try { |
| return zlib.gunzipSync(payload); |
| } catch (gzipErr) { |
| |
| try { |
| return zlib.inflateSync(payload); |
| } catch (deflateErr) { |
| |
| 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") |
| ); |
| return payload; |
| } |
| } |
| } |
| } |
| return payload; |
| } |
|
|
| |
| function readCursorFrame(buffer, offset, frameNum, tag) { |
| if (offset + 5 > buffer.length) { |
| debugLog(`[CURSOR BUFFER${tag}] Reached end, offset=${offset}, remaining=${buffer.length - offset}`); |
| return { status: "done" }; |
| } |
|
|
| const flags = buffer[offset]; |
| const length = buffer.readUInt32BE(offset + 1); |
| debugLog(`[CURSOR BUFFER${tag}] Frame ${frameNum + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`); |
|
|
| if (offset + 5 + length > buffer.length) { |
| debugLog(`[CURSOR BUFFER${tag}] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`); |
| return { status: "done" }; |
| } |
|
|
| let payload = buffer.slice(offset + 5, offset + 5 + length); |
| const newOffset = offset + 5 + length; |
| payload = decompressPayload(payload, flags); |
| if (!payload) { |
| debugLog(`[CURSOR BUFFER${tag}] Frame ${frameNum + 1}: decompression failed, skipping`); |
| return { status: "skip", offset: newOffset }; |
| } |
| return { status: "ok", payload, offset: newOffset }; |
| } |
|
|
| 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" } |
| }); |
| } |
|
|
| export class CursorExecutor extends BaseExecutor { |
| constructor() { |
| super("cursor", PROVIDERS.cursor); |
| } |
|
|
| buildUrl() { |
| return `${this.config.baseUrl}${this.config.chatPath}`; |
| } |
|
|
| 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"); |
| } |
|
|
| return buildCursorHeaders(accessToken, machineId, ghostMode); |
| } |
|
|
| transformRequest(model, body, stream, credentials) { |
| |
| |
| const messages = body.messages || []; |
| const tools = body.tools || []; |
| const reasoningEffort = body.reasoning_effort || null; |
| |
| const ua = credentials?.rawHeaders?.["user-agent"] || ""; |
| const forceAgentMode = ua.includes("claude-cli") || ua.includes("claude-code") || ua.includes("Claude Code"); |
| return generateCursorBody(messages, model, tools, reasoningEffort, forceAgentMode); |
| } |
|
|
| async makeFetchRequest(url, headers, body, signal, proxyOptions = null) { |
| const response = await proxyAwareFetch(url, { |
| method: "POST", |
| headers, |
| body, |
| signal |
| }, proxyOptions); |
|
|
| return { |
| status: response.status, |
| headers: Object.fromEntries(response.headers.entries()), |
| body: Buffer.from(await response.arrayBuffer()) |
| }; |
| } |
|
|
| makeHttp2Request(url, headers, body, signal) { |
| if (!http2) { |
| throw new Error("http2 module not available"); |
| } |
|
|
| const HTTP2_TIMEOUT_MS = 60000; |
|
|
| return new Promise((resolve, reject) => { |
| const urlObj = new URL(url); |
| const client = http2.connect(`https://${urlObj.host}`); |
| const chunks = []; |
| let responseHeaders = {}; |
| let settled = false; |
|
|
| |
| const finish = (fn) => (...args) => { |
| if (settled) return; |
| settled = true; |
| clearTimeout(hangTimeout); |
| client.close(); |
| fn(...args); |
| }; |
|
|
| |
| const hangTimeout = setTimeout(finish(() => { |
| reject(new Error("HTTP/2 request timed out")); |
| }), HTTP2_TIMEOUT_MS); |
|
|
| client.on("error", finish(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", finish(() => { |
| resolve({ |
| status: responseHeaders[":status"], |
| headers: responseHeaders, |
| body: Buffer.concat(chunks) |
| }); |
| })); |
| req.on("error", finish(reject)); |
|
|
| if (signal) { |
| const onAbort = finish(() => reject(new Error("Request aborted"))); |
| signal.addEventListener("abort", onAbort, { once: true }); |
| } |
|
|
| req.write(body); |
| req.end(); |
| }); |
| } |
|
|
| async execute({ model, body, stream, credentials, signal, log, proxyOptions = null }) { |
| const url = this.buildUrl(); |
| const headers = this.buildHeaders(credentials); |
| const transformedBody = this.transformRequest(model, body, stream, credentials); |
|
|
| try { |
| const shouldForceFetch = proxyOptions?.enabled === true || proxyOptions?.connectionProxyEnabled === true || !!proxyOptions?.vercelRelayUrl; |
| const response = (http2 && !shouldForceFetch) |
| ? await this.makeHttp2Request(url, headers, transformedBody, signal) |
| : await this.makeFetchRequest(url, headers, transformedBody, signal, proxyOptions); |
|
|
| 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 = ""; |
| let totalThinking = ""; |
| const toolCalls = []; |
| const toolCallsMap = new Map(); |
| const finalizedIds = new Set(); |
| let frameCount = 0; |
|
|
| debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`); |
|
|
| while (offset < buffer.length) { |
| const frame = readCursorFrame(buffer, offset, frameCount, ""); |
| if (frame.status === "done") break; |
| offset = frame.offset; |
| frameCount++; |
| if (frame.status === "skip") continue; |
| const payload = frame.payload; |
|
|
| |
| 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 (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)) { |
| |
| const existing = toolCallsMap.get(tc.id); |
| existing.function.arguments += tc.function.arguments; |
| existing.isLast = tc.isLast; |
| } else { |
| |
| toolCallsMap.set(tc.id, { ...tc }); |
| } |
|
|
| |
| 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; |
| if (result.thinking) totalThinking += result.thinking; |
| } |
|
|
| const visibleComposerContent = isComposerModel(model) |
| ? visibleComposerContentFromThinking(totalThinking) |
| : ""; |
| const finalContent = totalContent || visibleComposerContent; |
|
|
| debugLog( |
| `[CURSOR BUFFER] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, finalized toolCalls: ${toolCalls.length}` |
| ); |
|
|
| |
| for (const [id, tc] of toolCallsMap.entries()) { |
| |
| 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 = { |
| role: "assistant", |
| content: finalContent || null |
| }; |
|
|
| if (toolCalls.length > 0) { |
| message.tool_calls = toolCalls; |
| } |
|
|
| const usage = estimateUsage(body, finalContent.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 = ""; |
| let totalThinking = ""; |
| let emittedComposerThinkingContentLength = 0; |
| const toolCalls = []; |
| const toolCallsMap = new Map(); |
| const finalizedIds = new Set(); |
| const emittedToolCallIds = new Set(); |
| let frameCount = 0; |
|
|
| debugLog(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`); |
|
|
| while (offset < buffer.length) { |
| const frame = readCursorFrame(buffer, offset, frameCount, " SSE"); |
| if (frame.status === "done") break; |
| offset = frame.offset; |
| frameCount++; |
| if (frame.status === "skip") continue; |
| const payload = frame.payload; |
|
|
| |
| 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 (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(chatChunkSse({ id: responseId, created, model, delta: { role: "assistant", content: "" } })); |
| } |
|
|
| if (toolCallsMap.has(tc.id)) { |
| |
| const existing = toolCallsMap.get(tc.id); |
| const oldArgsLen = existing.function.arguments.length; |
| existing.function.arguments += tc.function.arguments; |
| existing.isLast = tc.isLast; |
|
|
| |
| if (tc.function.arguments) { |
| emittedToolCallIds.add(tc.id); |
| chunks.push(chatChunkSse({ |
| id: responseId, created, model, |
| delta: { |
| tool_calls: [ |
| { |
| index: existing.index, |
| id: tc.id, |
| type: "function", |
| function: { |
| name: tc.function.name, |
| arguments: tc.function.arguments |
| } |
| } |
| ] |
| } |
| })); |
| } |
| } else { |
| |
| const toolCallIndex = toolCalls.length; |
| finalizedIds.add(tc.id); |
| toolCalls.push({ ...tc, index: toolCallIndex }); |
| toolCallsMap.set(tc.id, { ...tc, index: toolCallIndex }); |
|
|
| |
| emittedToolCallIds.add(tc.id); |
| chunks.push(chatChunkSse({ |
| id: responseId, created, model, |
| delta: { |
| tool_calls: [ |
| { |
| index: toolCallIndex, |
| id: tc.id, |
| type: "function", |
| function: { |
| name: tc.function.name, |
| arguments: tc.function.arguments |
| } |
| } |
| ] |
| } |
| })); |
| } |
| } |
|
|
| if (result.text) { |
| totalContent += result.text; |
| chunks.push(chatChunkSse({ |
| id: responseId, created, model, |
| delta: |
| chunks.length === 0 && toolCalls.length === 0 |
| ? { role: "assistant", content: result.text } |
| : { content: result.text } |
| })); |
| } |
|
|
| if (isComposerModel(model) && result.thinking) { |
| totalThinking += result.thinking; |
| const visibleContent = visibleComposerContentFromThinking(totalThinking); |
| if (visibleContent.length > emittedComposerThinkingContentLength) { |
| const deltaContent = visibleContent.slice(emittedComposerThinkingContentLength); |
| emittedComposerThinkingContentLength = visibleContent.length; |
| totalContent += deltaContent; |
| chunks.push(chatChunkSse({ |
| id: responseId, created, model, |
| delta: |
| chunks.length === 0 && toolCalls.length === 0 |
| ? { role: "assistant", content: deltaContent } |
| : { content: deltaContent } |
| })); |
| } |
| } |
| } |
|
|
| debugLog( |
| `[CURSOR BUFFER SSE] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, toolCalls array: ${toolCalls.length}` |
| ); |
|
|
| |
| 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 |
| } |
| }); |
|
|
| |
| if (!emittedToolCallIds.has(tc.id)) { |
| chunks.push(chatChunkSse({ |
| id: responseId, created, model, |
| delta: { |
| tool_calls: [ |
| { |
| index: toolCallIndex, |
| id: tc.id, |
| type: "function", |
| function: { |
| name: tc.function.name, |
| arguments: tc.function.arguments |
| } |
| } |
| ] |
| } |
| })); |
| } |
| } |
| } |
|
|
| if (chunks.length === 0 && toolCalls.length === 0) { |
| chunks.push(chatChunkSse({ id: responseId, created, model, delta: { role: "assistant", content: "" } })); |
| } |
|
|
| 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(SSE_DONE); |
|
|
| return new Response(chunks.join(""), { |
| status: 200, |
| headers: { ...SSE_HEADERS } |
| }); |
| } |
|
|
| async refreshCredentials() { |
| return null; |
| } |
| } |
|
|
| export default CursorExecutor; |
|
|