mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 00:24:38 +00:00
fix(apicompat): preserve arguments from done events
This commit is contained in:
@@ -1621,6 +1621,108 @@ func TestBufferedResponseAccumulator_ToolCalls(t *testing.T) {
|
||||
assert.Equal(t, `{"city":"NYC"}`, output[0].Arguments)
|
||||
}
|
||||
|
||||
func TestResponsesEventToChatChunks_FunctionArgumentsDoneWithoutDeltas(t *testing.T) {
|
||||
state := NewResponsesEventToChatState()
|
||||
state.Model = "gpt-5-codex"
|
||||
state.SentRole = true
|
||||
|
||||
chunks := ResponsesEventToChatChunks(&ResponsesStreamEvent{
|
||||
Type: "response.output_item.added",
|
||||
OutputIndex: 1,
|
||||
Item: &ResponsesOutput{
|
||||
Type: "function_call",
|
||||
CallID: "call_lookup",
|
||||
Name: "lookup_item",
|
||||
Arguments: "",
|
||||
},
|
||||
}, state)
|
||||
require.Len(t, chunks, 1)
|
||||
|
||||
chunks = ResponsesEventToChatChunks(&ResponsesStreamEvent{
|
||||
Type: "response.function_call_arguments.done",
|
||||
OutputIndex: 1,
|
||||
CallID: "call_lookup",
|
||||
Name: "lookup_item",
|
||||
Arguments: `{"id":1}`,
|
||||
}, state)
|
||||
require.Len(t, chunks, 1)
|
||||
require.Len(t, chunks[0].Choices[0].Delta.ToolCalls, 1)
|
||||
assert.Equal(t, `{"id":1}`, chunks[0].Choices[0].Delta.ToolCalls[0].Function.Arguments)
|
||||
}
|
||||
|
||||
func TestResponsesEventToChatChunks_FunctionArgumentsDoneDoesNotDuplicateDeltas(t *testing.T) {
|
||||
state := NewResponsesEventToChatState()
|
||||
state.Model = "gpt-5-codex"
|
||||
state.SentRole = true
|
||||
|
||||
ResponsesEventToChatChunks(&ResponsesStreamEvent{
|
||||
Type: "response.output_item.added",
|
||||
OutputIndex: 0,
|
||||
Item: &ResponsesOutput{Type: "function_call", CallID: "call_lookup", Name: "lookup_item"},
|
||||
}, state)
|
||||
chunks := ResponsesEventToChatChunks(&ResponsesStreamEvent{
|
||||
Type: "response.function_call_arguments.delta",
|
||||
OutputIndex: 0,
|
||||
Delta: `{"id":1}`,
|
||||
}, state)
|
||||
require.Len(t, chunks, 1)
|
||||
|
||||
chunks = ResponsesEventToChatChunks(&ResponsesStreamEvent{
|
||||
Type: "response.function_call_arguments.done",
|
||||
OutputIndex: 0,
|
||||
Arguments: `{"id":1}`,
|
||||
}, state)
|
||||
assert.Empty(t, chunks)
|
||||
}
|
||||
|
||||
func TestBufferedResponseAccumulator_FunctionArgumentsDoneWithoutDeltas(t *testing.T) {
|
||||
acc := NewBufferedResponseAccumulator()
|
||||
acc.ProcessEvent(&ResponsesStreamEvent{
|
||||
Type: "response.output_item.added",
|
||||
OutputIndex: 1,
|
||||
Item: &ResponsesOutput{Type: "function_call", CallID: "call_lookup", Name: "lookup_item"},
|
||||
})
|
||||
acc.ProcessEvent(&ResponsesStreamEvent{
|
||||
Type: "response.function_call_arguments.done",
|
||||
OutputIndex: 1,
|
||||
Arguments: `{"id":1}`,
|
||||
})
|
||||
|
||||
resp := &ResponsesResponse{Output: []ResponsesOutput{{
|
||||
Type: "function_call",
|
||||
CallID: "call_lookup",
|
||||
Name: "lookup_item",
|
||||
Arguments: "",
|
||||
}}}
|
||||
acc.SupplementResponseOutput(resp)
|
||||
|
||||
require.Len(t, resp.Output, 1)
|
||||
assert.Equal(t, `{"id":1}`, resp.Output[0].Arguments)
|
||||
}
|
||||
|
||||
func TestBufferedResponseAccumulator_FunctionArgumentsDoneReplacesDeltas(t *testing.T) {
|
||||
acc := NewBufferedResponseAccumulator()
|
||||
acc.ProcessEvent(&ResponsesStreamEvent{
|
||||
Type: "response.output_item.added",
|
||||
OutputIndex: 0,
|
||||
Item: &ResponsesOutput{Type: "function_call", CallID: "call_lookup", Name: "lookup_item"},
|
||||
})
|
||||
acc.ProcessEvent(&ResponsesStreamEvent{
|
||||
Type: "response.function_call_arguments.delta",
|
||||
OutputIndex: 0,
|
||||
Delta: `{"id":1}`,
|
||||
})
|
||||
acc.ProcessEvent(&ResponsesStreamEvent{
|
||||
Type: "response.function_call_arguments.done",
|
||||
OutputIndex: 0,
|
||||
Arguments: `{"id":1}`,
|
||||
})
|
||||
|
||||
output := acc.BuildOutput()
|
||||
require.Len(t, output, 1)
|
||||
assert.Equal(t, `{"id":1}`, output[0].Arguments)
|
||||
}
|
||||
|
||||
func TestBufferedResponseAccumulator_Reasoning(t *testing.T) {
|
||||
acc := NewBufferedResponseAccumulator()
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ type ResponsesEventToChatState struct {
|
||||
Finalized bool // true after finish chunk has been emitted
|
||||
NextToolCallIndex int // next sequential tool_call index to assign
|
||||
OutputIndexToToolIndex map[int]int // Responses output_index → Chat tool_calls index
|
||||
OutputIndexToArguments map[int]string
|
||||
IncludeUsage bool
|
||||
Usage *ChatUsage
|
||||
}
|
||||
@@ -136,6 +137,7 @@ func NewResponsesEventToChatState() *ResponsesEventToChatState {
|
||||
ID: generateChatCmplID(),
|
||||
Created: time.Now().Unix(),
|
||||
OutputIndexToToolIndex: make(map[int]int),
|
||||
OutputIndexToArguments: make(map[int]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +156,8 @@ func ResponsesEventToChatChunks(evt *ResponsesStreamEvent, state *ResponsesEvent
|
||||
// 均按 OutputIndex 累加到对应工具调用。
|
||||
"response.custom_tool_call_input.delta":
|
||||
return resToChatHandleFuncArgsDelta(evt, state)
|
||||
case "response.function_call_arguments.done", "response.custom_tool_call_input.done":
|
||||
return resToChatHandleFuncArgsDone(evt, state)
|
||||
case "response.reasoning_summary_text.delta",
|
||||
// 原始推理文本增量(真实 Codex 客户端消费的 reasoning_text.delta),
|
||||
// 与 reasoning summary 一样映射为 reasoning_content。
|
||||
@@ -277,6 +281,7 @@ func resToChatHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEve
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
state.OutputIndexToArguments[evt.OutputIndex] += evt.Delta
|
||||
|
||||
return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
|
||||
ToolCalls: []ChatToolCall{{
|
||||
@@ -288,6 +293,33 @@ func resToChatHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEve
|
||||
})}
|
||||
}
|
||||
|
||||
func resToChatHandleFuncArgsDone(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
|
||||
idx, ok := state.OutputIndexToToolIndex[evt.OutputIndex]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
completed := evt.Arguments
|
||||
if evt.Type == "response.custom_tool_call_input.done" {
|
||||
completed = evt.Input
|
||||
}
|
||||
current := state.OutputIndexToArguments[evt.OutputIndex]
|
||||
if completed == "" || !strings.HasPrefix(completed, current) || completed == current {
|
||||
return nil
|
||||
}
|
||||
|
||||
remainder := completed[len(current):]
|
||||
state.OutputIndexToArguments[evt.OutputIndex] = completed
|
||||
return []ChatCompletionsChunk{makeChatDeltaChunk(state, ChatDelta{
|
||||
ToolCalls: []ChatToolCall{{
|
||||
Index: &idx,
|
||||
Function: ChatFunctionCall{
|
||||
Arguments: remainder,
|
||||
},
|
||||
}},
|
||||
})}
|
||||
}
|
||||
|
||||
func resToChatHandleReasoningDelta(evt *ResponsesStreamEvent, state *ResponsesEventToChatState) []ChatCompletionsChunk {
|
||||
if evt.Delta == "" {
|
||||
return nil
|
||||
@@ -453,9 +485,10 @@ func generateChatCmplID() string {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type bufferedFuncCall struct {
|
||||
CallID string
|
||||
Name string
|
||||
Args strings.Builder
|
||||
OutputIndex int
|
||||
CallID string
|
||||
Name string
|
||||
Args strings.Builder
|
||||
}
|
||||
|
||||
// BufferedResponseAccumulator collects content from Responses SSE delta events
|
||||
@@ -489,8 +522,9 @@ func (a *BufferedResponseAccumulator) ProcessEvent(event *ResponsesStreamEvent)
|
||||
idx := len(a.funcCalls)
|
||||
a.outputIndexToFuncIdx[event.OutputIndex] = idx
|
||||
a.funcCalls = append(a.funcCalls, bufferedFuncCall{
|
||||
CallID: event.Item.CallID,
|
||||
Name: event.Item.Name,
|
||||
OutputIndex: event.OutputIndex,
|
||||
CallID: event.Item.CallID,
|
||||
Name: event.Item.Name,
|
||||
})
|
||||
}
|
||||
case "response.function_call_arguments.delta", "response.custom_tool_call_input.delta":
|
||||
@@ -499,6 +533,17 @@ func (a *BufferedResponseAccumulator) ProcessEvent(event *ResponsesStreamEvent)
|
||||
_, _ = a.funcCalls[idx].Args.WriteString(event.Delta)
|
||||
}
|
||||
}
|
||||
case "response.function_call_arguments.done", "response.custom_tool_call_input.done":
|
||||
completed := event.Arguments
|
||||
if event.Type == "response.custom_tool_call_input.done" {
|
||||
completed = event.Input
|
||||
}
|
||||
if completed != "" {
|
||||
if idx, ok := a.outputIndexToFuncIdx[event.OutputIndex]; ok {
|
||||
a.funcCalls[idx].Args.Reset()
|
||||
_, _ = a.funcCalls[idx].Args.WriteString(completed)
|
||||
}
|
||||
}
|
||||
case "response.reasoning_summary_text.delta", "response.reasoning_text.delta":
|
||||
if event.Delta != "" {
|
||||
_, _ = a.reasoning.WriteString(event.Delta)
|
||||
@@ -550,15 +595,35 @@ func (a *BufferedResponseAccumulator) BuildOutput() []ResponsesOutput {
|
||||
return out
|
||||
}
|
||||
|
||||
// SupplementResponseOutput fills resp.Output from accumulated delta content
|
||||
// when the terminal event delivered an empty output array. If resp.Output is
|
||||
// already populated, this is a no-op (preserves backward compatibility).
|
||||
// SupplementResponseOutput fills resp.Output from accumulated stream content
|
||||
// when the terminal event delivered an empty output array. It also fills empty
|
||||
// function-call arguments from authoritative argument-done events.
|
||||
func (a *BufferedResponseAccumulator) SupplementResponseOutput(resp *ResponsesResponse) {
|
||||
if resp == nil || len(resp.Output) > 0 {
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
if !a.HasContent() {
|
||||
if len(resp.Output) == 0 {
|
||||
if a.HasContent() {
|
||||
resp.Output = a.BuildOutput()
|
||||
}
|
||||
return
|
||||
}
|
||||
resp.Output = a.BuildOutput()
|
||||
|
||||
for outputIndex := range resp.Output {
|
||||
item := &resp.Output[outputIndex]
|
||||
if item.Type != "function_call" || item.Arguments != "" {
|
||||
continue
|
||||
}
|
||||
for funcIndex := range a.funcCalls {
|
||||
call := &a.funcCalls[funcIndex]
|
||||
matchesCallID := item.CallID != "" && item.CallID == call.CallID
|
||||
if !matchesCallID && call.OutputIndex != outputIndex {
|
||||
continue
|
||||
}
|
||||
if call.Args.Len() > 0 {
|
||||
item.Arguments = call.Args.String()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user