@mariozechner/claude-bridge
Version:
Use non-Anthropic models with Claude Code by proxying requests through the lemmy unified interface
1,655 lines (1,650 loc) • 96.7 kB
JavaScript
// ../../packages/lemmy/dist/src/clients/anthropic.js
import Anthropic from "@anthropic-ai/sdk";
// ../../packages/lemmy/dist/src/tools/zod-converter.js
import { zodToJsonSchema } from "zod-to-json-schema";
function convertZodSchema(schema) {
return zodToJsonSchema(schema);
}
function zodToOpenAI(tool) {
const jsonSchema = convertZodSchema(tool.schema);
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: jsonSchema
}
};
}
function zodToAnthropic(tool) {
const jsonSchema = convertZodSchema(tool.schema);
const inputSchema = {
type: "object",
...jsonSchema
};
return {
name: tool.name,
description: tool.description,
input_schema: inputSchema
};
}
function zodToGoogle(tool) {
const jsonSchema = convertZodSchema(tool.schema);
return {
name: tool.name,
description: tool.description,
parameters: jsonSchema
};
}
function zodToMCP(tool) {
const jsonSchema = convertZodSchema(tool.schema);
return {
name: tool.name,
description: tool.description,
inputSchema: jsonSchema
};
}
// ../../packages/lemmy/dist/src/clients/anthropic.js
var AnthropicClient = class {
anthropic;
config;
constructor(config) {
this.config = config;
const isOAuthToken = config.apiKey.startsWith("sk-ant-oat");
this.anthropic = new Anthropic({
...isOAuthToken ? { authToken: config.apiKey } : { apiKey: config.apiKey },
baseURL: config.baseURL,
maxRetries: config.maxRetries ?? 3
});
}
getModel() {
return this.config.model;
}
getProvider() {
return "anthropic";
}
buildAnthropicParams(options, messages) {
const modelData = findModelData(this.config.model);
const defaultMaxTokens = options?.maxOutputTokens || modelData?.maxOutputTokens || 4096;
const maxThinkingTokens = options?.maxThinkingTokens || this.config.defaults?.maxThinkingTokens || 3e3;
const thinkingEnabled = options?.thinkingEnabled ?? this.config.defaults?.thinkingEnabled ?? false;
const maxTokens = thinkingEnabled ? Math.max(defaultMaxTokens, maxThinkingTokens + 1e3) : defaultMaxTokens;
const params = {
model: this.config.model,
max_tokens: maxTokens,
messages,
stream: true
};
const systemMessage = options.context?.getSystemMessage();
if (systemMessage) {
params.system = systemMessage;
}
if (options.temperature !== void 0)
params.temperature = options.temperature;
if (options.topK !== void 0)
params.top_k = options.topK;
if (options.topP !== void 0)
params.top_p = options.topP;
if (options.stopSequences !== void 0)
params.stop_sequences = [options.stopSequences];
if (options.serviceTier !== void 0)
params.service_tier = options.serviceTier;
if (options.userId !== void 0)
params.metadata = { user_id: options.userId };
if (thinkingEnabled) {
params.thinking = {
type: "enabled",
budget_tokens: maxThinkingTokens
};
params.temperature = 1;
}
if (options.toolChoice !== void 0) {
params.tool_choice = {
type: options.toolChoice,
...options.disableParallelToolUse !== void 0 && {
disable_parallel_tool_use: options.disableParallelToolUse
}
};
} else if (options.disableParallelToolUse !== void 0) {
params.tool_choice = {
type: "auto",
disable_parallel_tool_use: options.disableParallelToolUse
};
}
const tools = options?.context?.listTools() || [];
const anthropicTools = tools.map((tool) => zodToAnthropic(tool));
if (anthropicTools.length > 0) {
params.tools = anthropicTools;
}
return params;
}
async ask(input, options) {
const startTime = performance.now();
try {
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError };
}
const userInput = typeof input === "string" ? { content: input } : input;
const userMessage = {
role: "user",
...userInput.content !== void 0 && {
content: userInput.content
},
...userInput.toolResults !== void 0 && {
toolResults: userInput.toolResults
},
...userInput.attachments !== void 0 && {
attachments: userInput.attachments
},
timestamp: /* @__PURE__ */ new Date()
};
if (options?.context) {
options.context.addMessage(userMessage);
}
const messages = this.convertMessagesToAnthropic(options?.context?.getMessages() || [userMessage]);
const mergedOptions = { ...this.config.defaults, ...options };
const requestParams = this.buildAnthropicParams(mergedOptions, messages);
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError };
}
const stream = await this.anthropic.messages.create(requestParams, {
signal: options?.abortSignal
});
return await this.processStream(stream, options, startTime);
} catch (error) {
return this.handleError(error);
}
}
convertMessagesToAnthropic(contextMessages) {
const messages = [];
for (const msg of contextMessages) {
if (msg.role === "user") {
const contentBlocks = [];
if (msg.content?.trim()) {
contentBlocks.push({ type: "text", text: msg.content });
}
if (msg.toolResults && msg.toolResults.length > 0) {
for (const toolResult of msg.toolResults) {
contentBlocks.push({
type: "tool_result",
tool_use_id: toolResult.toolCallId,
content: toolResult.content
});
}
}
if (msg.attachments && msg.attachments.length > 0) {
for (const attachment of msg.attachments) {
if (attachment.type === "image") {
const supportedMimeTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!supportedMimeTypes.includes(attachment.mimeType)) {
throw new Error(`Unsupported image mime type: ${attachment.mimeType}. Supported types: ${supportedMimeTypes.join(", ")}`);
}
const dataStr = typeof attachment.data === "string" ? attachment.data : attachment.data.toString("base64");
if (dataStr.startsWith("http://") || dataStr.startsWith("https://")) {
contentBlocks.push({
type: "image",
source: {
type: "url",
url: dataStr
}
});
} else {
contentBlocks.push({
type: "image",
source: {
type: "base64",
media_type: attachment.mimeType,
data: dataStr
}
});
}
} else {
throw new Error(`Unsupported attachment type: ${attachment.type}. Anthropic only supports image attachments.`);
}
}
}
if (contentBlocks.length > 0) {
messages.push({
role: "user",
content: contentBlocks.length === 1 && contentBlocks[0]?.type === "text" ? contentBlocks[0].text : contentBlocks
});
}
} else if (msg.role === "assistant") {
const contentBlocks = [];
if (msg.thinking?.trim()) {
contentBlocks.push({
type: "thinking",
thinking: msg.thinking,
signature: msg.thinkingSignature || ""
});
}
if (msg.content?.trim()) {
contentBlocks.push({ type: "text", text: msg.content });
}
if (msg.toolCalls && msg.toolCalls.length > 0) {
for (const toolCall of msg.toolCalls) {
contentBlocks.push({
type: "tool_use",
id: toolCall.id,
name: toolCall.name,
input: toolCall.arguments
});
}
}
if (contentBlocks.length > 0) {
messages.push({
role: "assistant",
content: contentBlocks.length === 1 && contentBlocks[0]?.type === "text" ? contentBlocks[0].text : contentBlocks
});
}
}
}
return messages;
}
async processStream(stream, options, startTime) {
let content = "";
let thinkingContent = "";
let thinkingSignature = "";
let inputTokens = 0;
let outputTokens = 0;
let stopReason;
let toolCalls = [];
let currentToolCall = null;
let currentBlockType = null;
try {
for await (const event of stream) {
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted during streaming",
retryable: false
};
return { type: "error", error: modelError };
}
switch (event.type) {
case "message_start":
inputTokens = event.message.usage.input_tokens;
outputTokens = event.message.usage.output_tokens;
break;
case "content_block_delta":
if (event.delta.type === "text_delta") {
const chunk = event.delta.text;
if (currentBlockType === "thinking") {
thinkingContent += chunk;
options?.onThinkingChunk?.(chunk);
} else {
content += chunk;
options?.onChunk?.(chunk);
}
} else if (event.delta.type === "thinking_delta") {
const thinkingChunk = event.delta.thinking || "";
thinkingContent += thinkingChunk;
options?.onThinkingChunk?.(thinkingChunk);
} else if (event.delta.type === "signature_delta") {
thinkingSignature += event.delta.signature || "";
} else if (event.delta.type === "input_json_delta") {
if (currentToolCall) {
const currentArgs = typeof currentToolCall.arguments === "string" ? currentToolCall.arguments : "";
currentToolCall.arguments = currentArgs + event.delta.partial_json;
}
}
break;
case "content_block_start":
if (event.content_block.type === "tool_use") {
currentBlockType = "tool_use";
currentToolCall = {
id: event.content_block.id,
name: event.content_block.name,
arguments: ""
};
} else if (event.content_block.type === "text") {
currentBlockType = "text";
} else if (event.content_block.type === "thinking") {
currentBlockType = "thinking";
}
break;
case "content_block_stop":
if (currentBlockType === "tool_use" && currentToolCall && currentToolCall.id && currentToolCall.name) {
try {
let argsString = typeof currentToolCall.arguments === "string" ? currentToolCall.arguments : "{}";
if (argsString.trim() === "") {
argsString = "{}";
}
const parsedArgs = JSON.parse(argsString);
toolCalls.push({
id: currentToolCall.id,
name: currentToolCall.name,
arguments: parsedArgs
});
} catch (error) {
console.error("Failed to parse tool arguments:", error);
}
currentToolCall = null;
}
currentBlockType = null;
break;
case "message_delta":
if (event.delta.stop_reason) {
stopReason = event.delta.stop_reason;
}
if (event.usage?.output_tokens !== void 0) {
outputTokens = event.usage.output_tokens;
}
break;
case "message_stop":
break;
}
}
const tokens = {
input: inputTokens,
output: outputTokens
};
const cost = calculateTokenCost(this.config.model, tokens);
const endTime = performance.now();
const took = startTime ? (endTime - startTime) / 1e3 : 0;
const assistantMessage = {
role: "assistant",
...content && { content },
...toolCalls.length > 0 && { toolCalls },
...thinkingContent && { thinking: thinkingContent },
...thinkingSignature && { thinkingSignature },
usage: tokens,
provider: this.getProvider(),
model: this.getModel(),
timestamp: /* @__PURE__ */ new Date(),
took
};
if (options?.context) {
options.context.addMessage(assistantMessage);
}
const response = {
type: "success",
stopReason: this.mapStopReason(stopReason) || "complete",
message: assistantMessage,
tokens,
cost
};
return response;
} catch (error) {
return this.handleError(error);
}
}
mapStopReason(reason) {
switch (reason) {
case "end_turn":
return "complete";
case "max_tokens":
return "max_tokens";
case "stop_sequence":
return "stop_sequence";
case "tool_use":
return "tool_call";
default:
return void 0;
}
}
handleError(error) {
if (error instanceof DOMException && error.name === "AbortError") {
const modelError2 = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError2 };
}
if (error instanceof Error && "status" in error) {
const apiError = error;
const modelError2 = {
type: this.getErrorType(apiError.status),
message: apiError.message,
retryable: this.isRetryable(apiError.status),
...this.getRetryAfter(apiError) !== void 0 && {
retryAfter: this.getRetryAfter(apiError)
}
};
return { type: "error", error: modelError2 };
}
const modelError = {
type: "api_error",
message: error instanceof Error ? error.message : "Unknown error",
retryable: false
};
return { type: "error", error: modelError };
}
getErrorType(status) {
switch (status) {
case 401:
return "auth";
case 429:
return "rate_limit";
case 400:
case 404:
case 422:
return "invalid_request";
default:
return "api_error";
}
}
isRetryable(status) {
return status === 429 || status !== void 0 && status >= 500;
}
getRetryAfter(error) {
const retryAfter = error.headers?.["retry-after"];
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
return isNaN(seconds) ? void 0 : seconds;
}
return void 0;
}
};
// ../../packages/lemmy/dist/src/clients/google.js
import { GoogleGenAI } from "@google/genai";
var GoogleClient = class {
google;
config;
nextId = 0;
constructor(config) {
this.config = config;
this.google = new GoogleGenAI({
apiKey: config.apiKey,
...config.projectId && { project: config.projectId },
...config.baseURL && { apiUrl: config.baseURL }
});
}
getModel() {
return this.config.model;
}
getProvider() {
return "google";
}
buildGoogleParams(options) {
const modelData = findModelData(this.config.model);
const maxOutputTokens = options.maxOutputTokens || this.config.defaults?.maxOutputTokens || modelData?.maxOutputTokens || 4096;
const config = {
maxOutputTokens: options.maxOutputTokens || maxOutputTokens
};
if (options.temperature !== void 0)
config.temperature = options.temperature;
if (options.topP !== void 0)
config.topP = options.topP;
if (options.topK !== void 0)
config.topK = options.topK;
if (options.candidateCount !== void 0)
config.candidateCount = options.candidateCount;
if (options.stopSequences !== void 0)
config.stopSequences = Array.isArray(options.stopSequences) ? options.stopSequences : [options.stopSequences];
if (options.responseLogprobs !== void 0)
config.responseLogprobs = options.responseLogprobs;
if (options.logprobs !== void 0)
config.logprobs = options.logprobs;
if (options.presencePenalty !== void 0)
config.presencePenalty = options.presencePenalty;
if (options.frequencyPenalty !== void 0)
config.frequencyPenalty = options.frequencyPenalty;
if (options.seed !== void 0)
config.seed = options.seed;
if (options.responseMimeType !== void 0)
config.responseMimeType = options.responseMimeType;
const systemMessage = options.context?.getSystemMessage();
if (systemMessage) {
config.systemInstruction = systemMessage;
}
const tools = options?.context?.listTools() || [];
const googleTools = tools.map((tool) => zodToGoogle(tool));
if (googleTools && googleTools.length > 0) {
config.tools = [
{
functionDeclarations: googleTools
}
];
}
const includeThoughts = options.includeThoughts ?? false;
if (includeThoughts) {
config.thinkingConfig = {
includeThoughts,
...options.thinkingBudget && { thinkingBudget: options.thinkingBudget }
};
}
return {
model: this.config.model,
contents: [],
config
};
}
async ask(input, options) {
const startTime = performance.now();
try {
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError };
}
const userInput = typeof input === "string" ? { content: input } : input;
const userMessage = {
role: "user",
...userInput.content !== void 0 && {
content: userInput.content
},
...userInput.toolResults !== void 0 && {
toolResults: userInput.toolResults
},
...userInput.attachments !== void 0 && {
attachments: userInput.attachments
},
timestamp: /* @__PURE__ */ new Date()
};
if (options?.context) {
options.context.addMessage(userMessage);
}
const contents = this.convertMessagesToGoogle(options?.context?.getMessages() || [userMessage]);
const mergedOptions = { ...this.config.defaults, ...options };
const requestParams = this.buildGoogleParams(mergedOptions);
requestParams.contents = contents;
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError };
}
if (requestParams.config && options?.abortSignal) {
requestParams.config.abortSignal = options.abortSignal;
}
const stream = await this.google.models.generateContentStream(requestParams);
return await this.processStream(stream, options, startTime);
} catch (error) {
return this.handleError(error);
}
}
convertMessagesToGoogle(contextMessages) {
const contents = [];
for (const msg of contextMessages) {
if (msg.role === "user") {
const parts = [];
if (msg.content?.trim()) {
parts.push({ text: msg.content });
}
if (msg.toolResults && msg.toolResults.length > 0) {
for (const toolResult of msg.toolResults) {
const functionResponse = {
name: toolResult.toolCallId,
// Google uses function name for tool call ID
response: {
result: toolResult.content
}
};
parts.push({ functionResponse });
}
}
if (msg.attachments && msg.attachments.length > 0) {
for (const attachment of msg.attachments) {
if (attachment.type === "image") {
const supportedMimeTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (!supportedMimeTypes.includes(attachment.mimeType)) {
throw new Error(`Unsupported image mime type: ${attachment.mimeType}. Supported types: ${supportedMimeTypes.join(", ")}`);
}
const dataStr = typeof attachment.data === "string" ? attachment.data : attachment.data.toString("base64");
if (dataStr.startsWith("http://") || dataStr.startsWith("https://")) {
parts.push({
fileData: {
mimeType: attachment.mimeType,
fileUri: dataStr
}
});
} else {
parts.push({
inlineData: {
mimeType: attachment.mimeType,
data: dataStr
}
});
}
} else {
throw new Error(`Unsupported attachment type: ${attachment.type}. Google AI only supports image attachments.`);
}
}
}
if (parts.length > 0) {
contents.push({
role: "user",
parts
});
}
} else if (msg.role === "assistant") {
const parts = [];
if (msg.thinking?.trim()) {
parts.push({
text: msg.thinking,
thought: true
});
}
if (msg.content?.trim()) {
parts.push({ text: msg.content });
}
if (msg.toolCalls && msg.toolCalls.length > 0) {
for (const toolCall of msg.toolCalls) {
const functionCall = {
name: toolCall.name,
args: toolCall.arguments
};
parts.push({ functionCall });
}
}
if (parts.length > 0) {
contents.push({
role: "model",
parts
});
}
}
}
return contents;
}
async processStream(stream, options, startTime) {
let content = "";
let thinkingContent = "";
let inputTokens = 0;
let outputTokens = 0;
let stopReason;
let toolCalls = [];
try {
for await (const chunk of stream) {
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted during streaming",
retryable: false
};
return { type: "error", error: modelError };
}
if (chunk.candidates && chunk.candidates.length > 0) {
const candidate = chunk.candidates[0];
if (!candidate) {
continue;
}
if (chunk.usageMetadata) {
inputTokens = chunk.usageMetadata.promptTokenCount || 0;
outputTokens = chunk.usageMetadata.candidatesTokenCount || 0;
}
if (candidate.finishReason) {
stopReason = candidate.finishReason;
}
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.text) {
if (part.thought) {
thinkingContent += part.text;
options?.onThinkingChunk?.(part.text);
} else {
content += part.text;
options?.onChunk?.(part.text);
}
} else if (part.functionCall) {
const toolCall = {
id: part.functionCall.name + "_" + this.nextId++,
// Generate unique ID
name: part.functionCall.name || "unknown",
arguments: part.functionCall.args || {}
};
toolCalls.push(toolCall);
}
}
}
}
}
const tokens = {
input: inputTokens,
output: outputTokens
};
const cost = calculateTokenCost(this.config.model, tokens);
const endTime = performance.now();
const took = startTime ? (endTime - startTime) / 1e3 : 0;
const assistantMessage = {
role: "assistant",
...content && { content },
...toolCalls.length > 0 && { toolCalls },
...thinkingContent && { thinking: thinkingContent },
usage: tokens,
provider: this.getProvider(),
model: this.getModel(),
timestamp: /* @__PURE__ */ new Date(),
took
};
if (options?.context) {
options.context.addMessage(assistantMessage);
}
let finalStopReason = this.mapStopReason(stopReason) || "complete";
if (toolCalls.length > 0) {
finalStopReason = "tool_call";
}
const response = {
type: "success",
stopReason: finalStopReason,
message: assistantMessage,
tokens,
cost
};
return response;
} catch (error) {
return this.handleError(error);
}
}
mapStopReason(reason) {
switch (reason) {
case "STOP":
return "complete";
case "MAX_TOKENS":
return "max_tokens";
case "SAFETY":
return "stop_sequence";
case "RECITATION":
return "stop_sequence";
case "OTHER":
return "complete";
default:
return void 0;
}
}
handleError(error) {
if (error instanceof DOMException && error.name === "AbortError") {
const modelError2 = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError2 };
}
if (error && typeof error === "object") {
const apiError = error;
let status = apiError.status;
let message = apiError.message || "Unknown API error";
if (typeof message === "string" && message.includes("API key not valid")) {
status = 401;
message = "Invalid API key";
} else if (typeof message === "string" && message.includes("quota")) {
status = 429;
}
const modelError2 = {
type: this.getErrorType(status),
message,
retryable: this.isRetryable(status),
...this.getRetryAfter(apiError) !== void 0 && {
retryAfter: this.getRetryAfter(apiError)
}
};
return { type: "error", error: modelError2 };
}
const modelError = {
type: "api_error",
message: error instanceof Error ? error.message : "Unknown error",
retryable: false
};
return { type: "error", error: modelError };
}
getErrorType(status) {
switch (status) {
case 401:
return "auth";
case 429:
return "rate_limit";
case 400:
case 404:
case 422:
return "invalid_request";
default:
return "api_error";
}
}
isRetryable(status) {
return status === 429 || status !== void 0 && status >= 500;
}
getRetryAfter(error) {
const retryAfter = error.headers?.["retry-after"];
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
return isNaN(seconds) ? void 0 : seconds;
}
return void 0;
}
};
// ../../packages/lemmy/dist/src/clients/openai.js
import OpenAI from "openai";
var OpenAIClient = class {
openai;
config;
constructor(config) {
this.config = config;
this.openai = new OpenAI({
apiKey: config.apiKey,
organization: config.organization,
baseURL: config.baseURL,
maxRetries: config.maxRetries ?? 3
});
}
getModel() {
return this.config.model;
}
getProvider() {
return "openai";
}
buildOpenAIParams(options, messages) {
const params = {
model: this.config.model,
stream: true,
stream_options: { include_usage: true },
messages
};
const modelData = findModelData(this.config.model);
params.max_completion_tokens = options?.maxOutputTokens || this.config.defaults?.maxOutputTokens || modelData?.maxOutputTokens || 4096;
if (options.temperature !== void 0)
params.temperature = options.temperature;
if (options.topP !== void 0)
params.top_p = options.topP;
if (options.presencePenalty !== void 0)
params.presence_penalty = options.presencePenalty;
if (options.frequencyPenalty !== void 0)
params.frequency_penalty = options.frequencyPenalty;
if (options.logprobs !== void 0)
params.logprobs = options.logprobs;
if (options.topLogprobs !== void 0)
params.top_logprobs = options.topLogprobs;
if (options.maxCompletionTokens !== void 0)
params.max_completion_tokens = options.maxCompletionTokens;
if (options.n !== void 0)
params.n = options.n;
if (options.parallelToolCalls !== void 0)
params.parallel_tool_calls = options.parallelToolCalls;
if (options.responseFormat !== void 0) {
if (options.responseFormat === "text") {
params.response_format = { type: "text" };
} else if (options.responseFormat === "json_object") {
params.response_format = { type: "json_object" };
}
}
if (options.seed !== void 0)
params.seed = options.seed;
if (options.serviceTier !== void 0)
params.service_tier = options.serviceTier;
if (options.stop !== void 0)
params.stop = options.stop;
if (options.store !== void 0)
params.store = options.store;
if (options.toolChoice !== void 0)
params.tool_choice = options.toolChoice;
if (options.user !== void 0)
params.user = options.user;
if (options.reasoningEffort !== void 0)
params.reasoning_effort = options.reasoningEffort;
const tools = options?.context?.listTools() || [];
const openaiTools = tools.map((tool) => zodToOpenAI(tool));
if (openaiTools && openaiTools.length > 0) {
params.tools = openaiTools;
params.tool_choice = options.toolChoice || "auto";
}
return params;
}
async ask(input, options) {
const startTime = performance.now();
try {
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError };
}
const userInput = typeof input === "string" ? { content: input } : input;
const userMessage = {
role: "user",
...userInput.content !== void 0 && {
content: userInput.content
},
...userInput.toolResults !== void 0 && {
toolResults: userInput.toolResults
},
...userInput.attachments !== void 0 && {
attachments: userInput.attachments
},
timestamp: /* @__PURE__ */ new Date()
};
if (options?.context) {
options.context.addMessage(userMessage);
}
const messages = this.convertMessages(options?.context?.getMessages() || [userMessage]);
const systemMessage = options?.context?.getSystemMessage();
if (systemMessage) {
messages.unshift({ role: "system", content: systemMessage });
}
const mergedOptions = { ...this.config.defaults, ...options };
const requestParams = this.buildOpenAIParams(mergedOptions, messages);
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError };
}
const stream = await this.openai.chat.completions.create(requestParams, {
signal: options?.abortSignal
});
return await this.processStream(stream, options, startTime);
} catch (error) {
return this.handleError(error);
}
}
convertMessages(contextMessages) {
const messages = [];
for (const msg of contextMessages) {
if (msg.role === "user") {
const contentBlocks = [];
if (msg.content?.trim()) {
contentBlocks.push({ type: "text", text: msg.content });
}
if (msg.toolResults && msg.toolResults.length > 0) {
for (const toolResult of msg.toolResults) {
messages.push({
role: "tool",
tool_call_id: toolResult.toolCallId,
content: toolResult.content
});
}
}
if (msg.attachments && msg.attachments.length > 0) {
for (const attachment of msg.attachments) {
if (attachment.type === "image") {
const dataStr = typeof attachment.data === "string" && (attachment.data.startsWith("http://") || attachment.data.startsWith("https://")) ? attachment.data : `data:${attachment.mimeType};base64,${attachment.data.toString("base64")}`;
contentBlocks.push({
type: "image_url",
image_url: {
url: dataStr
// TODO: detail, needs to be piped through somehow, possibly per attachment..
}
});
}
}
}
if (msg.content?.trim() || msg.attachments && msg.attachments.length > 0) {
messages.push({
role: "user",
content: contentBlocks.length === 1 && contentBlocks[0]?.type === "text" ? contentBlocks[0].text : contentBlocks
});
}
} else if (msg.role === "assistant") {
if (msg.toolCalls && msg.toolCalls.length > 0) {
const toolCalls = msg.toolCalls.map((toolCall) => ({
id: toolCall.id,
type: "function",
function: {
name: toolCall.name,
arguments: JSON.stringify(toolCall.arguments)
}
}));
messages.push({
role: "assistant",
content: msg.content || null,
tool_calls: toolCalls
});
} else if (msg.content) {
messages.push({ role: "assistant", content: msg.content });
}
}
}
return messages;
}
async processStream(stream, options, startTime) {
let content = "";
let inputTokens = 0;
let outputTokens = 0;
let stopReason;
let toolCalls = [];
const currentToolCalls = /* @__PURE__ */ new Map();
try {
for await (const chunk of stream) {
if (options?.abortSignal?.aborted) {
const modelError = {
type: "invalid_request",
message: "Request was aborted during streaming",
retryable: false
};
return { type: "error", error: modelError };
}
if (chunk.usage) {
inputTokens = chunk.usage.prompt_tokens || 0;
outputTokens = chunk.usage.completion_tokens || 0;
}
const choice = chunk.choices?.[0];
if (!choice)
continue;
if (choice.delta?.content) {
const contentChunk = choice.delta.content;
content += contentChunk;
options?.onChunk?.(contentChunk);
}
if (choice.delta?.tool_calls) {
for (const toolCallDelta of choice.delta.tool_calls) {
const index = toolCallDelta.index;
if (!currentToolCalls.has(index)) {
currentToolCalls.set(index, {});
}
const currentToolCall = currentToolCalls.get(index);
if (toolCallDelta.id) {
currentToolCall.id = toolCallDelta.id;
}
if (toolCallDelta.function) {
if (toolCallDelta.function.name) {
currentToolCall.name = toolCallDelta.function.name;
}
if (toolCallDelta.function.arguments) {
currentToolCall.arguments = (currentToolCall.arguments || "") + toolCallDelta.function.arguments;
}
}
}
}
if (choice.finish_reason) {
stopReason = choice.finish_reason;
}
}
for (const [_, toolCallData] of currentToolCalls) {
if (toolCallData.id && toolCallData.name) {
try {
let argsString = toolCallData.arguments || "{}";
if (argsString.trim() === "") {
argsString = "{}";
}
const parsedArgs = JSON.parse(argsString);
toolCalls.push({
id: toolCallData.id,
name: toolCallData.name,
arguments: parsedArgs
});
} catch (error) {
console.error("Failed to parse tool arguments:", error);
}
}
}
if (inputTokens === 0 && outputTokens === 0 && content) {
inputTokens = Math.ceil(content.length / 6);
outputTokens = Math.ceil(content.length / 4);
}
const tokens = {
input: inputTokens,
output: outputTokens
};
const cost = calculateTokenCost(this.config.model, tokens);
const endTime = performance.now();
const took = startTime ? (endTime - startTime) / 1e3 : 0;
const assistantMessage = {
role: "assistant",
...content && { content },
...toolCalls.length > 0 && { toolCalls },
usage: tokens,
provider: this.getProvider(),
model: this.getModel(),
timestamp: /* @__PURE__ */ new Date(),
took
};
if (options?.context) {
options.context.addMessage(assistantMessage);
}
const response = {
type: "success",
stopReason: this.mapStopReason(stopReason) || "complete",
message: assistantMessage,
tokens,
cost
};
return response;
} catch (error) {
return this.handleError(error);
}
}
mapStopReason(reason) {
switch (reason) {
case "stop":
return "complete";
case "length":
return "max_tokens";
case "content_filter":
return "stop_sequence";
case "tool_calls":
return "tool_call";
default:
return void 0;
}
}
handleError(error) {
if (error instanceof DOMException && error.name === "AbortError") {
const modelError2 = {
type: "invalid_request",
message: "Request was aborted",
retryable: false
};
return { type: "error", error: modelError2 };
}
if (error instanceof Error && "status" in error) {
const apiError = error;
const modelError2 = {
type: this.getErrorType(apiError.status),
message: apiError.message + ":\n" + JSON.stringify(error),
retryable: this.isRetryable(apiError.status),
...this.getRetryAfter(apiError) !== void 0 && {
retryAfter: this.getRetryAfter(apiError)
}
};
return { type: "error", error: modelError2 };
}
const modelError = {
type: "api_error",
message: error instanceof Error ? error.message : JSON.stringify(error),
retryable: false
};
return { type: "error", error: modelError };
}
getErrorType(status) {
switch (status) {
case 401:
return "auth";
case 429:
return "rate_limit";
case 400:
case 404:
case 422:
return "invalid_request";
default:
return "api_error";
}
}
isRetryable(status) {
return status === 429 || status !== void 0 && status >= 500;
}
getRetryAfter(error) {
const retryAfter = error.headers?.["retry-after"];
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
return isNaN(seconds) ? void 0 : seconds;
}
return void 0;
}
};
// ../../packages/lemmy/dist/src/generated/models.js
var AnthropicModelData = {
"claude-2.0": {
contextWindow: 2e5,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-2.1": {
contextWindow: 2e5,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-3-5-haiku-20241022": {
contextWindow: 2e5,
maxOutputTokens: 8192,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.8,
outputPerMillion: 4
}
},
"claude-3-5-haiku-latest": {
contextWindow: 2e5,
maxOutputTokens: 8192,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.8,
outputPerMillion: 4
}
},
"claude-3-5-sonnet-20240620": {
contextWindow: 2e5,
maxOutputTokens: 8192,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-3-5-sonnet-20241022": {
contextWindow: 2e5,
maxOutputTokens: 8192,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-3-5-sonnet-latest": {
contextWindow: 2e5,
maxOutputTokens: 8192,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-3-7-sonnet-20250219": {
contextWindow: 2e5,
maxOutputTokens: 64e3,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-3-7-sonnet-latest": {
contextWindow: 2e5,
maxOutputTokens: 64e3,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-3-haiku-20240307": {
contextWindow: 2e5,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.25,
outputPerMillion: 1.25
}
},
"claude-3-opus-20240229": {
contextWindow: 2e5,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 15,
outputPerMillion: 75
}
},
"claude-3-opus-latest": {
contextWindow: 2e5,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 15,
outputPerMillion: 75
}
},
"claude-3-sonnet-20240229": {
contextWindow: 2e5,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
},
"claude-opus-4-20250514": {
contextWindow: 2e5,
maxOutputTokens: 32e3,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 15,
outputPerMillion: 75
}
},
"claude-sonnet-4-20250514": {
contextWindow: 2e5,
maxOutputTokens: 64e3,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 15
}
}
};
var OpenAIModelData = {
"babbage-002": {
contextWindow: 0,
maxOutputTokens: 16384,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.4,
outputPerMillion: 0.4
}
},
"chatgpt-4o-latest": {
contextWindow: 128e3,
maxOutputTokens: 16384,
supportsTools: false,
supportsImageInput: true,
pricing: {
inputPerMillion: 5,
outputPerMillion: 15
}
},
"codex-mini-latest": {
contextWindow: 2e5,
maxOutputTokens: 1e5,
supportsTools: false,
supportsImageInput: true,
pricing: {
inputPerMillion: 1.5,
outputPerMillion: 6
}
},
"computer-use-preview": {
contextWindow: 8192,
maxOutputTokens: 1024,
supportsTools: false,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 12
}
},
"computer-use-preview-2025-03-11": {
contextWindow: 8192,
maxOutputTokens: 1024,
supportsTools: false,
supportsImageInput: true,
pricing: {
inputPerMillion: 3,
outputPerMillion: 12
}
},
"davinci-002": {
contextWindow: 0,
maxOutputTokens: 16384,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 2,
outputPerMillion: 2
}
},
"gpt-3.5-turbo": {
contextWindow: 16385,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-3.5-turbo-0125": {
contextWindow: 16385,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-3.5-turbo-1106": {
contextWindow: 16385,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-3.5-turbo-16k": {
contextWindow: 16385,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-3.5-turbo-instruct": {
contextWindow: 16385,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-3.5-turbo-instruct-0914": {
contextWindow: 16385,
maxOutputTokens: 4096,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-4": {
contextWindow: 8192,
maxOutputTokens: 8192,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 30,
outputPerMillion: 60
}
},
"gpt-4-0125-preview": {
contextWindow: 4096,
maxOutputTokens: 16384,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-4-0613": {
contextWindow: 8192,
maxOutputTokens: 8192,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 30,
outputPerMillion: 60
}
},
"gpt-4-1106-preview": {
contextWindow: 4096,
maxOutputTokens: 16384,
supportsTools: false,
supportsImageInput: false,
pricing: {
inputPerMillion: 0.5,
outputPerMillion: 1.5
}
},
"gpt-4-turbo": {
contextWindow: 128e3,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 10,
outputPerMillion: 30
}
},
"gpt-4-turbo-2024-04-09": {
contextWindow: 128e3,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 10,
outputPerMillion: 30
}
},
"gpt-4-turbo-preview": {
contextWindow: 128e3,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 10,
outputPerMillion: 30
}
},
"gpt-4.1": {
contextWindow: 1047576,
maxOutputTokens: 32768,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 2,
outputPerMillion: 8
}
},
"gpt-4.1-2025-04-14": {
contextWindow: 1047576,
maxOutputTokens: 32768,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 2,
outputPerMillion: 8
}
},
"gpt-4.1-mini": {
contextWindow: 1047576,
maxOutputTokens: 32768,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.4,
outputPerMillion: 1.6
}
},
"gpt-4.1-mini-2025-04-14": {
contextWindow: 1047576,
maxOutputTokens: 32768,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.4,
outputPerMillion: 1.6
}
},
"gpt-4.1-nano": {
contextWindow: 1047576,
maxOutputTokens: 32768,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.1,
outputPerMillion: 0.4
}
},
"gpt-4.1-nano-2025-04-14": {
contextWindow: 1047576,
maxOutputTokens: 32768,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 0.1,
outputPerMillion: 0.4
}
},
"gpt-4.5-preview": {
contextWindow: 128e3,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 10,
outputPerMillion: 30
}
},
"gpt-4.5-preview-2025-02-27": {
contextWindow: 128e3,
maxOutputTokens: 4096,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 10,
outputPerMillion: 30
}
},
"gpt-4o": {
contextWindow: 128e3,
maxOutputTokens: 16384,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 2.5,
outputPerMillion: 10
}
},
"gpt-4o-2024-05-13": {
contextWindow: 128e3,
maxOutputTokens: 16384,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 2.5,
outputPerMillion: 10
}
},
"gpt-4o-2024-08-06": {
contextWindow: 128e3,
maxOutputTokens: 16384,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 2.5,
outputPerMillion: 10
}
},
"gpt-4o-2024-11-20": {
contextWindow: 128e3,
maxOutputTokens: 16384,
supportsTools: true,
supportsImageInput: true,
pricing: {
inputPerMillion: 2.5,
outputPerMillion: 10
}
},
"gpt-4o-audio-preview": {
contextWindow: