mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-09 09:56:49 +08:00
675 lines
22 KiB
Go
675 lines
22 KiB
Go
package claude
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
func TestConvertClaudeRequestToCodex_SystemMessageScenarios(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inputJSON string
|
|
wantHasDeveloper bool
|
|
wantTexts []string
|
|
}{
|
|
{
|
|
name: "No system field",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantHasDeveloper: false,
|
|
},
|
|
{
|
|
name: "Empty string system field",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"system": "",
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantHasDeveloper: false,
|
|
},
|
|
{
|
|
name: "String system field",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"system": "Be helpful",
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantHasDeveloper: true,
|
|
wantTexts: []string{"Be helpful"},
|
|
},
|
|
{
|
|
name: "Message system role does not become developer",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"messages": [
|
|
{"role": "system", "content": "Follow the project instructions"},
|
|
{"role": "user", "content": "hello"}
|
|
]
|
|
}`,
|
|
wantHasDeveloper: false,
|
|
},
|
|
{
|
|
name: "Array system field with filtered billing header",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"system": [
|
|
{"type": "text", "text": "x-anthropic-billing-header: tenant-123"},
|
|
{"type": "text", "text": "Block 1"},
|
|
{"type": "text", "text": "Block 2"}
|
|
],
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantHasDeveloper: true,
|
|
wantTexts: []string{"Block 1", "Block 2"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
inputs := resultJSON.Get("input").Array()
|
|
|
|
hasDeveloper := len(inputs) > 0 && inputs[0].Get("role").String() == "developer"
|
|
if hasDeveloper != tt.wantHasDeveloper {
|
|
t.Fatalf("got hasDeveloper = %v, want %v. Output: %s", hasDeveloper, tt.wantHasDeveloper, resultJSON.Get("input").Raw)
|
|
}
|
|
|
|
if !tt.wantHasDeveloper {
|
|
return
|
|
}
|
|
|
|
content := inputs[0].Get("content").Array()
|
|
if len(content) != len(tt.wantTexts) {
|
|
t.Fatalf("got %d system content items, want %d. Content: %s", len(content), len(tt.wantTexts), inputs[0].Get("content").Raw)
|
|
}
|
|
|
|
for i, wantText := range tt.wantTexts {
|
|
if gotType := content[i].Get("type").String(); gotType != "input_text" {
|
|
t.Fatalf("content[%d] type = %q, want %q", i, gotType, "input_text")
|
|
}
|
|
if gotText := content[i].Get("text").String(); gotText != wantText {
|
|
t.Fatalf("content[%d] text = %q, want %q", i, gotText, wantText)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_MessageSystemRoleWrapsAsUserReminder(t *testing.T) {
|
|
inputJSON := `{
|
|
"model": "claude-3-opus",
|
|
"system": [{"type": "text", "text": "Top-level rules"}],
|
|
"messages": [
|
|
{"role": "user", "content": "hello"},
|
|
{"role": "system", "content": "Follow the project instructions"},
|
|
{"role": "assistant", "content": [{"type": "text", "text": "ok"}]},
|
|
{"role": "system", "content": [{"type": "text", "text": "Use the current repo"}]}
|
|
]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
inputs := gjson.GetBytes(result, "input").Array()
|
|
if len(inputs) != 5 {
|
|
t.Fatalf("got %d input items, want 5: %s", len(inputs), gjson.GetBytes(result, "input").Raw)
|
|
}
|
|
|
|
if got := inputs[0].Get("role").String(); got != "developer" {
|
|
t.Fatalf("top-level system role = %q, want developer", got)
|
|
}
|
|
if got := inputs[2].Get("role").String(); got != "user" {
|
|
t.Fatalf("message-level system role = %q, want user", got)
|
|
}
|
|
if got := inputs[2].Get("content.0.text").String(); got != "<system-reminder>\nFollow the project instructions\n</system-reminder>" {
|
|
t.Fatalf("unexpected first reminder text: %q", got)
|
|
}
|
|
if got := inputs[4].Get("role").String(); got != "user" {
|
|
t.Fatalf("array message-level system role = %q, want user", got)
|
|
}
|
|
if got := inputs[4].Get("content.0.text").String(); got != "<system-reminder>\nUse the current repo\n</system-reminder>" {
|
|
t.Fatalf("unexpected second reminder text: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inputJSON string
|
|
wantParallelToolCalls bool
|
|
}{
|
|
{
|
|
name: "Default to true when tool_choice.disable_parallel_tool_use is absent",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantParallelToolCalls: true,
|
|
},
|
|
{
|
|
name: "Disable parallel tool calls when client opts out",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"tool_choice": {"disable_parallel_tool_use": true},
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantParallelToolCalls: false,
|
|
},
|
|
{
|
|
name: "Keep parallel tool calls enabled when client explicitly allows them",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"tool_choice": {"disable_parallel_tool_use": false},
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`,
|
|
wantParallelToolCalls: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
|
|
if got := resultJSON.Get("parallel_tool_calls").Bool(); got != tt.wantParallelToolCalls {
|
|
t.Fatalf("parallel_tool_calls = %v, want %v. Output: %s", got, tt.wantParallelToolCalls, string(result))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_ServiceTier(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serviceTierJSON string
|
|
speedJSON string
|
|
want string
|
|
wantExists bool
|
|
}{
|
|
{
|
|
name: "Priority passes through",
|
|
serviceTierJSON: `"priority"`,
|
|
want: "priority",
|
|
wantExists: true,
|
|
},
|
|
{
|
|
name: "Fast tier normalizes to priority",
|
|
serviceTierJSON: `"fast"`,
|
|
want: "priority",
|
|
wantExists: true,
|
|
},
|
|
{
|
|
name: "Unsupported tier is omitted",
|
|
serviceTierJSON: `"default"`,
|
|
},
|
|
{
|
|
name: "Non-string tier is omitted",
|
|
serviceTierJSON: `true`,
|
|
},
|
|
{
|
|
name: "Fast speed maps to priority",
|
|
speedJSON: `"fast"`,
|
|
want: "priority",
|
|
wantExists: true,
|
|
},
|
|
{
|
|
name: "Standard speed is omitted",
|
|
speedJSON: `"standard"`,
|
|
},
|
|
{
|
|
name: "Non-string speed is omitted",
|
|
speedJSON: `true`,
|
|
},
|
|
{
|
|
name: "Fast speed overrides unsupported Anthropic tier",
|
|
serviceTierJSON: `"auto"`,
|
|
speedJSON: `"fast"`,
|
|
want: "priority",
|
|
wantExists: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
inputJSON := []byte(`{
|
|
"model": "gpt-5.4",
|
|
"messages": [{"role": "user", "content": "Reply with OK"}]
|
|
}`)
|
|
if tt.serviceTierJSON != "" {
|
|
inputJSON, _ = sjson.SetRawBytes(inputJSON, "service_tier", []byte(tt.serviceTierJSON))
|
|
}
|
|
if tt.speedJSON != "" {
|
|
inputJSON, _ = sjson.SetRawBytes(inputJSON, "speed", []byte(tt.speedJSON))
|
|
}
|
|
|
|
result := ConvertClaudeRequestToCodex("gpt-5.4", inputJSON, false)
|
|
serviceTierResult := gjson.GetBytes(result, "service_tier")
|
|
if serviceTierResult.Exists() != tt.wantExists {
|
|
t.Fatalf("service_tier exists = %v, want %v. Output: %s", serviceTierResult.Exists(), tt.wantExists, string(result))
|
|
}
|
|
if !tt.wantExists {
|
|
return
|
|
}
|
|
if got := serviceTierResult.String(); got != tt.want {
|
|
t.Fatalf("service_tier = %q, want %q. Output: %s", got, tt.want, string(result))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_ShortenLongToolUseIDs(t *testing.T) {
|
|
longID := "toolu_" + strings.Repeat("a", 62)
|
|
if len(longID) <= 64 {
|
|
t.Fatalf("test setup error: longID length = %d, want > 64", len(longID))
|
|
}
|
|
|
|
inputJSON := `{
|
|
"model": "claude-3-opus",
|
|
"messages": [
|
|
{"role": "user", "content": [{"type":"text","text":"run pwd"}]},
|
|
{"role": "assistant", "content": [
|
|
{"type":"tool_use","id":"` + longID + `","name":"Bash","input":{"cmd":"pwd"}}
|
|
]},
|
|
{"role": "user", "content": [
|
|
{"type":"tool_result","tool_use_id":"` + longID + `","content":"ok"}
|
|
]}
|
|
]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
inputs := gjson.GetBytes(result, "input").Array()
|
|
|
|
var callID string
|
|
var outputCallID string
|
|
for _, item := range inputs {
|
|
switch item.Get("type").String() {
|
|
case "function_call":
|
|
callID = item.Get("call_id").String()
|
|
case "function_call_output":
|
|
outputCallID = item.Get("call_id").String()
|
|
}
|
|
}
|
|
|
|
if callID == "" {
|
|
t.Fatalf("missing function_call item. Output: %s", string(result))
|
|
}
|
|
if outputCallID == "" {
|
|
t.Fatalf("missing function_call_output item. Output: %s", string(result))
|
|
}
|
|
if callID != outputCallID {
|
|
t.Fatalf("call_id mismatch: function_call=%q function_call_output=%q. Output: %s", callID, outputCallID, string(result))
|
|
}
|
|
if len(callID) > 64 {
|
|
t.Fatalf("call_id length = %d, want <= 64: %q", len(callID), callID)
|
|
}
|
|
if callID == longID {
|
|
t.Fatalf("long call_id was not shortened: %q", callID)
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_ToolChoiceModeMapping(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
claudeToolChoice string
|
|
wantCodexToolChoice string
|
|
}{
|
|
{
|
|
name: "Any requires at least one tool",
|
|
claudeToolChoice: `{"type":"any"}`,
|
|
wantCodexToolChoice: "required",
|
|
},
|
|
{
|
|
name: "None disables tools",
|
|
claudeToolChoice: `{"type":"none"}`,
|
|
wantCodexToolChoice: "none",
|
|
},
|
|
{
|
|
name: "Auto stays auto",
|
|
claudeToolChoice: `{"type":"auto"}`,
|
|
wantCodexToolChoice: "auto",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
inputJSON := `{
|
|
"model": "claude-3-opus",
|
|
"tools": [
|
|
{"name": "lookup", "description": "Lookup", "input_schema": {"type":"object","properties":{}}}
|
|
],
|
|
"tool_choice": ` + tt.claudeToolChoice + `,
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
|
|
if got := resultJSON.Get("tool_choice").String(); got != tt.wantCodexToolChoice {
|
|
t.Fatalf("tool_choice = %q, want %q. Output: %s", got, tt.wantCodexToolChoice, string(result))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_ToolChoiceSpecificFunctionUsesConvertedName(t *testing.T) {
|
|
longName := "mcp__server_with_a_very_long_name_that_exceeds_sixty_four_characters__search"
|
|
inputJSON := `{
|
|
"model": "claude-3-opus",
|
|
"tools": [
|
|
{"name": "` + longName + `", "description": "Search", "input_schema": {"type":"object","properties":{}}}
|
|
],
|
|
"tool_choice": {"type":"tool","name":"` + longName + `"},
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
|
|
if got := resultJSON.Get("tool_choice.type").String(); got != "function" {
|
|
t.Fatalf("tool_choice.type = %q, want function. Output: %s", got, string(result))
|
|
}
|
|
toolName := resultJSON.Get("tools.0.name").String()
|
|
choiceName := resultJSON.Get("tool_choice.name").String()
|
|
if choiceName != toolName {
|
|
t.Fatalf("tool_choice.name = %q, want converted tool name %q. Output: %s", choiceName, toolName, string(result))
|
|
}
|
|
if choiceName == longName {
|
|
t.Fatalf("tool_choice.name should use shortened Codex tool name. Output: %s", string(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_WebSearchToolMapping(t *testing.T) {
|
|
inputJSON := `{
|
|
"model": "claude-3-opus",
|
|
"tools": [
|
|
{
|
|
"type": "web_search_20260209",
|
|
"name": "web_search",
|
|
"allowed_domains": ["example.com"],
|
|
"blocked_domains": ["blocked.example"],
|
|
"user_location": {
|
|
"type": "approximate",
|
|
"city": "Beijing",
|
|
"country": "CN",
|
|
"timezone": "Asia/Shanghai"
|
|
}
|
|
}
|
|
],
|
|
"tool_choice": {"type":"tool","name":"web_search"},
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
|
|
if got := resultJSON.Get("tools.0.type").String(); got != "web_search" {
|
|
t.Fatalf("tools.0.type = %q, want web_search. Output: %s", got, string(result))
|
|
}
|
|
if got := resultJSON.Get("tools.0.filters.allowed_domains.0").String(); got != "example.com" {
|
|
t.Fatalf("tools.0.filters.allowed_domains.0 = %q, want example.com. Output: %s", got, string(result))
|
|
}
|
|
if resultJSON.Get("tools.0.blocked_domains").Exists() {
|
|
t.Fatalf("tools.0.blocked_domains should not be forwarded to Codex. Output: %s", string(result))
|
|
}
|
|
if got := resultJSON.Get("tools.0.user_location.city").String(); got != "Beijing" {
|
|
t.Fatalf("tools.0.user_location.city = %q, want Beijing. Output: %s", got, string(result))
|
|
}
|
|
if got := resultJSON.Get("tool_choice.type").String(); got != "web_search" {
|
|
t.Fatalf("tool_choice.type = %q, want web_search. Output: %s", got, string(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_WebSearchToolChoiceUsesDeclaredTypedToolName(t *testing.T) {
|
|
inputJSON := `{
|
|
"model": "claude-opus-4-7",
|
|
"tools": [
|
|
{"type": "web_search_20250305", "name": "browser_search"},
|
|
{"name": "web_search", "description": "Local search", "input_schema": {"type":"object","properties":{}}}
|
|
],
|
|
"tool_choice": {"type":"tool","name":"web_search"},
|
|
"messages": [{"role": "user", "content": "hello"}]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
|
|
if got := resultJSON.Get("tool_choice.type").String(); got != "function" {
|
|
t.Fatalf("tool_choice.type = %q, want function. Output: %s", got, string(result))
|
|
}
|
|
if got := resultJSON.Get("tool_choice.name").String(); got != "web_search" {
|
|
t.Fatalf("tool_choice.name = %q, want web_search. Output: %s", got, string(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t *testing.T) {
|
|
signature := validCodexReasoningSignature()
|
|
inputJSON := `{
|
|
"model": "claude-3-opus",
|
|
"messages": [
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": "visible summary must not be replayed",
|
|
"signature": "` + signature + `"
|
|
},
|
|
{
|
|
"type": "text",
|
|
"text": "visible answer"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "continue"
|
|
}
|
|
]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
|
resultJSON := gjson.ParseBytes(result)
|
|
inputs := resultJSON.Get("input").Array()
|
|
if len(inputs) != 3 {
|
|
t.Fatalf("got %d input items, want 3. Output: %s", len(inputs), string(result))
|
|
}
|
|
|
|
reasoning := inputs[0]
|
|
if got := reasoning.Get("type").String(); got != "reasoning" {
|
|
t.Fatalf("first input type = %q, want reasoning. Output: %s", got, string(result))
|
|
}
|
|
if got := reasoning.Get("encrypted_content").String(); got != signature {
|
|
t.Fatalf("encrypted_content = %q, want %q", got, signature)
|
|
}
|
|
if got := reasoning.Get("summary").Raw; got != "[]" {
|
|
t.Fatalf("summary = %s, want []", got)
|
|
}
|
|
if got := reasoning.Get("content").Raw; got != "null" {
|
|
t.Fatalf("content = %s, want null", got)
|
|
}
|
|
|
|
assistantMessage := inputs[1]
|
|
if got := assistantMessage.Get("role").String(); got != "assistant" {
|
|
t.Fatalf("second input role = %q, want assistant. Output: %s", got, string(result))
|
|
}
|
|
if got := assistantMessage.Get("content.0.type").String(); got != "output_text" {
|
|
t.Fatalf("assistant content type = %q, want output_text", got)
|
|
}
|
|
if got := assistantMessage.Get("content.0.text").String(); got != "visible answer" {
|
|
t.Fatalf("assistant text = %q, want visible answer", got)
|
|
}
|
|
if strings.Contains(string(result), "visible summary must not be replayed") {
|
|
t.Fatalf("thinking text should not be replayed into Codex input. Output: %s", string(result))
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_PreservesContentOrderAcrossToolAndReasoningItems(t *testing.T) {
|
|
signature := validCodexReasoningSignature()
|
|
inputJSON := `{
|
|
"system": "system rules",
|
|
"messages": [
|
|
{"role":"assistant","content":[
|
|
{"type":"text","text":"before reasoning"},
|
|
{"type":"thinking","signature":"` + signature + `"},
|
|
{"type":"text","text":"before tool"},
|
|
{"type":"tool_use","id":"toolu_1","name":"lookup","input":{"query":"test"}},
|
|
{"type":"text","text":"after tool"}
|
|
]},
|
|
{"role":"user","content":[
|
|
{"type":"tool_result","tool_use_id":"toolu_1","content":[
|
|
{"type":"text","text":"tool output"},
|
|
{"type":"image","source":{"media_type":"image/png","data":"aW1hZ2U="}}
|
|
]},
|
|
{"type":"text","text":"continue"}
|
|
]}
|
|
],
|
|
"tools": [{"name":"lookup","input_schema":{"type":"object"}}]
|
|
}`
|
|
|
|
result := ConvertClaudeRequestToCodex("gpt-5.4", []byte(inputJSON), false)
|
|
inputs := gjson.GetBytes(result, "input").Array()
|
|
if len(inputs) != 8 {
|
|
t.Fatalf("got %d input items, want 8. Output: %s", len(inputs), result)
|
|
}
|
|
|
|
wantTypes := []string{"message", "message", "reasoning", "message", "function_call", "message", "function_call_output", "message"}
|
|
for i := 0; i < len(wantTypes); i++ {
|
|
if got := inputs[i].Get("type").String(); got != wantTypes[i] {
|
|
t.Fatalf("input[%d].type = %q, want %q. Output: %s", i, got, wantTypes[i], result)
|
|
}
|
|
}
|
|
|
|
if got := inputs[1].Get("content.0.text").String(); got != "before reasoning" {
|
|
t.Fatalf("input[1] text = %q, want before reasoning", got)
|
|
}
|
|
if got := inputs[3].Get("content.0.text").String(); got != "before tool" {
|
|
t.Fatalf("input[3] text = %q, want before tool", got)
|
|
}
|
|
if got := inputs[5].Get("content.0.text").String(); got != "after tool" {
|
|
t.Fatalf("input[5] text = %q, want after tool", got)
|
|
}
|
|
if got := inputs[6].Get("output.0.type").String(); got != "input_text" {
|
|
t.Fatalf("tool result output.0.type = %q, want input_text", got)
|
|
}
|
|
if got := inputs[6].Get("output.1.image_url").String(); got != "data:image/png;base64,aW1hZ2U=" {
|
|
t.Fatalf("tool result image_url = %q, want data URL", got)
|
|
}
|
|
if got := inputs[7].Get("content.0.text").String(); got != "continue" {
|
|
t.Fatalf("input[7] text = %q, want continue", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_AssistantGrokSignatureToReasoningItem(t *testing.T) {
|
|
signature := "HmlYdr2aCAqCYP/m9mr8PS6KOsdMs72FGDigmydR+Jsmuv8KX97yWPlbOwmXJgWn0CbHaCacdQD3+n5EvpgLfPNmafS3kdICBjRuDf4bzHy7uBiUhNVhqPtp/ee1y9q4imPE4LYgD1VZ4J+bp9mTeqA1+nC9Oue58CiNEMV9SVaGenCD+aBnVuSTzQhD32Y+68i6HLJW0Dx6ifaRfb8hxYtA/sPM+/FTvAMW11nRho5a2BBSkpnzfqqAz/e/vGJ77/bygpXM823QA9wL9i0X"
|
|
payload := []byte(`{"model":"grok-4.5","messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"summary","signature":""},{"type":"text","text":"answer"}]},{"role":"user","content":"next"}]}`)
|
|
payload, _ = sjson.SetBytes(payload, "messages.0.content.0.signature", signature)
|
|
|
|
out := ConvertClaudeRequestToCodex("grok-4.5", payload, false)
|
|
reasoning := gjson.GetBytes(out, "input.0")
|
|
if reasoning.Get("type").String() != "reasoning" {
|
|
t.Fatalf("input.0 type = %q, want reasoning; output=%s", reasoning.Get("type").String(), out)
|
|
}
|
|
if got := reasoning.Get("encrypted_content").String(); got != signature {
|
|
t.Fatalf("encrypted_content = %q, want Grok signature", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_IgnoresGrokSignatureForNonGrokTargets(t *testing.T) {
|
|
signature := "HmlYdr2aCAqCYP/m9mr8PS6KOsdMs72FGDigmydR+Jsmuv8KX97yWPlbOwmXJgWn0CbHaCacdQD3+n5EvpgLfPNmafS3kdICBjRuDf4bzHy7uBiUhNVhqPtp/ee1y9q4imPE4LYgD1VZ4J+bp9mTeqA1+nC9Oue58CiNEMV9SVaGenCD+aBnVuSTzQhD32Y+68i6HLJW0Dx6ifaRfb8hxYtA/sPM+/FTvAMW11nRho5a2BBSkpnzfqqAz/e/vGJ77/bygpXM823QA9wL9i0X"
|
|
payload := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"summary","signature":""},{"type":"text","text":"answer"}]},{"role":"user","content":"next"}]}`)
|
|
payload, _ = sjson.SetBytes(payload, "messages.0.content.0.signature", signature)
|
|
|
|
for _, modelName := range []string{"gpt-5.4", "claude-sonnet-4-6"} {
|
|
t.Run(modelName, func(t *testing.T) {
|
|
out := ConvertClaudeRequestToCodex(modelName, payload, false)
|
|
if got := countRequestInputItemsByType(out, "reasoning"); got != 0 {
|
|
t.Fatalf("got %d reasoning items for non-Grok target, want 0; output=%s", got, out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeRequestToCodex_IgnoresNonCodexThinkingSignatures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
inputJSON string
|
|
}{
|
|
{
|
|
name: "Ignore user thinking even with Codex-shaped signature",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": "user supplied thinking",
|
|
"signature": "` + validCodexReasoningSignature() + `"
|
|
},
|
|
{
|
|
"type": "text",
|
|
"text": "hello"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
},
|
|
{
|
|
name: "Ignore Anthropic native signature",
|
|
inputJSON: `{
|
|
"model": "claude-3-opus",
|
|
"messages": [
|
|
{
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "thinking",
|
|
"thinking": "anthropic thinking",
|
|
"signature": "Eo8Canthropic-state"
|
|
},
|
|
{
|
|
"type": "text",
|
|
"text": "visible answer"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false)
|
|
if got := countRequestInputItemsByType(result, "reasoning"); got != 0 {
|
|
t.Fatalf("got %d reasoning items, want 0. Output: %s", got, string(result))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func countRequestInputItemsByType(result []byte, itemType string) int {
|
|
count := 0
|
|
gjson.GetBytes(result, "input").ForEach(func(_, item gjson.Result) bool {
|
|
if item.Get("type").String() == itemType {
|
|
count++
|
|
}
|
|
return true
|
|
})
|
|
return count
|
|
}
|
|
|
|
func validCodexReasoningSignature() string {
|
|
raw := make([]byte, 1+8+16+16+32)
|
|
raw[0] = 0x80
|
|
raw[8] = 1
|
|
return base64.URLEncoding.EncodeToString(raw)
|
|
}
|