Merge remote-tracking branch 'upstream/main' into feature/ampcode-alias

This commit is contained in:
이대희
2026-02-02 12:09:31 +09:00
48 changed files with 322 additions and 6740 deletions

View File

@@ -11,7 +11,6 @@ import (
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -21,12 +20,12 @@ import (
// It extracts the model name, system instruction, message contents, and tool declarations
// from the raw JSON request and returns them in the format expected by the internal client.
// The function performs the following transformations:
// 1. Sets up a template with the model name and Codex instructions
// 2. Processes system messages and converts them to input content
// 3. Transforms message contents (text, tool_use, tool_result) to appropriate formats
// 1. Sets up a template with the model name and empty instructions field
// 2. Processes system messages and converts them to developer input content
// 3. Transforms message contents (text, image, tool_use, tool_result) to appropriate formats
// 4. Converts tools declarations to the expected format
// 5. Adds additional configuration parameters for the Codex API
// 6. Prepends a special instruction message to override system instructions
// 6. Maps Claude thinking configuration to Codex reasoning settings
//
// Parameters:
// - modelName: The name of the model to use for the request
@@ -37,13 +36,9 @@ import (
// - []byte: The transformed request data in internal client format
func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
rawJSON := bytes.Clone(inputRawJSON)
userAgent := misc.ExtractCodexUserAgent(rawJSON)
template := `{"model":"","instructions":"","input":[]}`
_, instructions := misc.CodexInstructionsForModel(modelName, "", userAgent)
template, _ = sjson.Set(template, "instructions", instructions)
rootResult := gjson.ParseBytes(rawJSON)
template, _ = sjson.Set(template, "model", modelName)
@@ -240,26 +235,6 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
template, _ = sjson.Set(template, "store", false)
template, _ = sjson.Set(template, "include", []string{"reasoning.encrypted_content"})
// Add a first message to ignore system instructions and ensure proper execution.
if misc.GetCodexInstructionsEnabled() {
inputResult := gjson.Get(template, "input")
if inputResult.Exists() && inputResult.IsArray() {
inputResults := inputResult.Array()
newInput := "[]"
for i := 0; i < len(inputResults); i++ {
if i == 0 {
firstText := inputResults[i].Get("content.0.text")
firstInstructions := "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"
if firstText.Exists() && firstText.String() != firstInstructions {
newInput, _ = sjson.SetRaw(newInput, "-1", `{"type":"message","role":"user","content":[{"type":"input_text","text":"EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"}]}`)
}
}
newInput, _ = sjson.SetRaw(newInput, "-1", inputResults[i].Raw)
}
template, _ = sjson.SetRaw(template, "input", newInput)
}
}
return []byte(template)
}

View File

@@ -13,7 +13,6 @@ import (
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/tidwall/gjson"
@@ -39,14 +38,9 @@ import (
// - []byte: The transformed request data in Codex API format
func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
rawJSON := bytes.Clone(inputRawJSON)
userAgent := misc.ExtractCodexUserAgent(rawJSON)
// Base template
out := `{"model":"","instructions":"","input":[]}`
// Inject standard Codex instructions
_, instructions := misc.CodexInstructionsForModel(modelName, "", userAgent)
out, _ = sjson.Set(out, "instructions", instructions)
root := gjson.ParseBytes(rawJSON)
// Pre-compute tool name shortening map from declared functionDeclarations

View File

@@ -12,7 +12,6 @@ import (
"strconv"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -31,7 +30,6 @@ import (
// - []byte: The transformed request data in OpenAI Responses API format
func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
rawJSON := bytes.Clone(inputRawJSON)
userAgent := misc.ExtractCodexUserAgent(rawJSON)
// Start with empty JSON object
out := `{"instructions":""}`
@@ -97,10 +95,6 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
// Extract system instructions from first system message (string or text object)
messages := gjson.GetBytes(rawJSON, "messages")
_, instructions := misc.CodexInstructionsForModel(modelName, "", userAgent)
if misc.GetCodexInstructionsEnabled() {
out, _ = sjson.Set(out, "instructions", instructions)
}
// if messages.IsArray() {
// arr := messages.Array()
// for i := 0; i < len(arr); i++ {

View File

@@ -2,18 +2,14 @@ package responses
import (
"bytes"
"strconv"
"strings"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
rawJSON := bytes.Clone(inputRawJSON)
userAgent := misc.ExtractCodexUserAgent(rawJSON)
rawJSON = misc.StripCodexUserAgent(rawJSON)
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
rawJSON, _ = sjson.SetBytes(rawJSON, "store", false)
@@ -26,87 +22,31 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte,
rawJSON, _ = sjson.DeleteBytes(rawJSON, "top_p")
rawJSON, _ = sjson.DeleteBytes(rawJSON, "service_tier")
originalInstructions := ""
originalInstructionsText := ""
originalInstructionsResult := gjson.GetBytes(rawJSON, "instructions")
if originalInstructionsResult.Exists() {
originalInstructions = originalInstructionsResult.Raw
originalInstructionsText = originalInstructionsResult.String()
}
hasOfficialInstructions, instructions := misc.CodexInstructionsForModel(modelName, originalInstructionsResult.String(), userAgent)
inputResult := gjson.GetBytes(rawJSON, "input")
var inputResults []gjson.Result
if inputResult.Exists() {
if inputResult.IsArray() {
inputResults = inputResult.Array()
} else if inputResult.Type == gjson.String {
newInput := `[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`
newInput, _ = sjson.SetRaw(newInput, "0.content.0.text", inputResult.Raw)
inputResults = gjson.Parse(newInput).Array()
}
} else {
inputResults = []gjson.Result{}
}
extractedSystemInstructions := false
if originalInstructions == "" && len(inputResults) > 0 {
for _, item := range inputResults {
if strings.EqualFold(item.Get("role").String(), "system") {
var builder strings.Builder
if content := item.Get("content"); content.Exists() && content.IsArray() {
content.ForEach(func(_, contentItem gjson.Result) bool {
text := contentItem.Get("text").String()
if builder.Len() > 0 && text != "" {
builder.WriteByte('\n')
}
builder.WriteString(text)
return true
})
}
originalInstructionsText = builder.String()
originalInstructions = strconv.Quote(originalInstructionsText)
extractedSystemInstructions = true
break
}
}
}
if hasOfficialInstructions {
newInput := "[]"
for _, item := range inputResults {
newInput, _ = sjson.SetRaw(newInput, "-1", item.Raw)
}
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", []byte(newInput))
return rawJSON
}
// log.Debugf("instructions not matched, %s\n", originalInstructions)
if len(inputResults) > 0 {
newInput := "[]"
firstMessageHandled := false
for _, item := range inputResults {
if extractedSystemInstructions && strings.EqualFold(item.Get("role").String(), "system") {
continue
}
if !firstMessageHandled {
firstText := item.Get("content.0.text")
firstInstructions := "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"
if firstText.Exists() && firstText.String() != firstInstructions {
firstTextTemplate := `{"type":"message","role":"user","content":[{"type":"input_text","text":"EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!"}]}`
firstTextTemplate, _ = sjson.Set(firstTextTemplate, "content.1.text", originalInstructionsText)
firstTextTemplate, _ = sjson.Set(firstTextTemplate, "content.1.type", "input_text")
newInput, _ = sjson.SetRaw(newInput, "-1", firstTextTemplate)
}
firstMessageHandled = true
}
newInput, _ = sjson.SetRaw(newInput, "-1", item.Raw)
}
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", []byte(newInput))
}
rawJSON, _ = sjson.SetBytes(rawJSON, "instructions", instructions)
// Convert role "system" to "developer" in input array to comply with Codex API requirements.
rawJSON = convertSystemRoleToDeveloper(rawJSON)
return rawJSON
}
// convertSystemRoleToDeveloper traverses the input array and converts any message items
// with role "system" to role "developer". This is necessary because Codex API does not
// accept "system" role in the input array.
func convertSystemRoleToDeveloper(rawJSON []byte) []byte {
inputResult := gjson.GetBytes(rawJSON, "input")
if !inputResult.IsArray() {
return rawJSON
}
inputArray := inputResult.Array()
result := rawJSON
// Directly modify role values for items with "system" role
for i := 0; i < len(inputArray); i++ {
rolePath := fmt.Sprintf("input.%d.role", i)
if gjson.GetBytes(result, rolePath).String() == "system" {
result, _ = sjson.SetBytes(result, rolePath, "developer")
}
}
return result
}

View File

@@ -0,0 +1,265 @@
package responses
import (
"testing"
"github.com/tidwall/gjson"
)
// TestConvertSystemRoleToDeveloper_BasicConversion tests the basic system -> developer role conversion
func TestConvertSystemRoleToDeveloper_BasicConversion(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "You are a pirate."}]
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Say hello."}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that system role was converted to developer
firstItemRole := gjson.Get(outputStr, "input.0.role")
if firstItemRole.String() != "developer" {
t.Errorf("Expected role 'developer', got '%s'", firstItemRole.String())
}
// Check that user role remains unchanged
secondItemRole := gjson.Get(outputStr, "input.1.role")
if secondItemRole.String() != "user" {
t.Errorf("Expected role 'user', got '%s'", secondItemRole.String())
}
// Check content is preserved
firstItemContent := gjson.Get(outputStr, "input.0.content.0.text")
if firstItemContent.String() != "You are a pirate." {
t.Errorf("Expected content 'You are a pirate.', got '%s'", firstItemContent.String())
}
}
// TestConvertSystemRoleToDeveloper_MultipleSystemMessages tests conversion with multiple system messages
func TestConvertSystemRoleToDeveloper_MultipleSystemMessages(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "You are helpful."}]
},
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "Be concise."}]
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello"}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that both system roles were converted
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "developer" {
t.Errorf("Expected first role 'developer', got '%s'", firstRole.String())
}
secondRole := gjson.Get(outputStr, "input.1.role")
if secondRole.String() != "developer" {
t.Errorf("Expected second role 'developer', got '%s'", secondRole.String())
}
// Check that user role is unchanged
thirdRole := gjson.Get(outputStr, "input.2.role")
if thirdRole.String() != "user" {
t.Errorf("Expected third role 'user', got '%s'", thirdRole.String())
}
}
// TestConvertSystemRoleToDeveloper_NoSystemMessages tests that requests without system messages are unchanged
func TestConvertSystemRoleToDeveloper_NoSystemMessages(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi there!"}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that user and assistant roles are unchanged
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "user" {
t.Errorf("Expected role 'user', got '%s'", firstRole.String())
}
secondRole := gjson.Get(outputStr, "input.1.role")
if secondRole.String() != "assistant" {
t.Errorf("Expected role 'assistant', got '%s'", secondRole.String())
}
}
// TestConvertSystemRoleToDeveloper_EmptyInput tests that empty input arrays are handled correctly
func TestConvertSystemRoleToDeveloper_EmptyInput(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": []
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that input is still an empty array
inputArray := gjson.Get(outputStr, "input")
if !inputArray.IsArray() {
t.Error("Input should still be an array")
}
if len(inputArray.Array()) != 0 {
t.Errorf("Expected empty array, got %d items", len(inputArray.Array()))
}
}
// TestConvertSystemRoleToDeveloper_NoInputField tests that requests without input field are unchanged
func TestConvertSystemRoleToDeveloper_NoInputField(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"stream": false
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check that other fields are still set correctly
stream := gjson.Get(outputStr, "stream")
if !stream.Bool() {
t.Error("Stream should be set to true by conversion")
}
store := gjson.Get(outputStr, "store")
if store.Bool() {
t.Error("Store should be set to false by conversion")
}
}
// TestConvertOpenAIResponsesRequestToCodex_OriginalIssue tests the exact issue reported by the user
func TestConvertOpenAIResponsesRequestToCodex_OriginalIssue(t *testing.T) {
// This is the exact input that was failing with "System messages are not allowed"
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": "You are a pirate. Always respond in pirate speak."
},
{
"type": "message",
"role": "user",
"content": "Say hello."
}
],
"stream": false
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Verify system role was converted to developer
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "developer" {
t.Errorf("Expected role 'developer', got '%s'", firstRole.String())
}
// Verify stream was set to true (as required by Codex)
stream := gjson.Get(outputStr, "stream")
if !stream.Bool() {
t.Error("Stream should be set to true")
}
// Verify other required fields for Codex
store := gjson.Get(outputStr, "store")
if store.Bool() {
t.Error("Store should be false")
}
parallelCalls := gjson.Get(outputStr, "parallel_tool_calls")
if !parallelCalls.Bool() {
t.Error("parallel_tool_calls should be true")
}
include := gjson.Get(outputStr, "include")
if !include.IsArray() || len(include.Array()) != 1 {
t.Error("include should be an array with one element")
} else if include.Array()[0].String() != "reasoning.encrypted_content" {
t.Errorf("Expected include[0] to be 'reasoning.encrypted_content', got '%s'", include.Array()[0].String())
}
}
// TestConvertSystemRoleToDeveloper_AssistantRole tests that assistant role is preserved
func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) {
inputJSON := []byte(`{
"model": "gpt-5.2",
"input": [
{
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": "You are helpful."}]
},
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Hello"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Hi!"}]
}
]
}`)
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
outputStr := string(output)
// Check system -> developer
firstRole := gjson.Get(outputStr, "input.0.role")
if firstRole.String() != "developer" {
t.Errorf("Expected first role 'developer', got '%s'", firstRole.String())
}
// Check user unchanged
secondRole := gjson.Get(outputStr, "input.1.role")
if secondRole.String() != "user" {
t.Errorf("Expected second role 'user', got '%s'", secondRole.String())
}
// Check assistant unchanged
thirdRole := gjson.Get(outputStr, "input.2.role")
if thirdRole.String() != "assistant" {
t.Errorf("Expected third role 'assistant', got '%s'", thirdRole.String())
}
}

