mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
feat(codex): map claude output_config format to text format
- Map Claude `output_config.format` with `json_schema` type to Codex `text.format`. - Preserve custom schema name and strict configuration with appropriate defaults. Closes: #5280
This commit is contained in:
@@ -30,6 +30,7 @@ import (
|
||||
// 4. Converts tools declarations to the expected format
|
||||
// 5. Adds additional configuration parameters for the Codex API
|
||||
// 6. Maps Claude thinking configuration to Codex reasoning settings
|
||||
// 7. Maps Claude output_config format to Codex text format
|
||||
//
|
||||
// Parameters:
|
||||
// - modelName: The name of the model to use for the request
|
||||
@@ -390,6 +391,24 @@ func convertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool,
|
||||
template, _ = sjson.SetBytes(template, "stream", true)
|
||||
template, _ = sjson.SetBytes(template, "store", false)
|
||||
template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"})
|
||||
|
||||
// Map Claude output_config.format to Codex Responses text.format.
|
||||
if format := rootResult.Get("output_config.format"); format.IsObject() && format.Get("type").String() == "json_schema" && format.Get("schema").IsObject() {
|
||||
name := "cli_proxy_structured_output"
|
||||
if n := format.Get("name").String(); n != "" {
|
||||
name = n
|
||||
}
|
||||
strict := true
|
||||
if s := format.Get("strict"); s.Exists() && s.Type == gjson.False {
|
||||
strict = false
|
||||
}
|
||||
translatedFormat := []byte(`{"type":"json_schema","name":"","strict":true,"schema":{}}`)
|
||||
translatedFormat, _ = sjson.SetBytes(translatedFormat, "name", name)
|
||||
translatedFormat, _ = sjson.SetBytes(translatedFormat, "strict", strict)
|
||||
translatedFormat, _ = sjson.SetRawBytes(translatedFormat, "schema", []byte(format.Get("schema").Raw))
|
||||
template, _ = sjson.SetRawBytes(template, "text.format", translatedFormat)
|
||||
}
|
||||
|
||||
if toolsResult.IsArray() {
|
||||
template, _ = sjson.SetRawBytes(template, "tools", translatorcommon.JoinRawArray(toolItems))
|
||||
}
|
||||
|
||||
@@ -710,3 +710,111 @@ func validCodexReasoningSignature() string {
|
||||
raw[8] = 1
|
||||
return base64.URLEncoding.EncodeToString(raw)
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToCodex_OutputConfigFormat(t *testing.T) {
|
||||
t.Run("Valid json_schema format", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "gpt-5.4",
|
||||
"max_tokens": 128,
|
||||
"messages": [
|
||||
{"role": "user", "content": "Return an object with one string field named answer."}
|
||||
],
|
||||
"output_config": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answer": {"type": "string"}
|
||||
},
|
||||
"required": ["answer"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
translated := ConvertClaudeRequestToCodex("gpt-5.4", payload, false)
|
||||
root := gjson.ParseBytes(translated)
|
||||
|
||||
if !root.Get("text.format").Exists() {
|
||||
t.Fatalf("expected text.format in translated payload, got: %s", translated)
|
||||
}
|
||||
if got := root.Get("text.format.type").String(); got != "json_schema" {
|
||||
t.Errorf("expected text.format.type to be 'json_schema', got %q", got)
|
||||
}
|
||||
if got := root.Get("text.format.name").String(); got != "cli_proxy_structured_output" {
|
||||
t.Errorf("expected text.format.name to be 'cli_proxy_structured_output', got %q", got)
|
||||
}
|
||||
if got := root.Get("text.format.strict").Bool(); !got {
|
||||
t.Errorf("expected text.format.strict to be true, got %v", got)
|
||||
}
|
||||
if got := root.Get("text.format.schema.properties.answer.type").String(); got != "string" {
|
||||
t.Errorf("expected schema.properties.answer.type to be 'string', got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Valid json_schema format with custom name and strict false", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "gpt-5.4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"}
|
||||
],
|
||||
"output_config": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "custom_schema",
|
||||
"strict": false,
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
translated := ConvertClaudeRequestToCodex("gpt-5.4", payload, false)
|
||||
root := gjson.ParseBytes(translated)
|
||||
|
||||
if got := root.Get("text.format.name").String(); got != "custom_schema" {
|
||||
t.Errorf("expected text.format.name to be 'custom_schema', got %q", got)
|
||||
}
|
||||
if got := root.Get("text.format.strict").Bool(); got != false {
|
||||
t.Errorf("expected text.format.strict to be false, got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No output_config.format", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "gpt-5.4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"}
|
||||
]
|
||||
}`)
|
||||
|
||||
translated := ConvertClaudeRequestToCodex("gpt-5.4", payload, false)
|
||||
root := gjson.ParseBytes(translated)
|
||||
if root.Get("text.format").Exists() {
|
||||
t.Fatalf("expected no text.format in translated payload, got: %s", translated)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("output_config with effort only", func(t *testing.T) {
|
||||
payload := []byte(`{
|
||||
"model": "gpt-5.4",
|
||||
"thinking": {"type": "adaptive"},
|
||||
"output_config": {"effort": "high"},
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"}
|
||||
]
|
||||
}`)
|
||||
|
||||
translated := ConvertClaudeRequestToCodex("gpt-5.4", payload, false)
|
||||
root := gjson.ParseBytes(translated)
|
||||
if root.Get("text.format").Exists() {
|
||||
t.Fatalf("expected no text.format in translated payload, got: %s", translated)
|
||||
}
|
||||
if got := root.Get("reasoning.effort").String(); got != "high" {
|
||||
t.Errorf("expected reasoning.effort to be 'high', got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user