Skip to content

fix(gemini): preserve thinking across relay formats - #1

Closed
karlorz wants to merge 27 commits into
mainfrom
release/responses-websocket-v0.13
Closed

fix(gemini): preserve thinking across relay formats#1
karlorz wants to merge 27 commits into
mainfrom
release/responses-websocket-v0.13

Conversation

@karlorz

@karlorz karlorz commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Test plan

  • go test ./dto ./service ./relay/channel/gemini -count=1 (all pass)
  • go build ./relay/channel/gemini/

KoSukeWork and others added 27 commits August 2, 2026 02:43
Audit follow-up on the WebSocket relay port:

- Billing: split call teardown into aborted vs settled. A client
  disconnect, upstream failure or terminal error event now settles on
  the observed usage (terminal-event usage, or output text counted via
  CountTextToken with the pre-consume prompt estimate) instead of
  refunding everything; refunds remain only when upstream never
  produced anything. Settlement bills a locked snapshot of usage so a
  late terminal event cannot race the reader.
- Validation: response.create frames now run the shared
  helper.ValidateResponsesRequest (model + max_output_tokens bound),
  closing the WebSocket bypass of the billing-multiplier limits. The
  input-required check stays HTTP-only since incremental WS turns may
  omit input.
- Token estimate: dto Input gains the function_call_output 'output' field
  so tool-result turns are counted and sensitive-checked instead
  of estimated at zero.
- Robustness: 30s write deadlines on both legs; target state lock
  split from the write lock so a stuck write cannot block close paths;
  SetReadLimit (WEBSOCKET_MAX_MESSAGE_MB, default 16MB) on client and
  upstream; per-user concurrent session cap
  (RESPONSES_WEBSOCKET_MAX_PER_USER, default 8).
- Cross-goroutine client read-deadline refresh kept, with the gorilla
  v1.5.3 passthrough justification documented in place.
- Dial errors sanitize the upstream URL via SanitizeURLForLog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit eb76195)
(cherry picked from commit 5decd88)
Resolve the read limit lazily so it follows constant.MaxRequestBodyMB
(populated by InitEnv after package init) unless WEBSOCKET_MAX_MESSAGE_MB
overrides it, and document why permessage-deflate stays disabled: gorilla
enforces SetReadLimit against the compressed wire length, so enabling it
would reopen the decompression-bomb hole on the WebSocket side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit d6942d6)
(cherry picked from commit 76bf0f0)
The stream field is stripped from response.create frames before parsing,
so GenRelayInfoResponses recorded WebSocket calls as non-stream, which
also hid their first-response time in the usage-log UI (the timing cell
only shows it for stream calls). Set IsStream explicitly like the
realtime relay does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 954cb82)
(cherry picked from commit b07e801)
问题:
在非 passThrough 模式下,客户端发送的 reasoning_content: "" 经过
Go struct 反序列化再序列化后,因 string + omitempty 无法区分空串和
字段缺失,导致空的思考内容被静默丢弃。

根因:
dto.Message.ReasoningContent 和 Message.Reasoning 使用 string(非指针)
加 omitempty,违反 AGENTS.md Rule 6(可选标量字段必须用指针类型)。

修复:
1. Message.ReasoningContent/Reasoning 类型从 string 改为 *string
   - nil = 字段缺失 → JSON 省略
   - &"" = 显式空串 → JSON 保留 reasoning_content: ""
2. 新增 Message.GetReasoningContent() 辅助方法
3. 更新所有读写处:relay-openai, relay-claude, relay-gemini, ollama
4. 新增测试覆盖空串保留、字段省略、getter 回退逻辑

(cherry picked from commit 8ca1033)
@karlorz karlorz closed this Aug 15, 2026
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.

5 participants