Skip to content

fix(amp): 修复Amp CLI 集成 缺失/无效 signature 导致的 TUI 崩溃与上游 400 问题#2403

Merged
luispater merged 2 commits intorouter-for-me:devfrom
CharTyr:clean-pr
Mar 30, 2026
Merged

fix(amp): 修复Amp CLI 集成 缺失/无效 signature 导致的 TUI 崩溃与上游 400 问题#2403
luispater merged 2 commits intorouter-for-me:devfrom
CharTyr:clean-pr

Conversation

@CharTyr
Copy link
Copy Markdown
Contributor

@CharTyr CharTyr commented Mar 30, 2026

变更说明

本 PR 修复了 Amp 相关请求/响应里 signature 缺失或无效时导致的两个问题:

  1. Amp TUI 渲染崩溃

    • 当响应中的 tool_use / thinking / streaming content_block 缺少 signature 字段时,Amp TUI 可能触发 P.signature.length 异常,表现为消息消失或界面无法正常渲染。
  2. 上游 API 400

    • 当历史消息中的 thinking block 带有空、空白或非字符串的 signature 时,请求转发到上游后可能被拒绝,返回 400 invalid_request_error

具体修改

1. 响应侧兼容处理

  • 为普通响应中的 tool_use / thinking block 补齐缺失的 signature
  • 为 streaming SSE 响应中的 content_block 补齐缺失的 signature
  • 当检测到 tool_use 时,抑制普通响应中的 thinking
  • 补充对 streaming content_block 形态 的处理:
    • 抑制 thinkingcontent_block_start
    • 抑制 thinking_delta
    • 抑制对应的 content_block_stop
    • 保留后续 tool_use block,避免 Amp TUI 在流式场景下继续触发兼容性问题

2. 请求侧清理

  • 在转发请求前清理 assistant 消息中的无效 thinking block
  • 以下情况都会被视为无效并移除:
    • signature 缺失
    • signaturenull
    • signature 为空字符串
    • signature 仅包含空白字符
    • signature 不是字符串类型

影响范围

仅影响 Amp 模块中的请求/响应改写逻辑,不改变其他 provider 的正常行为。
目标是增强对 Amp 客户端的兼容性,避免因历史脏数据或流式响应结构导致 UI 崩溃和上游拒绝请求。

验证情况

已验证以下定向测试通过:

  • go test ./internal/api/modules/amp -run "TestRewriteStreamChunk_SuppressesThinkingContentBlockFrames|TestSanitizeAmpRequestBody_RemovesWhitespaceAndNonStringSignatures" -count=1
  • go test ./internal/api/modules/amp -run TestRewriteStreamChunk_MultipleEvents -count=1

另外,先前已验证:

  • go build -o /tmp/cliproxyapi-plus-test ./cmd/server

背景

该修复主要针对以下现象:

  • Amp TUI 中回复显示后又消失
  • 日志中出现 P.signature.length
  • 请求上游时出现 400 invalid_request_error
  • 流式返回中同时出现 thinkingtool_use 时,Amp 客户端兼容性较差

Copy link
Copy Markdown
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

This pull request enhances the AMP module by implementing request sanitization and improving response rewriting logic. Key changes include the addition of SanitizeAmpRequestBody to remove thinking blocks with invalid signatures from upstream requests and updates to ResponseRewriter to inject required signatures and suppress thinking blocks for TUI compatibility. The rewriter now also supports dynamic transitions to streaming mode based on content type or buffer limits. Review feedback identifies that the streaming response path is currently missing the logic to suppress thinking blocks and provides a suggestion for more robust test initialization.

