900 lines
32 KiB
Go
900 lines
32 KiB
Go
// Package claude provides response translation functionality for Codex to Claude Code API compatibility.
|
|
// This package handles the conversion of Codex API responses into Claude Code-compatible
|
|
// Server-Sent Events (SSE) format, implementing a sophisticated state machine that manages
|
|
// different response types including text content, thinking processes, and function calls.
|
|
// The translation ensures proper sequencing of SSE events and maintains state across
|
|
// multiple response chunks to provide a seamless streaming experience.
|
|
package claude
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"strings"
|
|
|
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
var (
|
|
dataTag = []byte("data:")
|
|
)
|
|
|
|
// ConvertCodexResponseToClaudeParams holds parameters for response conversion.
|
|
type ConvertCodexResponseToClaudeParams struct {
|
|
HasEmittedToolUse bool
|
|
BlockIndex int
|
|
HasReceivedArgumentsDelta bool
|
|
FunctionCallBlockOpen bool
|
|
FunctionCallBlockCallID string
|
|
FunctionCallBlockIndex int
|
|
HasTextDelta bool
|
|
TextBlockOpen bool
|
|
ThinkingBlockOpen bool
|
|
ThinkingStopPending bool
|
|
ThinkingSignature string
|
|
ThinkingSummarySeen bool
|
|
WebSearchToolUseIDs map[string]struct{}
|
|
WebSearchToolResultIDs map[string]struct{}
|
|
LastWebSearchToolUseID string
|
|
PendingFunctionCalls map[string]*pendingCodexFunctionCall
|
|
LastPendingFunctionCallKey string
|
|
}
|
|
|
|
type pendingCodexFunctionCall struct {
|
|
CallID string
|
|
Arguments string
|
|
HasReceivedArgumentsDelta bool
|
|
StartEmitted bool
|
|
}
|
|
|
|
// ConvertCodexResponseToClaude performs sophisticated streaming response format conversion.
|
|
// This function implements a complex state machine that translates Codex API responses
|
|
// into Claude Code-compatible Server-Sent Events (SSE) format. It manages different response types
|
|
// and handles state transitions between content blocks, thinking processes, and function calls.
|
|
//
|
|
// Response type states: 0=none, 1=content, 2=thinking, 3=function
|
|
// The function maintains state across multiple calls to ensure proper SSE event sequencing.
|
|
//
|
|
// Parameters:
|
|
// - ctx: The context for the request, used for cancellation and timeout handling
|
|
// - modelName: The name of the model being used for the response (unused in current implementation)
|
|
// - rawJSON: The raw JSON response from the Codex API
|
|
// - param: A pointer to a parameter object for maintaining state between calls
|
|
//
|
|
// Returns:
|
|
// - [][]byte: A slice of Claude Code-compatible JSON responses
|
|
func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, param *any) [][]byte {
|
|
if *param == nil {
|
|
*param = &ConvertCodexResponseToClaudeParams{
|
|
BlockIndex: 0,
|
|
}
|
|
}
|
|
|
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
|
return [][]byte{}
|
|
}
|
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
|
|
|
output := make([]byte, 0, 512)
|
|
rootResult := gjson.ParseBytes(rawJSON)
|
|
params := (*param).(*ConvertCodexResponseToClaudeParams)
|
|
if params.ThinkingBlockOpen && params.ThinkingStopPending {
|
|
switch rootResult.Get("type").String() {
|
|
case "response.content_part.added", "response.completed", "response.incomplete":
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
}
|
|
}
|
|
|
|
typeResult := rootResult.Get("type")
|
|
typeStr := typeResult.String()
|
|
var template []byte
|
|
|
|
switch typeStr {
|
|
case "error":
|
|
output = append(output, codexStreamErrorToClaudeError(rootResult)...)
|
|
case "response.created":
|
|
template = []byte(`{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"claude-opus-4-1-20250805","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0},"content":[],"stop_reason":null}}`)
|
|
template, _ = sjson.SetBytes(template, "message.model", rootResult.Get("response.model").String())
|
|
template, _ = sjson.SetBytes(template, "message.id", rootResult.Get("response.id").String())
|
|
|
|
output = translatorcommon.AppendSSEEventBytes(output, "message_start", template, 2)
|
|
case "response.reasoning_summary_part.added":
|
|
if params.ThinkingBlockOpen && params.ThinkingStopPending {
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
}
|
|
params.ThinkingSummarySeen = true
|
|
output = append(output, startCodexThinkingBlock(params)...)
|
|
case "response.reasoning_summary_text.delta":
|
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`)
|
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
|
template, _ = sjson.SetBytes(template, "delta.thinking", rootResult.Get("delta").String())
|
|
|
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
|
case "response.reasoning_summary_part.done":
|
|
params.ThinkingStopPending = true
|
|
case "response.content_part.added":
|
|
if rootResult.Get("part.type").String() == "output_text" {
|
|
output = append(output, startCodexTextBlock(params)...)
|
|
}
|
|
case "response.output_text.delta":
|
|
params.HasTextDelta = true
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
output = append(output, startCodexTextBlock(params)...)
|
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`)
|
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
|
template, _ = sjson.SetBytes(template, "delta.text", rootResult.Get("delta").String())
|
|
|
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
|
case "response.content_part.done":
|
|
if rootResult.Get("part.type").String() == "output_text" {
|
|
output = append(output, stopCodexTextBlock(params)...)
|
|
}
|
|
case "response.web_search_call.searching", "response.web_search_call.completed", "response.web_search_call.in_progress":
|
|
// Wait for populated web_search_call items on output_item.done.
|
|
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")
|
|
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)
|
|
template, _ = sjson.SetBytes(template, "usage.output_tokens", outputTokens)
|
|
if cachedTokens > 0 {
|
|
template, _ = sjson.SetBytes(template, "usage.cache_read_input_tokens", cachedTokens)
|
|
}
|
|
|
|
output = translatorcommon.AppendSSEEventBytes(output, "message_delta", template, 2)
|
|
output = translatorcommon.AppendSSEEventBytes(output, "message_stop", []byte(`{"type":"message_stop"}`), 2)
|
|
case "response.output_item.added":
|
|
itemResult := rootResult.Get("item")
|
|
itemType := itemResult.Get("type").String()
|
|
switch itemType {
|
|
case "function_call":
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
output = append(output, stopCodexTextBlock(params)...)
|
|
params.HasReceivedArgumentsDelta = false
|
|
|
|
callID := codexFunctionCallID(itemResult)
|
|
name := itemResult.Get("name").String()
|
|
if name == "" {
|
|
recordPendingCodexFunctionCall(params, rootResult, itemResult)
|
|
break
|
|
}
|
|
|
|
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()
|
|
case "web_search_call":
|
|
// Defer server_tool_use until output_item.done carries action/query.
|
|
}
|
|
case "response.output_item.done":
|
|
itemResult := rootResult.Get("item")
|
|
itemType := itemResult.Get("type").String()
|
|
switch itemType {
|
|
case "message":
|
|
if params.HasTextDelta {
|
|
return [][]byte{output}
|
|
}
|
|
contentResult := itemResult.Get("content")
|
|
if !contentResult.Exists() || !contentResult.IsArray() {
|
|
return [][]byte{output}
|
|
}
|
|
var textBuilder strings.Builder
|
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
|
if part.Get("type").String() != "output_text" {
|
|
return true
|
|
}
|
|
if txt := part.Get("text").String(); txt != "" {
|
|
textBuilder.WriteString(txt)
|
|
}
|
|
return true
|
|
})
|
|
text := textBuilder.String()
|
|
if text == "" {
|
|
return [][]byte{output}
|
|
}
|
|
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
output = append(output, startCodexTextBlock(params)...)
|
|
|
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`)
|
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
|
template, _ = sjson.SetBytes(template, "delta.text", text)
|
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
|
|
|
output = append(output, stopCodexTextBlock(params)...)
|
|
params.HasTextDelta = true
|
|
case "function_call":
|
|
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 = codexFunctionCallID(itemResult)
|
|
}
|
|
blockIndex := params.BlockIndex
|
|
output = appendCodexFunctionCallStart(output, originalRequestRawJSON, callID, name, blockIndex)
|
|
params.HasEmittedToolUse = true
|
|
pending.StartEmitted = true
|
|
|
|
args := pending.Arguments
|
|
if args == "" {
|
|
args = itemResult.Get("arguments").String()
|
|
}
|
|
if args != "" {
|
|
output = appendCodexFunctionCallArgumentDelta(output, args, 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 != "" {
|
|
params.ThinkingSignature = signature
|
|
}
|
|
if params.ThinkingSummarySeen {
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
} else {
|
|
output = append(output, finalizeCodexSignatureOnlyThinkingBlock(params)...)
|
|
}
|
|
params.ThinkingSignature = ""
|
|
params.ThinkingSummarySeen = false
|
|
case "web_search_call":
|
|
output = appendCodexWebSearchToolResult(output, params, rootResult, itemResult)
|
|
}
|
|
case "response.function_call_arguments.delta":
|
|
delta := rootResult.Get("delta").String()
|
|
key := codexArgumentsFunctionCallKey(params, rootResult)
|
|
if pending, _ := pendingCodexFunctionCallForKey(params, key); pending != nil && !pending.StartEmitted {
|
|
pending.HasReceivedArgumentsDelta = true
|
|
pending.Arguments += delta
|
|
break
|
|
}
|
|
|
|
params.HasReceivedArgumentsDelta = true
|
|
output = appendCodexFunctionCallArgumentDelta(output, delta, params.BlockIndex)
|
|
case "response.function_call_arguments.done":
|
|
key := codexArgumentsFunctionCallKey(params, rootResult)
|
|
if pending, _ := pendingCodexFunctionCallForKey(params, key); pending != nil && !pending.StartEmitted {
|
|
if !pending.HasReceivedArgumentsDelta {
|
|
pending.Arguments = rootResult.Get("arguments").String()
|
|
}
|
|
break
|
|
}
|
|
|
|
if !params.HasReceivedArgumentsDelta {
|
|
if args := rootResult.Get("arguments").String(); args != "" {
|
|
output = appendCodexFunctionCallArgumentDelta(output, args, params.BlockIndex)
|
|
params.HasReceivedArgumentsDelta = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return [][]byte{output}
|
|
}
|
|
|
|
func codexStreamErrorToClaudeError(rootResult gjson.Result) []byte {
|
|
errorResult := rootResult.Get("error")
|
|
errType := strings.TrimSpace(errorResult.Get("type").String())
|
|
if errType == "" {
|
|
errType = strings.TrimSpace(rootResult.Get("error_type").String())
|
|
}
|
|
if errType == "" {
|
|
errType = "api_error"
|
|
}
|
|
|
|
code := strings.TrimSpace(errorResult.Get("code").String())
|
|
message := strings.TrimSpace(errorResult.Get("message").String())
|
|
if message == "" {
|
|
message = strings.TrimSpace(rootResult.Get("message").String())
|
|
}
|
|
if message == "" {
|
|
message = code
|
|
}
|
|
if message == "" {
|
|
message = errType
|
|
}
|
|
|
|
if code == "cyber_policy" || errType == "invalid_request" {
|
|
errType = "invalid_request_error"
|
|
}
|
|
|
|
out := []byte(`{"type":"error","error":{"type":"api_error","message":""}}`)
|
|
out, _ = sjson.SetBytes(out, "error.type", errType)
|
|
out, _ = sjson.SetBytes(out, "error.message", message)
|
|
return translatorcommon.AppendSSEEventBytes(nil, "error", out, 2)
|
|
}
|
|
|
|
// ConvertCodexResponseToClaudeNonStream converts a non-streaming Codex response to a non-streaming Claude Code response.
|
|
// This function processes the complete Codex response and transforms it into a single Claude Code-compatible
|
|
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
|
// the information into a single response that matches the Claude Code API format.
|
|
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) []byte {
|
|
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
|
|
|
rootResult := gjson.ParseBytes(rawJSON)
|
|
typeStr := rootResult.Get("type").String()
|
|
if typeStr != "response.completed" && typeStr != "response.incomplete" {
|
|
return []byte{}
|
|
}
|
|
|
|
responseData := rootResult.Get("response")
|
|
if !responseData.Exists() {
|
|
return []byte{}
|
|
}
|
|
|
|
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
|
|
out, _ = sjson.SetBytes(out, "id", responseData.Get("id").String())
|
|
out, _ = sjson.SetBytes(out, "model", responseData.Get("model").String())
|
|
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage"))
|
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
|
if cachedTokens > 0 {
|
|
out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens)
|
|
}
|
|
|
|
hasToolCall := false
|
|
webSearchSeen := make(map[string]struct{})
|
|
|
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
|
output.ForEach(func(_, item gjson.Result) bool {
|
|
switch item.Get("type").String() {
|
|
case "reasoning":
|
|
thinkingBuilder := strings.Builder{}
|
|
signature := item.Get("encrypted_content").String()
|
|
if summary := item.Get("summary"); summary.Exists() {
|
|
if summary.IsArray() {
|
|
summary.ForEach(func(_, part gjson.Result) bool {
|
|
if txt := part.Get("text"); txt.Exists() {
|
|
thinkingBuilder.WriteString(txt.String())
|
|
} else {
|
|
thinkingBuilder.WriteString(part.String())
|
|
}
|
|
return true
|
|
})
|
|
} else {
|
|
thinkingBuilder.WriteString(summary.String())
|
|
}
|
|
}
|
|
if thinkingBuilder.Len() == 0 {
|
|
if content := item.Get("content"); content.Exists() {
|
|
if content.IsArray() {
|
|
content.ForEach(func(_, part gjson.Result) bool {
|
|
if txt := part.Get("text"); txt.Exists() {
|
|
thinkingBuilder.WriteString(txt.String())
|
|
} else {
|
|
thinkingBuilder.WriteString(part.String())
|
|
}
|
|
return true
|
|
})
|
|
} else {
|
|
thinkingBuilder.WriteString(content.String())
|
|
}
|
|
}
|
|
}
|
|
if thinkingBuilder.Len() > 0 || signature != "" {
|
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
|
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
|
if signature != "" {
|
|
block, _ = sjson.SetBytes(block, "signature", signature)
|
|
}
|
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
|
}
|
|
case "message":
|
|
if content := item.Get("content"); content.Exists() {
|
|
if content.IsArray() {
|
|
content.ForEach(func(_, part gjson.Result) bool {
|
|
if part.Get("type").String() == "output_text" {
|
|
text := part.Get("text").String()
|
|
if text != "" {
|
|
block := []byte(`{"type":"text","text":""}`)
|
|
block, _ = sjson.SetBytes(block, "text", text)
|
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
} else {
|
|
text := content.String()
|
|
if text != "" {
|
|
block := []byte(`{"type":"text","text":""}`)
|
|
block, _ = sjson.SetBytes(block, "text", text)
|
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
|
}
|
|
}
|
|
}
|
|
case "web_search_call":
|
|
out = appendCodexWebSearchNonStreamContent(out, item, webSearchSeen)
|
|
case "function_call":
|
|
hasToolCall = true
|
|
name := item.Get("name").String()
|
|
if original, ok := revNames[name]; ok {
|
|
name = original
|
|
}
|
|
|
|
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
|
toolBlock, _ = sjson.SetBytes(toolBlock, "id", shortenCodexCallIDIfNeeded(util.SanitizeClaudeToolID(item.Get("call_id").String())))
|
|
toolBlock, _ = sjson.SetBytes(toolBlock, "name", name)
|
|
inputRaw := "{}"
|
|
if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
|
|
argsJSON := gjson.Parse(argsStr)
|
|
if argsJSON.IsObject() {
|
|
inputRaw = argsJSON.Raw
|
|
}
|
|
}
|
|
toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw))
|
|
out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
out, _ = sjson.SetBytes(out, "stop_reason", mapCodexStopReasonToClaude(codexStopReason(responseData), hasToolCall))
|
|
out = setClaudeStopSequence(out, "stop_sequence", responseData)
|
|
|
|
return out
|
|
}
|
|
|
|
func codexStopReason(responseData gjson.Result) string {
|
|
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
|
if stopReason.String() == "stop" && codexStopSequence(responseData).String() != "" {
|
|
return "stop_sequence"
|
|
}
|
|
return stopReason.String()
|
|
}
|
|
if reason := responseData.Get("incomplete_details.reason"); reason.Exists() && reason.String() != "" {
|
|
return reason.String()
|
|
}
|
|
if codexStopSequence(responseData).String() != "" {
|
|
return "stop_sequence"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func mapCodexStopReasonToClaude(stopReason string, hasToolCall bool) string {
|
|
if hasToolCall {
|
|
return "tool_use"
|
|
}
|
|
|
|
switch stopReason {
|
|
case "", "stop", "completed":
|
|
return "end_turn"
|
|
case "max_tokens", "max_output_tokens":
|
|
return "max_tokens"
|
|
case "tool_use", "tool_calls", "function_call":
|
|
return "end_turn"
|
|
case "end_turn", "stop_sequence", "pause_turn", "refusal", "model_context_window_exceeded":
|
|
return stopReason
|
|
case "content_filter":
|
|
return "refusal"
|
|
default:
|
|
return "end_turn"
|
|
}
|
|
}
|
|
|
|
func codexStopSequence(responseData gjson.Result) gjson.Result {
|
|
return responseData.Get("stop_sequence")
|
|
}
|
|
|
|
func setClaudeStopSequence(out []byte, path string, responseData gjson.Result) []byte {
|
|
if stopSequence := codexStopSequence(responseData); stopSequence.Exists() && stopSequence.String() != "" {
|
|
out, _ = sjson.SetRawBytes(out, path, []byte(stopSequence.Raw))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func codexFunctionCallKey(rootResult, itemResult gjson.Result) string {
|
|
if outputIndex := rootResult.Get("output_index"); outputIndex.Exists() {
|
|
return "output:" + outputIndex.Raw
|
|
}
|
|
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
|
|
}
|
|
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 || key == "" {
|
|
return nil, ""
|
|
}
|
|
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, keysForPendingCodexFunctionCall(params, pending)
|
|
}
|
|
}
|
|
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 = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
func appendCodexFunctionCallStart(output []byte, originalRequestRawJSON []byte, callID, name string, blockIndex int) []byte {
|
|
template := []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`)
|
|
template, _ = sjson.SetBytes(template, "index", blockIndex)
|
|
template, _ = sjson.SetBytes(template, "content_block.id", shortenCodexCallIDIfNeeded(util.SanitizeClaudeToolID(callID)))
|
|
template, _ = sjson.SetBytes(template, "content_block.name", resolveCodexClaudeToolUseName(originalRequestRawJSON, name))
|
|
return translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2)
|
|
}
|
|
|
|
func appendCodexFunctionCallArgumentDelta(output []byte, partialJSON string, blockIndex int) []byte {
|
|
template := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
|
template, _ = sjson.SetBytes(template, "index", blockIndex)
|
|
template, _ = sjson.SetBytes(template, "delta.partial_json", partialJSON)
|
|
return translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
|
}
|
|
|
|
func appendCodexFunctionCallStop(output []byte, blockIndex int) []byte {
|
|
template := []byte(`{"type":"content_block_stop","index":0}`)
|
|
template, _ = sjson.SetBytes(template, "index", blockIndex)
|
|
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 {
|
|
return orig
|
|
}
|
|
return name
|
|
}
|
|
|
|
func extractResponsesUsage(usage gjson.Result) (int64, int64, int64) {
|
|
if !usage.Exists() || usage.Type == gjson.Null {
|
|
return 0, 0, 0
|
|
}
|
|
|
|
inputTokens := usage.Get("input_tokens").Int()
|
|
outputTokens := usage.Get("output_tokens").Int()
|
|
cachedTokens := usage.Get("input_tokens_details.cached_tokens").Int()
|
|
|
|
if cachedTokens > 0 {
|
|
if inputTokens >= cachedTokens {
|
|
inputTokens -= cachedTokens
|
|
} else {
|
|
inputTokens = 0
|
|
}
|
|
}
|
|
|
|
return inputTokens, outputTokens, cachedTokens
|
|
}
|
|
|
|
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
|
func buildReverseMapFromClaudeOriginalShortToOriginal(original []byte) map[string]string {
|
|
tools := gjson.GetBytes(original, "tools")
|
|
rev := map[string]string{}
|
|
if !tools.IsArray() {
|
|
return rev
|
|
}
|
|
var names []string
|
|
arr := tools.Array()
|
|
for i := 0; i < len(arr); i++ {
|
|
n := arr[i].Get("name").String()
|
|
if n != "" {
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
if len(names) > 0 {
|
|
m := buildShortNameMap(names)
|
|
for orig, short := range m {
|
|
rev[short] = orig
|
|
}
|
|
}
|
|
return rev
|
|
}
|
|
|
|
func ClaudeTokenCount(_ context.Context, count int64) []byte {
|
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
|
}
|
|
|
|
func startCodexTextBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
|
if params.TextBlockOpen {
|
|
return nil
|
|
}
|
|
|
|
template := []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`)
|
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
|
params.TextBlockOpen = true
|
|
|
|
return translatorcommon.AppendSSEEventBytes(nil, "content_block_start", template, 2)
|
|
}
|
|
|
|
func stopCodexTextBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
|
if !params.TextBlockOpen {
|
|
return nil
|
|
}
|
|
|
|
template := []byte(`{"type":"content_block_stop","index":0}`)
|
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
|
params.TextBlockOpen = false
|
|
params.BlockIndex++
|
|
|
|
return translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", template, 2)
|
|
}
|
|
|
|
func startCodexThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
|
if params.ThinkingBlockOpen {
|
|
return nil
|
|
}
|
|
|
|
template := []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`)
|
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
|
params.ThinkingBlockOpen = true
|
|
params.ThinkingStopPending = false
|
|
|
|
return translatorcommon.AppendSSEEventBytes(nil, "content_block_start", template, 2)
|
|
}
|
|
|
|
func finalizeCodexSignatureOnlyThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
|
if params.ThinkingSignature == "" {
|
|
return nil
|
|
}
|
|
|
|
output := startCodexThinkingBlock(params)
|
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
return output
|
|
}
|
|
|
|
func finalizeCodexThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
|
if !params.ThinkingBlockOpen {
|
|
return nil
|
|
}
|
|
|
|
output := make([]byte, 0, 256)
|
|
if params.ThinkingSignature != "" {
|
|
signatureDelta := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`)
|
|
signatureDelta, _ = sjson.SetBytes(signatureDelta, "index", params.BlockIndex)
|
|
signatureDelta, _ = sjson.SetBytes(signatureDelta, "delta.signature", params.ThinkingSignature)
|
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", signatureDelta, 2)
|
|
}
|
|
|
|
contentBlockStop := []byte(`{"type":"content_block_stop","index":0}`)
|
|
contentBlockStop, _ = sjson.SetBytes(contentBlockStop, "index", params.BlockIndex)
|
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", contentBlockStop, 2)
|
|
|
|
params.BlockIndex++
|
|
params.ThinkingBlockOpen = false
|
|
params.ThinkingStopPending = false
|
|
|
|
return output
|
|
}
|