UNPKG

@mariozechner/claude-bridge

Version:

Use non-Anthropic models with Claude Code by proxying requests through the lemmy unified interface

1,357 lines (1,330 loc) 61 kB
import { VERSION } from "./chunk-INRIKTE2.js"; import { AnthropicModelData, Context, GoogleModelData, ModelToProvider, OpenAIModelData, createClientForModel, findModelData, getProviderForModel } from "./chunk-V2JHLRLD.js"; // src/interceptor.ts import fs2 from "fs"; import path2 from "path"; // src/transforms/tool-schemas.ts import { z } from "zod"; function jsonSchemaToZod(jsonSchema) { if (!jsonSchema || typeof jsonSchema !== "object") { return z.any(); } if (jsonSchema.$ref && jsonSchema.definitions) { const refPath = jsonSchema.$ref; if (refPath.startsWith("#/definitions/")) { const definitionName = refPath.substring("#/definitions/".length); const definition = jsonSchema.definitions[definitionName]; if (definition) { return jsonSchemaToZod(definition); } } } const type = jsonSchema.type; switch (type) { case "string": let stringSchema = z.string(); if (jsonSchema.description) { stringSchema = stringSchema.describe(jsonSchema.description); } return stringSchema; case "number": let numberSchema = z.number(); if (jsonSchema.description) { numberSchema = numberSchema.describe(jsonSchema.description); } return numberSchema; case "integer": let intSchema = z.number().int(); if (jsonSchema.description) { intSchema = intSchema.describe(jsonSchema.description); } return intSchema; case "boolean": let boolSchema = z.boolean(); if (jsonSchema.description) { boolSchema = boolSchema.describe(jsonSchema.description); } return boolSchema; case "array": const itemSchema = jsonSchema.items ? jsonSchemaToZod(jsonSchema.items) : z.any(); let arraySchema = z.array(itemSchema); if (jsonSchema.description) { arraySchema = arraySchema.describe(jsonSchema.description); } return arraySchema; case "object": const shape = {}; if (jsonSchema.properties) { for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { shape[key] = jsonSchemaToZod(propSchema); } } let objectSchema = z.object(shape); if (jsonSchema.required && Array.isArray(jsonSchema.required)) { const requiredFields = new Set(jsonSchema.required); const newShape = {}; for (const [key, schema] of Object.entries(shape)) { newShape[key] = requiredFields.has(key) ? schema : schema.optional(); } objectSchema = z.object(newShape); } else { const newShape = {}; for (const [key, schema] of Object.entries(shape)) { newShape[key] = schema.optional(); } objectSchema = z.object(newShape); } if (jsonSchema.description) { objectSchema = objectSchema.describe(jsonSchema.description); } return objectSchema; default: return z.any(); } } function convertAnthropicToolToLemmy(anthropicTool) { try { const zodSchema = jsonSchemaToZod(anthropicTool.input_schema); return { name: anthropicTool.name, description: anthropicTool.description || "", schema: zodSchema, execute: async () => { throw new Error("Tool execution not supported in bridge mode"); } }; } catch (error) { console.warn(`Failed to convert Anthropic tool ${anthropicTool.name} to Zod:`, error); return null; } } // src/transforms/anthropic-to-lemmy.ts function transformAnthropicToLemmy(anthropicRequest, toolIdMapping) { const context = new Context(); const currentTime = /* @__PURE__ */ new Date(); if (anthropicRequest.system) { if (typeof anthropicRequest.system === "string") { context.setSystemMessage(anthropicRequest.system); } else { const systemText = anthropicRequest.system.filter((block) => block.type === "text").map((block) => "text" in block ? block.text : "").join("\n"); if (systemText) { context.setSystemMessage(systemText); } } } if (anthropicRequest.tools) { for (const anthropicTool of anthropicRequest.tools) { if (anthropicTool.type === "custom" || !anthropicTool.type) { const lemmyTool = convertAnthropicToolToLemmy(anthropicTool); if (lemmyTool) { context.addTool(lemmyTool); } } } } for (const anthropicMessage of anthropicRequest.messages) { if (anthropicMessage.role === "user") { const userMessage = convertAnthropicUserMessage(anthropicMessage, currentTime, toolIdMapping); context.addMessage(userMessage); } else if (anthropicMessage.role === "assistant") { const assistantMessage = convertAnthropicAssistantMessage( anthropicMessage, currentTime, anthropicRequest.model ); context.addMessage(assistantMessage); } } return context.serialize(); } function convertAnthropicUserMessage(anthropicMessage, timestamp, toolIdMapping) { const userMessage = { role: "user", timestamp }; if (typeof anthropicMessage.content === "string") { userMessage.content = anthropicMessage.content; return userMessage; } const contentBlocks = Array.isArray(anthropicMessage.content) ? anthropicMessage.content : []; let textContent = ""; const toolResults = []; const attachments = []; for (const block of contentBlocks) { switch (block.type) { case "text": if ("text" in block && block.text) { textContent += block.text; } break; case "tool_result": if ("tool_use_id" in block && "content" in block && block.tool_use_id) { const originalApiId = toolIdMapping?.get(block.tool_use_id) || block.tool_use_id; if (typeof block.content === "string") { toolResults.push({ toolCallId: originalApiId, content: block.content }); } else { toolResults.push({ toolCallId: originalApiId, content: JSON.stringify(block.content), ...block.content }); } } break; case "image": if ("source" in block && block.source) { const source = block.source; let data; let mimeType; if ("data" in source && source.data) { data = source.data; mimeType = "media_type" in source && source.media_type ? source.media_type : "image/jpeg"; } else if ("url" in source && source.url) { data = source.url; mimeType = "image/jpeg"; } else { continue; } attachments.push({ type: "image", data, mimeType }); } break; case "document": break; } } if (textContent) userMessage.content = textContent; if (toolResults.length > 0) userMessage.toolResults = toolResults; if (attachments.length > 0) userMessage.attachments = attachments; return userMessage; } function convertAnthropicAssistantMessage(anthropicMessage, timestamp, model) { const assistantMessage = { role: "assistant", timestamp, // Required fields - we'll set defaults since we don't have the actual response data usage: { input: 0, output: 0 }, provider: "anthropic", model, took: 0 }; if (typeof anthropicMessage.content === "string") { assistantMessage.content = anthropicMessage.content; return assistantMessage; } const contentBlocks = Array.isArray(anthropicMessage.content) ? anthropicMessage.content : []; let textContent = ""; const toolCalls = []; let thinking = ""; let thinkingSignature = ""; for (const block of contentBlocks) { switch (block.type) { case "text": if ("text" in block && block.text) { textContent += block.text; } break; case "thinking": if ("thinking" in block && block.thinking) { thinking += block.thinking; } if ("signature" in block && block.signature) { thinkingSignature += block.signature; } break; case "tool_use": if ("id" in block && "name" in block && block.id && block.name) { toolCalls.push({ id: block.id, name: block.name, arguments: "input" in block && block.input ? block.input : {} }); } break; } } if (textContent) assistantMessage.content = textContent; if (toolCalls.length > 0) assistantMessage.toolCalls = toolCalls; if (thinking) assistantMessage.thinking = thinking; if (thinkingSignature) assistantMessage.thinkingSignature = thinkingSignature; return assistantMessage; } // src/transforms/lemmy-to-anthropic.ts function generateClaudeToolId() { return `toolu_${Date.now().toString(36)}${Math.random().toString(36).substring(2, 15)}`; } function createAnthropicSSE(askResult, model, toolIdMapping) { const messageId = `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; return new ReadableStream({ start(controller) { const encoder = new TextEncoder(); const writeEvent = (eventType, data) => { controller.enqueue(encoder.encode(`event: ${eventType} data: ${JSON.stringify(data)} `)); }; if (askResult.type !== "success") { const errorMessage = askResult.error?.message || JSON.stringify(askResult.error) || "Request failed"; writeEvent("error", { type: "error", error: { type: "internal_server_error", message: errorMessage } }); controller.close(); return; } writeEvent("message_start", { type: "message_start", message: { id: messageId, type: "message", role: "assistant", model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: askResult.tokens?.input || 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 0, service_tier: "standard" } } }); let blockIndex = 0; if (askResult.message.thinking) { writeEvent("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "thinking" } }); const thinking = askResult.message.thinking; for (let i = 0; i < thinking.length; i += 50) { writeEvent("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "thinking_delta", thinking: thinking.slice(i, i + 50) } }); } writeEvent("content_block_stop", { type: "content_block_stop", index: blockIndex }); blockIndex++; } if (askResult.message.content) { writeEvent("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "text", text: "" } }); const content = askResult.message.content; for (let i = 0; i < content.length; i += 50) { writeEvent("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "text_delta", text: content.slice(i, i + 50) } }); } writeEvent("content_block_stop", { type: "content_block_stop", index: blockIndex }); blockIndex++; } if (askResult.message.toolCalls?.length) { for (const toolCall of askResult.message.toolCalls) { const claudeId = generateClaudeToolId(); if (toolIdMapping) { toolIdMapping.set(claudeId, toolCall.id); } writeEvent("content_block_start", { type: "content_block_start", index: blockIndex, content_block: { type: "tool_use", id: claudeId, name: toolCall.name, input: {} } }); const argsJson = JSON.stringify(toolCall.arguments); for (let i = 0; i < argsJson.length; i += 50) { writeEvent("content_block_delta", { type: "content_block_delta", index: blockIndex, delta: { type: "input_json_delta", partial_json: argsJson.slice(i, i + 50) } }); } writeEvent("content_block_stop", { type: "content_block_stop", index: blockIndex }); blockIndex++; } } const stopReason = askResult.message.toolCalls?.length ? "tool_use" : "end_turn"; writeEvent("message_delta", { type: "message_delta", delta: { stop_reason: stopReason, stop_sequence: null }, usage: { output_tokens: askResult.tokens?.output || 0 } }); writeEvent("message_stop", { type: "message_stop" }); controller.close(); } }); } // src/interceptor.ts import { z as z2 } from "zod"; // src/utils/logger.ts import fs from "fs"; import path from "path"; var NullLogger = class { log(message) { } error(message) { } }; var FileLogger = class { logFile; constructor(logDir) { this.logFile = path.join(logDir, "log.txt"); fs.writeFileSync(this.logFile, `[${(/* @__PURE__ */ new Date()).toISOString()}] Claude Bridge Logger Started `); } log(message) { try { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); fs.appendFileSync(this.logFile, `[${timestamp}] ${message} `); } catch { } } error(message) { try { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); fs.appendFileSync(this.logFile, `[${timestamp}] ERROR: ${message} `); } catch { } } }; // src/utils/sse.ts function parseSSE(sseData) { const events = []; const lines = sseData.split("\n"); let currentEvent = {}; for (const line of lines) { if (line.startsWith("data:")) { try { currentEvent = JSON.parse(line.substring(5).trim()); } catch { currentEvent.data = line.substring(5).trim(); } } else if (line.trim() === "" && Object.keys(currentEvent).length > 0) { events.push({ ...currentEvent }); currentEvent = {}; } } if (Object.keys(currentEvent).length > 0) events.push(currentEvent); return events; } function extractAssistantFromSSE(events, logger) { try { let content = "", thinking = ""; const toolCalls = []; let errorMessage = ""; for (const event of events) { if (event.type === "error") { errorMessage = event.error?.message || JSON.stringify(event.error) || "Unknown error"; } else if (event.type === "content_block_delta") { if (event.delta?.type === "text_delta") content += event.delta.text || ""; if (event.delta?.type === "thinking_delta") thinking += event.delta.thinking || ""; } else if (event.type === "content_block_start" && event.content_block?.type === "tool_use") { toolCalls.push({ id: event.content_block.id, name: event.content_block.name, arguments: {} }); } else if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta" && toolCalls.length > 0) { const lastTool = toolCalls[toolCalls.length - 1]; lastTool.argumentsJson = (lastTool.argumentsJson || "") + (event.delta.partial_json || ""); } } for (const tool of toolCalls) { if (tool.argumentsJson) { try { tool.arguments = JSON.parse(tool.argumentsJson); delete tool.argumentsJson; } catch { tool.arguments = tool.argumentsJson; delete tool.argumentsJson; } } } const message = { role: "assistant" }; if (thinking) message.thinking = thinking; if (content) message.content = content; if (toolCalls.length > 0) message.toolCalls = toolCalls; if (errorMessage) message.content = `Error: ${errorMessage}`; return Object.keys(message).length > 1 ? message : null; } catch (error) { logger?.error(`Failed to extract assistant response: ${error instanceof Error ? error.message : String(error)}`); return null; } } // src/utils/request-parser.ts function redactHeaders(headers) { const result = { ...headers }; const sensitiveKeys = [ "authorization", "x-api-key", "x-auth-token", "cookie", "set-cookie", "x-session-token", "x-access-token", "bearer", "proxy-authorization" ]; for (const [key, value] of Object.entries(result)) { if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) { result[key] = value.length > 14 ? `${value.substring(0, 10)}...${value.slice(-4)}` : value.length > 4 ? `${value.substring(0, 2)}...${value.slice(-2)}` : "[REDACTED]"; } } return result; } async function parseAnthropicMessageCreateRequest(url, init, logger) { let body = null; if (init.body) { try { if (typeof init.body !== "string") throw new Error("Anthropic request body must be a string"); body = JSON.parse(init.body); } catch (error) { logger?.error( `Failed to parse Anthropic request body: ${error instanceof Error ? error.message : String(error)}` ); body = null; } } if (!body) throw Error("Anthropic request body must not be null"); return { url, timestamp: Date.now() / 1e3, method: init.method || "POST", headers: redactHeaders(Object.fromEntries(new Headers(init.headers || {}).entries())), body }; } async function parseResponse(response) { const contentType = response.headers.get("content-type") || ""; let body, body_raw; try { if (contentType.includes("application/json")) { body = await response.json(); } else { body_raw = await response.text(); } } catch { } const result = { timestamp: Date.now() / 1e3, status_code: response.status, headers: redactHeaders(Object.fromEntries(response.headers.entries())) }; if (body !== void 0) result.body = body; if (body_raw !== void 0) result.body_raw = body_raw; return result; } function isAnthropicAPI(url) { return url.includes("api.anthropic.com") && url.includes("/v1/messages"); } function generateRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } // src/utils/provider.ts async function createProviderClient(config) { const modelData = findModelData(config.model); let provider; let client; if (modelData) { provider = getProviderForModel(config.model); const providerConfig = buildProviderConfig(provider, config); client = createClientForModel(config.model, providerConfig); } else { provider = config.provider; const providerConfig = buildProviderConfig(provider, config); switch (provider) { case "openai": { const { lemmy } = await import("./src-MX23HMG2.js"); client = lemmy.openai(providerConfig); break; } case "google": { const { lemmy } = await import("./src-MX23HMG2.js"); client = lemmy.google(providerConfig); break; } case "anthropic": { const { lemmy } = await import("./src-MX23HMG2.js"); client = lemmy.anthropic(providerConfig); break; } default: const _exhaustiveCheck = provider; throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); } } return { client, provider, model: config.model, modelData: modelData || null // null for unknown models }; } function buildProviderConfig(provider, config) { const baseConfig = { model: config.model, apiKey: config.apiKey || getDefaultApiKey(provider), ...config.baseURL && { baseURL: config.baseURL }, ...config.maxRetries && { maxRetries: config.maxRetries } }; switch (provider) { case "anthropic": return baseConfig; case "openai": return baseConfig; case "google": return baseConfig; default: const _exhaustiveCheck = provider; throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); } } function getDefaultApiKey(provider) { switch (provider) { case "anthropic": const anthropicKey = process.env["ANTHROPIC_API_KEY"]; if (!anthropicKey) throw new Error("ANTHROPIC_API_KEY environment variable is required"); return anthropicKey; case "openai": const openaiKey = process.env["OPENAI_API_KEY"]; if (!openaiKey) throw new Error("OPENAI_API_KEY environment variable is required"); return openaiKey; case "google": const googleKey = process.env["GOOGLE_API_KEY"]; if (!googleKey) throw new Error("GOOGLE_API_KEY environment variable is required"); return googleKey; default: const _exhaustiveCheck = provider; throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); } } function validateCapabilities(modelData, anthropicRequest, logger) { const warnings = []; const adjustments = {}; if (anthropicRequest.max_tokens && anthropicRequest.max_tokens > modelData.maxOutputTokens) { warnings.push( `Requested max_tokens (${anthropicRequest.max_tokens}) exceeds model limit (${modelData.maxOutputTokens}). Will be clamped to model maximum.` ); adjustments.maxOutputTokens = modelData.maxOutputTokens; logger?.log(`\u26A0\uFE0F Max tokens clamped: ${anthropicRequest.max_tokens} \u2192 ${modelData.maxOutputTokens}`); } if (anthropicRequest.tools && anthropicRequest.tools.length > 0 && !modelData.supportsTools) { warnings.push(`Model ${anthropicRequest.model} does not support tools. Tool calls will be disabled.`); adjustments.toolsDisabled = true; logger?.log(`\u26A0\uFE0F Tools disabled for model without tool support`); } const hasImages = anthropicRequest.messages?.some( (msg) => Array.isArray(msg.content) ? msg.content.some((block) => block.type === "image") : false ); if (hasImages && !modelData.supportsImageInput) { warnings.push(`Model ${anthropicRequest.model} does not support image input. Images will be ignored.`); adjustments.imagesIgnored = true; logger?.log(`\u26A0\uFE0F Images ignored for model without image support`); } return { valid: warnings.length === 0, warnings, adjustments }; } function convertThinkingParameters(provider, anthropicRequest) { const baseOptions = { maxOutputTokens: anthropicRequest.max_tokens }; switch (provider) { case "anthropic": return { ...baseOptions, // Anthropic uses the same thinking parameters ...anthropicRequest.thinking?.type == "enabled" && { thinkingEnabled: true }, ...anthropicRequest.thinking?.type == "enabled" && anthropicRequest.thinking.budget_tokens !== void 0 && { maxThinkingTokens: anthropicRequest.thinking.budget_tokens } }; case "google": const options = { ...baseOptions, // Google uses includeThoughts for thinking ...anthropicRequest.thinking?.type == "enabled" && { includeThoughts: true }, ...anthropicRequest.thinking?.type == "enabled" && anthropicRequest.thinking.budget_tokens !== void 0 && { thinkingBudget: anthropicRequest.thinking.budget_tokens } }; return options; case "openai": return { ...baseOptions, ...anthropicRequest.thinking?.type == "enabled" && { reasoningEffort: "medium" } }; default: const _exhaustiveCheck = provider; throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); } } // src/interceptor.ts var ClaudeBridgeInterceptor = class _ClaudeBridgeInterceptor { config; logger; requestsFile; transformedFile; contextFile; traceFile; clientInfo; pendingRequests = /* @__PURE__ */ new Map(); toolIdMapping = /* @__PURE__ */ new Map(); // claudeId -> originalApiId /** * Create a new interceptor instance (async factory) */ static async create(config) { const instance = new _ClaudeBridgeInterceptor(); await instance.initialize(config); return instance; } constructor() { } async initialize(config) { this.config = { logDirectory: ".claude-bridge", logLevel: "info", debug: false, ...config }; if (this.config.trace) { this.config.debug = true; } if (this.config.debug) { const logDir = this.config.logDirectory; if (!fs2.existsSync(logDir)) fs2.mkdirSync(logDir, { recursive: true }); this.logger = new FileLogger(logDir); const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, -5); this.requestsFile = path2.join(logDir, `requests-${timestamp}.jsonl`); this.transformedFile = path2.join(logDir, `transformed-${timestamp}.jsonl`); this.contextFile = path2.join(logDir, `context-${timestamp}.jsonl`); this.traceFile = path2.join(logDir, `trace-${timestamp}.jsonl`); fs2.writeFileSync(this.requestsFile, ""); fs2.writeFileSync(this.transformedFile, ""); fs2.writeFileSync(this.contextFile, ""); fs2.writeFileSync(this.traceFile, ""); } else { this.logger = new NullLogger(); this.requestsFile = ""; this.transformedFile = ""; this.contextFile = ""; this.traceFile = ""; } this.clientInfo = await createProviderClient(this.config); this.logger.log(`Requests logged to ${this.requestsFile}`); this.logger.log(`Transformed requests logged to ${this.transformedFile}`); this.logger.log(`Initialized ${this.clientInfo.provider} client for model: ${this.clientInfo.model}`); } instrumentFetch() { if (!global.fetch || global.fetch.__claudeBridgeInstrumented) return; const originalFetch = global.fetch; global.fetch = async (input, init = {}) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (!isAnthropicAPI(url)) return originalFetch(input, init); return this.handleAnthropicRequest(originalFetch, input, init); }; global.fetch.__claudeBridgeInstrumented = true; this.logger.log("Claude Bridge interceptor initialized"); } async handleAnthropicRequest(originalFetch, input, init) { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; this.logger.log(`Intercepted Claude request: ${url}`); if (init.signal?.aborted) { this.logger.log(`Request already aborted: ${url}`); throw new DOMException("Request was aborted", "AbortError"); } const requestId = generateRequestId(); const requestData = await parseAnthropicMessageCreateRequest(url, init, this.logger); this.detectProblematicMessagePatterns(requestData); const transformResult = await this.tryTransform(requestData); this.pendingRequests.set(requestId, { ...requestData, abortSignal: init.signal }); if (this.config.trace && requestData.body) { const anthropicRequest = requestData.body; const traceEntry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), model: anthropicRequest.model, system_prompt: anthropicRequest.system || null, tools: anthropicRequest.tools || null, thinking_enabled: !!anthropicRequest.thinking, max_tokens: anthropicRequest.max_tokens, temperature: anthropicRequest.temperature, messages: transformResult ? transformResult.messages : anthropicRequest.messages, ...transformResult && { serialized_context: transformResult } }; fs2.appendFileSync(this.traceFile, JSON.stringify(traceEntry) + "\n"); } try { if (init.signal?.aborted) { this.logger.log(`Request aborted before provider call: ${url}`); throw new DOMException("Request was aborted", "AbortError"); } const response = this.config.trace || !transformResult ? await originalFetch(input, init) : await this.callProvider( transformResult, requestData.body, init.signal == null ? void 0 : init.signal ); await this.logComplete(requestData, response, transformResult, requestId); this.pendingRequests.delete(requestId); return response; } catch (error) { this.pendingRequests.delete(requestId); throw error; } } async tryTransform(requestData) { try { if (requestData.method !== "POST" || !requestData.body) return null; const anthropicRequest = requestData.body; if (!this.config.trace && anthropicRequest.model?.toLowerCase().includes("haiku")) { this.logger.log(`Skipping transformation for haiku model: ${anthropicRequest.model}`); return null; } return transformAnthropicToLemmy(anthropicRequest, this.toolIdMapping); } catch (error) { if (error instanceof Error && error.message.includes("Multi-turn conversations")) { this.logger.log(`Skipping transformation for multi-turn conversation: ${error.message}`); return null; } this.logger.error(`Failed to transform request: ${error instanceof Error ? error.message : String(error)}`); return null; } } async callProvider(transformResult, originalRequest, abortSignal) { try { let validation = { valid: true, warnings: [], adjustments: {} }; if (this.clientInfo.modelData) { validation = validateCapabilities(this.clientInfo.modelData, originalRequest, this.logger); if (!validation.valid) { validation.warnings.forEach((warning) => this.logger.log(`\u26A0\uFE0F ${warning}`)); } } else { this.logger.log(`\u26A0\uFE0F Skipping capability validation for unknown model: ${this.clientInfo.model}`); } const dummyTools = transformResult.tools.map((tool) => ({ name: tool.name, description: tool.description, schema: this.safeJsonSchemaToZod(tool.jsonSchema), execute: async () => { throw new Error("Tool execution not supported in bridge mode"); } })); const context = Context.deserialize(transformResult, dummyTools); const lastMessage = context.getMessages().pop(); let askInput = ""; if (lastMessage?.role === "user") { const userMessage = lastMessage; askInput = { ...userMessage.content && { content: userMessage.content }, ...userMessage.toolResults && { toolResults: userMessage.toolResults }, ...userMessage.attachments && { attachments: userMessage.attachments } }; } const askOptions = convertThinkingParameters(this.clientInfo.provider, originalRequest); if (validation.adjustments.maxOutputTokens) { askOptions.maxOutputTokens = validation.adjustments.maxOutputTokens; } if (this.config.maxOutputTokens) { askOptions.maxOutputTokens = this.config.maxOutputTokens; this.logger.log(`Overriding maxOutputTokens with config value: ${this.config.maxOutputTokens}`); } if (abortSignal) { askOptions.abortSignal = abortSignal; } if (abortSignal?.aborted) { this.logger.log("Request aborted before provider ask call"); throw new DOMException("Request was aborted", "AbortError"); } this.logger.log(`Calling ${this.clientInfo.provider} with model: ${this.clientInfo.model}`); const askResult = await this.clientInfo.client.ask(askInput, { context, ...askOptions }); if (askResult.type !== "success") { this.logger.error(`${this.clientInfo.provider} error response: ${JSON.stringify(askResult.error)}`); throw new Error(askResult.error?.message || JSON.stringify(askResult.error) || "Request failed"); } return new Response( createAnthropicSSE(askResult, originalRequest.model, this.toolIdMapping), { status: 200, statusText: "OK", headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive", "anthropic-request-id": generateRequestId() } } ); } catch (error) { this.logProviderError(error); throw error; } } async logComplete(requestData, response, transformResult, requestId) { const responseData = await parseResponse(response.clone()); const pair = { request: requestData, response: responseData, logged_at: (/* @__PURE__ */ new Date()).toISOString() }; if (this.config.debug) { fs2.appendFileSync(this.requestsFile, JSON.stringify(pair) + "\n"); } if (transformResult) { const decodedSSE = responseData.body_raw && responseData.headers["content-type"]?.includes("text/event-stream") ? parseSSE(responseData.body_raw) : void 0; const contextWithResponse = { ...transformResult }; const assistantResponse = decodedSSE ? extractAssistantFromSSE(decodedSSE, this.logger) : null; if (assistantResponse) { contextWithResponse.messages = [...contextWithResponse.messages, assistantResponse]; } if (this.config.debug) { const logEntry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), messages: contextWithResponse.messages }; fs2.appendFileSync(this.contextFile, JSON.stringify(logEntry) + "\n"); } const transformEntry = { timestamp: Date.now() / 1e3, request_id: requestId, raw_request: requestData.body, lemmy_context: contextWithResponse, bridge_config: { provider: this.config.provider || "unknown", model: this.config.model || "unknown" }, raw_response: responseData, decoded_sse: decodedSSE, logged_at: (/* @__PURE__ */ new Date()).toISOString() }; if (this.config.debug) { fs2.appendFileSync(this.transformedFile, JSON.stringify(transformEntry) + "\n"); } this.logger.log(`Transformed and logged request with response to ${this.transformedFile}`); } this.logger.log(`Logged request-response pair to ${this.requestsFile}`); } safeJsonSchemaToZod(jsonSchema) { try { return jsonSchemaToZod(jsonSchema); } catch { return z2.any(); } } logProviderError(error) { this.logger.error(`CRITICAL: ${this.clientInfo.provider} request failed with detailed error information:`); this.logger.error(`Error type: ${typeof error}`); this.logger.error(`Error constructor: ${error?.constructor?.name}`); if (error instanceof Error) { this.logger.error(`Error message: ${error.message}`); this.logger.error(`Error stack: ${error.stack}`); if ("cause" in error && error.cause) this.logger.error(`Error cause: ${JSON.stringify(error.cause)}`); if ("code" in error) this.logger.error(`Error code: ${error.code}`); if ("status" in error) this.logger.error(`HTTP status: ${error.status}`); } else { this.logger.error(`Non-Error object: ${JSON.stringify(error, null, 2)}`); } this.logger.error( `Config: ${JSON.stringify({ provider: this.config.provider, model: this.config.model, apiKey: this.config.apiKey ? `${this.config.apiKey.substring(0, 10)}...` : "NOT_SET" })}` ); } detectProblematicMessagePatterns(requestData) { if (!requestData.body?.messages || !Array.isArray(requestData.body.messages)) { return; } const messages = requestData.body.messages; for (let i = 0; i < messages.length - 1; i++) { const currentMessage = messages[i]; const nextMessage = messages[i + 1]; if (currentMessage && nextMessage && currentMessage.role === "assistant" && currentMessage.tool_calls && Array.isArray(currentMessage.tool_calls) && currentMessage.tool_calls.length > 0 && nextMessage.role === "user" && !nextMessage.tool_call_id && !nextMessage.tool_result_id) { this.logger.log( `\u{1F6A8} DETECTED PROBLEMATIC PATTERN: Assistant message with ${currentMessage.tool_calls.length} tool calls (position ${i}) followed by user message without tool results (position ${i + 1})` ); this.logger.log( `Tool call IDs: ${currentMessage.tool_calls.map((tc) => tc.id).join(", ")}` ); this.logger.log( `User message content preview: ${typeof nextMessage.content === "string" ? nextMessage.content.substring(0, 100) : "[complex content]"}` ); } } } cleanup() { this.logger.log("Cleaning up interceptor..."); for (const [, requestData] of this.pendingRequests.entries()) { const orphaned = { request: requestData, response: null, note: "ORPHANED_REQUEST", logged_at: (/* @__PURE__ */ new Date()).toISOString() }; if (this.config.debug) { fs2.appendFileSync(this.requestsFile, JSON.stringify(orphaned) + "\n"); } } this.pendingRequests.clear(); this.logger.log(`Cleanup complete.`); } }; var globalInterceptor = null; var eventListenersSetup = false; async function initializeInterceptor(config) { if (globalInterceptor) { console.warn("\u26A0\uFE0F Interceptor already initialized"); return globalInterceptor; } if (!process.env["CLAUDE_BRIDGE_CONFIG"]) { throw new Error("CLAUDE_BRIDGE_CONFIG environment variable not set"); } let defaultConfig; try { defaultConfig = JSON.parse(process.env["CLAUDE_BRIDGE_CONFIG"]); } catch (error) { console.error("\u274C Failed to parse CLAUDE_BRIDGE_CONFIG:", error); throw new Error("Invalid CLAUDE_BRIDGE_CONFIG JSON"); } globalInterceptor = await ClaudeBridgeInterceptor.create({ ...defaultConfig, ...config }); globalInterceptor.instrumentFetch(); if (!eventListenersSetup) { const cleanup = () => globalInterceptor?.cleanup(); process.on("exit", cleanup); process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("uncaughtException", (error) => { console.error("Uncaught exception:", error); cleanup(); process.exit(1); }); eventListenersSetup = true; } return globalInterceptor; } function getInterceptor() { return globalInterceptor; } // src/cli.ts import * as fs4 from "fs"; import * as os from "os"; // ../../packages/lemmy-cli-args/dist/schema-introspection.js import { z as z3 } from "zod"; // ../../packages/lemmy-cli-args/dist/provider-validation.js function validateProvider(provider, validProviders) { return validProviders.includes(provider); } function getValidProviders() { const providers = ["anthropic", "openai", "google"]; const exhaustiveCheck = { anthropic: true, openai: true, google: true }; for (const provider of providers) { if (!exhaustiveCheck[provider]) { throw new Error(`Provider ${provider} missing from exhaustive check`); } } return providers; } function getModelProvider(model, modelToProvider) { return modelToProvider[model]; } function getCapableModels(config, targetProvider) { const capableModels = { anthropic: [], openai: [], google: [] }; for (const [registryName, registry] of Object.entries(config.modelRegistries)) { for (const [model, data] of Object.entries(registry)) { const meetsRequirements = !config.requiredCapabilities || (!config.requiredCapabilities.tools || data.supportsTools) && (!config.requiredCapabilities.images || data.supportsImageInput) && (!config.requiredCapabilities.minContextWindow || data.contextWindow >= config.requiredCapabilities.minContextWindow) && (!config.requiredCapabilities.minOutputTokens || data.maxOutputTokens >= config.requiredCapabilities.minOutputTokens); if (meetsRequirements) { const provider = getModelProvider(model, config.modelToProvider); if (provider && (!targetProvider || provider === targetProvider)) { capableModels[provider].push(model); } } } } return capableModels; } function filterProviders(allProviders, excludeProviders) { return allProviders.filter((provider) => !excludeProviders.includes(provider)); } // ../../packages/lemmy-cli-args/dist/command-generation.js import { Command, Option } from "commander"; import { z as z4 } from "zod"; // src/cli.ts import { spawnSync, execSync } from "child_process"; import path4 from "path"; import { fileURLToPath } from "url"; // src/patch-claude.ts import fs3 from "fs"; import path3 from "path"; function patchClaudeBinary(claudePath, logDir) { if (!fs3.existsSync(logDir)) { fs3.mkdirSync(logDir, { recursive: true }); } const claudeFilename = path3.basename(claudePath); const backupPath = path3.join(logDir, `${claudeFilename}.backup`); if (!fs3.existsSync(backupPath)) { fs3.copyFileSync(claudePath, backupPath); console.log(`\u{1F4C1} Created backup at ${backupPath}`); } const content = fs3.readFileSync(claudePath, "utf8"); const patterns = [ // Standard pattern: if(PF5())process.exit(1); /if\([A-Za-z0-9_$]+\(\)\)process\.exit\(1\);/g, // With spaces: if (PF5()) process.exit(1); /if\s*\([A-Za-z0-9_$]+\(\)\)\s*process\.exit\(1\);/g, // Different exit codes: if(PF5())process.exit(2); /if\([A-Za-z0-9_$]+\(\)\)process\.exit\(\d+\);/g ]; let patchedContent = content; let patched = false; for (const pattern of patterns) { const newContent = patchedContent.replace(pattern, "if(false)process.exit(1);"); if (newContent !== patchedContent) { patchedContent = newContent; patched = true; console.log(`\u{1F527} Applied patch for pattern: ${pattern}`); } } if (!patched) { console.log("\u26A0\uFE0F No anti-debugging pattern found - Claude binary may have changed"); return claudePath; } fs3.writeFileSync(claudePath, patchedContent); console.log(`\u{1F527} Patched Claude binary (backup saved to ${backupPath})`); return claudePath; } // src/cli.ts process.removeAllListeners("warning"); var modelValidationConfig = { allowUnknownModels: true, requiredCapabilities: { tools: true, images: true }, modelRegistries: { anthropic: AnthropicModelData, openai: OpenAIModelData, google: GoogleModelData }, modelToProvider: ModelToProvider }; function getCapableModelsLocal() { return getCapableModels(modelValidationConfig); } function getCapableModelsForProvider(provider) { const allCapableModels = getCapableModelsLocal(); return allCapableModels[provider] || []; } function getNonAnthropicProviders() { return filterProviders(getValidProviders(), ["anthropic"]); } function formatModelInfo(model, data) { const tools = data.supportsTools ? "\u2713" : "\u2717"; const images = data.supportsImageInput ? "\u2713" : "\u2717"; const maxInput = data.contextWindow.toLocaleString(); const maxOutput = data.maxOutputTokens.toLocaleString(); return ` ${model.padEnd(35)} ${tools.padStart(6)} ${images.padStart(7)} ${maxInput.padStart(12)} ${maxOutput.padStart(12)}`; } function showHelp() { console.log(`claude-bridge - Use non-Anthropic models with Claude Code Version: ${VERSION} USAGE: claude-bridge Show all available providers claude-bridge <provider> Show models for a provider claude-bridge <provider> <model> Run with provider and model claude-bridge --trace <claude args> Spy on Claude Code \u2194 Anthropic communication claude-bridge --version Show version information claude-bridge --help Show this help EXAMPLES: # Natural discovery flow claude-bridge # Shows: openai, google claude-bridge openai # Shows OpenAI models claude-bridge google # Shows Google models # Execution claude-bridge openai gpt-4o claude-bridge google gemini-2.0-flash-exp # With custom configuration claude-bridge openai gpt-4o --apiKey sk-... --baseURL https://api.openai.com/v1 # Single-shot prompts claude-bridge openai gpt-4o -p "Hello world" claude-bridge google gemini-1.5-pro -p "Debug this code" OPTIONS: --apiKey <key> API key for the provider --baseURL <url> Custom API base URL --maxRetries <num> Maximum number of retries for failed requests --max-output-tokens <num> Maximum output tokens (overrides provider defaults) --log-dir <dir> Directory for log files (default: .claude-bridge) --claude-binary <path> Path to Claude Code CLI binary (default: auto-detect) --patch-claude Patch Claude binary to disable anti-debugging checks --debug Enable debug logging (requests/responses to .claude-bridge/) --trace Spy mode: log all Claude \u2194 Anthropic communication (implies --debug) --version Show version information --help, -h Show this help ENVIRONMENT VARIABLES: OPENAI_API_KEY API key for OpenAI (if --apiKey not provided) GOOGLE_API_KEY API key for Google (if --apiKey not provided) NOTE: Only models with both tools and image support are shown by default. Use --debug to enable request/response logging to .claude-bridge/ `); } function showProviders() { const nonAnthropicProviders = getNonAnthropicProviders(); console.log(`Available providers (only showing providers with capable models): `); for (const provider of nonAnthropicProviders) { const models = getCapableModelsForProvider(provider); if (models.length > 0) { switch (provider) { case "openai": console.log(` openai OpenAI models (GPT-4o, etc.)`); break; case "google": console.log(` google Google models (Gemini, etc.)`); break; default: { const _exhaustiveCheck = provider; _exhaustiveCheck; } } } } console.log(` Usage: claude-bridge <provider> Show models for a provider claude-bridge --help Show detailed help Examples: claude-bridge openai # Show OpenAI models claude-bridge google # Show Google models`); } function showProviderModels(provider) { const validProviders = getValidProviders(); if (!validateProvider(provider, validProviders)) { console.error(`\u274C Invalid provider: ${provider}`); const nonAnthropicProviders = getNonAnthropicProviders(); console.error(`Available providers: ${nonAnthropicProviders.join(", ")}`); process.exit(1); } if (provider === "anthropic") { console.error(`\u274C Anthropic provider not supported for bridging`); const validProviders2 = getNonAnthropicProviders(); console.error(`Available providers: ${validProviders2.join(", ")}`); process.exit(1); } const models = getCapableModelsForProvider(provider); if (models.length === 0) { console.error(`\u274C No capable models found for provider: ${provider}`); const validProviders2 = getNonAnthropicProviders(); console.error(`Available providers: ${validProviders2.join(", ")}`); process.exit(1); } let providerDisplayName; if (provider === "openai") { providerDisplayName = "OpenAI"; } else if (provider === "google") { providerDisplayName = "Google"; } else { console.error(`\u274C Unexpected provider: ${provider}`); process.exit(1); } console.log(`${providerDisplayName} models with tools and image support: `); console.log( ` ${"Model".padEnd(35)} ${"Tools".padStart(6)} ${"Images".padStart(7)} ${"Max Input".padStart(12)} ${"Max Output".padStart(12)}` ); console.log( ` ${"".padEnd(35, "\u2500")} ${"".padStart(6, "\u2500")} ${"".padStart(7, "\u2500")} ${"".padStart(12, "\u2500")} ${"".padStart(12, "\u2500")}` ); const sortedModels = models.map((model) => ({ model, data: findModelData(model) })).filter((item) => item.data !== void 0).sort((a, b) => b.data.contextWindow - a.data.contextWindow).map((item) => item.model); for (const model of sortedModels) { const data = findModelData(model); if (data) { console.log(formatModelInfo(model, data)); } } console.log(` Usage:`); console.log(` claude-bridge ${provider} <model> Run with specific model`); console.log(` claude-bridge --help Show detailed help`); console.log(` Examples:`); console.log(` claude-bridge ${provider} ${sortedModels[0]}`); if (sortedModels[1]) { console.log(` claude-bridge ${provider} ${sortedModels[1]}`); } } function parseArguments(argv) { const args = { claudeArgs: [] }; let i = 2; while (i < argv.length) { const arg = argv[i]; if (arg === "--version") { args.version = true; i++; } else if (arg === "--help" || arg === "-h") { args.help = true; i++; } else if (arg === "--trace") { args.trace = true; i++; } else if (arg === "--apiKey") { if (i + 1 < argv.length && argv[i + 1] !== void 0) { args.apiKey = argv[++i]; } i++; } else if (arg === "--baseURL") { if (i + 1 < argv.length && argv[i + 1] !== void 0) { args.baseURL = argv[++i]; } i++; } else if (arg === "--maxRetries") { if (i + 1 < argv.length && argv[i + 1] !== void 0) { const nextArg = argv[++i]; if (nextArg !== void 0) { const retries = parseInt(nextArg, 10); if (isNaN(retries) || retries < 0) { console.error(`\u274C Invalid --maxRetries value: ${nextArg}`); process.exit(1); } args.maxRetries = retries; } } i++; } else if (arg === "--max-output-tokens") { if (i + 1 < argv.length && argv[i + 1] !== void 0) { const nextArg = argv[++i]; if (nextArg !== void 0) { const tokens = parseInt(nextArg, 10); if (isNaN(tokens) || tokens < 1) { console.error(`\u274C Invalid --max-