Skip to content

fix: 修复 Gemini 和 OpenAI Chat Completions 流式响应的 usage 解析问题#153

Merged
ding113 merged 9 commits intoding113:devfrom
sususu98:fix/gemini
Nov 21, 2025
Merged

fix: 修复 Gemini 和 OpenAI Chat Completions 流式响应的 usage 解析问题#153
ding113 merged 9 commits intoding113:devfrom
sususu98:fix/gemini

Conversation

@sususu98
Copy link
Contributor

问题描述

Gemini 流式响应 (:streamGenerateContent) 和 OpenAI Chat Completions 流式响应返回的 SSE 格式只有 data: 行,没有 event: 前缀,导致 parseSSEData() 函数无法解析 usageMetadata/usage 字段,从而无法计费。

影响范围

  • Gemini API (/v1beta/models/xxx:streamGenerateContent) - 无计费
  • OpenAI Chat Completions (/v1/chat/completions) 流式 - usage 可能丢失
  • Claude Messages API (/v1/messages) - 正常(有 event: 前缀)
  • OpenAI Response API (/v1/responses) - 正常(有 event: 前缀)

根本原因

  1. SSE 格式差异

    • Claude/Response API: event: xxx\ndata: {...} (标准 SSE 格式)
    • Gemini/Chat Completions: data: {...} (简化格式,无 event 行)
  2. 原有逻辑缺陷

    • parseSSEData() 要求必须有 eventName,否则直接跳过解析
    • response-handler.ts 只在检测到 includes("event:") 时才解析 SSE
    • 导致 Gemini 和 Chat Completions 的流式响应无法提取 usage

修复方案

1. src/lib/utils/sse.ts

