Skip to content

fix(transformer): preserve item_reference for function_call_output in OpenAI Responses - #106

Merged
Hureru merged 2 commits into
devfrom
fix-responses-item-reference
Jun 29, 2026
Merged

fix(transformer): preserve item_reference for function_call_output in OpenAI Responses#106
Hureru merged 2 commits into
devfrom
fix-responses-item-reference

Conversation

@Hureru

@Hureru Hureru commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Fixes #101

Problem

When Chat Completions clients (codex, cherrystudio) call OpenAI Responses upstream channels with tool calls, requests fail with:

function_call_output requires item_reference ids matching each call_id on HTTP requests

Root Cause

The chat→responses protocol conversion was missing two required fields:

  • function_call items lack unique id field
  • function_call_output items lack item_reference field (must point to corresponding function_call's id)

Changes

  1. Add ItemReference field to ResponsesItem struct (both inbound and outbound)
  2. Generate unique IDs for function_call items using crypto/rand (thread-safe)
  3. Build call_id → item_id mapping in convertInputFromMessages
  4. Set ItemReference in convertToolMessageToResponses
  5. Enhanced sanitizeResponsesRawItems to auto-populate missing/null/empty item_reference
  6. Add fallback using atomic counter if crypto/rand fails

Security Fixes (from code review)

  • Replaced LCG PRNG with crypto/rand to avoid data race on global state
  • Handle null and empty string item_reference values in sanitizer
  • Ensure inbound struct preserves item_reference in typed passthrough

Testing

  • ✅ All existing tests pass
  • ✅ 4 new tests added covering basic functionality, null values, empty strings, and end-to-end
  • -race detector validation passed

Files Changed

  • internal/transformer/outbound/openai/response.go
  • internal/transformer/inbound/openai/response.go
  • internal/transformer/outbound/openai/response_function_call_test.go (new)

Summary by CodeRabbit

  • New Features

    • Enhanced OpenAI response item handling so function_call_output items keep a link back to their originating function_call.
    • This reference is preserved through request building and JSON serialization.
  • Bug Fixes

    • Automatically fills in missing function_call IDs and backfills/corrects item_reference values when they’re absent, empty, or null.
  • Tests

    • Added unit tests covering reference generation, backfilling/correction behavior, and end-to-end preservation.

… OpenAI Responses

Fixes #101, #103

Root cause:
When Chat Completions clients (codex, cherrystudio) call OpenAI Responses upstream
channels with tool calls, the chat→responses conversion loses critical fields:
- function_call items lack unique IDs
- function_call_output items lack item_reference (required by Responses HTTP API)

The upstream rejects with:
"function_call_output requires item_reference ids matching each call_id on HTTP requests"

Changes:
1. Add ItemReference field to ResponsesItem struct (both inbound and outbound)
2. Generate unique IDs for function_call items using crypto/rand (thread-safe)
3. Build call_id → item_id mapping in convertInputFromMessages
4. Set ItemReference in convertToolMessageToResponses using the mapping
5. Enhanced sanitizeResponsesRawItems to auto-populate missing/null/empty item_reference
6. Add fallback using atomic counter if crypto/rand fails
7. Add 4 tests including null/empty value handling

Security fixes (from code review):
- Replaced LCG PRNG with crypto/rand to avoid data race on global state
- Handle null and empty string item_reference values in sanitizer
- Ensure inbound struct preserves item_reference in typed passthrough

Coverage:
- Fixes chat→responses conversion for tool calls (#101, #103)
- Preserves item_reference in raw items passthrough (native Responses clients)
- Thread-safe with -race detector validation
- All existing tests pass + 4 new tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c63b8ac-990a-44f1-809e-fdab90dd2f6e

📥 Commits

Reviewing files that changed from the base of the PR and between b1e7e3e and 59dc6ba.

📒 Files selected for processing (2)
  • internal/transformer/outbound/openai/response.go
  • internal/transformer/outbound/openai/response_function_call_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/transformer/outbound/openai/response.go

📝 Walkthrough

Walkthrough

Adds item_reference to OpenAI Responses items in inbound and outbound transformers. Generates IDs for function-call items, links tool outputs back to their source calls during conversion, backfills missing references during sanitization, and adds tests for the full linkage path.

Changes

function_call_output item_reference linkage

Layer / File(s) Summary
ResponsesItem struct extensions
internal/transformer/inbound/openai/response.go, internal/transformer/outbound/openai/response.go
Adds optional ItemReference *string to ResponsesItem in both transformer packages; outbound also keeps Output *ResponsesInput in the same struct block.
Item ID generation utility
internal/transformer/outbound/openai/response.go
Adds generateResponsesItemID() plus imports for crypto/rand, sync/atomic, and time, with a timestamp-plus-counter fallback when randomness fails.
Message conversion mapping
internal/transformer/outbound/openai/response.go
Builds a call_id -> item_id map in convertInputFromMessages, assigns generated IDs to function_call items, passes the map into tool conversion, and sets item_reference on function_call_output items.
Raw payload sanitization
internal/transformer/outbound/openai/response.go
Updates sanitizeResponsesRawItems to derive IDs from raw function_call items and backfill missing or empty item_reference values on raw function_call_output items.
Unit tests
internal/transformer/outbound/openai/response_function_call_test.go
Adds five tests covering generated IDs, item-reference backfill, null/empty correction, missing function-call ID handling, and end-to-end marshal preservation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A function called out with a twinkly name,
Its output replied with a linked little frame.
From rand and a counter, new IDs took flight,
And references snapped into place just right. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preserving item_reference for function_call_output in OpenAI Responses.
Linked Issues check ✅ Passed The changes address the reported Chat Completions→Responses tool-call failure by adding item_reference handling and matching references for function_call_output.
Out of Scope Changes check ✅ Passed The added ID generation, sanitization, and tests are all directly tied to fixing item_reference handling.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/transformer/outbound/openai/response_function_call_test.go`:
- Around line 174-186: The test for MarshalResponsesInputItems is only checking
that function_call_output contains an item_reference field, which can miss a
wrong linkage. Tighten the assertions in response_function_call_test by
capturing both the function_call and function_call_output entries from the
marshaled items, then compare item_reference directly against the originating
function_call.id. Use the existing MarshalResponsesInputItems flow and the
item["type"] handling to locate the right assertions, and verify the IDs match
exactly rather than just checking presence.

In `@internal/transformer/outbound/openai/response.go`:
- Around line 1688-1718: The backfill logic in the function that builds the
call_id-to-item_id map and sanitizes function_call_output items is skipping raw
function_call entries that have a call_id but no id, which leaves item_reference
unset later. Update the mapping step to generate or assign an id for raw
function_call items before populating callIDToItemID, so the later
function_call_output backfill can always resolve item_reference correctly. Keep
the fix localized around the existing item iteration logic and the
callIDToItemID lookup used by the sanitization pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b57420a1-5c94-4be0-ace2-e5a9ce46b020

📥 Commits

Reviewing files that changed from the base of the PR and between 5bcb02d and b1e7e3e.

📒 Files selected for processing (3)
  • internal/transformer/inbound/openai/response.go
  • internal/transformer/outbound/openai/response.go
  • internal/transformer/outbound/openai/response_function_call_test.go

Comment thread internal/transformer/outbound/openai/response_function_call_test.go Outdated
Comment thread internal/transformer/outbound/openai/response.go Outdated
…and tighten item_reference tests

sanitizeResponsesRawItems now generates an id for raw function_call entries
that have call_id but no id, so the function_call_output backfill always
resolves item_reference. Also strengthens
TestMarshalResponsesInputItemsPreservesItemReference to assert the reference
matches the originating function_call.id rather than just checking presence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Hureru
Hureru merged commit 2bf7f98 into dev Jun 29, 2026
4 checks passed
@Hureru
Hureru deleted the fix-responses-item-reference branch June 29, 2026 18:26
Hureru added a commit that referenced this pull request Jun 30, 2026
…#92) (#107)

* feat(relay): implement HTTP replay mechanism for OpenAI Responses API (#92)

实现本地 HTTP replay 机制,解决上游中转站拒绝 previous_response_id 的兼容性问题。

## 背景
当 OpenAI Responses HTTP 请求携带 previous_response_id 时,部分上游中转站返回:
  previous_response_id is only supported on Responses WebSocket v2
同时这些上游的 WebSocket 升级路径不可用(426/101 handshake 失败)。

## 实现
1. **本地状态存储** (responses_replay_store.go)
   - 使用 sync.Map 作为进程内存储,key 格式:apiKeyID:groupID:requestModel:hash(responseID)
   - 每次 HTTP /v1/responses 成功后保存 replay 状态(渠道、key、replay window)
   - 支持 TTL 自动过期,默认继承 group.SessionKeepTime

2. **请求转换** (relay.go)
   - 检测到 previous_response_id 时,从本地加载上一次状态
   - 调用 BuildReplayRequest 转为自包含形式(合并历史 + 移除 previous_response_id)
   - 优先复用上一次成功的渠道/key(通过 NewIteratorWithPreference)

3. **可观测性**
   - 记录 ws_mode=replay 和 ws_recovery=replay 便于排障
   - Debug 日志记录状态加载、转换、路由、保存全流程

## 兼容性
- 与 PR #106 的 item_reference 修复兼容(MarshalResponsesInputItems 现在正确生成 id 和 item_reference)
- 复用现有 wsConversationState 和 replay 逻辑,无需数据库迁移
- 不影响 WebSocket 原生续接和 passthrough 模式

## 测试覆盖
- 单元测试:状态存储、加载、TTL、隔离性
- 集成测试:完整 replay 流程、流式请求、不同 group 隔离、tool calls

* fix(relay): address critical HTTP replay issues from code review

修复代码审查中发现的关键问题:

## High Priority 修复

1. **多轮 replay 状态持久化**
   - 移除 !IsOpenAIExactReplayRequest() 的保存限制
   - exact replay 成功后继续保存新状态(基于已有状态累积)
   - 支持连续多轮 HTTP replay (resp1 -> resp2 -> resp3)

2. **容量控制与主动清理**
   - 增加最大条目数限制 (10,000)
   - 增加最大内存限制 (100MB)
   - 后台定时清理过期条目 (5分钟间隔)
   - 状态大小估算与容量检查
   - 增加统计指标 (entries, totalSize)

3. **历史合并失败回退**
   - 验证 BuildReplayRequest 是否成功生成 RawInputItems
   - 合并失败时保留原始 previous_response_id
   - 放弃本地 replay,允许回退到原生续接
   - 增加 warn 日志记录失败原因

## Medium Priority 改进

4. **增强错误日志**
   - GetInternalResponse 失败时记录 debug 日志
   - 历史合并失败记录 warn 日志
   - 容量超限记录 warn 日志

5. **降低碰撞风险**
   - SHA256 hash 截断从 16 hex (64-bit) 增加到 32 hex (128-bit)

## 测试覆盖

6. **新增测试**
   - TestHTTPReplayMultiTurnChain: 验证连续 3 轮 replay
   - TestHTTPReplayFailedMergeKeepsOriginalRequest: 验证合并失败时的回退

## 影响范围
- 修复后支持稳定的多轮 HTTP replay continuation
- 防止内存泄漏和无限增长
- 提升可观测性和故障排查能力

* fix(relay): address second-round code review issues

修复第二轮代码审查中发现的所有关键问题。

## High Priority 修复

1. **流式响应保存问题** (High 1)
   - 优先使用 `metrics.InternalResponse`(已由 collectResponse 填充)
   - 避免二次调用 `GetInternalResponse()` 消耗 streaming 聚合器
   - 确保流式请求的 replay state 能正确保存

2. **统计一致性问题** (High 2)
   - 使用 `Swap()` 原子操作保证统计与 map 一致
   - 所有 Delete 路径统一更新 entries/totalSize
   - `resetResponsesReplayStore()` 同时重置统计
   - 容量检查在 Swap 后执行,允许更新已有 key

## Medium Priority 改进

3. **容量检查逻辑** (Medium 1)
   - 更新已有 key 时只计算 size delta,不检查 entries 上限
   - 新 key 才检查容量,超限时回滚

4. **测试改进** (Medium 4)
   - 修复 `TestHTTPReplayFailedMergeKeepsOriginalRequest`
   - 使用真正会失败的场景(空 Messages + 空 RawInputItems)
   - 验证 relay.go 的 fallback 校验逻辑

5. **代码格式** (Low 1)
   - 运行 gofmt 格式化 responses_replay_store.go

## 技术细节

### 统计一致性实现
```go
old, loaded := responsesReplayStore.Swap(key, newEntry)
if loaded {
    // 更新:只调整 size 差值
    responsesReplayStoreStats.totalSize.Add(delta)
} else {
    // 新增:增加 entries,检查容量,超限则回滚
    currentEntries := responsesReplayStoreStats.entries.Add(1)
    if overflow { Delete + rollback stats }
}
```

### 流式安全保存
```go
// 优先使用已收集的响应(streaming 安全)
internalResponse := metrics.InternalResponse
if internalResponse == nil {
    // fallback 到 GetInternalResponse
}
```

## 测试结果
- ✅ 全部 replay 测试通过
- ✅ go test -race 无数据竞争(replay 部分)
- ✅ gofmt 检查通过

* fix(relay): use CompareAndDelete for replay store to prevent concurrent deletion race

修复第三轮审查发现的并发安全问题。

## 问题

在高并发场景下,replay store 的 Delete 操作可能误删并发写入的新 entry:

1. **loadResponsesReplayState()**: goroutine A 判断 entry 过期,准备 Delete(key)
2. **同时**: goroutine B 对同一 key Swap 进新的有效 entry
3. **误删**: goroutine A 执行 Delete(key),误删了 B 刚写入的新 entry

同样的问题存在于:
- sweepExpiredResponsesReplayStates() 删除过期 entry
- storeResponsesReplayState() 容量回滚删除

## 修复

使用 `sync.Map.CompareAndDelete(key, oldValue)` 替代 `Delete(key)`:
- 只删除我们检查过的那个 entry
- 如果 key 的 value 已被并发修改,删除失败,避免误删
- 统计更新仅在删除成功后执行

## 代码位置

- loadResponsesReplayState(): L106-108, L112-115
- sweepExpiredResponsesReplayStates(): L63-66, L70-74
- storeResponsesReplayState(): L168-171

## 技术细节

```go
// Before (有竞态)
if entry.expiresAt.After(now) {
    responsesReplayStore.Delete(key)  // 可能误删并发写入的新 entry
    responsesReplayStoreStats.entries.Add(-1)
}

// After (并发安全)
if entry.expiresAt.After(now) {
    if responsesReplayStore.CompareAndDelete(key, entry) {  // 只删除这个 entry
        responsesReplayStoreStats.entries.Add(-1)  // 仅删除成功时更新
    }
}
```

* fix(relay): harden replay store capacity check, size estimation, and test coverage

- Add capacity rollback when Swap replacement exceeds size limit
- Replace fixed-multiplier estimateStateSize with per-field traversal
- Fix RawResponsesOutputItems to use function_call instead of function_call_output
- Assert multi-turn chain preserves all turns in replayed RawInputItems
- Strengthen clone isolation test with nested structure mutations
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.

[Bug] 反代any站报错

1 participant