mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
fix(translator): resolve pending codex tool calls on terminal response
This commit is contained in:
@@ -23,9 +23,12 @@ var (
|
||||
|
||||
// ConvertCodexResponseToClaudeParams holds parameters for response conversion.
|
||||
type ConvertCodexResponseToClaudeParams struct {
|
||||
HasToolCall bool
|
||||
HasEmittedToolUse bool
|
||||
BlockIndex int
|
||||
HasReceivedArgumentsDelta bool
|
||||
FunctionCallBlockOpen bool
|
||||
FunctionCallBlockCallID string
|
||||
FunctionCallBlockIndex int
|
||||
HasTextDelta bool
|
||||
TextBlockOpen bool
|
||||
ThinkingBlockOpen bool
|
||||
@@ -42,7 +45,6 @@ type ConvertCodexResponseToClaudeParams struct {
|
||||
type pendingCodexFunctionCall struct {
|
||||
CallID string
|
||||
Arguments string
|
||||
BlockIndex int
|
||||
HasReceivedArgumentsDelta bool
|
||||
StartEmitted bool
|
||||
}
|
||||
@@ -66,8 +68,7 @@ type pendingCodexFunctionCall struct {
|
||||
func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, param *any) [][]byte {
|
||||
if *param == nil {
|
||||
*param = &ConvertCodexResponseToClaudeParams{
|
||||
HasToolCall: false,
|
||||
BlockIndex: 0,
|
||||
BlockIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +136,10 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
case "response.completed", "response.incomplete":
|
||||
template = []byte(`{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||
responseData := rootResult.Get("response")
|
||||
template, _ = sjson.SetBytes(template, "delta.stop_reason", mapCodexStopReasonToClaude(codexStopReason(responseData), params.HasToolCall))
|
||||
output = hydrateOpenCodexFunctionCallFromTerminal(output, params, responseData)
|
||||
output = append(output, finalizeCodexOpenContentBlocks(params)...)
|
||||
output = appendPendingCodexFunctionCallsFromTerminal(output, params, originalRequestRawJSON, responseData)
|
||||
template, _ = sjson.SetBytes(template, "delta.stop_reason", mapCodexStopReasonToClaude(codexStopReason(responseData), params.HasEmittedToolUse))
|
||||
template = setClaudeStopSequence(template, "delta.stop_sequence", responseData)
|
||||
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage"))
|
||||
template, _ = sjson.SetBytes(template, "usage.input_tokens", inputTokens)
|
||||
@@ -153,28 +157,25 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
case "function_call":
|
||||
output = append(output, finalizeCodexThinkingBlock(params)...)
|
||||
output = append(output, stopCodexTextBlock(params)...)
|
||||
params.HasToolCall = true
|
||||
params.HasReceivedArgumentsDelta = false
|
||||
|
||||
callID := itemResult.Get("call_id").String()
|
||||
callID := codexFunctionCallID(itemResult)
|
||||
name := itemResult.Get("name").String()
|
||||
key := codexFunctionCallKey(rootResult, itemResult)
|
||||
if name == "" {
|
||||
if params.PendingFunctionCalls == nil {
|
||||
params.PendingFunctionCalls = map[string]*pendingCodexFunctionCall{}
|
||||
}
|
||||
params.PendingFunctionCalls[key] = &pendingCodexFunctionCall{
|
||||
CallID: callID,
|
||||
BlockIndex: params.BlockIndex,
|
||||
}
|
||||
params.LastPendingFunctionCallKey = key
|
||||
params.BlockIndex++
|
||||
recordPendingCodexFunctionCall(params, rootResult, itemResult)
|
||||
break
|
||||
}
|
||||
|
||||
delete(params.PendingFunctionCalls, key)
|
||||
output = appendCodexFunctionCallStart(output, originalRequestRawJSON, callID, name, params.BlockIndex)
|
||||
output = appendCodexFunctionCallArgumentDelta(output, "", params.BlockIndex)
|
||||
if pending, pendingKeys := pendingCodexFunctionCallForDone(params, rootResult, itemResult); pending != nil {
|
||||
deletePendingCodexFunctionCallAliases(params, pendingKeys)
|
||||
}
|
||||
blockIndex := params.BlockIndex
|
||||
output = appendCodexFunctionCallStart(output, originalRequestRawJSON, callID, name, blockIndex)
|
||||
params.HasEmittedToolUse = true
|
||||
output = appendCodexFunctionCallArgumentDelta(output, "", blockIndex)
|
||||
params.FunctionCallBlockOpen = true
|
||||
params.FunctionCallBlockCallID = callID
|
||||
params.FunctionCallBlockIndex = blockIndex
|
||||
case "reasoning":
|
||||
params.ThinkingSummarySeen = false
|
||||
params.ThinkingSignature = itemResult.Get("encrypted_content").String()
|
||||
@@ -219,17 +220,18 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
output = append(output, stopCodexTextBlock(params)...)
|
||||
params.HasTextDelta = true
|
||||
case "function_call":
|
||||
key := codexFunctionCallKey(rootResult, itemResult)
|
||||
if pending, pendingKey := pendingCodexFunctionCallForKey(params, key); pending != nil && !pending.StartEmitted {
|
||||
if pending, pendingKeys := pendingCodexFunctionCallForDone(params, rootResult, itemResult); pending != nil && !pending.StartEmitted {
|
||||
name := itemResult.Get("name").String()
|
||||
if name == "" {
|
||||
return [][]byte{output}
|
||||
}
|
||||
callID := pending.CallID
|
||||
if callID == "" {
|
||||
callID = itemResult.Get("call_id").String()
|
||||
callID = codexFunctionCallID(itemResult)
|
||||
}
|
||||
output = appendCodexFunctionCallStart(output, originalRequestRawJSON, callID, name, pending.BlockIndex)
|
||||
blockIndex := params.BlockIndex
|
||||
output = appendCodexFunctionCallStart(output, originalRequestRawJSON, callID, name, blockIndex)
|
||||
params.HasEmittedToolUse = true
|
||||
pending.StartEmitted = true
|
||||
|
||||
args := pending.Arguments
|
||||
@@ -237,17 +239,20 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
args = itemResult.Get("arguments").String()
|
||||
}
|
||||
if args != "" {
|
||||
output = appendCodexFunctionCallArgumentDelta(output, args, pending.BlockIndex)
|
||||
output = appendCodexFunctionCallArgumentDelta(output, args, blockIndex)
|
||||
}
|
||||
output = appendCodexFunctionCallStop(output, pending.BlockIndex)
|
||||
|
||||
delete(params.PendingFunctionCalls, pendingKey)
|
||||
if params.LastPendingFunctionCallKey == pendingKey {
|
||||
params.LastPendingFunctionCallKey = ""
|
||||
}
|
||||
} else {
|
||||
output = appendCodexFunctionCallStop(output, params.BlockIndex)
|
||||
output = appendCodexFunctionCallStop(output, blockIndex)
|
||||
params.BlockIndex++
|
||||
|
||||
deletePendingCodexFunctionCallAliases(params, pendingKeys)
|
||||
} else if params.FunctionCallBlockOpen {
|
||||
if !params.HasReceivedArgumentsDelta {
|
||||
if args := itemResult.Get("arguments").String(); args != "" {
|
||||
output = appendCodexFunctionCallArgumentDelta(output, args, params.FunctionCallBlockIndex)
|
||||
params.HasReceivedArgumentsDelta = true
|
||||
}
|
||||
}
|
||||
output = appendCodexOpenFunctionCallStop(output, params)
|
||||
}
|
||||
case "reasoning":
|
||||
if signature := itemResult.Get("encrypted_content").String(); signature != "" {
|
||||
@@ -286,6 +291,7 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
if !params.HasReceivedArgumentsDelta {
|
||||
if args := rootResult.Get("arguments").String(); args != "" {
|
||||
output = appendCodexFunctionCallArgumentDelta(output, args, params.BlockIndex)
|
||||
params.HasReceivedArgumentsDelta = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,7 +488,7 @@ func mapCodexStopReasonToClaude(stopReason string, hasToolCall bool) string {
|
||||
case "max_tokens", "max_output_tokens":
|
||||
return "max_tokens"
|
||||
case "tool_use", "tool_calls", "function_call":
|
||||
return "tool_use"
|
||||
return "end_turn"
|
||||
case "end_turn", "stop_sequence", "pause_turn", "refusal", "model_context_window_exceeded":
|
||||
return stopReason
|
||||
case "content_filter":
|
||||
@@ -507,12 +513,23 @@ func codexFunctionCallKey(rootResult, itemResult gjson.Result) string {
|
||||
if outputIndex := rootResult.Get("output_index"); outputIndex.Exists() {
|
||||
return "output:" + outputIndex.Raw
|
||||
}
|
||||
if callID := itemResult.Get("call_id").String(); callID != "" {
|
||||
if callID := codexFunctionCallID(itemResult); callID != "" {
|
||||
return "call:" + callID
|
||||
}
|
||||
return "last"
|
||||
}
|
||||
|
||||
func codexFunctionCallID(itemResult gjson.Result) string {
|
||||
return itemResult.Get("call_id").String()
|
||||
}
|
||||
|
||||
func codexFunctionCallIDKey(callID string) string {
|
||||
if callID == "" {
|
||||
return ""
|
||||
}
|
||||
return "call:" + callID
|
||||
}
|
||||
|
||||
func codexArgumentsFunctionCallKey(params *ConvertCodexResponseToClaudeParams, rootResult gjson.Result) string {
|
||||
if outputIndex := rootResult.Get("output_index"); outputIndex.Exists() {
|
||||
return "output:" + outputIndex.Raw
|
||||
@@ -520,21 +537,88 @@ func codexArgumentsFunctionCallKey(params *ConvertCodexResponseToClaudeParams, r
|
||||
return params.LastPendingFunctionCallKey
|
||||
}
|
||||
|
||||
func recordPendingCodexFunctionCall(params *ConvertCodexResponseToClaudeParams, rootResult, itemResult gjson.Result) {
|
||||
if params.PendingFunctionCalls == nil {
|
||||
params.PendingFunctionCalls = map[string]*pendingCodexFunctionCall{}
|
||||
}
|
||||
|
||||
pending := &pendingCodexFunctionCall{CallID: codexFunctionCallID(itemResult)}
|
||||
key := codexFunctionCallKey(rootResult, itemResult)
|
||||
params.PendingFunctionCalls[key] = pending
|
||||
if callIDKey := codexFunctionCallIDKey(pending.CallID); callIDKey != "" {
|
||||
params.PendingFunctionCalls[callIDKey] = pending
|
||||
}
|
||||
params.LastPendingFunctionCallKey = key
|
||||
}
|
||||
|
||||
func pendingCodexFunctionCallForKey(params *ConvertCodexResponseToClaudeParams, key string) (*pendingCodexFunctionCall, string) {
|
||||
if params == nil || params.PendingFunctionCalls == nil {
|
||||
if params == nil || params.PendingFunctionCalls == nil || key == "" {
|
||||
return nil, ""
|
||||
}
|
||||
if key != "" {
|
||||
pending, ok := params.PendingFunctionCalls[key]
|
||||
if !ok {
|
||||
return nil, ""
|
||||
}
|
||||
return pending, key
|
||||
}
|
||||
|
||||
func pendingCodexFunctionCallForDone(params *ConvertCodexResponseToClaudeParams, rootResult, itemResult gjson.Result) (*pendingCodexFunctionCall, []string) {
|
||||
if params == nil || params.PendingFunctionCalls == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
keys := []string{codexFunctionCallKey(rootResult, itemResult)}
|
||||
callID := codexFunctionCallID(itemResult)
|
||||
if callID != "" {
|
||||
keys = appendUniqueCodexFunctionCallKey(keys, codexFunctionCallIDKey(callID))
|
||||
} else if !rootResult.Get("output_index").Exists() && params.LastPendingFunctionCallKey != "" {
|
||||
keys = appendUniqueCodexFunctionCallKey(keys, params.LastPendingFunctionCallKey)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if pending, ok := params.PendingFunctionCalls[key]; ok {
|
||||
return pending, key
|
||||
return pending, keysForPendingCodexFunctionCall(params, pending)
|
||||
}
|
||||
}
|
||||
if params.LastPendingFunctionCallKey != "" {
|
||||
if pending, ok := params.PendingFunctionCalls[params.LastPendingFunctionCallKey]; ok {
|
||||
return pending, params.LastPendingFunctionCallKey
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func appendUniqueCodexFunctionCallKey(keys []string, key string) []string {
|
||||
if key == "" {
|
||||
return keys
|
||||
}
|
||||
for _, existing := range keys {
|
||||
if existing == key {
|
||||
return keys
|
||||
}
|
||||
}
|
||||
return append(keys, key)
|
||||
}
|
||||
|
||||
func keysForPendingCodexFunctionCall(params *ConvertCodexResponseToClaudeParams, pending *pendingCodexFunctionCall) []string {
|
||||
if params == nil || pending == nil || params.PendingFunctionCalls == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, 2)
|
||||
for key, candidate := range params.PendingFunctionCalls {
|
||||
if candidate == pending {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func deletePendingCodexFunctionCallAliases(params *ConvertCodexResponseToClaudeParams, keys []string) {
|
||||
if params == nil || params.PendingFunctionCalls == nil {
|
||||
return
|
||||
}
|
||||
for _, key := range keys {
|
||||
delete(params.PendingFunctionCalls, key)
|
||||
if params.LastPendingFunctionCallKey == key {
|
||||
params.LastPendingFunctionCallKey = ""
|
||||
}
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func appendCodexFunctionCallStart(output []byte, originalRequestRawJSON []byte, callID, name string, blockIndex int) []byte {
|
||||
@@ -558,6 +642,134 @@ func appendCodexFunctionCallStop(output []byte, blockIndex int) []byte {
|
||||
return translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2)
|
||||
}
|
||||
|
||||
func appendCodexOpenFunctionCallStop(output []byte, params *ConvertCodexResponseToClaudeParams) []byte {
|
||||
if params == nil || !params.FunctionCallBlockOpen {
|
||||
return output
|
||||
}
|
||||
|
||||
blockIndex := params.FunctionCallBlockIndex
|
||||
output = appendCodexFunctionCallStop(output, blockIndex)
|
||||
if params.BlockIndex <= blockIndex {
|
||||
params.BlockIndex = blockIndex + 1
|
||||
}
|
||||
params.FunctionCallBlockOpen = false
|
||||
params.FunctionCallBlockCallID = ""
|
||||
params.FunctionCallBlockIndex = 0
|
||||
return output
|
||||
}
|
||||
|
||||
func hydrateOpenCodexFunctionCallFromTerminal(output []byte, params *ConvertCodexResponseToClaudeParams, responseData gjson.Result) []byte {
|
||||
if params == nil || !params.FunctionCallBlockOpen || params.HasReceivedArgumentsDelta {
|
||||
return output
|
||||
}
|
||||
|
||||
responseData.Get("output").ForEach(func(_, item gjson.Result) bool {
|
||||
if item.Get("type").String() != "function_call" || codexFunctionCallID(item) != params.FunctionCallBlockCallID {
|
||||
return true
|
||||
}
|
||||
if args := item.Get("arguments").String(); args != "" {
|
||||
output = appendCodexFunctionCallArgumentDelta(output, args, params.FunctionCallBlockIndex)
|
||||
params.HasReceivedArgumentsDelta = true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return output
|
||||
}
|
||||
|
||||
func appendPendingCodexFunctionCallsFromTerminal(output []byte, params *ConvertCodexResponseToClaudeParams, originalRequestRawJSON []byte, responseData gjson.Result) []byte {
|
||||
if params == nil || len(params.PendingFunctionCalls) == 0 {
|
||||
return output
|
||||
}
|
||||
|
||||
responseData.Get("output").ForEach(func(index, item gjson.Result) bool {
|
||||
if item.Get("type").String() != "function_call" {
|
||||
return true
|
||||
}
|
||||
|
||||
pending, pendingKeys := pendingCodexFunctionCallForTerminalItem(params, index, item)
|
||||
if pending == nil {
|
||||
return true
|
||||
}
|
||||
if pending.StartEmitted {
|
||||
deletePendingCodexFunctionCallAliases(params, pendingKeys)
|
||||
return true
|
||||
}
|
||||
|
||||
name := item.Get("name").String()
|
||||
if name == "" {
|
||||
deletePendingCodexFunctionCallAliases(params, pendingKeys)
|
||||
return true
|
||||
}
|
||||
callID := pending.CallID
|
||||
if callID == "" {
|
||||
callID = codexFunctionCallID(item)
|
||||
}
|
||||
|
||||
blockIndex := params.BlockIndex
|
||||
output = appendCodexFunctionCallStart(output, originalRequestRawJSON, callID, name, blockIndex)
|
||||
params.HasEmittedToolUse = true
|
||||
pending.StartEmitted = true
|
||||
|
||||
args := item.Get("arguments").String()
|
||||
if args == "" {
|
||||
args = pending.Arguments
|
||||
}
|
||||
if args != "" {
|
||||
output = appendCodexFunctionCallArgumentDelta(output, args, blockIndex)
|
||||
}
|
||||
output = appendCodexFunctionCallStop(output, blockIndex)
|
||||
params.BlockIndex++
|
||||
|
||||
deletePendingCodexFunctionCallAliases(params, pendingKeys)
|
||||
return true
|
||||
})
|
||||
|
||||
clearPendingCodexFunctionCalls(params)
|
||||
return output
|
||||
}
|
||||
|
||||
func pendingCodexFunctionCallForTerminalItem(params *ConvertCodexResponseToClaudeParams, outputIndex, item gjson.Result) (*pendingCodexFunctionCall, []string) {
|
||||
if params == nil || params.PendingFunctionCalls == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
keys := make([]string, 0, 3)
|
||||
if callID := codexFunctionCallID(item); callID != "" {
|
||||
keys = appendUniqueCodexFunctionCallKey(keys, codexFunctionCallIDKey(callID))
|
||||
}
|
||||
if itemOutputIndex := item.Get("output_index"); itemOutputIndex.Exists() {
|
||||
keys = appendUniqueCodexFunctionCallKey(keys, "output:"+itemOutputIndex.Raw)
|
||||
}
|
||||
if outputIndex.Exists() {
|
||||
keys = appendUniqueCodexFunctionCallKey(keys, "output:"+outputIndex.Raw)
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
if pending, ok := params.PendingFunctionCalls[key]; ok {
|
||||
return pending, keysForPendingCodexFunctionCall(params, pending)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func clearPendingCodexFunctionCalls(params *ConvertCodexResponseToClaudeParams) {
|
||||
if params == nil || params.PendingFunctionCalls == nil {
|
||||
return
|
||||
}
|
||||
for key := range params.PendingFunctionCalls {
|
||||
delete(params.PendingFunctionCalls, key)
|
||||
}
|
||||
params.LastPendingFunctionCallKey = ""
|
||||
}
|
||||
|
||||
func finalizeCodexOpenContentBlocks(params *ConvertCodexResponseToClaudeParams) []byte {
|
||||
output := make([]byte, 0, 256)
|
||||
output = append(output, finalizeCodexThinkingBlock(params)...)
|
||||
output = append(output, stopCodexTextBlock(params)...)
|
||||
output = appendCodexOpenFunctionCallStop(output, params)
|
||||
return output
|
||||
}
|
||||
|
||||
func resolveCodexClaudeToolUseName(originalRequestRawJSON []byte, name string) string {
|
||||
rev := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
||||
if orig, ok := rev[name]; ok {
|
||||
|
||||
@@ -587,6 +587,225 @@ func TestConvertCodexResponseToClaude_StreamFunctionCallDefersStartUntilDoneName
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamUnnamedFunctionCallDoneByCallIDKeepsPendingSlots(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","description":"lookup"}]}`)
|
||||
var param any
|
||||
|
||||
chunks := [][]byte{
|
||||
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5"}}`),
|
||||
[]byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"call_first"},"output_index":1}`),
|
||||
[]byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"call_second"},"output_index":2}`),
|
||||
[]byte(`data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_first","name":"lookup","arguments":"{\"id\":1}"}}`),
|
||||
[]byte(`data: {"type":"response.output_item.done","item":{"type":"function_call","call_id":"call_second","name":"lookup","arguments":"{\"id\":2}"}}`),
|
||||
}
|
||||
|
||||
var outputs [][]byte
|
||||
for _, chunk := range chunks {
|
||||
outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...)
|
||||
}
|
||||
|
||||
var toolIDs []string
|
||||
var startIndices []int64
|
||||
var stopIndices []int64
|
||||
var argumentDeltas []string
|
||||
for _, out := range outputs {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
|
||||
switch data.Get("type").String() {
|
||||
case "content_block_start":
|
||||
if data.Get("content_block.type").String() == "tool_use" {
|
||||
toolIDs = append(toolIDs, data.Get("content_block.id").String())
|
||||
startIndices = append(startIndices, data.Get("index").Int())
|
||||
}
|
||||
case "content_block_delta":
|
||||
if data.Get("delta.type").String() == "input_json_delta" {
|
||||
argumentDeltas = append(argumentDeltas, data.Get("delta.partial_json").String())
|
||||
}
|
||||
case "content_block_stop":
|
||||
stopIndices = append(stopIndices, data.Get("index").Int())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(toolIDs) != 2 || toolIDs[0] != "call_first" || toolIDs[1] != "call_second" {
|
||||
t.Fatalf("unexpected tool IDs: %v; outputs=%q", toolIDs, outputs)
|
||||
}
|
||||
if len(startIndices) != 2 || startIndices[0] != 0 || startIndices[1] != 1 {
|
||||
t.Fatalf("unexpected start indices: %v; outputs=%q", startIndices, outputs)
|
||||
}
|
||||
if len(stopIndices) != 2 || stopIndices[0] != 0 || stopIndices[1] != 1 {
|
||||
t.Fatalf("unexpected stop indices: %v; outputs=%q", stopIndices, outputs)
|
||||
}
|
||||
if len(argumentDeltas) != 2 || argumentDeltas[0] != `{"id":1}` || argumentDeltas[1] != `{"id":2}` {
|
||||
t.Fatalf("unexpected argument deltas: %v; outputs=%q", argumentDeltas, outputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamDeferredUnnamedFunctionCallDoesNotReserveBlockIndex(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","description":"lookup"}]}`)
|
||||
var param any
|
||||
|
||||
chunks := [][]byte{
|
||||
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5"}}`),
|
||||
[]byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"call_hidden"},"output_index":1}`),
|
||||
[]byte(`data: {"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]},"output_index":2}`),
|
||||
}
|
||||
|
||||
var outputs [][]byte
|
||||
for _, chunk := range chunks {
|
||||
outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...)
|
||||
}
|
||||
|
||||
for _, out := range outputs {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
|
||||
if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "text" {
|
||||
if got := data.Get("index").Int(); got != 0 {
|
||||
t.Fatalf("text block index = %d, want 0; outputs=%q", got, outputs)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("missing text content_block_start; outputs=%q", outputs)
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamTerminalOutputHydratesOpenFunctionCallArguments(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","description":"lookup"}]}`)
|
||||
var param any
|
||||
|
||||
chunks := [][]byte{
|
||||
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5"}}`),
|
||||
[]byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"call_1","name":"lookup"},"output_index":1}`),
|
||||
[]byte(`data: {"type":"response.completed","response":{"stop_reason":"stop","usage":{"input_tokens":1,"output_tokens":1},"output":[{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"query\":\"example\"}"}]}}`),
|
||||
}
|
||||
|
||||
var outputs [][]byte
|
||||
for _, chunk := range chunks {
|
||||
outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...)
|
||||
}
|
||||
|
||||
var finalArgumentPosition = -1
|
||||
var stopPosition = -1
|
||||
var messageDeltaPosition = -1
|
||||
position := 0
|
||||
for _, out := range outputs {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
position++
|
||||
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
|
||||
switch data.Get("type").String() {
|
||||
case "content_block_delta":
|
||||
if data.Get("delta.type").String() == "input_json_delta" && data.Get("delta.partial_json").String() == `{"query":"example"}` {
|
||||
finalArgumentPosition = position
|
||||
}
|
||||
case "content_block_stop":
|
||||
if data.Get("index").Int() == 0 {
|
||||
stopPosition = position
|
||||
}
|
||||
case "message_delta":
|
||||
messageDeltaPosition = position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if finalArgumentPosition == -1 {
|
||||
t.Fatalf("missing terminal argument delta; outputs=%q", outputs)
|
||||
}
|
||||
if stopPosition == -1 {
|
||||
t.Fatalf("missing content_block_stop for open function call; outputs=%q", outputs)
|
||||
}
|
||||
if messageDeltaPosition == -1 {
|
||||
t.Fatalf("missing message_delta; outputs=%q", outputs)
|
||||
}
|
||||
if !(finalArgumentPosition < stopPosition && stopPosition < messageDeltaPosition) {
|
||||
t.Fatalf("unexpected event order: args=%d stop=%d message_delta=%d; outputs=%q", finalArgumentPosition, stopPosition, messageDeltaPosition, outputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamTerminalOutputEmitsPendingUnnamedFunctionCall(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","description":"lookup"}]}`)
|
||||
var param any
|
||||
|
||||
chunks := [][]byte{
|
||||
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5"}}`),
|
||||
[]byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"call_1"},"output_index":1}`),
|
||||
[]byte(`data: {"type":"response.function_call_arguments.done","arguments":"{\"query\":\"example\"}","output_index":1}`),
|
||||
[]byte(`data: {"type":"response.completed","response":{"stop_reason":"stop","usage":{"input_tokens":1,"output_tokens":1},"output":[{"type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\"query\":\"example\"}"}]}}`),
|
||||
}
|
||||
|
||||
var outputs [][]byte
|
||||
for _, chunk := range chunks {
|
||||
outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...)
|
||||
}
|
||||
outputText := string(bytes.Join(outputs, nil))
|
||||
|
||||
if strings.Count(outputText, `"type":"tool_use"`) != 1 {
|
||||
t.Fatalf("expected one terminal tool_use block, got output:\n%s", outputText)
|
||||
}
|
||||
if !strings.Contains(outputText, `"name":"lookup"`) || !strings.Contains(outputText, `"partial_json":"{\"query\":\"example\"}"`) {
|
||||
t.Fatalf("expected terminal tool name and arguments, got output:\n%s", outputText)
|
||||
}
|
||||
gotReason, ok := findClaudeStreamStopReason(outputs)
|
||||
if !ok {
|
||||
t.Fatalf("missing message_delta; outputs=%q", outputs)
|
||||
}
|
||||
if gotReason != "tool_use" {
|
||||
t.Fatalf("stop_reason = %q, want tool_use. Outputs=%q", gotReason, outputs)
|
||||
}
|
||||
toolUsePosition := strings.Index(outputText, `"type":"tool_use"`)
|
||||
messageDeltaPosition := strings.Index(outputText, `"type":"message_delta"`)
|
||||
if toolUsePosition < 0 || messageDeltaPosition < 0 || toolUsePosition > messageDeltaPosition {
|
||||
t.Fatalf("terminal tool_use must be emitted before message_delta:\n%s", outputText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamUnresolvedPendingFunctionCallDoesNotForceToolUseStopReason(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[{"name":"lookup","description":"lookup"}]}`)
|
||||
var param any
|
||||
|
||||
chunks := [][]byte{
|
||||
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5"}}`),
|
||||
[]byte(`data: {"type":"response.output_item.added","item":{"type":"function_call","call_id":"call_hidden"},"output_index":1}`),
|
||||
[]byte(`data: {"type":"response.completed","response":{"stop_reason":"stop","usage":{"input_tokens":1,"output_tokens":1},"output":[]}}`),
|
||||
}
|
||||
|
||||
var outputs [][]byte
|
||||
for _, chunk := range chunks {
|
||||
outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...)
|
||||
}
|
||||
outputText := string(bytes.Join(outputs, nil))
|
||||
|
||||
if strings.Contains(outputText, `"type":"tool_use"`) {
|
||||
t.Fatalf("unresolved pending function_call must not emit tool_use:\n%s", outputText)
|
||||
}
|
||||
gotReason, ok := findClaudeStreamStopReason(outputs)
|
||||
if !ok {
|
||||
t.Fatalf("missing message_delta; outputs=%q", outputs)
|
||||
}
|
||||
if gotReason != "end_turn" {
|
||||
t.Fatalf("stop_reason = %q, want end_turn. Outputs=%q", gotReason, outputs)
|
||||
}
|
||||
params, ok := param.(*ConvertCodexResponseToClaudeParams)
|
||||
if !ok || len(params.PendingFunctionCalls) != 0 || params.LastPendingFunctionCallKey != "" {
|
||||
t.Fatalf("pending function calls were not cleared: %#v", param)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToClaude_StreamEmptyOutputUsesOutputItemDoneMessageFallback(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
originalRequest := []byte(`{"tools":[]}`)
|
||||
|
||||
Reference in New Issue
Block a user