View File

@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
@@ -20,7 +19,7 @@ func ConvertCodexResponseToOpenAIResponses(ctx context.Context, modelName string
typeStr := typeResult.String()
if typeStr == "response.created" || typeStr == "response.in_progress" || typeStr == "response.completed" {
if gjson.GetBytes(rawJSON, "response.instructions").Exists() {
instructions := selectInstructions(originalRequestRawJSON, requestRawJSON)
instructions := gjson.GetBytes(originalRequestRawJSON, "instructions").String()
rawJSON, _ = sjson.SetBytes(rawJSON, "response.instructions", instructions)
}
}
@@ -42,15 +41,8 @@ func ConvertCodexResponseToOpenAIResponsesNonStream(_ context.Context, modelName
responseResult := rootResult.Get("response")
template := responseResult.Raw
if responseResult.Get("instructions").Exists() {
template, _ = sjson.Set(template, "instructions", selectInstructions(originalRequestRawJSON, requestRawJSON))
instructions := gjson.GetBytes(originalRequestRawJSON, "instructions").String()
template, _ = sjson.Set(template, "instructions", instructions)
}
return template
}
func selectInstructions(originalRequestRawJSON, requestRawJSON []byte) string {
userAgent := misc.ExtractCodexUserAgent(originalRequestRawJSON)
if misc.IsOpenCodeUserAgent(userAgent) {
return gjson.GetBytes(requestRawJSON, "instructions").String()
}
return gjson.GetBytes(originalRequestRawJSON, "instructions").String()
}