const flushEvent = () => {
- if (!eventName || dataLines.length === 0) {
+ // 修改:支持没有 event: 前缀的纯 data: 格式(Gemini 流式响应)
+ // 如果没有 eventName,使用默认值 "message"
+ if (dataLines.length === 0) {
    return;
  }

  try {
    const data = JSON.parse(dataStr);
-   events.push({ event: eventName, data });
+   events.push({ event: eventName || "message", data });
  } catch {
-   events.push({ event: eventName, data: dataStr });
+   events.push({ event: eventName || "message", data: dataStr });
  }
};

改动说明

  • 移除 !eventName 的检查条件
  • 使用默认事件名 "message" 替代空 eventName(符合 SSE 规范)
  • 支持没有 event: 前缀的纯 data: 格式

2. src/app/v1/_lib/proxy/response-handler.ts

+ // SSE 解析:支持两种格式
+ // 1. 标准 SSE (event: + data:) - Claude/OpenAI
+ // 2. 纯 data: 格式 - Gemini
- if (!usageMetrics && responseText.includes("event:")) {
+ if (!usageMetrics && responseText.includes("data:")) {
    const events = parseSSEData(responseText);
    // ... 后续解析逻辑
  }

改动说明

  • 将触发条件从 includes("event:") 改为 includes("data:")
  • 扩大 SSE 解析范围,支持所有4个接口格式

技术细节

SSE 规范兼容性

根据 W3C SSE 规范

  • SSE 标准允许省略 event:
  • 默认事件名为 "message"
  • 修改后的代码符合标准规范

向后兼容性

完全兼容,所有现有格式继续正常工作:

接口 格式 状态
Claude Messages API event: xxx\ndata: {...} ✅ 继续正常
OpenAI Response API event: xxx\ndata: {...} ✅ 继续正常
Gemini API data: {...} 修复成功
OpenAI Chat Completions data: {...} 改进 usage 提取

风险评估 (Codex Review)

已通过 Codex MCP 代码审查,评估结果:

  • 无向后兼容性问题 - 现有 Claude/OpenAI 格式不受影响
  • 误匹配风险很小 - 后续有 typeof event.data === "object" 类型检查
  • 性能影响可忽略 - SSE 解析是线性扫描,开销很小
  • 边界情况处理正确 - data: [DONE] 等文本会被正确忽略

详细 Review 结果:

parseSSEData 去掉 eventName 必填并默认成 "message",符合 SSE 规范的默认事件名,对 Anthropic(有显式 event:)兼容不变,对 OpenAI/Gemini 这类默认事件名的流式响应反而更易解析 usage,未看到向后兼容风险。下游只在日志字符串里用 event,不会受默认值影响业务逻辑。

测试验证

  • ✅ TypeScript 编译通过 (bun run typecheck)
  • ✅ 代码格式化完成 (bun run format)
  • Gemini 流式响应计费验证通过 - 生产环境测试成功
  • ✅ Codex 代码审查通过

生产环境验证

部署后测试结果:

-- Gemini 流式响应(修复前)
SELECT model, endpoint, input_tokens, output_tokens, cost_usd
FROM message_request
WHERE model = 'gemini-3-pro-preview' 
  AND endpoint LIKE '%:streamGenerateContent'
  AND created_at < '2025-11-20 13:54:00';
-- 结果:input_tokens = NULL, output_tokens = NULL, cost_usd = 0 ❌

-- Gemini 流式响应(修复后)
SELECT model, endpoint, input_tokens, output_tokens, cost_usd
FROM message_request
WHERE model = 'gemini-3-pro-preview'
  AND endpoint LIKE '%:streamGenerateContent' 
  AND created_at > '2025-11-20 13:54:00';
-- 结果:input_tokens = 476, output_tokens = 1345, cost_usd > 0 ✅

Checklist

  • 代码已格式化
  • TypeScript 编译通过
  • Codex 代码审查通过
  • 生产环境验证通过
  • 文档已更新(commit message)
  • 向后兼容性已验证

🤖 Generated with Claude Code

ding113 and others added 9 commits November 9, 2025 19:07
feat(errors): 扩展不可重试的客户端错误定义和模式
## 问题描述

当用户使用 Claude CLI 请求 /v1/messages 端点(Claude Messages API 格式),
但请求的模型不是 claude-* 开头(如 gemini-3-pro-preview)时,系统会错误
地选择 provider_type: "codex" 的供应商,导致请求格式与供应商类型不匹配。

根本原因:系统根据**模型名称**(是否以 `claude-` 开头)判断目标供应商类型,
而不是根据**请求格式**(session.originalFormat)。

## 修复方案

### 1. 新增格式兼容性检查函数
- 新增 `checkFormatProviderTypeCompatibility()` 辅助函数
- 根据 ClientFormat 和 ProviderType 判断兼容性
- 映射关系:
  * claude → claude | claude-auth
  * response → codex
  * openai → openai-compatible
  * gemini → gemini
  * gemini-cli → gemini-cli

### 2. 修改供应商筛选逻辑
- 在模型匹配检查**之前**增加格式类型匹配检查(Step 1b)
- 只选择与请求格式兼容的供应商类型
- 向后兼容:如果 session.originalFormat 未设置,跳过此检查

### 3. 优化 targetType 计算
- 将 decisionContext.targetType 的判断从基于模型名称改为基于 session.originalFormat
- 修复:不再使用 `requestedModel.startsWith("claude-")` 判断

### 4. 增加过滤原因
- 新增 `format_type_mismatch` 过滤原因
- 记录格式不兼容的供应商及详细原因

### 5. 扩展类型定义
- 扩展 decisionContext.targetType 支持所有供应商类型
- 更新过滤原因枚举

## 影响范围

- ✅ 修复了 Claude 格式请求非 Claude 模型时的格式错配问题
- ✅ 支持 claude 类型供应商通过 allowedModels 或 model_redirects 处理非 Claude 模型
- ✅ 向后兼容:不影响现有的正常请求

## 测试验证

- ✅ TypeScript 类型检查通过
- ✅ 代码格式化通过
- ✅ 数据库查询验证:ccr 供应商(provider_type: "claude")成功处理 gemini-3-pro-preview 请求

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
## 问题描述

Gemini 流式响应 (`:streamGenerateContent`) 和 OpenAI Chat Completions
流式响应返回的 SSE 格式只有 `data:` 行,没有 `event:` 前缀,导致
`parseSSEData()` 函数无法解析 `usageMetadata`/`usage` 字段,从而无法计费。

**影响范围**:
- ❌ Gemini API (`/v1beta/models/xxx:streamGenerateContent`) - 无计费
- ❌ OpenAI Chat Completions (`/v1/chat/completions`) 流式 - usage 可能丢失
- ✅ Claude Messages API (`/v1/messages`) - 正常(有 `event:` 前缀)
- ✅ OpenAI Response API (`/v1/responses`) - 正常(有 `event:` 前缀)

## 根本原因

1. **SSE 格式差异**:
   - Claude/Response API: `event: xxx\ndata: {...}` (标准 SSE)
   - Gemini/Chat Completions: `data: {...}` (简化格式,无 event 行)

2. **原有逻辑缺陷**:
   - `parseSSEData()` 要求必须有 `eventName`,否则直接跳过
   - `response-handler.ts` 只在检测到 `includes("event:")` 时才解析 SSE
   - 导致 Gemini 和 Chat Completions 的流式响应无法提取 usage

## 修复方案

### 1. `src/lib/utils/sse.ts`
- 移除 `!eventName` 的检查条件
- 使用默认事件名 `"message"` 替代空 eventName(符合 SSE 规范)
- 支持没有 `event:` 前缀的纯 `data:` 格式

### 2. `src/app/v1/_lib/proxy/response-handler.ts`
- 将触发条件从 `includes("event:")` 改为 `includes("data:")`
- 扩大 SSE 解析范围,支持所有格式

## 技术细节

**SSE 规范兼容性**:
- SSE 标准允许省略 `event:` 行,默认事件名为 `"message"`
- 修改后的代码符合 W3C SSE 规范

**向后兼容性**:
- ✅ Claude Messages API (有 event:) - 继续正常工作
- ✅ OpenAI Response API (有 event:) - 继续正常工作
- ✅ Gemini API (无 event:) - 修复成功,现在能计费
- ✅ OpenAI Chat Completions (无 event:) - 改进 usage 提取

**风险评估** (Codex Review):
- ✅ 无向后兼容性问题
- ✅ 误匹配风险很小(后续有类型检查)
- ✅ 性能影响可忽略
- ✅ 边界情况处理正确

## 测试验证

- ✅ TypeScript 编译通过
- ✅ 代码格式化完成
- ✅ Gemini 流式响应计费验证通过
- ✅ Codex 代码审查通过

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @sususu98, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求主要旨在解决 Gemini 和 OpenAI Chat Completions 流式响应中 usage 字段解析不准确的问题,这直接影响了计费的准确性。通过优化 SSE 数据解析逻辑和调整响应处理器的触发条件,现在系统能够更健壮地处理不同格式的流式数据,确保所有支持的 API 都能正确提取使用量信息。此外,还增强了供应商选择机制,使其能根据原始请求格式更智能地匹配兼容的供应商,提升了系统的稳定性和灵活性。

Highlights

  • 修复 Gemini/OpenAI 流式响应计费问题: 解决了 Gemini 和 OpenAI Chat Completions 流式响应中 usage 字段无法正确解析导致计费缺失的问题。
  • 优化 SSE 数据解析逻辑: 调整了 parseSSEData 函数,使其能够正确处理只有 data: 行而没有 event: 前缀的 SSE 格式,并使用默认事件名 "message"。
  • 改进响应处理器触发条件: 将 response-handler.ts 中 SSE 解析的触发条件从 includes("event:") 扩展到 includes("data:"),以覆盖更多流式响应场景。
  • 增强供应商选择兼容性: 引入了根据原始请求格式限制供应商类型的功能,确保客户端请求格式与供应商类型兼容,避免格式错配。
  • 更新类型定义和决策上下文: 扩展了 ProviderChainItemtargetTypereason 枚举,以支持新的供应商类型和格式不匹配的排除原因。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 很好地解决了 Gemini 和 OpenAI 流式响应中因缺少 event: 前缀导致的 usage 解析失败问题。修改 sse.ts 以兼容无 event: 的 SSE 格式,并调整 response-handler.ts 的触发条件,从根源上修复了计费统计的 bug,并且符合 SSE 规范。

此外,相关的 provider-selector.ts 和类型定义文件的修改,虽然超出了 bug 修复的范畴,但很好地增强了系统的可扩展性,为支持更多类型的供应商(如 Gemini)打下了坚实的基础。这些架构上的改进是值得称赞的。

代码整体质量很高,考虑了向后兼容和各种边界情况。我只有一个关于代码简化的建议。

@ding113 ding113 merged commit 08503e8 into ding113:dev Nov 21, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments