@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
JavaScript
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-