skalpel 2.0.7 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +31 -3
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +31 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +31 -3
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +31 -3
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.js +31 -3
- package/dist/proxy/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/proxy-runner.js
CHANGED
|
@@ -27,10 +27,18 @@ function stripSkalpelHeaders(headers) {
|
|
|
27
27
|
delete cleaned["X-Skalpel-SDK-Version"];
|
|
28
28
|
return cleaned;
|
|
29
29
|
}
|
|
30
|
+
var SKALPEL_ONLY_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
31
|
+
402,
|
|
32
|
+
// Payment Required — Skalpel billing gate
|
|
33
|
+
409
|
|
34
|
+
// Conflict — Skalpel duplicate-request lock
|
|
35
|
+
]);
|
|
30
36
|
function isSkalpelBackendFailure(response, err) {
|
|
31
37
|
if (err) return true;
|
|
32
38
|
if (!response) return true;
|
|
33
39
|
if (response.status >= 500) return true;
|
|
40
|
+
if (SKALPEL_ONLY_STATUS_CODES.has(response.status)) return true;
|
|
41
|
+
if (response.status === 429) return true;
|
|
34
42
|
return false;
|
|
35
43
|
}
|
|
36
44
|
async function doStreamingFetch(url, body, headers) {
|
|
@@ -72,8 +80,12 @@ async function handleStreamingRequest(_req, res, _config, _source, body, skalpel
|
|
|
72
80
|
"Content-Type": "text/event-stream",
|
|
73
81
|
"Cache-Control": "no-cache"
|
|
74
82
|
});
|
|
83
|
+
const errorPayload = {
|
|
84
|
+
type: "error",
|
|
85
|
+
error: { type: "proxy_error", message: errMsg }
|
|
86
|
+
};
|
|
75
87
|
res.write(`event: error
|
|
76
|
-
data: ${JSON.stringify(
|
|
88
|
+
data: ${JSON.stringify(errorPayload)}
|
|
77
89
|
|
|
78
90
|
`);
|
|
79
91
|
res.end();
|
|
@@ -106,8 +118,12 @@ data: ${JSON.stringify({ error: errMsg })}
|
|
|
106
118
|
sseHeaders["Cache-Control"] = "no-cache";
|
|
107
119
|
res.writeHead(response.status, sseHeaders);
|
|
108
120
|
if (!response.body) {
|
|
121
|
+
const noBodyError = {
|
|
122
|
+
type: "error",
|
|
123
|
+
error: { type: "proxy_error", message: "no response body" }
|
|
124
|
+
};
|
|
109
125
|
res.write(`event: error
|
|
110
|
-
data: ${JSON.stringify(
|
|
126
|
+
data: ${JSON.stringify(noBodyError)}
|
|
111
127
|
|
|
112
128
|
`);
|
|
113
129
|
res.end();
|
|
@@ -124,8 +140,12 @@ data: ${JSON.stringify({ error: "no response body" })}
|
|
|
124
140
|
}
|
|
125
141
|
} catch (err) {
|
|
126
142
|
logger.error(`streaming error: ${err.message}`);
|
|
143
|
+
const errorPayload = {
|
|
144
|
+
type: "error",
|
|
145
|
+
error: { type: "proxy_error", message: err.message }
|
|
146
|
+
};
|
|
127
147
|
res.write(`event: error
|
|
128
|
-
data: ${JSON.stringify(
|
|
148
|
+
data: ${JSON.stringify(errorPayload)}
|
|
129
149
|
|
|
130
150
|
`);
|
|
131
151
|
}
|
|
@@ -147,10 +167,18 @@ function shouldRouteToSkalpel(path4, source) {
|
|
|
147
167
|
const pathname = path4.split("?")[0];
|
|
148
168
|
return SKALPEL_EXACT_PATHS.has(pathname);
|
|
149
169
|
}
|
|
170
|
+
var SKALPEL_ONLY_STATUS_CODES2 = /* @__PURE__ */ new Set([
|
|
171
|
+
402,
|
|
172
|
+
// Payment Required — Skalpel billing gate
|
|
173
|
+
409
|
|
174
|
+
// Conflict — Skalpel duplicate-request lock
|
|
175
|
+
]);
|
|
150
176
|
function isSkalpelBackendFailure2(response, err) {
|
|
151
177
|
if (err) return true;
|
|
152
178
|
if (!response) return true;
|
|
153
179
|
if (response.status >= 500) return true;
|
|
180
|
+
if (SKALPEL_ONLY_STATUS_CODES2.has(response.status)) return true;
|
|
181
|
+
if (response.status === 429) return true;
|
|
154
182
|
return false;
|
|
155
183
|
}
|
|
156
184
|
function buildForwardHeaders(req, config2, source, useSkalpel) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/proxy/server.ts","../../src/proxy/streaming.ts","../../src/proxy/handler.ts","../../src/proxy/health.ts","../../src/proxy/pid.ts","../../src/proxy/logger.ts","../../src/proxy/config.ts","../../src/cli/proxy-runner.ts"],"sourcesContent":["import http from 'node:http';\nimport type { ProxyConfig, ProxyStatus } from './types.js';\nimport { handleRequest } from './handler.js';\nimport { handleHealthRequest } from './health.js';\nimport { writePid, readPid, removePid } from './pid.js';\nimport { Logger } from './logger.js';\n\nlet proxyStartTime = 0;\n\nexport function startProxy(config: ProxyConfig): { anthropicServer: http.Server; openaiServer: http.Server } {\n const logger = new Logger(config.logFile, config.logLevel);\n const startTime = Date.now();\n proxyStartTime = Date.now();\n\n const anthropicServer = http.createServer((req, res) => {\n if (req.url === '/health' && req.method === 'GET') {\n handleHealthRequest(res, config, startTime);\n return;\n }\n handleRequest(req, res, config, 'claude-code', logger);\n });\n\n const openaiServer = http.createServer((req, res) => {\n if (req.url === '/health' && req.method === 'GET') {\n handleHealthRequest(res, config, startTime);\n return;\n }\n handleRequest(req, res, config, 'codex', logger);\n });\n\n // Handle port binding errors (EADDRINUSE, EACCES, etc.)\n anthropicServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n logger.error(`Port ${config.anthropicPort} is already in use. Another Skalpel proxy or process may be running.`);\n } else {\n logger.error(`Anthropic proxy failed to bind port ${config.anthropicPort}: ${err.message}`);\n }\n removePid(config.pidFile);\n process.exit(1);\n });\n\n openaiServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n logger.error(`Port ${config.openaiPort} is already in use. Another Skalpel proxy or process may be running.`);\n } else {\n logger.error(`OpenAI proxy failed to bind port ${config.openaiPort}: ${err.message}`);\n }\n removePid(config.pidFile);\n process.exit(1);\n });\n\n anthropicServer.listen(config.anthropicPort, () => {\n logger.info(`Anthropic proxy listening on port ${config.anthropicPort}`);\n });\n\n openaiServer.listen(config.openaiPort, () => {\n logger.info(`OpenAI proxy listening on port ${config.openaiPort}`);\n });\n\n writePid(config.pidFile);\n logger.info(`Proxy started (pid=${process.pid}) ports=${config.anthropicPort},${config.openaiPort}`);\n\n const cleanup = () => {\n logger.info('Shutting down proxy...');\n anthropicServer.close();\n openaiServer.close();\n removePid(config.pidFile);\n process.exit(0);\n };\n\n process.on('SIGTERM', cleanup);\n process.on('SIGINT', cleanup);\n\n // Catch unexpected errors so PID file is always cleaned up\n process.on('uncaughtException', (err) => {\n logger.error(`Uncaught exception: ${err.message}`);\n removePid(config.pidFile);\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logger.error(`Unhandled rejection: ${reason}`);\n removePid(config.pidFile);\n process.exit(1);\n });\n\n return { anthropicServer, openaiServer };\n}\n\nexport function stopProxy(config: ProxyConfig): boolean {\n const pid = readPid(config.pidFile);\n if (pid === null) return false;\n\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Process already gone\n }\n\n removePid(config.pidFile);\n return true;\n}\n\nexport function getProxyStatus(config: ProxyConfig): ProxyStatus {\n const pid = readPid(config.pidFile);\n return {\n running: pid !== null,\n pid,\n uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,\n anthropicPort: config.anthropicPort,\n openaiPort: config.openaiPort,\n };\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { ProxyConfig } from './types.js';\nimport type { Logger } from './logger.js';\n\nconst HOP_BY_HOP = new Set([\n 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',\n 'te', 'trailer', 'transfer-encoding', 'upgrade',\n]);\n\nconst STRIP_HEADERS = new Set([\n ...HOP_BY_HOP,\n 'content-encoding', 'content-length',\n]);\n\n/** Remove Skalpel-specific headers so a direct-to-Anthropic fallback request is clean. */\nfunction stripSkalpelHeaders(headers: Record<string, string>): Record<string, string> {\n const cleaned = { ...headers };\n delete cleaned['X-Skalpel-API-Key'];\n delete cleaned['X-Skalpel-Source'];\n delete cleaned['X-Skalpel-Agent-Type'];\n delete cleaned['X-Skalpel-SDK-Version'];\n return cleaned;\n}\n\n/** Returns true if the error or HTTP status indicates the Skalpel backend\n * itself is unreachable or broken (not a normal API error from Anthropic). */\nfunction isSkalpelBackendFailure(response: Response | null, err: unknown): boolean {\n if (err) return true;\n if (!response) return true;\n if (response.status >= 500) return true;\n return false;\n}\n\nasync function doStreamingFetch(\n url: string,\n body: string,\n headers: Record<string, string>,\n): Promise<Response> {\n return fetch(url, { method: 'POST', headers, body });\n}\n\nexport async function handleStreamingRequest(\n _req: IncomingMessage,\n res: ServerResponse,\n _config: ProxyConfig,\n _source: string,\n body: string,\n skalpelUrl: string,\n directUrl: string,\n useSkalpel: boolean,\n forwardHeaders: Record<string, string>,\n logger: Logger,\n): Promise<void> {\n let response: Response | null = null;\n let fetchError: unknown = null;\n let usedFallback = false;\n\n // Try Skalpel backend first (if this request should be optimized)\n if (useSkalpel) {\n try {\n response = await doStreamingFetch(skalpelUrl, body, forwardHeaders);\n } catch (err) {\n fetchError = err;\n }\n\n // If Skalpel backend is down, fall back to direct Anthropic API\n if (isSkalpelBackendFailure(response, fetchError)) {\n logger.warn(`streaming: Skalpel backend failed (${fetchError ? (fetchError as Error).message : `status ${response?.status}`}), falling back to direct Anthropic API`);\n usedFallback = true;\n response = null;\n fetchError = null;\n const directHeaders = stripSkalpelHeaders(forwardHeaders);\n try {\n response = await doStreamingFetch(directUrl, body, directHeaders);\n } catch (err) {\n fetchError = err;\n }\n }\n } else {\n // Non-Skalpel path — go direct\n try {\n response = await doStreamingFetch(directUrl, body, forwardHeaders);\n } catch (err) {\n fetchError = err;\n }\n }\n\n // If even the direct request failed, return error\n if (!response || fetchError) {\n const errMsg = fetchError ? (fetchError as Error).message : 'no response from upstream';\n logger.error(`streaming fetch failed: ${errMsg}`);\n res.writeHead(502, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n });\n res.write(`event: error\\ndata: ${JSON.stringify({ error: errMsg })}\\n\\n`);\n res.end();\n return;\n }\n\n if (usedFallback) {\n logger.info('streaming: using direct Anthropic API fallback');\n }\n\n // For non-2xx responses, pass through as-is so the SDK can parse error bodies correctly\n if (response.status >= 300) {\n const errorBody = Buffer.from(await response.arrayBuffer());\n logger.error(`streaming upstream error: status=${response.status} body=${errorBody.toString().slice(0, 500)}`);\n const passthroughHeaders: Record<string, string> = {};\n for (const [key, value] of response.headers.entries()) {\n if (!STRIP_HEADERS.has(key)) {\n passthroughHeaders[key] = value;\n }\n }\n passthroughHeaders['content-length'] = String(errorBody.length);\n res.writeHead(response.status, passthroughHeaders);\n res.end(errorBody);\n return;\n }\n\n // Build SSE headers, stripping hop-by-hop/encoding and normalizing content-type\n const sseHeaders: Record<string, string> = {};\n for (const [key, value] of response.headers.entries()) {\n if (!STRIP_HEADERS.has(key) && key !== 'content-type') {\n sseHeaders[key] = value;\n }\n }\n sseHeaders['Content-Type'] = 'text/event-stream';\n sseHeaders['Cache-Control'] = 'no-cache';\n res.writeHead(response.status, sseHeaders);\n\n if (!response.body) {\n res.write(`event: error\\ndata: ${JSON.stringify({ error: 'no response body' })}\\n\\n`);\n res.end();\n return;\n }\n\n try {\n const reader = (response.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const chunk = decoder.decode(value, { stream: true });\n res.write(chunk);\n }\n } catch (err) {\n logger.error(`streaming error: ${(err as Error).message}`);\n res.write(`event: error\\ndata: ${JSON.stringify({ error: (err as Error).message })}\\n\\n`);\n }\n\n res.end();\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { ProxyConfig } from './types.js';\nimport { handleStreamingRequest } from './streaming.js';\nimport type { Logger } from './logger.js';\n\n// Exact paths that should route through Skalpel optimization for Claude Code.\n// Sub-paths like /v1/messages/count_tokens go direct to Anthropic.\nconst SKALPEL_EXACT_PATHS = new Set(['/v1/messages']);\n\nfunction collectBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString()));\n req.on('error', reject);\n });\n}\n\nexport function shouldRouteToSkalpel(path: string, source: string): boolean {\n if (source !== 'claude-code') return true;\n // Strip query string — Claude Code sends /v1/messages?beta=true\n const pathname = path.split('?')[0];\n return SKALPEL_EXACT_PATHS.has(pathname);\n}\n\n/** Returns true if the error or HTTP status indicates the Skalpel backend\n * itself is unreachable or broken (not a normal API error from Anthropic). */\nfunction isSkalpelBackendFailure(response: Response | null, err: unknown): boolean {\n // Network-level failure (DNS, connection refused, timeout, etc.)\n if (err) return true;\n if (!response) return true;\n // 5xx from the Skalpel backend means the backend is having issues\n if (response.status >= 500) return true;\n return false;\n}\n\n/** Build the set of headers to forward, adding Skalpel-specific headers when routing through Skalpel. */\nexport function buildForwardHeaders(\n req: IncomingMessage,\n config: ProxyConfig,\n source: string,\n useSkalpel: boolean,\n): Record<string, string> {\n const forwardHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers)) {\n if (value !== undefined) {\n forwardHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n delete forwardHeaders['host'];\n delete forwardHeaders['connection'];\n\n if (useSkalpel) {\n forwardHeaders['X-Skalpel-API-Key'] = config.apiKey;\n forwardHeaders['X-Skalpel-Source'] = source;\n forwardHeaders['X-Skalpel-Agent-Type'] = source;\n forwardHeaders['X-Skalpel-SDK-Version'] = 'proxy-1.0.0';\n\n // Claude Code may send either x-api-key (API key auth) or\n // Authorization: Bearer (OAuth auth). Only convert Bearer to x-api-key\n // for actual API keys (sk-ant-*). OAuth tokens must stay as\n // Authorization: Bearer — Anthropic rejects them in x-api-key.\n if (source === 'claude-code' && !forwardHeaders['x-api-key']) {\n const authHeader = forwardHeaders['authorization'] ?? '';\n if (authHeader.toLowerCase().startsWith('bearer ')) {\n const token = authHeader.slice(7).trim();\n if (token.startsWith('sk-ant-')) {\n forwardHeaders['x-api-key'] = token;\n }\n }\n }\n }\n\n return forwardHeaders;\n}\n\n/** Remove Skalpel-specific headers so a direct-to-Anthropic fallback request is clean. */\nfunction stripSkalpelHeaders(headers: Record<string, string>): Record<string, string> {\n const cleaned = { ...headers };\n delete cleaned['X-Skalpel-API-Key'];\n delete cleaned['X-Skalpel-Source'];\n delete cleaned['X-Skalpel-Agent-Type'];\n delete cleaned['X-Skalpel-SDK-Version'];\n return cleaned;\n}\n\n// Headers that should not be forwarded by a proxy.\n// Also strips content-encoding and content-length because fetch()\n// automatically decompresses gzip/deflate/br responses — the body we\n// read via arrayBuffer() is already decompressed, so forwarding the\n// original content-encoding header causes the client to try to decompress\n// plain text → ZlibError.\nconst STRIP_RESPONSE_HEADERS = new Set([\n 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',\n 'te', 'trailer', 'transfer-encoding', 'upgrade',\n 'content-encoding', 'content-length',\n]);\n\nfunction extractResponseHeaders(response: Response): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const [key, value] of response.headers.entries()) {\n if (!STRIP_RESPONSE_HEADERS.has(key)) {\n headers[key] = value;\n }\n }\n return headers;\n}\n\nexport async function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n config: ProxyConfig,\n source: string,\n logger: Logger,\n): Promise<void> {\n const start = Date.now();\n const method = req.method ?? 'GET';\n const path = req.url ?? '/';\n\n try {\n const body = await collectBody(req);\n const useSkalpel = shouldRouteToSkalpel(path, source);\n const forwardHeaders = buildForwardHeaders(req, config, source, useSkalpel);\n\n let isStreaming = false;\n if (body) {\n try {\n const parsed = JSON.parse(body);\n isStreaming = parsed.stream === true;\n } catch {\n // Not JSON — treat as non-streaming\n }\n }\n\n if (isStreaming) {\n const skalpelUrl = `${config.remoteBaseUrl}${path}`;\n const directUrl = `${config.anthropicDirectUrl}${path}`;\n await handleStreamingRequest(req, res, config, source, body, skalpelUrl, directUrl, useSkalpel, forwardHeaders, logger);\n logger.info(`${method} ${path} source=${source} streaming latency=${Date.now() - start}ms`);\n return;\n }\n\n const skalpelUrl = `${config.remoteBaseUrl}${path}`;\n const directUrl = `${config.anthropicDirectUrl}${path}`;\n const fetchBody = method !== 'GET' && method !== 'HEAD' ? body : undefined;\n\n let response: Response | null = null;\n let fetchError: unknown = null;\n let usedFallback = false;\n\n // Try Skalpel backend first (if this request should be optimized)\n if (useSkalpel) {\n try {\n response = await fetch(skalpelUrl, { method, headers: forwardHeaders, body: fetchBody });\n } catch (err) {\n fetchError = err;\n }\n\n // If Skalpel backend is down, fall back to direct Anthropic API\n if (isSkalpelBackendFailure(response, fetchError)) {\n logger.warn(`${method} ${path} Skalpel backend failed (${fetchError ? (fetchError as Error).message : `status ${response?.status}`}), falling back to direct Anthropic API`);\n usedFallback = true;\n response = null;\n fetchError = null;\n const directHeaders = stripSkalpelHeaders(forwardHeaders);\n try {\n response = await fetch(directUrl, { method, headers: directHeaders, body: fetchBody });\n } catch (err) {\n fetchError = err;\n }\n }\n } else {\n // Non-Skalpel path — go direct\n try {\n response = await fetch(directUrl, { method, headers: forwardHeaders, body: fetchBody });\n } catch (err) {\n fetchError = err;\n }\n }\n\n // If even the direct request failed, return 502\n if (!response || fetchError) {\n throw fetchError ?? new Error('no response from upstream');\n }\n\n const responseHeaders = extractResponseHeaders(response);\n const responseBody = Buffer.from(await response.arrayBuffer());\n responseHeaders['content-length'] = String(responseBody.length);\n res.writeHead(response.status, responseHeaders);\n res.end(responseBody);\n\n logger.info(`${method} ${path} source=${source} status=${response.status}${usedFallback ? ' (fallback)' : ''} latency=${Date.now() - start}ms`);\n } catch (err) {\n logger.error(`${method} ${path} source=${source} error=${(err as Error).message}`);\n if (!res.headersSent) {\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'proxy_error', message: (err as Error).message }));\n }\n }\n}\n","import type { ServerResponse } from 'node:http';\nimport type { ProxyConfig } from './types.js';\n\nexport function handleHealthRequest(\n res: ServerResponse,\n config: ProxyConfig,\n startTime: number,\n): void {\n const body = JSON.stringify({\n status: 'ok',\n uptime: Date.now() - startTime,\n ports: {\n anthropic: config.anthropicPort,\n openai: config.openaiPort,\n },\n version: 'proxy-1.0.0',\n });\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(body);\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nexport function writePid(pidFile: string): void {\n fs.mkdirSync(path.dirname(pidFile), { recursive: true });\n fs.writeFileSync(pidFile, String(process.pid));\n}\n\nexport function readPid(pidFile: string): number | null {\n try {\n const raw = fs.readFileSync(pidFile, 'utf-8').trim();\n const pid = parseInt(raw, 10);\n if (isNaN(pid)) return null;\n return isRunning(pid) ? pid : null;\n } catch {\n return null;\n }\n}\n\nexport function isRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function removePid(pidFile: string): void {\n try {\n fs.unlinkSync(pidFile);\n } catch {\n // Already removed\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nconst MAX_SIZE = 5 * 1024 * 1024; // 5MB\nconst MAX_ROTATIONS = 3;\n\nconst LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } as const;\n\nexport class Logger {\n private logFile: string;\n private level: keyof typeof LEVELS;\n\n constructor(logFile: string, level: keyof typeof LEVELS = 'info') {\n this.logFile = logFile;\n this.level = level;\n fs.mkdirSync(path.dirname(logFile), { recursive: true });\n }\n\n debug(msg: string): void { this.log('debug', msg); }\n info(msg: string): void { this.log('info', msg); }\n warn(msg: string): void { this.log('warn', msg); }\n error(msg: string): void { this.log('error', msg); }\n\n private log(level: keyof typeof LEVELS, msg: string): void {\n if (LEVELS[level] < LEVELS[this.level]) return;\n\n const line = `${new Date().toISOString()} [${level.toUpperCase()}] ${msg}\\n`;\n\n if (level === 'debug' || level === 'error') {\n process.stderr.write(line);\n }\n\n try {\n this.rotate();\n fs.appendFileSync(this.logFile, line);\n } catch {\n // Best-effort logging\n }\n }\n\n private rotate(): void {\n try {\n const stat = fs.statSync(this.logFile);\n if (stat.size < MAX_SIZE) return;\n } catch {\n return;\n }\n\n for (let i = MAX_ROTATIONS; i >= 1; i--) {\n const src = i === 1 ? this.logFile : `${this.logFile}.${i - 1}`;\n const dst = `${this.logFile}.${i}`;\n try {\n fs.renameSync(src, dst);\n } catch {\n // File may not exist\n }\n }\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport type { ProxyConfig } from './types.js';\n\nfunction expandHome(filePath: string): string {\n if (filePath.startsWith('~')) {\n return path.join(os.homedir(), filePath.slice(1));\n }\n return filePath;\n}\n\nconst DEFAULTS: ProxyConfig = {\n apiKey: '',\n remoteBaseUrl: 'https://api.skalpel.ai',\n anthropicDirectUrl: 'https://api.anthropic.com',\n anthropicPort: 18100,\n openaiPort: 18101,\n logLevel: 'info',\n logFile: '~/.skalpel/logs/proxy.log',\n pidFile: '~/.skalpel/proxy.pid',\n configFile: '~/.skalpel/config.json',\n};\n\nexport function loadConfig(configPath?: string): ProxyConfig {\n const filePath = expandHome(configPath ?? DEFAULTS.configFile);\n let fileConfig: Partial<ProxyConfig> = {};\n\n try {\n const raw = fs.readFileSync(filePath, 'utf-8');\n fileConfig = JSON.parse(raw) as Partial<ProxyConfig>;\n } catch {\n // Config file doesn't exist or is invalid — use defaults\n }\n\n return {\n apiKey: fileConfig.apiKey ?? DEFAULTS.apiKey,\n remoteBaseUrl: fileConfig.remoteBaseUrl ?? DEFAULTS.remoteBaseUrl,\n anthropicDirectUrl: fileConfig.anthropicDirectUrl ?? DEFAULTS.anthropicDirectUrl,\n anthropicPort: fileConfig.anthropicPort ?? DEFAULTS.anthropicPort,\n openaiPort: fileConfig.openaiPort ?? DEFAULTS.openaiPort,\n logLevel: fileConfig.logLevel ?? DEFAULTS.logLevel,\n logFile: expandHome(fileConfig.logFile ?? DEFAULTS.logFile),\n pidFile: expandHome(fileConfig.pidFile ?? DEFAULTS.pidFile),\n configFile: filePath,\n };\n}\n\nexport function saveConfig(config: ProxyConfig): void {\n const dir = path.dirname(config.configFile);\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(config.configFile, JSON.stringify(config, null, 2) + '\\n');\n}\n","import { startProxy } from '../proxy/server.js';\nimport { loadConfig } from '../proxy/config.js';\n\nconst config = loadConfig();\nstartProxy(config);\n"],"mappings":"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","names":["path","isSkalpelBackendFailure","config","stripSkalpelHeaders","skalpelUrl","directUrl","config","fs","path","config","fs","path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/proxy/server.ts","../../src/proxy/streaming.ts","../../src/proxy/handler.ts","../../src/proxy/health.ts","../../src/proxy/pid.ts","../../src/proxy/logger.ts","../../src/proxy/config.ts","../../src/cli/proxy-runner.ts"],"sourcesContent":["import http from 'node:http';\nimport type { ProxyConfig, ProxyStatus } from './types.js';\nimport { handleRequest } from './handler.js';\nimport { handleHealthRequest } from './health.js';\nimport { writePid, readPid, removePid } from './pid.js';\nimport { Logger } from './logger.js';\n\nlet proxyStartTime = 0;\n\nexport function startProxy(config: ProxyConfig): { anthropicServer: http.Server; openaiServer: http.Server } {\n const logger = new Logger(config.logFile, config.logLevel);\n const startTime = Date.now();\n proxyStartTime = Date.now();\n\n const anthropicServer = http.createServer((req, res) => {\n if (req.url === '/health' && req.method === 'GET') {\n handleHealthRequest(res, config, startTime);\n return;\n }\n handleRequest(req, res, config, 'claude-code', logger);\n });\n\n const openaiServer = http.createServer((req, res) => {\n if (req.url === '/health' && req.method === 'GET') {\n handleHealthRequest(res, config, startTime);\n return;\n }\n handleRequest(req, res, config, 'codex', logger);\n });\n\n // Handle port binding errors (EADDRINUSE, EACCES, etc.)\n anthropicServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n logger.error(`Port ${config.anthropicPort} is already in use. Another Skalpel proxy or process may be running.`);\n } else {\n logger.error(`Anthropic proxy failed to bind port ${config.anthropicPort}: ${err.message}`);\n }\n removePid(config.pidFile);\n process.exit(1);\n });\n\n openaiServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n logger.error(`Port ${config.openaiPort} is already in use. Another Skalpel proxy or process may be running.`);\n } else {\n logger.error(`OpenAI proxy failed to bind port ${config.openaiPort}: ${err.message}`);\n }\n removePid(config.pidFile);\n process.exit(1);\n });\n\n anthropicServer.listen(config.anthropicPort, () => {\n logger.info(`Anthropic proxy listening on port ${config.anthropicPort}`);\n });\n\n openaiServer.listen(config.openaiPort, () => {\n logger.info(`OpenAI proxy listening on port ${config.openaiPort}`);\n });\n\n writePid(config.pidFile);\n logger.info(`Proxy started (pid=${process.pid}) ports=${config.anthropicPort},${config.openaiPort}`);\n\n const cleanup = () => {\n logger.info('Shutting down proxy...');\n anthropicServer.close();\n openaiServer.close();\n removePid(config.pidFile);\n process.exit(0);\n };\n\n process.on('SIGTERM', cleanup);\n process.on('SIGINT', cleanup);\n\n // Catch unexpected errors so PID file is always cleaned up\n process.on('uncaughtException', (err) => {\n logger.error(`Uncaught exception: ${err.message}`);\n removePid(config.pidFile);\n process.exit(1);\n });\n\n process.on('unhandledRejection', (reason) => {\n logger.error(`Unhandled rejection: ${reason}`);\n removePid(config.pidFile);\n process.exit(1);\n });\n\n return { anthropicServer, openaiServer };\n}\n\nexport function stopProxy(config: ProxyConfig): boolean {\n const pid = readPid(config.pidFile);\n if (pid === null) return false;\n\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n // Process already gone\n }\n\n removePid(config.pidFile);\n return true;\n}\n\nexport function getProxyStatus(config: ProxyConfig): ProxyStatus {\n const pid = readPid(config.pidFile);\n return {\n running: pid !== null,\n pid,\n uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,\n anthropicPort: config.anthropicPort,\n openaiPort: config.openaiPort,\n };\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { ProxyConfig } from './types.js';\nimport type { Logger } from './logger.js';\n\nconst HOP_BY_HOP = new Set([\n 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',\n 'te', 'trailer', 'transfer-encoding', 'upgrade',\n]);\n\nconst STRIP_HEADERS = new Set([\n ...HOP_BY_HOP,\n 'content-encoding', 'content-length',\n]);\n\n/** Remove Skalpel-specific headers so a direct-to-Anthropic fallback request is clean. */\nfunction stripSkalpelHeaders(headers: Record<string, string>): Record<string, string> {\n const cleaned = { ...headers };\n delete cleaned['X-Skalpel-API-Key'];\n delete cleaned['X-Skalpel-Source'];\n delete cleaned['X-Skalpel-Agent-Type'];\n delete cleaned['X-Skalpel-SDK-Version'];\n return cleaned;\n}\n\n// Status codes that only the Skalpel backend returns — Anthropic never\n// uses these, so receiving one means the Skalpel middleware rejected the\n// request and we should bypass it and go direct to Anthropic.\nconst SKALPEL_ONLY_STATUS_CODES = new Set([\n 402, // Payment Required — Skalpel billing gate\n 409, // Conflict — Skalpel duplicate-request lock\n]);\n\n/** Returns true if the error or HTTP status indicates the Skalpel backend\n * itself is unreachable or broken (not a normal API error from Anthropic). */\nfunction isSkalpelBackendFailure(response: Response | null, err: unknown): boolean {\n if (err) return true;\n if (!response) return true;\n if (response.status >= 500) return true;\n if (SKALPEL_ONLY_STATUS_CODES.has(response.status)) return true;\n // 429 from Skalpel may be its own rate limit (not Anthropic's).\n // Falling back is harmless: if Anthropic also rate-limits, Claude Code\n // sees Anthropic's properly-formatted 429 instead of Skalpel's.\n if (response.status === 429) return true;\n return false;\n}\n\nasync function doStreamingFetch(\n url: string,\n body: string,\n headers: Record<string, string>,\n): Promise<Response> {\n return fetch(url, { method: 'POST', headers, body });\n}\n\nexport async function handleStreamingRequest(\n _req: IncomingMessage,\n res: ServerResponse,\n _config: ProxyConfig,\n _source: string,\n body: string,\n skalpelUrl: string,\n directUrl: string,\n useSkalpel: boolean,\n forwardHeaders: Record<string, string>,\n logger: Logger,\n): Promise<void> {\n let response: Response | null = null;\n let fetchError: unknown = null;\n let usedFallback = false;\n\n // Try Skalpel backend first (if this request should be optimized)\n if (useSkalpel) {\n try {\n response = await doStreamingFetch(skalpelUrl, body, forwardHeaders);\n } catch (err) {\n fetchError = err;\n }\n\n // If Skalpel backend is down, fall back to direct Anthropic API\n if (isSkalpelBackendFailure(response, fetchError)) {\n logger.warn(`streaming: Skalpel backend failed (${fetchError ? (fetchError as Error).message : `status ${response?.status}`}), falling back to direct Anthropic API`);\n usedFallback = true;\n response = null;\n fetchError = null;\n const directHeaders = stripSkalpelHeaders(forwardHeaders);\n try {\n response = await doStreamingFetch(directUrl, body, directHeaders);\n } catch (err) {\n fetchError = err;\n }\n }\n } else {\n // Non-Skalpel path — go direct\n try {\n response = await doStreamingFetch(directUrl, body, forwardHeaders);\n } catch (err) {\n fetchError = err;\n }\n }\n\n // If even the direct request failed, return error\n if (!response || fetchError) {\n const errMsg = fetchError ? (fetchError as Error).message : 'no response from upstream';\n logger.error(`streaming fetch failed: ${errMsg}`);\n res.writeHead(502, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n });\n const errorPayload = {\n type: 'error',\n error: { type: 'proxy_error', message: errMsg },\n };\n res.write(`event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`);\n res.end();\n return;\n }\n\n if (usedFallback) {\n logger.info('streaming: using direct Anthropic API fallback');\n }\n\n // For non-2xx responses, pass through as-is so the SDK can parse error bodies correctly\n if (response.status >= 300) {\n const errorBody = Buffer.from(await response.arrayBuffer());\n logger.error(`streaming upstream error: status=${response.status} body=${errorBody.toString().slice(0, 500)}`);\n const passthroughHeaders: Record<string, string> = {};\n for (const [key, value] of response.headers.entries()) {\n if (!STRIP_HEADERS.has(key)) {\n passthroughHeaders[key] = value;\n }\n }\n passthroughHeaders['content-length'] = String(errorBody.length);\n res.writeHead(response.status, passthroughHeaders);\n res.end(errorBody);\n return;\n }\n\n // Build SSE headers, stripping hop-by-hop/encoding and normalizing content-type\n const sseHeaders: Record<string, string> = {};\n for (const [key, value] of response.headers.entries()) {\n if (!STRIP_HEADERS.has(key) && key !== 'content-type') {\n sseHeaders[key] = value;\n }\n }\n sseHeaders['Content-Type'] = 'text/event-stream';\n sseHeaders['Cache-Control'] = 'no-cache';\n res.writeHead(response.status, sseHeaders);\n\n if (!response.body) {\n const noBodyError = {\n type: 'error',\n error: { type: 'proxy_error', message: 'no response body' },\n };\n res.write(`event: error\\ndata: ${JSON.stringify(noBodyError)}\\n\\n`);\n res.end();\n return;\n }\n\n try {\n const reader = (response.body as ReadableStream<Uint8Array>).getReader();\n const decoder = new TextDecoder();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const chunk = decoder.decode(value, { stream: true });\n res.write(chunk);\n }\n } catch (err) {\n logger.error(`streaming error: ${(err as Error).message}`);\n // Use Anthropic-compatible error event format so Claude Code's SDK\n // can parse it and handle the interrupted stream properly.\n const errorPayload = {\n type: 'error',\n error: { type: 'proxy_error', message: (err as Error).message },\n };\n res.write(`event: error\\ndata: ${JSON.stringify(errorPayload)}\\n\\n`);\n }\n\n res.end();\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { ProxyConfig } from './types.js';\nimport { handleStreamingRequest } from './streaming.js';\nimport type { Logger } from './logger.js';\n\n// Exact paths that should route through Skalpel optimization for Claude Code.\n// Sub-paths like /v1/messages/count_tokens go direct to Anthropic.\nconst SKALPEL_EXACT_PATHS = new Set(['/v1/messages']);\n\nfunction collectBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString()));\n req.on('error', reject);\n });\n}\n\nexport function shouldRouteToSkalpel(path: string, source: string): boolean {\n if (source !== 'claude-code') return true;\n // Strip query string — Claude Code sends /v1/messages?beta=true\n const pathname = path.split('?')[0];\n return SKALPEL_EXACT_PATHS.has(pathname);\n}\n\n// Status codes that only the Skalpel backend returns — Anthropic never\n// uses these, so receiving one means the Skalpel middleware rejected the\n// request and we should bypass it and go direct to Anthropic.\nconst SKALPEL_ONLY_STATUS_CODES = new Set([\n 402, // Payment Required — Skalpel billing gate\n 409, // Conflict — Skalpel duplicate-request lock\n]);\n\n/** Returns true if the error or HTTP status indicates the Skalpel backend\n * itself is unreachable or broken (not a normal API error from Anthropic). */\nfunction isSkalpelBackendFailure(response: Response | null, err: unknown): boolean {\n // Network-level failure (DNS, connection refused, timeout, etc.)\n if (err) return true;\n if (!response) return true;\n // 5xx from the Skalpel backend means the backend is having issues\n if (response.status >= 500) return true;\n // Skalpel-only status codes — Anthropic never returns these\n if (SKALPEL_ONLY_STATUS_CODES.has(response.status)) return true;\n // 429 from Skalpel may be its own rate limit (not Anthropic's).\n // Falling back is harmless: if Anthropic also rate-limits, Claude Code\n // sees Anthropic's properly-formatted 429 instead of Skalpel's.\n if (response.status === 429) return true;\n return false;\n}\n\n/** Build the set of headers to forward, adding Skalpel-specific headers when routing through Skalpel. */\nexport function buildForwardHeaders(\n req: IncomingMessage,\n config: ProxyConfig,\n source: string,\n useSkalpel: boolean,\n): Record<string, string> {\n const forwardHeaders: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers)) {\n if (value !== undefined) {\n forwardHeaders[key] = Array.isArray(value) ? value.join(', ') : value;\n }\n }\n delete forwardHeaders['host'];\n delete forwardHeaders['connection'];\n\n if (useSkalpel) {\n forwardHeaders['X-Skalpel-API-Key'] = config.apiKey;\n forwardHeaders['X-Skalpel-Source'] = source;\n forwardHeaders['X-Skalpel-Agent-Type'] = source;\n forwardHeaders['X-Skalpel-SDK-Version'] = 'proxy-1.0.0';\n\n // Claude Code may send either x-api-key (API key auth) or\n // Authorization: Bearer (OAuth auth). Only convert Bearer to x-api-key\n // for actual API keys (sk-ant-*). OAuth tokens must stay as\n // Authorization: Bearer — Anthropic rejects them in x-api-key.\n if (source === 'claude-code' && !forwardHeaders['x-api-key']) {\n const authHeader = forwardHeaders['authorization'] ?? '';\n if (authHeader.toLowerCase().startsWith('bearer ')) {\n const token = authHeader.slice(7).trim();\n if (token.startsWith('sk-ant-')) {\n forwardHeaders['x-api-key'] = token;\n }\n }\n }\n }\n\n return forwardHeaders;\n}\n\n/** Remove Skalpel-specific headers so a direct-to-Anthropic fallback request is clean. */\nfunction stripSkalpelHeaders(headers: Record<string, string>): Record<string, string> {\n const cleaned = { ...headers };\n delete cleaned['X-Skalpel-API-Key'];\n delete cleaned['X-Skalpel-Source'];\n delete cleaned['X-Skalpel-Agent-Type'];\n delete cleaned['X-Skalpel-SDK-Version'];\n return cleaned;\n}\n\n// Headers that should not be forwarded by a proxy.\n// Also strips content-encoding and content-length because fetch()\n// automatically decompresses gzip/deflate/br responses — the body we\n// read via arrayBuffer() is already decompressed, so forwarding the\n// original content-encoding header causes the client to try to decompress\n// plain text → ZlibError.\nconst STRIP_RESPONSE_HEADERS = new Set([\n 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',\n 'te', 'trailer', 'transfer-encoding', 'upgrade',\n 'content-encoding', 'content-length',\n]);\n\nfunction extractResponseHeaders(response: Response): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const [key, value] of response.headers.entries()) {\n if (!STRIP_RESPONSE_HEADERS.has(key)) {\n headers[key] = value;\n }\n }\n return headers;\n}\n\nexport async function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n config: ProxyConfig,\n source: string,\n logger: Logger,\n): Promise<void> {\n const start = Date.now();\n const method = req.method ?? 'GET';\n const path = req.url ?? '/';\n\n try {\n const body = await collectBody(req);\n const useSkalpel = shouldRouteToSkalpel(path, source);\n const forwardHeaders = buildForwardHeaders(req, config, source, useSkalpel);\n\n let isStreaming = false;\n if (body) {\n try {\n const parsed = JSON.parse(body);\n isStreaming = parsed.stream === true;\n } catch {\n // Not JSON — treat as non-streaming\n }\n }\n\n if (isStreaming) {\n const skalpelUrl = `${config.remoteBaseUrl}${path}`;\n const directUrl = `${config.anthropicDirectUrl}${path}`;\n await handleStreamingRequest(req, res, config, source, body, skalpelUrl, directUrl, useSkalpel, forwardHeaders, logger);\n logger.info(`${method} ${path} source=${source} streaming latency=${Date.now() - start}ms`);\n return;\n }\n\n const skalpelUrl = `${config.remoteBaseUrl}${path}`;\n const directUrl = `${config.anthropicDirectUrl}${path}`;\n const fetchBody = method !== 'GET' && method !== 'HEAD' ? body : undefined;\n\n let response: Response | null = null;\n let fetchError: unknown = null;\n let usedFallback = false;\n\n // Try Skalpel backend first (if this request should be optimized)\n if (useSkalpel) {\n try {\n response = await fetch(skalpelUrl, { method, headers: forwardHeaders, body: fetchBody });\n } catch (err) {\n fetchError = err;\n }\n\n // If Skalpel backend is down, fall back to direct Anthropic API\n if (isSkalpelBackendFailure(response, fetchError)) {\n logger.warn(`${method} ${path} Skalpel backend failed (${fetchError ? (fetchError as Error).message : `status ${response?.status}`}), falling back to direct Anthropic API`);\n usedFallback = true;\n response = null;\n fetchError = null;\n const directHeaders = stripSkalpelHeaders(forwardHeaders);\n try {\n response = await fetch(directUrl, { method, headers: directHeaders, body: fetchBody });\n } catch (err) {\n fetchError = err;\n }\n }\n } else {\n // Non-Skalpel path — go direct\n try {\n response = await fetch(directUrl, { method, headers: forwardHeaders, body: fetchBody });\n } catch (err) {\n fetchError = err;\n }\n }\n\n // If even the direct request failed, return 502\n if (!response || fetchError) {\n throw fetchError ?? new Error('no response from upstream');\n }\n\n const responseHeaders = extractResponseHeaders(response);\n const responseBody = Buffer.from(await response.arrayBuffer());\n responseHeaders['content-length'] = String(responseBody.length);\n res.writeHead(response.status, responseHeaders);\n res.end(responseBody);\n\n logger.info(`${method} ${path} source=${source} status=${response.status}${usedFallback ? ' (fallback)' : ''} latency=${Date.now() - start}ms`);\n } catch (err) {\n logger.error(`${method} ${path} source=${source} error=${(err as Error).message}`);\n if (!res.headersSent) {\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'proxy_error', message: (err as Error).message }));\n }\n }\n}\n","import type { ServerResponse } from 'node:http';\nimport type { ProxyConfig } from './types.js';\n\nexport function handleHealthRequest(\n res: ServerResponse,\n config: ProxyConfig,\n startTime: number,\n): void {\n const body = JSON.stringify({\n status: 'ok',\n uptime: Date.now() - startTime,\n ports: {\n anthropic: config.anthropicPort,\n openai: config.openaiPort,\n },\n version: 'proxy-1.0.0',\n });\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(body);\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nexport function writePid(pidFile: string): void {\n fs.mkdirSync(path.dirname(pidFile), { recursive: true });\n fs.writeFileSync(pidFile, String(process.pid));\n}\n\nexport function readPid(pidFile: string): number | null {\n try {\n const raw = fs.readFileSync(pidFile, 'utf-8').trim();\n const pid = parseInt(raw, 10);\n if (isNaN(pid)) return null;\n return isRunning(pid) ? pid : null;\n } catch {\n return null;\n }\n}\n\nexport function isRunning(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function removePid(pidFile: string): void {\n try {\n fs.unlinkSync(pidFile);\n } catch {\n // Already removed\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nconst MAX_SIZE = 5 * 1024 * 1024; // 5MB\nconst MAX_ROTATIONS = 3;\n\nconst LEVELS = { debug: 0, info: 1, warn: 2, error: 3 } as const;\n\nexport class Logger {\n private logFile: string;\n private level: keyof typeof LEVELS;\n\n constructor(logFile: string, level: keyof typeof LEVELS = 'info') {\n this.logFile = logFile;\n this.level = level;\n fs.mkdirSync(path.dirname(logFile), { recursive: true });\n }\n\n debug(msg: string): void { this.log('debug', msg); }\n info(msg: string): void { this.log('info', msg); }\n warn(msg: string): void { this.log('warn', msg); }\n error(msg: string): void { this.log('error', msg); }\n\n private log(level: keyof typeof LEVELS, msg: string): void {\n if (LEVELS[level] < LEVELS[this.level]) return;\n\n const line = `${new Date().toISOString()} [${level.toUpperCase()}] ${msg}\\n`;\n\n if (level === 'debug' || level === 'error') {\n process.stderr.write(line);\n }\n\n try {\n this.rotate();\n fs.appendFileSync(this.logFile, line);\n } catch {\n // Best-effort logging\n }\n }\n\n private rotate(): void {\n try {\n const stat = fs.statSync(this.logFile);\n if (stat.size < MAX_SIZE) return;\n } catch {\n return;\n }\n\n for (let i = MAX_ROTATIONS; i >= 1; i--) {\n const src = i === 1 ? this.logFile : `${this.logFile}.${i - 1}`;\n const dst = `${this.logFile}.${i}`;\n try {\n fs.renameSync(src, dst);\n } catch {\n // File may not exist\n }\n }\n }\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport type { ProxyConfig } from './types.js';\n\nfunction expandHome(filePath: string): string {\n if (filePath.startsWith('~')) {\n return path.join(os.homedir(), filePath.slice(1));\n }\n return filePath;\n}\n\nconst DEFAULTS: ProxyConfig = {\n apiKey: '',\n remoteBaseUrl: 'https://api.skalpel.ai',\n anthropicDirectUrl: 'https://api.anthropic.com',\n anthropicPort: 18100,\n openaiPort: 18101,\n logLevel: 'info',\n logFile: '~/.skalpel/logs/proxy.log',\n pidFile: '~/.skalpel/proxy.pid',\n configFile: '~/.skalpel/config.json',\n};\n\nexport function loadConfig(configPath?: string): ProxyConfig {\n const filePath = expandHome(configPath ?? DEFAULTS.configFile);\n let fileConfig: Partial<ProxyConfig> = {};\n\n try {\n const raw = fs.readFileSync(filePath, 'utf-8');\n fileConfig = JSON.parse(raw) as Partial<ProxyConfig>;\n } catch {\n // Config file doesn't exist or is invalid — use defaults\n }\n\n return {\n apiKey: fileConfig.apiKey ?? DEFAULTS.apiKey,\n remoteBaseUrl: fileConfig.remoteBaseUrl ?? DEFAULTS.remoteBaseUrl,\n anthropicDirectUrl: fileConfig.anthropicDirectUrl ?? DEFAULTS.anthropicDirectUrl,\n anthropicPort: fileConfig.anthropicPort ?? DEFAULTS.anthropicPort,\n openaiPort: fileConfig.openaiPort ?? DEFAULTS.openaiPort,\n logLevel: fileConfig.logLevel ?? DEFAULTS.logLevel,\n logFile: expandHome(fileConfig.logFile ?? DEFAULTS.logFile),\n pidFile: expandHome(fileConfig.pidFile ?? DEFAULTS.pidFile),\n configFile: filePath,\n };\n}\n\nexport function saveConfig(config: ProxyConfig): void {\n const dir = path.dirname(config.configFile);\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(config.configFile, JSON.stringify(config, null, 2) + '\\n');\n}\n","import { startProxy } from '../proxy/server.js';\nimport { loadConfig } from '../proxy/config.js';\n\nconst config = loadConfig();\nstartProxy(config);\n"],"mappings":"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","names":["path","SKALPEL_ONLY_STATUS_CODES","isSkalpelBackendFailure","config","stripSkalpelHeaders","skalpelUrl","directUrl","config","fs","path","config","fs","path"]}
|
package/dist/index.cjs
CHANGED
|
@@ -532,10 +532,18 @@ function stripSkalpelHeaders(headers) {
|
|
|
532
532
|
delete cleaned["X-Skalpel-SDK-Version"];
|
|
533
533
|
return cleaned;
|
|
534
534
|
}
|
|
535
|
+
var SKALPEL_ONLY_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
536
|
+
402,
|
|
537
|
+
// Payment Required — Skalpel billing gate
|
|
538
|
+
409
|
|
539
|
+
// Conflict — Skalpel duplicate-request lock
|
|
540
|
+
]);
|
|
535
541
|
function isSkalpelBackendFailure(response, err) {
|
|
536
542
|
if (err) return true;
|
|
537
543
|
if (!response) return true;
|
|
538
544
|
if (response.status >= 500) return true;
|
|
545
|
+
if (SKALPEL_ONLY_STATUS_CODES.has(response.status)) return true;
|
|
546
|
+
if (response.status === 429) return true;
|
|
539
547
|
return false;
|
|
540
548
|
}
|
|
541
549
|
async function doStreamingFetch(url, body, headers) {
|
|
@@ -577,8 +585,12 @@ async function handleStreamingRequest(_req, res, _config, _source, body, skalpel
|
|
|
577
585
|
"Content-Type": "text/event-stream",
|
|
578
586
|
"Cache-Control": "no-cache"
|
|
579
587
|
});
|
|
588
|
+
const errorPayload = {
|
|
589
|
+
type: "error",
|
|
590
|
+
error: { type: "proxy_error", message: errMsg }
|
|
591
|
+
};
|
|
580
592
|
res.write(`event: error
|
|
581
|
-
data: ${JSON.stringify(
|
|
593
|
+
data: ${JSON.stringify(errorPayload)}
|
|
582
594
|
|
|
583
595
|
`);
|
|
584
596
|
res.end();
|
|
@@ -611,8 +623,12 @@ data: ${JSON.stringify({ error: errMsg })}
|
|
|
611
623
|
sseHeaders["Cache-Control"] = "no-cache";
|
|
612
624
|
res.writeHead(response.status, sseHeaders);
|
|
613
625
|
if (!response.body) {
|
|
626
|
+
const noBodyError = {
|
|
627
|
+
type: "error",
|
|
628
|
+
error: { type: "proxy_error", message: "no response body" }
|
|
629
|
+
};
|
|
614
630
|
res.write(`event: error
|
|
615
|
-
data: ${JSON.stringify(
|
|
631
|
+
data: ${JSON.stringify(noBodyError)}
|
|
616
632
|
|
|
617
633
|
`);
|
|
618
634
|
res.end();
|
|
@@ -629,8 +645,12 @@ data: ${JSON.stringify({ error: "no response body" })}
|
|
|
629
645
|
}
|
|
630
646
|
} catch (err) {
|
|
631
647
|
logger.error(`streaming error: ${err.message}`);
|
|
648
|
+
const errorPayload = {
|
|
649
|
+
type: "error",
|
|
650
|
+
error: { type: "proxy_error", message: err.message }
|
|
651
|
+
};
|
|
632
652
|
res.write(`event: error
|
|
633
|
-
data: ${JSON.stringify(
|
|
653
|
+
data: ${JSON.stringify(errorPayload)}
|
|
634
654
|
|
|
635
655
|
`);
|
|
636
656
|
}
|
|
@@ -652,10 +672,18 @@ function shouldRouteToSkalpel(path4, source) {
|
|
|
652
672
|
const pathname = path4.split("?")[0];
|
|
653
673
|
return SKALPEL_EXACT_PATHS.has(pathname);
|
|
654
674
|
}
|
|
675
|
+
var SKALPEL_ONLY_STATUS_CODES2 = /* @__PURE__ */ new Set([
|
|
676
|
+
402,
|
|
677
|
+
// Payment Required — Skalpel billing gate
|
|
678
|
+
409
|
|
679
|
+
// Conflict — Skalpel duplicate-request lock
|
|
680
|
+
]);
|
|
655
681
|
function isSkalpelBackendFailure2(response, err) {
|
|
656
682
|
if (err) return true;
|
|
657
683
|
if (!response) return true;
|
|
658
684
|
if (response.status >= 500) return true;
|
|
685
|
+
if (SKALPEL_ONLY_STATUS_CODES2.has(response.status)) return true;
|
|
686
|
+
if (response.status === 429) return true;
|
|
659
687
|
return false;
|
|
660
688
|
}
|
|
661
689
|
function buildForwardHeaders(req, config, source, useSkalpel) {
|