Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
|
|
@ -0,0 +1,312 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||
rawJSON := inputRawJSON
|
||||
|
||||
inputResult := util.GetGJSONBytesNoCopy(rawJSON, "input")
|
||||
if inputResult.Type == gjson.String {
|
||||
input, _ := sjson.SetBytes([]byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`), "0.content.0.text", inputResult.String())
|
||||
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", input)
|
||||
inputResult = util.GetGJSONBytesNoCopy(rawJSON, "input")
|
||||
}
|
||||
|
||||
rawJSON = setCodexRequiredBool(rawJSON, "stream", true)
|
||||
rawJSON = setCodexRequiredBool(rawJSON, "store", false)
|
||||
rawJSON = setCodexRequiredBool(rawJSON, "parallel_tool_calls", true)
|
||||
rawJSON = setCodexRequiredInclude(rawJSON)
|
||||
// Codex Responses rejects token limit fields, so strip them out before forwarding.
|
||||
rawJSON = deleteCodexRequestFields(rawJSON, "max_output_tokens", "max_completion_tokens", "temperature", "top_p")
|
||||
if serviceTier := gjson.GetBytes(rawJSON, "service_tier"); serviceTier.Exists() && serviceTier.String() != "priority" {
|
||||
rawJSON = deleteCodexRequestFields(rawJSON, "service_tier")
|
||||
}
|
||||
|
||||
rawJSON = deleteCodexRequestFields(rawJSON, "truncation", "prompt_cache_options", "prompt_cache_retention")
|
||||
rawJSON = stripCodexResponsesCacheBreakpoints(rawJSON)
|
||||
rawJSON = applyResponsesCompactionCompatibility(rawJSON)
|
||||
|
||||
// Delete the user field as it is not supported by the Codex upstream.
|
||||
rawJSON = deleteCodexRequestFields(rawJSON, "user")
|
||||
|
||||
// Convert role "system" to "developer" in input array to comply with Codex API requirements.
|
||||
rawJSON = convertSystemRoleToDeveloper(rawJSON)
|
||||
rawJSON = normalizeCodexBuiltinTools(rawJSON)
|
||||
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
func setCodexRequiredBool(rawJSON []byte, path string, value bool) []byte {
|
||||
current := gjson.GetBytes(rawJSON, path)
|
||||
if value && current.Type == gjson.True || !value && current.Type == gjson.False {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
updated, errSet := sjson.SetBytes(rawJSON, path, value)
|
||||
if errSet != nil {
|
||||
return rawJSON
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func setCodexRequiredInclude(rawJSON []byte) []byte {
|
||||
current := gjson.GetBytes(rawJSON, "include")
|
||||
values := current.Array()
|
||||
if current.IsArray() && len(values) == 1 && values[0].Type == gjson.String && values[0].String() == "reasoning.encrypted_content" {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
updated, errSet := sjson.SetRawBytes(rawJSON, "include", []byte(`["reasoning.encrypted_content"]`))
|
||||
if errSet != nil {
|
||||
return rawJSON
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func deleteCodexRequestFields(rawJSON []byte, paths ...string) []byte {
|
||||
for _, path := range paths {
|
||||
if !gjson.GetBytes(rawJSON, path).Exists() {
|
||||
continue
|
||||
}
|
||||
|
||||
updated, errDelete := sjson.DeleteBytes(rawJSON, path)
|
||||
if errDelete == nil {
|
||||
rawJSON = updated
|
||||
}
|
||||
}
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
// stripCodexResponsesCacheBreakpoints removes any "prompt_cache_breakpoint" hint
|
||||
// attached to individual input[].content[] items. Some clients (e.g. GitHub
|
||||
// Copilot CLI) attach this field per content item when targeting the OpenAI
|
||||
// Responses format. Codex Responses rejects it outright:
|
||||
// {"error":{"message":"prompt_cache_breakpoint is not supported on this model", ...}}.
|
||||
// The top-level prompt_cache_options strip above does not cover this nested case.
|
||||
func stripCodexResponsesCacheBreakpoints(rawJSON []byte) []byte {
|
||||
if !bytes.Contains(rawJSON, []byte(`"prompt_cache_breakpoint"`)) {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
input := util.GetGJSONBytesNoCopy(rawJSON, "input")
|
||||
if !input.IsArray() {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
inputItems := input.Array()
|
||||
if len(inputItems) == 0 {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
changed := false
|
||||
rebuiltInput := make([][]byte, 0, len(inputItems))
|
||||
for _, item := range inputItems {
|
||||
itemRaw := []byte(item.Raw)
|
||||
content := item.Get("content")
|
||||
if content.IsArray() {
|
||||
updatedContent, contentChanged := stripPromptCacheBreakpointFromContent(content)
|
||||
if contentChanged {
|
||||
if updatedItem, errSet := sjson.SetRawBytes(itemRaw, "content", updatedContent); errSet == nil {
|
||||
itemRaw = updatedItem
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
rebuiltInput = append(rebuiltInput, itemRaw)
|
||||
}
|
||||
if !changed {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
updated, errSet := sjson.SetRawBytes(rawJSON, "input", translatorcommon.JoinRawArray(rebuiltInput))
|
||||
if errSet != nil {
|
||||
return rawJSON
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
// stripPromptCacheBreakpointFromContent removes "prompt_cache_breakpoint" from each
|
||||
// content part that carries it and reports whether anything changed.
|
||||
func stripPromptCacheBreakpointFromContent(content gjson.Result) ([]byte, bool) {
|
||||
parts := content.Array()
|
||||
hasBreakpoint := false
|
||||
for _, part := range parts {
|
||||
if part.Get("prompt_cache_breakpoint").Exists() {
|
||||
hasBreakpoint = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasBreakpoint {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
changed := false
|
||||
rebuiltParts := make([][]byte, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
partRaw := []byte(part.Raw)
|
||||
if part.Get("prompt_cache_breakpoint").Exists() {
|
||||
if updated, errDelete := sjson.DeleteBytes(partRaw, "prompt_cache_breakpoint"); errDelete == nil {
|
||||
partRaw = updated
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
rebuiltParts = append(rebuiltParts, partRaw)
|
||||
}
|
||||
if !changed {
|
||||
return nil, false
|
||||
}
|
||||
return translatorcommon.JoinRawArray(rebuiltParts), true
|
||||
}
|
||||
|
||||
// applyResponsesCompactionCompatibility handles OpenAI Responses context_management.compaction
|
||||
// for Codex upstream compatibility.
|
||||
//
|
||||
// Codex /responses currently rejects context_management with:
|
||||
// {"detail":"Unsupported parameter: context_management"}.
|
||||
//
|
||||
// Compatibility strategy:
|
||||
// 1) Remove context_management before forwarding to Codex upstream.
|
||||
func applyResponsesCompactionCompatibility(rawJSON []byte) []byte {
|
||||
if !gjson.GetBytes(rawJSON, "context_management").Exists() {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
rawJSON, _ = sjson.DeleteBytes(rawJSON, "context_management")
|
||||
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 {
|
||||
return convertSystemRoleToDeveloperWithInput(rawJSON, util.GetGJSONBytesNoCopy(rawJSON, "input"))
|
||||
}
|
||||
|
||||
func convertSystemRoleToDeveloperWithInput(rawJSON []byte, inputResult gjson.Result) []byte {
|
||||
if !inputResult.IsArray() {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
inputItems := inputResult.Array()
|
||||
if len(inputItems) == 0 {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
hasSystemRole := false
|
||||
for _, item := range inputItems {
|
||||
if item.IsObject() && item.Get("role").String() == "system" {
|
||||
hasSystemRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSystemRole {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
changed := false
|
||||
rebuiltInput := make([]json.RawMessage, 0, len(inputItems))
|
||||
for _, item := range inputItems {
|
||||
itemRaw := []byte(item.Raw)
|
||||
if item.IsObject() && item.Get("role").String() == "system" {
|
||||
updatedItem, errSetItem := sjson.SetRawBytes(itemRaw, "role", []byte(`"developer"`))
|
||||
if errSetItem != nil {
|
||||
return rawJSON
|
||||
}
|
||||
itemRaw = updatedItem
|
||||
changed = true
|
||||
}
|
||||
rebuiltInput = append(rebuiltInput, json.RawMessage(itemRaw))
|
||||
}
|
||||
if !changed {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
inputRaw, errMarshalInput := json.Marshal(rebuiltInput)
|
||||
if errMarshalInput != nil {
|
||||
return rawJSON
|
||||
}
|
||||
updated, errSetInput := sjson.SetRawBytes(rawJSON, "input", inputRaw)
|
||||
if errSetInput != nil {
|
||||
return rawJSON
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
// normalizeCodexBuiltinTools rewrites legacy/preview built-in tool variants to the
|
||||
// stable names expected by the current Codex upstream.
|
||||
func normalizeCodexBuiltinTools(rawJSON []byte) []byte {
|
||||
result := normalizeCodexBuiltinToolArray(rawJSON, "tools")
|
||||
result = normalizeCodexBuiltinToolAtPath(result, "tool_choice.type")
|
||||
return normalizeCodexBuiltinToolArray(result, "tool_choice.tools")
|
||||
}
|
||||
|
||||
func normalizeCodexBuiltinToolArray(rawJSON []byte, path string) []byte {
|
||||
tools := gjson.GetBytes(rawJSON, path)
|
||||
if !tools.IsArray() {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
changed := false
|
||||
var toolItems [][]byte
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
item := []byte(tool.Raw)
|
||||
currentType := tool.Get("type").String()
|
||||
normalizedType := normalizeCodexBuiltinToolType(currentType)
|
||||
if normalizedType != "" {
|
||||
updated, errSetType := sjson.SetBytes(item, "type", normalizedType)
|
||||
if errSetType == nil {
|
||||
item = updated
|
||||
changed = true
|
||||
log.Debugf("codex responses: normalized builtin tool type at %s.%d.type from %q to %q", path, len(toolItems), currentType, normalizedType)
|
||||
}
|
||||
}
|
||||
toolItems = append(toolItems, item)
|
||||
return true
|
||||
})
|
||||
if !changed {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
updated, errSetTools := sjson.SetRawBytes(rawJSON, path, translatorcommon.JoinRawArray(toolItems))
|
||||
if errSetTools != nil {
|
||||
return rawJSON
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func normalizeCodexBuiltinToolAtPath(rawJSON []byte, path string) []byte {
|
||||
currentType := gjson.GetBytes(rawJSON, path).String()
|
||||
normalizedType := normalizeCodexBuiltinToolType(currentType)
|
||||
if normalizedType == "" {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
updated, err := sjson.SetBytes(rawJSON, path, normalizedType)
|
||||
if err != nil {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
log.Debugf("codex responses: normalized builtin tool type at %s from %q to %q", path, currentType, normalizedType)
|
||||
return updated
|
||||
}
|
||||
|
||||
// normalizeCodexBuiltinToolType centralizes the current known Codex Responses
|
||||
// built-in tool alias compatibility. If Codex introduces more legacy aliases,
|
||||
// extend this helper instead of adding path-specific rewrite logic elsewhere.
|
||||
func normalizeCodexBuiltinToolType(toolType string) string {
|
||||
switch toolType {
|
||||
case "web_search_preview", "web_search_preview_2025_03_11":
|
||||
return "web_search"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,680 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
var benchmarkConvertSystemRoleOutput []byte
|
||||
var benchmarkConvertNormalizedOutput []byte
|
||||
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToCodexReusesNormalizedPayload(t *testing.T) {
|
||||
inputJSON := []byte(`{"model":"gpt-5.6","stream":true,"store":false,"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"service_tier":"priority","input":[{"type":"message","role":"user","content":"hello"}]}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.6", inputJSON, true)
|
||||
|
||||
if &output[0] != &inputJSON[0] {
|
||||
t.Fatal("normalized request payload was copied")
|
||||
}
|
||||
if string(output) != string(inputJSON) {
|
||||
t.Fatalf("normalized request changed:\n got: %s\nwant: %s", output, inputJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToCodexNormalizesRequiredFields(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model":"gpt-5.6",
|
||||
"stream":"true",
|
||||
"store":true,
|
||||
"parallel_tool_calls":false,
|
||||
"include":["file_search_call.results","reasoning.encrypted_content"],
|
||||
"max_output_tokens":4096,
|
||||
"max_completion_tokens":4096,
|
||||
"temperature":0.2,
|
||||
"top_p":0.9,
|
||||
"service_tier":"standard",
|
||||
"truncation":"auto",
|
||||
"prompt_cache_options":{"mode":"implicit"},
|
||||
"prompt_cache_retention":"24h",
|
||||
"user":"request-owner",
|
||||
"input":[{"type":"message","role":"system","content":"hello"}]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.6", inputJSON, true)
|
||||
|
||||
if stream := gjson.GetBytes(output, "stream"); stream.Type != gjson.True {
|
||||
t.Fatalf("stream = %s, want true", stream.Raw)
|
||||
}
|
||||
if store := gjson.GetBytes(output, "store"); store.Type != gjson.False {
|
||||
t.Fatalf("store = %s, want false", store.Raw)
|
||||
}
|
||||
if parallel := gjson.GetBytes(output, "parallel_tool_calls"); parallel.Type != gjson.True {
|
||||
t.Fatalf("parallel_tool_calls = %s, want true", parallel.Raw)
|
||||
}
|
||||
include := gjson.GetBytes(output, "include").Array()
|
||||
if len(include) != 1 || include[0].Type != gjson.String || include[0].String() != "reasoning.encrypted_content" {
|
||||
t.Fatalf("include = %s, want reasoning.encrypted_content only", gjson.GetBytes(output, "include").Raw)
|
||||
}
|
||||
if role := gjson.GetBytes(output, "input.0.role").String(); role != "developer" {
|
||||
t.Fatalf("input.0.role = %q, want developer", role)
|
||||
}
|
||||
for _, path := range []string{
|
||||
"max_output_tokens",
|
||||
"max_completion_tokens",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"service_tier",
|
||||
"truncation",
|
||||
"prompt_cache_options",
|
||||
"prompt_cache_retention",
|
||||
"user",
|
||||
} {
|
||||
if gjson.GetBytes(output, path).Exists() {
|
||||
t.Fatalf("%s should be removed: %s", path, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToCodex_FiltersPromptCacheRetention(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.6-terra",
|
||||
"prompt_cache_retention": "24h",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "hello"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.6-terra", inputJSON, true)
|
||||
if gjson.GetBytes(output, "prompt_cache_retention").Exists() {
|
||||
t.Fatalf("prompt_cache_retention should be removed: %s", string(output))
|
||||
}
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToCodex_NormalizesWebSearchPreview(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.4-mini",
|
||||
"input": "find latest OpenAI model news",
|
||||
"tools": [
|
||||
{"type": "web_search_preview_2025_03_11"}
|
||||
],
|
||||
"tool_choice": {
|
||||
"type": "allowed_tools",
|
||||
"tools": [
|
||||
{"type": "web_search_preview"},
|
||||
{"type": "web_search_preview_2025_03_11"}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.4-mini", inputJSON, false)
|
||||
|
||||
if got := gjson.GetBytes(output, "tools.0.type").String(); got != "web_search" {
|
||||
t.Fatalf("tools.0.type = %q, want %q: %s", got, "web_search", string(output))
|
||||
}
|
||||
if got := gjson.GetBytes(output, "tool_choice.type").String(); got != "allowed_tools" {
|
||||
t.Fatalf("tool_choice.type = %q, want %q: %s", got, "allowed_tools", string(output))
|
||||
}
|
||||
if got := gjson.GetBytes(output, "tool_choice.tools.0.type").String(); got != "web_search" {
|
||||
t.Fatalf("tool_choice.tools.0.type = %q, want %q: %s", got, "web_search", string(output))
|
||||
}
|
||||
if got := gjson.GetBytes(output, "tool_choice.tools.1.type").String(); got != "web_search" {
|
||||
t.Fatalf("tool_choice.tools.1.type = %q, want %q: %s", got, "web_search", string(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToCodex_NormalizesTopLevelToolChoicePreviewAlias(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.4-mini",
|
||||
"input": "find latest OpenAI model news",
|
||||
"tool_choice": {"type": "web_search_preview_2025_03_11"}
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.4-mini", inputJSON, false)
|
||||
|
||||
if got := gjson.GetBytes(output, "tool_choice.type").String(); got != "web_search" {
|
||||
t.Fatalf("tool_choice.type = %q, want %q: %s", got, "web_search", string(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserFieldDeletion(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.2",
|
||||
"user": "test-user",
|
||||
"input": [{"role": "user", "content": "Hello"}]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
// Verify user field is deleted
|
||||
userField := gjson.Get(outputStr, "user")
|
||||
if userField.Exists() {
|
||||
t.Errorf("user field should be deleted, but it was found with value: %s", userField.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextManagementCompactionCompatibility(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.2",
|
||||
"context_management": [
|
||||
{
|
||||
"type": "compaction",
|
||||
"compact_threshold": 12000
|
||||
}
|
||||
],
|
||||
"input": [{"role":"user","content":"hello"}]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
if gjson.Get(outputStr, "context_management").Exists() {
|
||||
t.Fatalf("context_management should be removed for Codex compatibility")
|
||||
}
|
||||
if gjson.Get(outputStr, "truncation").Exists() {
|
||||
t.Fatalf("truncation should be removed for Codex compatibility")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncationRemovedForCodexCompatibility(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.2",
|
||||
"truncation": "disabled",
|
||||
"input": [{"role":"user","content":"hello"}]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
if gjson.Get(outputStr, "truncation").Exists() {
|
||||
t.Fatalf("truncation should be removed for Codex compatibility")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripCodexResponsesCacheBreakpoints(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.2",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Hello world",
|
||||
"prompt_cache_breakpoint": {"mode": "explicit"}
|
||||
},
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Second part"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
if strings.Contains(outputStr, "prompt_cache_breakpoint") {
|
||||
t.Fatalf("prompt_cache_breakpoint should not exist in the output JSON")
|
||||
}
|
||||
if gjson.Get(outputStr, "input.0.content.0.text").String() != "Hello world" {
|
||||
t.Fatalf("text content should be preserved")
|
||||
}
|
||||
if gjson.Get(outputStr, "input.0.content.1.text").String() != "Second part" {
|
||||
t.Fatalf("second content part should be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripCodexResponsesCacheBreakpoints_WithSystemRole(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gpt-5.2",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "system",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "System prompt",
|
||||
"prompt_cache_breakpoint": {"mode": "explicit"}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "User query",
|
||||
"prompt_cache_breakpoint": {"mode": "explicit"}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
// Check system role is converted to developer
|
||||
if gjson.Get(outputStr, "input.0.role").String() != "developer" {
|
||||
t.Fatalf("expected role 'developer', got %q", gjson.Get(outputStr, "input.0.role").String())
|
||||
}
|
||||
// Check prompt_cache_breakpoint is completely removed from payload
|
||||
if strings.Contains(outputStr, "prompt_cache_breakpoint") {
|
||||
t.Fatalf("prompt_cache_breakpoint should not exist in the output JSON")
|
||||
}
|
||||
if gjson.Get(outputStr, "input.0.content.0.text").String() != "System prompt" {
|
||||
t.Fatalf("expected system prompt text preserved, got %q", gjson.Get(outputStr, "input.0.content.0.text").String())
|
||||
}
|
||||
if gjson.Get(outputStr, "input.1.content.0.text").String() != "User query" {
|
||||
t.Fatalf("expected user query text preserved, got %q", gjson.Get(outputStr, "input.1.content.0.text").String())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConvertSystemRoleToDeveloperLargeInput(b *testing.B) {
|
||||
cases := []struct {
|
||||
name string
|
||||
inputJSON []byte
|
||||
}{
|
||||
{
|
||||
name: "200_input_1_system",
|
||||
inputJSON: makeLargeResponsesInputForBenchmark(200, 200),
|
||||
},
|
||||
{
|
||||
name: "200_input_2_system",
|
||||
inputJSON: makeLargeResponsesInputForBenchmark(200, 100),
|
||||
},
|
||||
{
|
||||
name: "2000_input_20_system",
|
||||
inputJSON: makeLargeResponsesInputForBenchmark(2000, 100),
|
||||
},
|
||||
}
|
||||
benchmarks := []struct {
|
||||
name string
|
||||
fn func([]byte) []byte
|
||||
}{
|
||||
{
|
||||
name: "previous_root_path_rewrite",
|
||||
fn: convertSystemRoleToDeveloperPreviousRootPathRewriteForBenchmark,
|
||||
},
|
||||
{
|
||||
name: "current_rebuilt_input_json_marshal",
|
||||
fn: convertSystemRoleToDeveloper,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
for _, benchmark := range benchmarks {
|
||||
b.Run(testCase.name+"/"+benchmark.name, func(b *testing.B) {
|
||||
output := benchmark.fn(testCase.inputJSON)
|
||||
if got := gjson.GetBytes(output, "input.0.role").String(); got != "developer" {
|
||||
b.Fatalf("input.0.role = %q, want %q", got, "developer")
|
||||
}
|
||||
if got := gjson.GetBytes(output, "input.1.role").String(); got != "user" {
|
||||
b.Fatalf("input.1.role = %q, want %q", got, "user")
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(testCase.inputJSON)))
|
||||
b.ResetTimer()
|
||||
|
||||
var benchmarkOutput []byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkOutput = benchmark.fn(testCase.inputJSON)
|
||||
}
|
||||
benchmarkConvertSystemRoleOutput = benchmarkOutput
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConvertOpenAIResponsesRequestToCodexNormalizedPayload(b *testing.B) {
|
||||
cases := []struct {
|
||||
name string
|
||||
inputJSON []byte
|
||||
}{
|
||||
{name: "1KiB", inputJSON: makeNormalizedResponsesRequestForBenchmark(1 << 10)},
|
||||
{name: "1MiB", inputJSON: makeNormalizedResponsesRequestForBenchmark(1 << 20)},
|
||||
{name: "8MiB", inputJSON: makeNormalizedResponsesRequestForBenchmark(8 << 20)},
|
||||
}
|
||||
|
||||
for _, testCase := range cases {
|
||||
b.Run(testCase.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(testCase.inputJSON)))
|
||||
b.ResetTimer()
|
||||
|
||||
var output []byte
|
||||
for b.Loop() {
|
||||
output = ConvertOpenAIResponsesRequestToCodex("gpt-5.6", testCase.inputJSON, true)
|
||||
}
|
||||
benchmarkConvertNormalizedOutput = output
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeNormalizedResponsesRequestForBenchmark(contentBytes int) []byte {
|
||||
var builder strings.Builder
|
||||
builder.Grow(contentBytes + 256)
|
||||
builder.WriteString(`{"model":"gpt-5.6","stream":true,"store":false,"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"input":[{"type":"message","role":"user","content":"`)
|
||||
builder.WriteString(strings.Repeat("x", contentBytes))
|
||||
builder.WriteString(`"}]}`)
|
||||
return []byte(builder.String())
|
||||
}
|
||||
|
||||
func makeLargeResponsesInputForBenchmark(inputCount int, systemEvery int) []byte {
|
||||
var builder strings.Builder
|
||||
builder.Grow(inputCount * 96)
|
||||
builder.WriteString(`{"model":"gpt-5.2","input":[`)
|
||||
for i := 0; i < inputCount; i++ {
|
||||
if i > 0 {
|
||||
builder.WriteByte(',')
|
||||
}
|
||||
role := "user"
|
||||
if i%systemEvery == 0 {
|
||||
role = "system"
|
||||
}
|
||||
builder.WriteString(`{"type":"message","role":"`)
|
||||
builder.WriteString(role)
|
||||
builder.WriteString(`","content":[{"type":"input_text","text":"message `)
|
||||
builder.WriteString(strconv.Itoa(i))
|
||||
builder.WriteString(`"}]}`)
|
||||
}
|
||||
builder.WriteString(`]}`)
|
||||
return []byte(builder.String())
|
||||
}
|
||||
|
||||
func convertSystemRoleToDeveloperPreviousRootPathRewriteForBenchmark(rawJSON []byte) []byte {
|
||||
inputResult := gjson.GetBytes(rawJSON, "input")
|
||||
if !inputResult.IsArray() {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
inputArray := inputResult.Array()
|
||||
result := rawJSON
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertCodexResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
||||
// to OpenAI Responses SSE events (response.*).
|
||||
|
||||
func ConvertCodexResponseToOpenAIResponses(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte {
|
||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||
rawJSON = setResponsesModel(rawJSON, modelName, originalRequestRawJSON, requestRawJSON)
|
||||
out := make([]byte, 0, len(rawJSON)+len("data: "))
|
||||
out = append(out, []byte("data: ")...)
|
||||
out = append(out, rawJSON...)
|
||||
return [][]byte{out}
|
||||
}
|
||||
return [][]byte{setResponsesModel(rawJSON, modelName, originalRequestRawJSON, requestRawJSON)}
|
||||
}
|
||||
|
||||
func setResponsesModel(rawJSON []byte, modelName string, originalRequestRawJSON, requestRawJSON []byte) []byte {
|
||||
eventType := gjson.GetBytes(rawJSON, "type").String()
|
||||
if eventType != "response.created" && eventType != "response.in_progress" {
|
||||
return rawJSON
|
||||
}
|
||||
if gjson.GetBytes(rawJSON, "response.model").Exists() {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
requestModelName := translatorcommon.RequestModelName(originalRequestRawJSON, requestRawJSON)
|
||||
if requestModelName == "" {
|
||||
requestModelName = modelName
|
||||
}
|
||||
if requestModelName == "" {
|
||||
return rawJSON
|
||||
}
|
||||
|
||||
updated, errSet := sjson.SetBytes(rawJSON, "response.model", requestModelName)
|
||||
if errSet != nil {
|
||||
return rawJSON
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
// ConvertCodexResponseToOpenAIResponsesNonStream builds a single Responses JSON
|
||||
// from a non-streaming OpenAI Chat Completions response.
|
||||
func ConvertCodexResponseToOpenAIResponsesNonStream(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) []byte {
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
// Verify this is a terminal response event.
|
||||
responseType := rootResult.Get("type").String()
|
||||
if responseType != "response.completed" && responseType != "response.incomplete" {
|
||||
return []byte{}
|
||||
}
|
||||
responseResult := rootResult.Get("response")
|
||||
return []byte(responseResult.Raw)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertCodexResponseToOpenAIResponses_CreatedIncludesOriginalRequestModel(t *testing.T) {
|
||||
request := []byte(`{"model":"original-codex-model"}`)
|
||||
translatedRequest := []byte(`{"model":"translated-codex-model"}`)
|
||||
for eventName, raw := range map[string][]byte{
|
||||
"response.created": []byte(`data: {"type":"response.created","response":{"id":"resp_1"}}`),
|
||||
"response.in_progress": []byte(`data: {"type":"response.in_progress","response":{"id":"resp_1"}}`),
|
||||
} {
|
||||
outputs := ConvertCodexResponseToOpenAIResponses(context.Background(), "fallback-model", request, translatedRequest, raw, nil)
|
||||
if len(outputs) != 1 {
|
||||
t.Fatalf("%s outputs = %d, want 1", eventName, len(outputs))
|
||||
}
|
||||
if got := gjson.GetBytes(outputs[0], "response.model").String(); got != "original-codex-model" {
|
||||
t.Fatalf("%s models = %q, want original-codex-model; payload=%s", eventName, got, outputs[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCodexResponseToOpenAIResponsesNonStreamIncomplete(t *testing.T) {
|
||||
raw := []byte(`{"type":"response.incomplete","response":{"id":"resp_1","status":"incomplete","incomplete_details":{"reason":"max_output_tokens"},"output":[],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}}`)
|
||||
|
||||
out := ConvertCodexResponseToOpenAIResponsesNonStream(context.Background(), "gpt-5.5", nil, nil, raw, nil)
|
||||
|
||||
if got := gjson.GetBytes(out, "status").String(); got != "incomplete" {
|
||||
t.Fatalf("status = %q, want incomplete; payload=%s", got, out)
|
||||
}
|
||||
if got := gjson.GetBytes(out, "incomplete_details.reason").String(); got != "max_output_tokens" {
|
||||
t.Fatalf("incomplete reason = %q, want max_output_tokens; payload=%s", got, out)
|
||||
}
|
||||
}
|
||||
19
backend/internal/translator/codex/openai/responses/init.go
Normal file
19
backend/internal/translator/codex/openai/responses/init.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
. "github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
OpenaiResponse,
|
||||
Codex,
|
||||
ConvertOpenAIResponsesRequestToCodex,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertCodexResponseToOpenAIResponses,
|
||||
NonStream: ConvertCodexResponseToOpenAIResponsesNonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue