route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
343 lines • 12.8 kB
JavaScript
;
/**
* 简化的OpenAI-Compatible Client
* 移除多余的日志和复杂的判断逻辑
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAICompatibleClient = void 0;
const axios_1 = __importDefault(require("axios"));
const types_1 = require("@/types");
const finish_reason_handler_1 = require("@/utils/finish-reason-handler");
class OpenAICompatibleClient {
config;
name;
type = 'openai';
httpClient;
endpoint;
apiKey;
maxRetries = 3;
retryDelay = 1000;
constructor(config, providerId) {
this.config = config;
this.name = providerId;
this.endpoint = config.endpoint;
const credentials = config.authentication.credentials;
const apiKey = credentials ? (credentials.apiKey || credentials.api_key) : '';
this.apiKey = Array.isArray(apiKey) ? apiKey[0] : apiKey;
if (!this.endpoint) {
throw new Error(`OpenAI-compatible provider ${providerId} requires endpoint configuration`);
}
if (config.authentication.type !== 'none' && !this.apiKey) {
throw new Error(`OpenAI-compatible provider ${providerId} requires API key configuration`);
}
// 使用endpoint作为完整URL包括路径
this.httpClient = axios_1.default.create({
baseURL: this.endpoint,
timeout: 300000, // 5 minute timeout
headers: {
'Content-Type': 'application/json',
'User-Agent': 'claude-code-router/2.0.0',
'Authorization': `Bearer ${this.apiKey}`
}
});
}
/**
* Check if provider is healthy
*/
async isHealthy() {
return !!(this.endpoint && this.apiKey);
}
/**
* Execute request with retry logic
*/
async executeWithRetry(requestFn, operation, requestId) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await requestFn();
}
catch (error) {
lastError = error;
const isRateLimited = error?.response?.status === 429;
if (!this.isRetryableError(error)) {
break;
}
if (attempt < this.maxRetries - 1) {
await this.waitForRetry(attempt, isRateLimited);
}
}
}
throw lastError;
}
/**
* Wait for retry delay
*/
async waitForRetry(attempt, isRateLimited = false) {
let delay;
if (isRateLimited) {
if (attempt === 0)
delay = 1000;
else if (attempt === 1)
delay = 5000;
else
delay = 60000;
}
else {
delay = this.retryDelay * Math.pow(2, attempt);
}
await new Promise(resolve => setTimeout(resolve, delay));
}
/**
* Check if error is retryable
*/
isRetryableError(error) {
if (error.response?.status) {
const status = error.response.status;
return status === 429 || status === 502 || status === 503 || status === 504;
}
return false;
}
/**
* Send request to OpenAI-compatible API
*/
async sendRequest(request) {
const requestId = request.metadata?.requestId || 'unknown';
try {
const openaiRequest = this.convertToOpenAI(request);
const response = await this.executeWithRetry(() => this.httpClient.post(this.endpoint, openaiRequest), `${this.name} sendRequest`, requestId);
return this.convertFromOpenAI(response.data, request);
}
catch (error) {
if (error instanceof types_1.ProviderError) {
throw error;
}
if (error?.response?.status === 429) {
throw new types_1.ProviderError(`${this.name} rate limit exceeded`, this.name, 429, error?.response?.data);
}
const errorMessage = error instanceof Error ? error.message : String(error);
throw new types_1.ProviderError(`${this.name} request failed: ${errorMessage}`, this.name, 500, error);
}
}
/**
* Send streaming request
*/
async *sendStreamRequest(request) {
const requestId = request.metadata?.requestId || 'unknown';
let lastFinishReason = (0, finish_reason_handler_1.mapFinishReason)('stop');
try {
const openaiRequest = { ...this.convertToOpenAI(request), stream: true };
const response = await this.executeWithRetry(() => this.httpClient.post('', openaiRequest, {
responseType: 'stream',
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
}), `${this.name} sendStreamRequest`, requestId);
if (response.status < 200 || response.status >= 300) {
throw new types_1.ProviderError(`${this.name} API returned status ${response.status}`, this.name, response.status);
}
const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
let buffer = '';
let messageId = `msg_${Date.now()}`;
let hasStarted = false;
let hasContentBlock = false;
// Send initial events
if (!hasStarted) {
yield {
event: 'message_start',
data: {
type: 'message_start',
message: {
id: messageId,
type: 'message',
role: 'assistant',
content: [],
model: request.model,
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 0, output_tokens: 0 }
}
}
};
yield {
event: 'ping',
data: { type: 'ping' }
};
hasStarted = true;
}
let streamEnded = false;
for await (const chunk of response.data) {
if (streamEnded)
break;
try {
const decodedChunk = decoder.decode(chunk, { stream: true });
buffer += decodedChunk;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') {
streamEnded = true;
break;
}
try {
const parsed = JSON.parse(data);
const choice = parsed.choices?.[0];
if (!choice?.delta)
continue;
// Capture finish reason for final event
const finishReason = choice.finish_reason;
if (finishReason) {
lastFinishReason = (0, finish_reason_handler_1.mapFinishReason)(finishReason);
}
const anthropicEvent = this.convertStreamChunkToAnthropic(parsed);
if (anthropicEvent) {
yield anthropicEvent;
}
}
catch (error) {
// Ignore parsing errors
}
}
}
}
catch (decodeError) {
// Ignore decode errors
}
}
// Send completion events
if (hasContentBlock) {
yield {
event: 'content_block_stop',
data: { type: 'content_block_stop', index: 0 }
};
}
yield {
event: 'stream_complete',
data: {
type: 'stream_complete',
finish_reason: lastFinishReason
}
};
}
catch (error) {
if (error instanceof types_1.ProviderError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
throw new types_1.ProviderError(`${this.name} streaming request failed: ${errorMessage}`, this.name, 500, error);
}
}
/**
* Convert BaseRequest to OpenAI format
*/
convertToOpenAI(request) {
const anthropicRequest = {
model: request.model,
messages: this.convertMessages(request.messages),
max_tokens: request.max_tokens || 131072,
temperature: request.temperature,
stream: false
};
// Add system message if present
if (request.metadata?.system) {
const systemContent = Array.isArray(request.metadata.system)
? request.metadata.system.map((s) => s.text || s).join('\n')
: request.metadata.system;
anthropicRequest.messages.unshift({
role: 'system',
content: systemContent
});
}
// Add tools if present
if (request.metadata?.tools) {
const tools = Array.isArray(request.metadata.tools) ? request.metadata.tools : [request.metadata.tools];
anthropicRequest.tools = tools.map((tool) => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.input_schema
}
}));
}
return anthropicRequest;
}
/**
* Convert messages to OpenAI format
*/
convertMessages(messages) {
return messages.map(msg => ({
role: msg.role,
content: this.convertContent(msg.content)
}));
}
/**
* Convert content to OpenAI format
*/
convertContent(content) {
if (typeof content === 'string') {
return content;
}
if (Array.isArray(content)) {
return content.map(block => {
if (block.type === 'text') {
return block.text;
}
else if (block.type === 'tool_result') {
return typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
}
return JSON.stringify(block);
}).join('\n');
}
return JSON.stringify(content);
}
/**
* Convert OpenAI response to BaseResponse
*/
convertFromOpenAI(response, originalRequest) {
const choice = response.choices?.[0];
if (!choice) {
throw new Error('No choices in OpenAI response');
}
return {
id: response.id,
model: originalRequest.model,
role: 'assistant',
content: [{ type: 'text', text: choice.message.content }],
stop_reason: (0, finish_reason_handler_1.mapFinishReason)(choice.finish_reason),
usage: {
input_tokens: response.usage?.prompt_tokens || 0,
output_tokens: response.usage?.completion_tokens || 0
}
};
}
/**
* Convert OpenAI streaming chunk to Anthropic format
*/
convertStreamChunkToAnthropic(chunk) {
const choice = chunk.choices?.[0];
if (!choice)
return null;
// Handle content deltas
if (choice.delta?.content) {
return {
event: 'content_block_delta',
data: {
type: 'content_block_delta',
index: 0,
delta: {
type: 'text_delta',
text: choice.delta.content
}
}
};
}
return null;
}
}
exports.OpenAICompatibleClient = OpenAICompatibleClient;
//# sourceMappingURL=client.js.map