Comment on lines +301 to +315
func (rw *ResponseRewriter) rewriteStreamEvent(data []byte) []byte {
// Inject empty signature where needed
data = ensureAmpSignature(data)

// Rewrite model name
if rw.originalModel != "" {
for _, path := range modelFieldPaths {
if gjson.GetBytes(data, path).Exists() {
data, _ = sjson.SetBytes(data, path, rw.originalModel)
}
}
}

return bytes.Join(lines, []byte("\n"))
return data
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The streaming response path seems to be missing the logic to suppress thinking blocks. The function suppressAmpThinking has been implemented to handle this for streaming events (like content_block_start, thinking_delta, etc.) but it's not being called from rewriteStreamEvent.

This is likely an oversight from the refactoring where rewriteStreamChunk was changed to call rewriteStreamEvent instead of rewriteModelInResponse. Without this, streaming thinking blocks won't be suppressed, which is one of the main goals of this PR.

You should call suppressAmpThinking here. Note that suppressAmpThinking can return nil for suppressed events, so the calling function rewriteStreamChunk will also need a small change to handle this and avoid writing an empty data: line.

func (rw *ResponseRewriter) rewriteStreamEvent(data []byte) []byte {
	// Suppress thinking blocks before any other processing.
	data = rw.suppressAmpThinking(data)
	if len(data) == 0 {
		return nil
	}

	// Inject empty signature where needed
	data = ensureAmpSignature(data)

	// Rewrite model name
	if rw.originalModel != "" {
		for _, path := range modelFieldPaths {
			if gjson.GetBytes(data, path).Exists() {
				data, _ = sjson.SetBytes(data, path, rw.originalModel)
			}
		}
	}

	return data
}

}

func TestRewriteStreamChunk_SuppressesThinkingContentBlockFrames(t *testing.T) {
rw := &ResponseRewriter{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

It's good practice to initialize the ResponseRewriter fully for tests, similar to how NewResponseRewriter does it. While the implementation currently handles a nil map for suppressedContentBlock with a nil check, explicitly initializing it makes the test setup more robust and less dependent on that implementation detail.

Suggested change
rw := &ResponseRewriter{}
rw := &ResponseRewriter{suppressedContentBlock: make(map[int]struct{})}

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04ba8c8bc3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +301 to +304
func (rw *ResponseRewriter) rewriteStreamEvent(data []byte) []byte {
// Inject empty signature where needed
data = ensureAmpSignature(data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Route streaming events through thinking suppression

suppressAmpThinking now contains the logic that drops streamed content_block_start/thinking_delta/content_block_stop frames, but rewriteStreamChunk sends SSE JSON to rewriteStreamEvent, and this function never calls that suppression path. In practice, any streamed response that includes thinking frames will still emit them, so the Amp client compatibility issue this commit targets remains in the streaming path.

Useful? React with 👍 / 👎.

Comment on lines +39 to +40
return bytes.Contains(data, []byte("data:")) ||
bytes.Contains(data, []byte("event:")) ||
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Narrow SSE detection to actual frame prefixes

The SSE heuristic treats any chunk containing substrings like "data:" or "event:" as streaming, even for normal JSON bodies. When that happens, Write switches to streaming mode and bypasses rewriteModelInResponse, so model/signature rewrites are skipped unless lines literally start with data: . A non-streaming JSON response containing text such as "...data: ..." is enough to trigger this regression.

Useful? React with 👍 / 👎.

…ion, test init

- Call suppressAmpThinking in rewriteStreamEvent for streaming path
- Handle nil return from suppressAmpThinking to skip suppressed events
- Narrow looksLikeSSEChunk to line-prefix detection (HasPrefix vs Contains)
- Initialize suppressedContentBlock map in test
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b15453c369

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +128 to 131
bodyBytes = SanitizeAmpRequestBody(bodyBytes)

// Restore the body for the handler to read
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recompute request length after sanitizing body

When SanitizeAmpRequestBody removes invalid thinking blocks, bodyBytes can become shorter, but the request length metadata is never updated before the proxy path runs. In the len(providers)==0 branch, proxy.ServeHTTP forwards c.Request with the stale Content-Length/ContentLength, which can cause outbound write failures (body shorter than declared length) and return gateway errors instead of reaching Amp upstream. After replacing the body, also update or clear the length fields.

Useful? React with 👍 / 👎.

@luispater luispater changed the base branch from main to dev March 30, 2026 04:53
@luispater luispater merged commit d267522 into router-for-me:dev Mar 30, 2026
2 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