mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Introduce a runtime settings layer that hydrates persisted config at startup and reapplies aliases, payload rules, cache behavior, CLI compatibility, usage tuning, and related switches when settings change or SQLite updates. Replace the legacy prompt injection middleware path with a guardrail registry that supports prompt injection detection, PII masking, disabled guardrail overrides, and post-call response handling across the chat pipeline. Add a metadata registry for model catalog and alias resolution so catalog endpoints return enriched capabilities plus diagnostic headers and typed alias errors instead of ad hoc responses. Convert unsupported built-in web_search tools into an OmniRoute fallback tool, execute them through builtin skills, and preserve Responses API function call output with sanitized usage fields. Centralize provider header fingerprints for GitHub, Cursor, Qwen, Qoder, Kiro, and Antigravity, and migrate management passwords from env or plaintext storage into persisted bcrypt hashes during startup and login.
911 lines
28 KiB
TypeScript
911 lines
28 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 { 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;
|