- Implemented OpenAI compatible API proxy server - Support for Anthropic and custom OpenAI format conversion - Automatic API key refresh with WorkOS OAuth - SSE streaming response transformation - Smart header management for Factory endpoints - Chinese documentation
173 lines
5.1 KiB
JavaScript
173 lines
5.1 KiB
JavaScript
import { logDebug } from '../logger.js';
|
|
|
|
export function transformToAnthropic(openaiRequest) {
|
|
logDebug('Transforming OpenAI request to Anthropic format');
|
|
|
|
const anthropicRequest = {
|
|
model: openaiRequest.model,
|
|
messages: [],
|
|
stream: openaiRequest.stream !== false
|
|
};
|
|
|
|
// Handle max_tokens
|
|
if (openaiRequest.max_tokens) {
|
|
anthropicRequest.max_tokens = openaiRequest.max_tokens;
|
|
} else if (openaiRequest.max_completion_tokens) {
|
|
anthropicRequest.max_tokens = openaiRequest.max_completion_tokens;
|
|
} else {
|
|
anthropicRequest.max_tokens = 4096;
|
|
}
|
|
|
|
// Extract system message(s) and transform other messages
|
|
let systemContent = [];
|
|
|
|
if (openaiRequest.messages && Array.isArray(openaiRequest.messages)) {
|
|
for (const msg of openaiRequest.messages) {
|
|
// Handle system messages separately
|
|
if (msg.role === 'system') {
|
|
if (typeof msg.content === 'string') {
|
|
systemContent.push({
|
|
type: 'text',
|
|
text: msg.content
|
|
});
|
|
} else if (Array.isArray(msg.content)) {
|
|
for (const part of msg.content) {
|
|
if (part.type === 'text') {
|
|
systemContent.push({
|
|
type: 'text',
|
|
text: part.text
|
|
});
|
|
} else {
|
|
systemContent.push(part);
|
|
}
|
|
}
|
|
}
|
|
continue; // Skip adding system messages to messages array
|
|
}
|
|
|
|
const anthropicMsg = {
|
|
role: msg.role,
|
|
content: []
|
|
};
|
|
|
|
if (typeof msg.content === 'string') {
|
|
anthropicMsg.content.push({
|
|
type: 'text',
|
|
text: msg.content
|
|
});
|
|
} else if (Array.isArray(msg.content)) {
|
|
for (const part of msg.content) {
|
|
if (part.type === 'text') {
|
|
anthropicMsg.content.push({
|
|
type: 'text',
|
|
text: part.text
|
|
});
|
|
} else if (part.type === 'image_url') {
|
|
anthropicMsg.content.push({
|
|
type: 'image',
|
|
source: part.image_url
|
|
});
|
|
} else {
|
|
anthropicMsg.content.push(part);
|
|
}
|
|
}
|
|
}
|
|
|
|
anthropicRequest.messages.push(anthropicMsg);
|
|
}
|
|
}
|
|
|
|
// Add system parameter if system content exists
|
|
if (systemContent.length > 0) {
|
|
anthropicRequest.system = systemContent;
|
|
}
|
|
|
|
// Transform tools if present
|
|
if (openaiRequest.tools && Array.isArray(openaiRequest.tools)) {
|
|
anthropicRequest.tools = openaiRequest.tools.map(tool => {
|
|
if (tool.type === 'function') {
|
|
return {
|
|
name: tool.function.name,
|
|
description: tool.function.description,
|
|
input_schema: tool.function.parameters || {}
|
|
};
|
|
}
|
|
return tool;
|
|
});
|
|
}
|
|
|
|
// Pass through other compatible parameters
|
|
if (openaiRequest.temperature !== undefined) {
|
|
anthropicRequest.temperature = openaiRequest.temperature;
|
|
}
|
|
if (openaiRequest.top_p !== undefined) {
|
|
anthropicRequest.top_p = openaiRequest.top_p;
|
|
}
|
|
if (openaiRequest.stop !== undefined) {
|
|
anthropicRequest.stop_sequences = Array.isArray(openaiRequest.stop)
|
|
? openaiRequest.stop
|
|
: [openaiRequest.stop];
|
|
}
|
|
|
|
logDebug('Transformed Anthropic request', anthropicRequest);
|
|
return anthropicRequest;
|
|
}
|
|
|
|
export function getAnthropicHeaders(authHeader, clientHeaders = {}, isStreaming = true) {
|
|
// Generate unique IDs if not provided
|
|
const sessionId = clientHeaders['x-session-id'] || generateUUID();
|
|
const messageId = clientHeaders['x-assistant-message-id'] || generateUUID();
|
|
|
|
const headers = {
|
|
'accept': 'application/json',
|
|
'content-type': 'application/json',
|
|
'anthropic-version': '2023-06-01',
|
|
'anthropic-beta': 'interleaved-thinking-2025-05-14',
|
|
'x-api-key': 'placeholder',
|
|
'authorization': authHeader || '',
|
|
'x-model-provider': 'anthropic',
|
|
'x-factory-client': 'cli',
|
|
'x-session-id': sessionId,
|
|
'x-assistant-message-id': messageId,
|
|
'user-agent': 'a$/JS 0.57.0',
|
|
'x-stainless-timeout': '600',
|
|
'connection': 'keep-alive'
|
|
};
|
|
|
|
// Pass through Stainless SDK headers with defaults
|
|
const stainlessDefaults = {
|
|
'x-stainless-arch': 'x64',
|
|
'x-stainless-lang': 'js',
|
|
'x-stainless-os': 'MacOS',
|
|
'x-stainless-runtime': 'node',
|
|
'x-stainless-retry-count': '0',
|
|
'x-stainless-package-version': '0.57.0',
|
|
'x-stainless-runtime-version': 'v24.3.0'
|
|
};
|
|
|
|
// Set helper-method based on streaming
|
|
if (isStreaming) {
|
|
headers['x-stainless-helper-method'] = 'stream';
|
|
}
|
|
|
|
// Copy Stainless headers from client or use defaults
|
|
Object.keys(stainlessDefaults).forEach(header => {
|
|
headers[header] = clientHeaders[header] || stainlessDefaults[header];
|
|
});
|
|
|
|
// Override timeout from defaults if client provided
|
|
if (clientHeaders['x-stainless-timeout']) {
|
|
headers['x-stainless-timeout'] = clientHeaders['x-stainless-timeout'];
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
function generateUUID() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|