Skip to content

Commit 67ffc01

Browse files
fix: partial port of e0919ce - validation types + cache id stripping
1 parent bc8cbe9 commit 67ffc01

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

api/validation.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,13 @@ func ValidateInput() ValidationRule {
622622
"message": true,
623623
"function_call": true,
624624
"function_call_output": true,
625+
"input_text": true,
626+
"input_image": true,
627+
"output_text": true,
628+
"refusal": true,
629+
"input_file": true,
630+
"computer_screenshot": true,
631+
"summary_text": true,
625632
"file": true,
626633
"image": true,
627634
}

proxy/response_cache.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,22 +223,49 @@ func cacheCompletedResponse(expandedInputRaw []byte, completedData []byte) {
223223

224224
var items []json.RawMessage
225225

226-
// 添加展开后的请求 input items
226+
// 添加展开后的请求 input items(剥离 id)
227227
inputItems := gjson.ParseBytes(expandedInputRaw)
228228
if inputItems.IsArray() {
229229
inputItems.ForEach(func(_, v gjson.Result) bool {
230-
items = append(items, json.RawMessage(v.Raw))
230+
if item, ok := stripResponseItemID(json.RawMessage(v.Raw)); ok {
231+
items = append(items, item)
232+
}
231233
return true
232234
})
233235
}
234236

235-
// 添加响应 output items
237+
// 添加响应 output 中真正需要续链的工具上下文;reasoning/message 等
238+
// 服务端输出 item 带有 rs_/msg_ id,store=false 时回灌会触发 item not found。
236239
output.ForEach(func(_, v gjson.Result) bool {
237-
items = append(items, json.RawMessage(v.Raw))
240+
if v.Get("type").String() != "function_call" {
241+
return true
242+
}
243+
if item, ok := stripResponseItemID(json.RawMessage(v.Raw)); ok {
244+
items = append(items, item)
245+
}
238246
return true
239247
})
240248

241249
if len(items) > 0 {
242250
setResponseCache(respID, items)
243251
}
244252
}
253+
254+
// stripResponseItemID 去掉缓存 item 的 id 字段(保留 call_id)。
255+
// 服务端返回的 rs_/msg_/fc_ 前缀 id 在 store=false 的 replay 场景下
256+
// 会触发上游 "item not found",而 call_id 是请求方自定义的工具调用链 id,必须保留。
257+
func stripResponseItemID(raw json.RawMessage) (json.RawMessage, bool) {
258+
var item map[string]any
259+
if err := json.Unmarshal(raw, &item); err != nil || item == nil {
260+
return raw, true
261+
}
262+
if _, exists := item["id"]; !exists {
263+
return raw, true
264+
}
265+
delete(item, "id")
266+
stripped, err := json.Marshal(item)
267+
if err != nil {
268+
return nil, false
269+
}
270+
return stripped, true
271+
}

proxy/response_cache_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,37 @@ func TestExpandPreviousResponseLeavesBodyUntouchedOnCacheMiss(t *testing.T) {
6060
t.Fatalf("body mutated on cache miss; got=%s want=%s", got, body)
6161
}
6262
}
63+
64+
func TestCacheCompletedResponseStripsItemIDsAndSkipsReasoning(t *testing.T) {
65+
resetResponseCacheForTest()
66+
67+
cacheCompletedResponse(
68+
[]byte(`[{"type":"message","id":"msg_input","role":"user","content":"call a tool"}]`),
69+
[]byte(`{"type":"response.completed","response":{"id":"resp_strip","output":[`+
70+
`{"type":"reasoning","id":"rs_0609","encrypted_content":"opaque"},`+
71+
`{"type":"message","id":"msg_output","role":"assistant","content":[{"type":"output_text","text":"thinking"}]},`+
72+
`{"type":"function_call","id":"fc_123","call_id":"call_abc","name":"lookup","arguments":"{}"}`+
73+
`]}}`),
74+
)
75+
76+
cached := getResponseCache("resp_strip")
77+
if len(cached) != 2 {
78+
t.Fatalf("cached items = %d, want input message + function_call only (reasoning/message output should be skipped)", len(cached))
79+
}
80+
if typ := gjson.GetBytes(cached[0], "type").String(); typ != "message" {
81+
t.Fatalf("cached[0].type = %q, want message", typ)
82+
}
83+
if id := gjson.GetBytes(cached[0], "id"); id.Exists() {
84+
t.Fatalf("cached input id should be stripped, got %s", id.Raw)
85+
}
86+
if typ := gjson.GetBytes(cached[1], "type").String(); typ != "function_call" {
87+
t.Fatalf("cached[1].type = %q, want function_call", typ)
88+
}
89+
if id := gjson.GetBytes(cached[1], "id"); id.Exists() {
90+
t.Fatalf("cached function_call id should be stripped, got %s", id.Raw)
91+
}
92+
// call_id 必须保留——它是续链所依赖的关键字段
93+
if callID := gjson.GetBytes(cached[1], "call_id").String(); callID != "call_abc" {
94+
t.Fatalf("cached function_call call_id = %q, want call_abc (must be preserved)", callID)
95+
}
96+
}

0 commit comments

Comments
 (0)