UNPKG

@kdump/code-cli-any-llm

Version:

> A unified gateway for the Gemini, opencode, crush, and Qwen Code AI CLIs

950 lines 35.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var ClaudeCodeProvider_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClaudeCodeProvider = void 0; const common_1 = require("@nestjs/common"); const config_1 = require("@nestjs/config"); const crypto_1 = require("crypto"); const node_stream_1 = require("node:stream"); const request_transformer_1 = require("../../transformers/request.transformer"); const response_transformer_1 = require("../../transformers/response.transformer"); const stream_transformer_1 = require("../../transformers/stream.transformer"); const tokenizer_service_1 = require("../../services/tokenizer.service"); const ToolCallProcessor_1 = require("../../utils/zhipu/ToolCallProcessor"); class ClaudeRequestError extends Error { status; body; constructor(status, body) { super(`Claude Code request failed with status ${status}`); this.status = status; this.body = body; } } let ClaudeCodeProvider = ClaudeCodeProvider_1 = class ClaudeCodeProvider { configService; requestTransformer; responseTransformer; tokenizerService; toolCallProcessor; logger = new common_1.Logger(ClaudeCodeProvider_1.name); config; enabled = false; initialized = false; constructor(configService, requestTransformer, responseTransformer, tokenizerService, toolCallProcessor) { this.configService = configService; this.requestTransformer = requestTransformer; this.responseTransformer = responseTransformer; this.tokenizerService = tokenizerService; this.toolCallProcessor = toolCallProcessor; } onModuleInit() { this.ensureInitialized(); } isEnabled() { this.ensureInitialized(); return this.enabled; } getConfig() { this.ensureInitialized(); return this.config; } async generateFromGemini(geminiRequest, targetModel) { const config = this.ensureEnabledConfig(); const openAIRequest = this.prepareOpenAIRequest(geminiRequest, targetModel); const payload = this.buildClaudePayload(openAIRequest, config, false); const response = await this.sendClaudeMessagesRequest(payload, config, false); if (!response.ok) { await this.handleErrorResponse(response); } const data = (await response.json()); const openAIResponse = this.convertClaudeResponseToOpenAI(data, config.model); return this.responseTransformer.transformResponse(openAIResponse); } async *streamFromGemini(geminiRequest, targetModel) { const config = this.ensureEnabledConfig(); const openAIRequest = this.prepareOpenAIRequest(geminiRequest, targetModel); openAIRequest.stream = true; const payload = this.buildClaudePayload(openAIRequest, config, true); const response = await this.sendClaudeMessagesRequest(payload, config, true); if (!response.ok) { await this.handleErrorResponse(response); } if (!response.body) { throw new Error('Claude Code streaming response body is empty'); } const stream = node_stream_1.Readable.fromWeb(response.body); const context = { responseId: (0, crypto_1.randomUUID)(), model: config.model, created: Math.floor(Date.now() / 1000), started: false, toolCallStates: new Map(), finalChunkSent: false, }; const streamTransformer = new stream_transformer_1.StreamTransformer(this.tokenizerService, this.toolCallProcessor); const promptTokenCount = this.computePromptTokens(geminiRequest, targetModel); streamTransformer.initializeForModel(targetModel, promptTokenCount); for await (const chunk of this.parseClaudeStream(stream, context)) { const geminiChunk = streamTransformer.transformStreamChunk(chunk); yield geminiChunk; } const bufferedText = streamTransformer.getBufferedText(); if (bufferedText) { const finalChunk = { candidates: [ { content: { role: 'model', parts: [{ text: bufferedText }], }, index: 0, finishReason: 'STOP', }, ], }; streamTransformer.applyUsageMetadata(finalChunk); yield finalChunk; } streamTransformer.reset(); } async generateContent(request) { const config = this.ensureEnabledConfig(); const openAIRequest = { ...request, stream: false, }; const payload = this.buildClaudePayload(openAIRequest, config, false); const response = await this.sendClaudeMessagesRequest(payload, config, false); if (!response.ok) { await this.handleErrorResponse(response); } const data = (await response.json()); return this.convertClaudeResponseToOpenAI(data, config.model); } async *generateContentStream(request) { const config = this.ensureEnabledConfig(); const openAIRequest = { ...request, stream: true, }; const payload = this.buildClaudePayload(openAIRequest, config, true); const response = await this.sendClaudeMessagesRequest(payload, config, true); if (!response.ok) { await this.handleErrorResponse(response); } if (!response.body) { throw new Error('Claude Code streaming response body is empty'); } const stream = node_stream_1.Readable.fromWeb(response.body); const context = { responseId: (0, crypto_1.randomUUID)(), model: config.model, created: Math.floor(Date.now() / 1000), started: false, toolCallStates: new Map(), finalChunkSent: false, }; for await (const chunk of this.parseClaudeStream(stream, context)) { yield chunk; } } listModels() { const config = this.ensureEnabledConfig(); return Promise.resolve([ { id: config.model, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'claude-code', }, ]); } async healthCheck() { try { const config = this.ensureEnabledConfig(); const url = this.buildUrl(config.baseURL, 'v1/models?limit=1'); const headers = this.buildHeaders(false, config); delete headers['content-type']; const controller = new AbortController(); const timeoutHandle = setTimeout(() => controller.abort(), config.timeout); try { const response = await fetch(url.toString(), { method: 'GET', headers, signal: controller.signal, }); if (!response.ok) { await this.handleErrorResponse(response); } } finally { clearTimeout(timeoutHandle); } return { status: 'healthy', details: { provider: 'Claude Code', baseURL: config.baseURL, model: config.model, }, }; } catch (error) { return { status: 'unhealthy', details: { provider: 'Claude Code', error: error.message, }, }; } } ensureInitialized() { if (!this.initialized) { this.refreshConfig(); } } ensureEnabledConfig() { this.ensureInitialized(); if (!this.enabled || !this.config) { throw new Error('Claude Code provider is not enabled.'); } return this.config; } prepareOpenAIRequest(geminiRequest, targetModel) { return this.requestTransformer.transformRequest(geminiRequest, targetModel); } refreshConfig() { const providerInput = this.configService.get('aiProvider') || 'claudeCode'; const normalizedProvider = providerInput.trim().toLowerCase(); if (normalizedProvider !== 'claudecode' && normalizedProvider !== 'claude-code') { this.enabled = false; this.config = undefined; this.initialized = true; return; } const rawConfig = this.configService.get('claudeCode'); if (!rawConfig) { this.logger.error('Claude Code provider configuration missing; disabling provider.'); this.enabled = false; this.config = undefined; this.initialized = true; return; } if (!rawConfig.apiKey || !rawConfig.apiKey.trim()) { this.logger.error('Claude Code provider is missing an API key; disabling provider.'); this.enabled = false; this.config = undefined; this.initialized = true; return; } const resolved = { apiKey: rawConfig.apiKey.trim(), baseURL: rawConfig.baseURL || 'https://open.bigmodel.cn/api/anthropic', model: rawConfig.model || 'claude-sonnet-4-5-20250929', timeout: rawConfig.timeout ?? 1800000, anthropicVersion: rawConfig.anthropicVersion || '2023-06-01', beta: rawConfig.beta, userAgent: rawConfig.userAgent || 'claude-cli/2.0.1 (external, cli)', xApp: rawConfig.xApp || 'cli', dangerousDirectBrowserAccess: rawConfig.dangerousDirectBrowserAccess ?? true, maxOutputTokens: rawConfig.maxOutputTokens, extraHeaders: rawConfig.extraHeaders, }; this.config = resolved; this.enabled = true; this.initialized = true; this.logger.log(`Claude Code provider initialized with model: ${resolved.model}`); } buildClaudePayload(request, config, stream) { const { messages, system } = this.transformMessages(request.messages); const tools = this.transformTools(request.tools); const payload = { model: config.model, messages, stream, max_tokens: request.max_tokens ?? config.maxOutputTokens ?? 4096, }; if (request.temperature !== undefined) { payload.temperature = request.temperature; } if (request.top_p !== undefined) { payload.top_p = request.top_p; } if (system) { payload.system = system; } if (tools && tools.length > 0) { payload.tools = tools; } const toolChoice = this.transformToolChoice(request.tool_choice); if (toolChoice) { payload.tool_choice = toolChoice; } if (request.stop) { payload.stop_sequences = Array.isArray(request.stop) ? request.stop : [request.stop]; } return payload; } transformMessages(messages = []) { const anthropicMessages = []; const systemPrompts = []; let pendingToolResults = []; const flushToolResults = () => { if (pendingToolResults.length > 0) { anthropicMessages.push({ role: 'user', content: pendingToolResults, }); pendingToolResults = []; } }; for (const message of messages) { if (message.role === 'system' && typeof message.content === 'string') { systemPrompts.push(message.content.trim()); continue; } if (message.role === 'user') { flushToolResults(); const textContent = this.extractTextContent(message.content); if (textContent.trim().length > 0) { anthropicMessages.push({ role: 'user', content: textContent, }); } continue; } if (message.role === 'assistant') { flushToolResults(); const text = this.extractTextContent(message.content); const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : []; if (toolCalls.length === 0) { if (text.trim().length > 0) { anthropicMessages.push({ role: 'assistant', content: text, }); } continue; } const blocks = []; if (text.trim()) { blocks.push({ type: 'text', text }); } for (const toolCall of toolCalls) { const id = this.normalizeToAnthropicToolId(toolCall.id || (0, crypto_1.randomUUID)()); let input = {}; if (toolCall.function?.arguments) { input = this.parseJson(toolCall.function.arguments); } blocks.push({ type: 'tool_use', id, name: toolCall.function?.name || 'tool', input, }); } anthropicMessages.push({ role: 'assistant', content: blocks, }); continue; } if (message.role === 'tool') { const toolResultId = this.normalizeToAnthropicToolId(message.tool_call_id || (0, crypto_1.randomUUID)()); const content = this.normalizeToolResultContent(message.content); pendingToolResults.push({ type: 'tool_result', tool_use_id: toolResultId, content, }); } } flushToolResults(); if (anthropicMessages.length === 0) { anthropicMessages.push({ role: 'user', content: 'Hello' }); } if (anthropicMessages[0]?.role !== 'user') { anthropicMessages.unshift({ role: 'user', content: 'Continue' }); } const system = systemPrompts.length > 0 ? systemPrompts.join('\n\n') : undefined; return { messages: anthropicMessages, system }; } transformTools(tools) { if (!tools || tools.length === 0) { return undefined; } return tools.map((tool) => { const inputSchema = tool.function?.parameters && typeof tool.function.parameters === 'object' ? { ...tool.function.parameters } : { type: 'object' }; if (!inputSchema.type) { inputSchema.type = 'object'; } return { name: tool.function?.name || 'tool', description: tool.function?.description, input_schema: inputSchema, }; }); } extractTextContent(content) { if (typeof content === 'string') { return content; } if (!content) { return ''; } if (Array.isArray(content)) { return content .map((part) => this.extractTextFromPart(part)) .filter((value) => value.length > 0) .join(''); } const single = this.extractTextFromPart(content); return single; } extractTextFromPart(part) { if (!part) { return ''; } if (typeof part === 'string') { return part; } if (typeof part !== 'object') { return ''; } const candidate = part; if (typeof candidate.text === 'string') { return candidate.text; } if (typeof candidate.value === 'string') { return candidate.value; } if (typeof candidate.input_text === 'string') { return candidate.input_text; } try { return JSON.stringify(part); } catch { return ''; } } transformToolChoice(toolChoice) { if (!toolChoice || toolChoice === 'auto') { return undefined; } if (toolChoice === 'none') { return { type: 'none' }; } if (typeof toolChoice === 'object' && toolChoice.type === 'function' && toolChoice.function?.name) { return { type: 'tool', name: toolChoice.function.name, }; } return undefined; } buildHeaders(stream, config) { const headers = { 'content-type': 'application/json', accept: stream ? 'text/event-stream' : 'application/json', 'anthropic-version': config.anthropicVersion, 'x-api-key': config.apiKey, 'user-agent': config.userAgent, 'x-app': config.xApp, 'anthropic-dangerous-direct-browser-access': config.dangerousDirectBrowserAccess ? 'true' : 'false', 'x-stainless-lang': 'js', 'x-stainless-package-version': '0.60.0', 'x-stainless-runtime': 'node', 'x-stainless-runtime-version': process.version, 'x-stainless-os': this.normalizeOS(process.platform), 'x-stainless-arch': process.arch, 'x-stainless-timeout': String(Math.ceil((config.timeout ?? 1800000) / 1000)), 'x-stainless-retry-count': '0', }; if (stream) { headers['x-stainless-helper-method'] = 'stream'; } if (config.beta && config.beta.length > 0) { headers['anthropic-beta'] = config.beta.join(','); } if (config.extraHeaders) { for (const [key, value] of Object.entries(config.extraHeaders)) { if (value !== undefined && value !== null) { headers[key] = value; } } } return headers; } sanitizeHeaders(headers) { const sanitized = { ...headers }; const sensitiveKeys = ['x-api-key', 'authorization']; for (const key of sensitiveKeys) { if (sanitized[key]) { sanitized[key] = '***'; } } return sanitized; } async sendClaudeMessagesRequest(payload, config, stream) { const url = this.buildUrl(config.baseURL, 'v1/messages'); const headers = this.buildHeaders(stream, config); const sanitizedHeaders = this.sanitizeHeaders(headers); try { this.logger.verbose(`ClaudeCodeProvider -> 请求: ${url.toString()} ${JSON.stringify(payload)}`); this.logger.verbose(`ClaudeCodeProvider -> 请求头: ${JSON.stringify(sanitizedHeaders)}`); } catch (error) { this.logger.warn(`ClaudeCodeProvider -> 请求日志序列化失败: ${error.message}`); } const controller = new AbortController(); const timeoutHandle = setTimeout(() => controller.abort(), config.timeout); try { const response = await fetch(url.toString(), { method: 'POST', headers, body: JSON.stringify(payload), signal: controller.signal, }); try { const responseHeaders = Object.fromEntries(response.headers.entries()); this.logger.verbose(`ClaudeCodeProvider -> 响应状态: ${response.status} ${response.statusText}`); this.logger.verbose(`ClaudeCodeProvider -> 响应头: ${JSON.stringify(responseHeaders)}`); if (!stream) { const cloned = response.clone(); const bodyText = await cloned.text(); this.logger.verbose(`ClaudeCodeProvider -> 响应报文: ${bodyText}`); } } catch (error) { this.logger.warn(`ClaudeCodeProvider -> 响应日志处理失败: ${error.message}`); } return response; } finally { clearTimeout(timeoutHandle); } } async handleErrorResponse(response) { const status = response.status; let body = ''; try { body = await response.text(); } catch { body = ''; } try { const parsed = body ? JSON.parse(body) : undefined; if (parsed?.error?.message) { throw new Error(parsed.error.message); } } catch { } throw new ClaudeRequestError(status, body || response.statusText); } convertClaudeResponseToOpenAI(message, model) { const toolCalls = []; const textParts = []; for (const block of message.content) { if (block.type === 'text') { textParts.push(block.text); } else if (block.type === 'tool_use') { const argumentsString = JSON.stringify(block.input ?? {}); toolCalls.push({ id: this.normalizeToOpenAIToolId(block.id), type: 'function', function: { name: block.name, arguments: argumentsString, }, }); } } const responseMessage = { role: 'assistant', content: textParts.length > 0 ? textParts.join('') : null, }; if (toolCalls.length > 0) { responseMessage.tool_calls = toolCalls; } const finishReason = this.mapStopReason(message.stop_reason, toolCalls.length > 0); const usage = message.usage ? { prompt_tokens: message.usage.input_tokens ?? 0, completion_tokens: message.usage.output_tokens ?? 0, total_tokens: message.usage.total_tokens ?? (message.usage.input_tokens ?? 0) + (message.usage.output_tokens ?? 0), } : undefined; return { id: message.id || (0, crypto_1.randomUUID)(), object: 'chat.completion', created: Math.floor(Date.now() / 1000), model, choices: [ { index: 0, message: responseMessage, finish_reason: finishReason, }, ], usage, }; } computePromptTokens(request, model) { let totalTokens = this.tokenizerService.countTokensInRequest(request.contents || [], model); const systemInstruction = request.systemInstruction; if (typeof systemInstruction === 'string') { totalTokens += this.tokenizerService.countTokens(systemInstruction, model); } else if (systemInstruction?.parts) { totalTokens += this.tokenizerService.countTokensInRequest([systemInstruction], model); } return totalTokens; } parseJson(value) { try { return JSON.parse(value); } catch { return value; } } normalizeToolResultContent(content) { if (typeof content !== 'string') { try { return JSON.stringify(content ?? {}); } catch { return ''; } } const trimmed = content.trim(); if (!trimmed) { return ''; } try { const parsed = JSON.parse(trimmed); return typeof parsed === 'string' ? parsed : JSON.stringify(parsed); } catch { return trimmed; } } normalizeToAnthropicToolId(id) { if (id.startsWith('toolu_')) { return id; } if (id.startsWith('hist_tool_')) { return 'toolu_' + id.slice('hist_tool_'.length); } if (id.startsWith('call_')) { return 'toolu_' + id.slice('call_'.length); } return id.startsWith('toolu_') ? id : `toolu_${id}`; } normalizeToOpenAIToolId(id) { if (id.startsWith('call_')) { return id; } if (id.startsWith('toolu_')) { return 'call_' + id.slice('toolu_'.length); } if (id.startsWith('hist_tool_')) { return 'call_' + id.slice('hist_tool_'.length); } return `call_${id}`; } mapStopReason(reason, hasToolCalls) { if (!reason) { return hasToolCalls ? 'tool_calls' : 'stop'; } switch (reason) { case 'max_tokens': return 'length'; case 'tool_use': return 'tool_calls'; case 'end_turn': case 'stop_sequence': return hasToolCalls ? 'tool_calls' : 'stop'; default: return hasToolCalls ? 'tool_calls' : 'stop'; } } normalizeOS(platform) { switch (platform) { case 'win32': return 'Windows'; case 'darwin': return 'Darwin'; default: return 'Linux'; } } async *parseClaudeStream(stream, context) { let buffer = ''; for await (const chunk of stream) { const chunkText = chunk.toString('utf8'); this.logger.verbose(`ClaudeCodeProvider -> 流式片段: ${chunkText.trim() || '[空片段]'}`); buffer += chunkText; buffer = buffer.replace(/\r\n/g, '\n'); let boundaryIndex = buffer.indexOf('\n\n'); while (boundaryIndex !== -1) { const rawEvent = buffer.slice(0, boundaryIndex).trim(); buffer = buffer.slice(boundaryIndex + 2); boundaryIndex = buffer.indexOf('\n\n'); if (!rawEvent) { continue; } this.logger.verbose(`ClaudeCodeProvider -> SSE事件: ${rawEvent}`); let eventName; const dataLines = []; for (const line of rawEvent.split('\n')) { if (line.startsWith('event:')) { eventName = line.slice(6).trim(); } if (line.startsWith('data:')) { dataLines.push(line.slice(5).trim()); } } if (dataLines.length === 0) { continue; } const dataPayload = dataLines.join(''); if (dataPayload === '[DONE]') { if (!context.finalChunkSent) { context.finalChunkSent = true; yield this.createFinalChunk(context); } return; } try { const parsed = JSON.parse(dataPayload); const chunks = this.handleStreamEvent(eventName || parsed.type, parsed, context); if (Array.isArray(chunks)) { for (const item of chunks) { if (item) { yield item; } } } else if (chunks) { yield chunks; } } catch (error) { this.logger.warn(`Failed to parse Claude Code SSE chunk: ${error.message}`); } } } } handleStreamEvent(eventType, payload, context) { switch (eventType) { case 'message_start': { const message = payload.message; context.responseId = message?.id || context.responseId || (0, crypto_1.randomUUID)(); context.model = message?.model || context.model; context.created = Math.floor(Date.now() / 1000); return undefined; } case 'content_block_start': { const contentBlock = payload.content_block; const index = payload.index ?? 0; if (contentBlock?.type === 'tool_use') { const anthropicId = contentBlock.id || (0, crypto_1.randomUUID)(); const state = { index, anthropicId, openaiId: this.normalizeToOpenAIToolId(anthropicId), name: contentBlock.name, arguments: '', }; context.toolCallStates.set(index, state); } return undefined; } case 'content_block_delta': { const index = payload.index ?? 0; const delta = payload.delta; if (!delta) { return undefined; } if (delta.type === 'text_delta' && typeof delta.text === 'string') { return this.createTextChunk(delta.text, context); } if (delta.type === 'input_json_delta') { const state = context.toolCallStates.get(index); if (state && typeof delta.partial_json === 'string') { state.arguments += delta.partial_json; } } return undefined; } case 'content_block_stop': { const index = payload.index ?? 0; const state = context.toolCallStates.get(index); if (state) { const argumentsString = this.normalizeArguments(state.arguments); const chunk = this.createToolCallChunk(state, argumentsString, context); context.toolCallStates.delete(index); return chunk; } return undefined; } case 'message_delta': { const delta = payload.delta; const usage = payload.usage; if (usage) { context.usage = usage; } if (delta?.stop_reason) { context.finishReason = this.mapStopReason(delta.stop_reason, false); if (!context.finalChunkSent) { context.finalChunkSent = true; return this.createFinalChunk(context); } } return undefined; } case 'message_stop': { if (!context.finalChunkSent) { context.finalChunkSent = true; return this.createFinalChunk(context); } return undefined; } default: return undefined; } } createTextChunk(text, context) { const delta = { content: text, }; if (!context.started) { delta.role = 'assistant'; context.started = true; } return { id: context.responseId, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: context.model, choices: [ { index: 0, delta, }, ], }; } createToolCallChunk(state, argumentsString, context) { const delta = { tool_calls: [ { index: state.index, id: state.openaiId, type: 'function', function: { name: state.name || 'tool', arguments: argumentsString, }, }, ], }; if (!context.started) { delta.role = 'assistant'; context.started = true; } context.finishReason = 'tool_calls'; return { id: context.responseId, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: context.model, choices: [ { index: 0, delta, }, ], }; } createFinalChunk(context) { const usage = context.usage ? { prompt_tokens: context.usage.input_tokens ?? 0, completion_tokens: context.usage.output_tokens ?? 0, total_tokens: context.usage.total_tokens ?? (context.usage.input_tokens ?? 0) + (context.usage.output_tokens ?? 0), } : undefined; return { id: context.responseId, object: 'chat.completion.chunk', created: context.created, model: context.model, choices: [ { index: 0, delta: {}, finish_reason: context.finishReason || 'stop', }, ], usage, }; } normalizeArguments(argumentsBuffer) { const trimmed = argumentsBuffer.trim(); if (!trimmed) { return '{}'; } try { const parsed = JSON.parse(trimmed); return JSON.stringify(parsed); } catch { return trimmed; } } buildUrl(baseUrl, path) { const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; return new URL(path, normalizedBase); } }; exports.ClaudeCodeProvider = ClaudeCodeProvider; exports.ClaudeCodeProvider = ClaudeCodeProvider = ClaudeCodeProvider_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [config_1.ConfigService, request_transformer_1.RequestTransformer, response_transformer_1.ResponseTransformer, tokenizer_service_1.TokenizerService, ToolCallProcessor_1.ToolCallProcessor]) ], ClaudeCodeProvider); //# sourceMappingURL=claude-code.provider.js.map