From 28efa1367b54dfdf17660610c4f7ad82774c06ea Mon Sep 17 00:00:00 2001 From: dreamhunter2333 Date: Wed, 17 Dec 2025 18:53:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=A2=91=E9=81=93?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E5=92=8C=20Claude=20API=20streaming=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 频道多选下拉框修复: - 将点击事件改为 checkbox onchange 事件 - 新增 handleChannelCheckboxChange 函数 - 避免手动切换导致状态重复切换的问题 2. Claude API 支持完善: - 删除客户端的 Authorization header 避免冲突 - 支持 message_complete 事件(Claude 最新格式) - 保留 message_stop 处理作为向后兼容 - 正确提取 streaming 和 non-streaming 的 token usage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/providers/claude-proxy.ts | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/providers/claude-proxy.ts b/src/providers/claude-proxy.ts index 31890f5..775fd72 100644 --- a/src/providers/claude-proxy.ts +++ b/src/providers/claude-proxy.ts @@ -24,7 +24,7 @@ type ClaudeMessageResponse = { type ClaudeStreamEvent = { type: "message_start" | "content_block_start" | "content_block_delta" | - "content_block_stop" | "message_delta" | "message_stop" | "ping" | "error"; + "content_block_stop" | "message_delta" | "message_stop" | "message_complete" | "ping" | "error"; message?: { usage: { input_tokens: number; @@ -36,15 +36,17 @@ type ClaudeStreamEvent = { stop_sequence?: string | null; }; usage?: { + input_tokens?: number; output_tokens: number; }; } /** * Builds the proxy request for Claude API + * - Removes Authorization header (client may send OpenAI format) * - Sets x-api-key header (Claude uses this instead of Authorization) * - Sets anthropic-version header (required by Claude API) - * - Forces stream: true for usage tracking (similar to OpenAI pattern) + * - Preserves all other headers from original request */ const buildProxyRequest = ( request: Request, @@ -59,6 +61,9 @@ const buildProxyRequest = ( const targetHeaders = new Headers(request.headers) + // Remove Authorization header (client may send OpenAI format: "Authorization: Bearer sk-xxx") + targetHeaders.delete("Authorization") + // Claude uses x-api-key header instead of Authorization targetHeaders.set("x-api-key", config.api_key) @@ -110,12 +115,17 @@ const checkoutUsageData = async ( * * Claude SSE format: * event: message_start - * data: {"type": "message_start", "message": {..., "usage": {"input_tokens": 26, "output_tokens": 0}}} + * data: {"type": "message_start", "message": {..., "usage": {"input_tokens": 20, "output_tokens": 1}}} * * event: message_delta - * data: {"type": "message_delta", "delta": {...}, "usage": {"output_tokens": 40}} + * data: {"type": "message_delta", "delta": {...}, "usage": {"output_tokens": 50}} * - * We accumulate input_tokens from message_start and output_tokens from message_delta + * event: message_complete + * data: {"type": "message_complete", "message": {..., "usage": {"input_tokens": 20, "output_tokens": 50}}} + * + * We can either: + * 1. Accumulate input_tokens from message_start and output_tokens from message_delta + * 2. Get complete usage from message_complete event (preferred as it's more reliable) */ const processStreamData = async ( lines: string[], @@ -147,7 +157,17 @@ const processStreamData = async ( usageAccumulator.output_tokens = event.usage.output_tokens } - // When stream ends, save the accumulated usage + // Handle message_complete event (has complete usage info) + if (event.type === "message_complete" && event.message?.usage) { + const usage: Usage = { + prompt_tokens: event.message.usage.input_tokens, + completion_tokens: event.message.usage.output_tokens, + total_tokens: event.message.usage.input_tokens + event.message.usage.output_tokens, + } + await saveUsage(usage) + } + + // When stream ends with message_stop (fallback for old API versions) if (event.type === "message_stop") { const usage: Usage = { prompt_tokens: usageAccumulator.input_tokens,