2026-01-09 18:35:58 +08:00
package service
import (
2026-04-24 12:03:19 +00:00
"encoding/json"
2026-03-11 21:12:07 +08:00
"fmt"
2026-01-09 18:35:58 +08:00
"strings"
)
var codexModelMap = map [ string ] string {
2026-04-23 17:28:01 +08:00
"gpt-5.5" : "gpt-5.5" ,
2026-03-06 08:14:04 +08:00
"gpt-5.4" : "gpt-5.4" ,
2026-03-19 19:00:22 +08:00
"gpt-5.4-mini" : "gpt-5.4-mini" ,
2026-03-06 08:14:04 +08:00
"gpt-5.4-none" : "gpt-5.4" ,
"gpt-5.4-low" : "gpt-5.4" ,
"gpt-5.4-medium" : "gpt-5.4" ,
"gpt-5.4-high" : "gpt-5.4" ,
"gpt-5.4-xhigh" : "gpt-5.4" ,
"gpt-5.4-chat-latest" : "gpt-5.4" ,
2026-02-13 09:28:07 +08:00
"gpt-5.3" : "gpt-5.3-codex" ,
"gpt-5.3-none" : "gpt-5.3-codex" ,
"gpt-5.3-low" : "gpt-5.3-codex" ,
"gpt-5.3-medium" : "gpt-5.3-codex" ,
"gpt-5.3-high" : "gpt-5.3-codex" ,
"gpt-5.3-xhigh" : "gpt-5.3-codex" ,
"gpt-5.3-codex" : "gpt-5.3-codex" ,
2026-04-20 22:01:09 +08:00
"gpt-5.3-codex-spark" : "gpt-5.3-codex-spark" ,
"gpt-5.3-codex-spark-low" : "gpt-5.3-codex-spark" ,
"gpt-5.3-codex-spark-medium" : "gpt-5.3-codex-spark" ,
"gpt-5.3-codex-spark-high" : "gpt-5.3-codex-spark" ,
"gpt-5.3-codex-spark-xhigh" : "gpt-5.3-codex-spark" ,
2026-02-13 09:28:07 +08:00
"gpt-5.3-codex-low" : "gpt-5.3-codex" ,
"gpt-5.3-codex-medium" : "gpt-5.3-codex" ,
"gpt-5.3-codex-high" : "gpt-5.3-codex" ,
"gpt-5.3-codex-xhigh" : "gpt-5.3-codex" ,
"gpt-5.2" : "gpt-5.2" ,
"gpt-5.2-none" : "gpt-5.2" ,
"gpt-5.2-low" : "gpt-5.2" ,
"gpt-5.2-medium" : "gpt-5.2" ,
"gpt-5.2-high" : "gpt-5.2" ,
"gpt-5.2-xhigh" : "gpt-5.2" ,
2026-01-09 18:35:58 +08:00
}
type codexTransformResult struct {
Modified bool
NormalizedModel string
PromptCacheKey string
}
2026-04-23 15:13:57 +00:00
const (
codexImageGenerationBridgeMarker = "<sub2api-codex-image-generation>"
codexImageGenerationBridgeText = codexImageGenerationBridgeMarker + "\nWhen the user asks for raster image generation or editing, use the OpenAI Responses native `image_generation` tool attached to this request. The local Codex client may not expose an `image_gen` namespace, but that does not mean image generation is unavailable. Do not ask the user to switch to CLI fallback solely because `image_gen` is absent.\n</sub2api-codex-image-generation>"
2026-04-24 07:42:31 +00:00
codexSparkImageUnsupportedMarker = "<sub2api-codex-spark-image-unsupported>"
codexSparkImageUnsupportedText = codexSparkImageUnsupportedMarker + "\nThe current model is gpt-5.3-codex-spark, which does not support image generation, image editing, image input, the `image_generation` tool, or Codex `image_gen`/`$imagegen` workflows. If the user asks for image generation or image editing, clearly explain this model limitation and ask them to switch to a non-Spark Codex model such as gpt-5.3-codex or gpt-5.4. Do not claim that the local environment merely lacks image_gen tooling, and do not suggest CLI fallback as the primary fix while the model remains Spark.\n</sub2api-codex-spark-image-unsupported>"
2026-04-23 15:13:57 +00:00
)
2026-03-06 18:50:28 +08:00
func applyCodexOAuthTransform ( reqBody map [ string ] any , isCodexCLI bool , isCompact bool ) codexTransformResult {
2026-01-09 18:35:58 +08:00
result := codexTransformResult {}
2026-01-13 16:47:35 +08:00
// 工具续链需求会影响存储策略与 input 过滤逻辑。
needsToolContinuation := NeedsToolContinuation ( reqBody )
2026-01-09 18:35:58 +08:00
model := ""
if v , ok := reqBody [ "model" ].( string ); ok {
model = v
}
2026-03-24 19:20:15 +08:00
normalizedModel := strings . TrimSpace ( model )
2026-01-09 18:35:58 +08:00
if normalizedModel != "" {
if model != normalizedModel {
reqBody [ "model" ] = normalizedModel
result . Modified = true
}
result . NormalizedModel = normalizedModel
}
2026-03-06 18:50:28 +08:00
if isCompact {
if _ , ok := reqBody [ "store" ]; ok {
delete ( reqBody , "store" )
result . Modified = true
}
if _ , ok := reqBody [ "stream" ]; ok {
delete ( reqBody , "stream" )
result . Modified = true
}
} else {
// OAuth 走 ChatGPT internal API 时,store 必须为 false;显式 true 也会强制覆盖。
// 避免上游返回 "Store must be set to false"。
if v , ok := reqBody [ "store" ].( bool ); ! ok || v {
reqBody [ "store" ] = false
result . Modified = true
}
if v , ok := reqBody [ "stream" ].( bool ); ! ok || ! v {
reqBody [ "stream" ] = true
result . Modified = true
}
2026-01-09 18:35:58 +08:00
}
2026-02-17 00:00:52 +01:00
// Strip parameters unsupported by codex models via the Responses API.
for _ , key := range [] string {
"max_output_tokens" ,
"max_completion_tokens" ,
"temperature" ,
"top_p" ,
"frequency_penalty" ,
"presence_penalty" ,
2026-04-11 22:48:45 +08:00
// prompt_cache_retention is a newer Responses API parameter (cache TTL).
// The ChatGPT internal Codex endpoint rejects it with
// "Unsupported parameter: prompt_cache_retention". Defense-in-depth
// for any OAuth path that reaches this transform — the Cursor
// Responses-shape short-circuit in ForwardAsChatCompletions strips
// it earlier too, but we keep this line so other OAuth callers are
// equally protected.
"prompt_cache_retention" ,
2026-02-17 00:00:52 +01:00
} {
if _ , ok := reqBody [ key ]; ok {
delete ( reqBody , key )
result . Modified = true
}
2026-01-09 18:35:58 +08:00
}
2026-03-12 20:52:35 +08:00
// 兼容遗留的 functions 和 function_call,转换为 tools 和 tool_choice
if functionsRaw , ok := reqBody [ "functions" ]; ok {
if functions , k := functionsRaw .([] any ); k {
tools := make ([] any , 0 , len ( functions ))
for _ , f := range functions {
tools = append ( tools , map [ string ] any {
"type" : "function" ,
"function" : f ,
})
}
reqBody [ "tools" ] = tools
}
delete ( reqBody , "functions" )
result . Modified = true
}
if fcRaw , ok := reqBody [ "function_call" ]; ok {
if fcStr , ok := fcRaw .( string ); ok {
// e.g. "auto", "none"
reqBody [ "tool_choice" ] = fcStr
} else if fcObj , ok := fcRaw .( map [ string ] any ); ok {
// e.g. {"name": "my_func"}
if name , ok := fcObj [ "name" ].( string ); ok && strings . TrimSpace ( name ) != "" {
reqBody [ "tool_choice" ] = map [ string ] any {
"type" : "function" ,
"function" : map [ string ] any {
"name" : name ,
},
}
}
}
delete ( reqBody , "function_call" )
result . Modified = true
}
2026-01-09 18:35:58 +08:00
if normalizeCodexTools ( reqBody ) {
result . Modified = true
}
2026-04-24 12:03:19 +00:00
if normalizeCodexToolChoice ( reqBody ) {
result . Modified = true
}
2026-01-09 18:35:58 +08:00
if v , ok := reqBody [ "prompt_cache_key" ].( string ); ok {
result . PromptCacheKey = strings . TrimSpace ( v )
}
2026-03-16 21:20:46 +08:00
// 提取 input 中 role:"system" 消息至 instructions( OAuth 上游不支持 system role)。
if extractSystemMessagesFromInput ( reqBody ) {
result . Modified = true
}
2026-02-03 21:22:33 +08:00
// instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法
if applyInstructions ( reqBody , isCodexCLI ) {
result . Modified = true
2026-01-10 03:12:56 +08:00
}
2026-04-24 07:42:31 +00:00
if isCodexSparkModel ( normalizedModel ) && applyCodexSparkImageUnsupportedInstructions ( reqBody ) {
result . Modified = true
}
2026-01-10 03:12:56 +08:00
2026-01-13 16:47:35 +08:00
// 续链场景保留 item_reference 与 id,避免 call_id 上下文丢失。
2026-01-09 18:35:58 +08:00
if input , ok := reqBody [ "input" ].([] any ); ok {
2026-04-24 12:03:19 +00:00
if normalizedInput , modified := normalizeCodexToolRoleMessages ( input ); modified {
input = normalizedInput
result . Modified = true
}
if normalizedInput , modified := normalizeCodexMessageContentText ( input ); modified {
input = normalizedInput
result . Modified = true
}
2026-01-13 16:47:35 +08:00
input = filterCodexInput ( input , needsToolContinuation )
2026-01-09 18:35:58 +08:00
reqBody [ "input" ] = input
result . Modified = true
2026-03-10 23:31:54 +08:00
} else if inputStr , ok := reqBody [ "input" ].( string ); ok {
// ChatGPT codex endpoint requires input to be a list, not a string.
// Convert string input to the expected message array format.
trimmed := strings . TrimSpace ( inputStr )
if trimmed != "" {
reqBody [ "input" ] = [] any {
map [ string ] any {
"type" : "message" ,
"role" : "user" ,
"content" : inputStr ,
},
}
} else {
reqBody [ "input" ] = [] any {}
}
result . Modified = true
2026-01-09 18:35:58 +08:00
}
return result
}
2026-04-24 12:03:19 +00:00
func normalizeCodexToolChoice ( reqBody map [ string ] any ) bool {
choice , ok := reqBody [ "tool_choice" ]
if ! ok || choice == nil {
return false
}
choiceMap , ok := choice .( map [ string ] any )
if ! ok {
return false
}
choiceType := strings . TrimSpace ( firstNonEmptyString ( choiceMap [ "type" ]))
if choiceType == "" || codexToolsContainType ( reqBody [ "tools" ], choiceType ) {
return false
}
reqBody [ "tool_choice" ] = "auto"
return true
}
func codexToolsContainType ( rawTools any , toolType string ) bool {
tools , ok := rawTools .([] any )
if ! ok || strings . TrimSpace ( toolType ) == "" {
return false
}
for _ , rawTool := range tools {
tool , ok := rawTool .( map [ string ] any )
if ! ok {
continue
}
if strings . TrimSpace ( firstNonEmptyString ( tool [ "type" ])) == toolType {
return true
}
}
return false
}
func normalizeCodexToolRoleMessages ( input [] any ) ([] any , bool ) {
if len ( input ) == 0 {
return input , false
}
modified := false
normalized := make ([] any , 0 , len ( input ))
for _ , item := range input {
m , ok := item .( map [ string ] any )
if ! ok {
normalized = append ( normalized , item )
continue
}
role , _ := m [ "role" ].( string )
if strings . TrimSpace ( role ) != "tool" {
normalized = append ( normalized , item )
continue
}
callID := firstNonEmptyString ( m [ "call_id" ], m [ "tool_call_id" ], m [ "id" ])
callID = strings . TrimSpace ( callID )
if callID == "" {
// Responses does not accept role:"tool". If no call id is available,
// preserve the text as a user message instead of sending invalid input.
fallback := make ( map [ string ] any , len ( m ))
for key , value := range m {
fallback [ key ] = value
}
fallback [ "role" ] = "user"
delete ( fallback , "tool_call_id" )
normalized = append ( normalized , fallback )
modified = true
continue
}
output := extractTextFromContent ( m [ "content" ])
if output == "" {
if value , ok := m [ "output" ].( string ); ok {
output = value
}
}
if output == "" && m [ "content" ] != nil {
if b , err := json . Marshal ( m [ "content" ]); err == nil {
output = string ( b )
}
}
normalized = append ( normalized , map [ string ] any {
"type" : "function_call_output" ,
"call_id" : callID ,
"output" : output ,
})
modified = true
}
if ! modified {
return input , false
}
return normalized , true
}
func normalizeCodexMessageContentText ( input [] any ) ([] any , bool ) {
if len ( input ) == 0 {
return input , false
}
modified := false
normalized := make ([] any , 0 , len ( input ))
for _ , item := range input {
m , ok := item .( map [ string ] any )
if ! ok || strings . TrimSpace ( firstNonEmptyString ( m [ "type" ])) != "message" {
normalized = append ( normalized , item )
continue
}
parts , ok := m [ "content" ].([] any )
if ! ok {
normalized = append ( normalized , item )
continue
}
var newItem map [ string ] any
var newParts [] any
ensureItemCopy := func () {
if newItem != nil {
return
}
newItem = make ( map [ string ] any , len ( m ))
for key , value := range m {
newItem [ key ] = value
}
newParts = make ([] any , len ( parts ))
copy ( newParts , parts )
}
for i , rawPart := range parts {
part , ok := rawPart .( map [ string ] any )
if ! ok {
continue
}
text , hasText := part [ "text" ]
if ! hasText {
continue
}
if _ , ok := text .( string ); ok {
continue
}
ensureItemCopy ()
newPart := make ( map [ string ] any , len ( part ))
for key , value := range part {
newPart [ key ] = value
}
newPart [ "text" ] = stringifyCodexContentText ( text )
newParts [ i ] = newPart
modified = true
}
if newItem != nil {
newItem [ "content" ] = newParts
normalized = append ( normalized , newItem )
continue
}
normalized = append ( normalized , item )
}
if ! modified {
return input , false
}
return normalized , true
}
func stringifyCodexContentText ( value any ) string {
switch v := value .( type ) {
case string :
return v
case nil :
return ""
default :
if b , err := json . Marshal ( v ); err == nil {
return string ( b )
}
return fmt . Sprint ( v )
}
}
2026-01-09 18:35:58 +08:00
func normalizeCodexModel ( model string ) string {
2026-04-23 09:53:57 +08:00
model = strings . TrimSpace ( model )
2026-01-09 18:35:58 +08:00
if model == "" {
2026-04-20 22:01:09 +08:00
return "gpt-5.4"
2026-01-09 18:35:58 +08:00
}
2026-04-23 09:53:57 +08:00
if isOpenAIImageGenerationModel ( model ) {
return model
}
2026-01-09 18:35:58 +08:00
modelID := model
if strings . Contains ( modelID , "/" ) {
parts := strings . Split ( modelID , "/" )
modelID = parts [ len ( parts ) - 1 ]
}
if mapped := getNormalizedCodexModel ( modelID ); mapped != "" {
return mapped
}
normalized := strings . ToLower ( modelID )
2026-04-23 17:28:01 +08:00
if strings . Contains ( normalized , "gpt-5.5" ) || strings . Contains ( normalized , "gpt 5.5" ) {
return "gpt-5.5"
}
2026-03-19 19:00:22 +08:00
if strings . Contains ( normalized , "gpt-5.4-mini" ) || strings . Contains ( normalized , "gpt 5.4 mini" ) {
return "gpt-5.4-mini"
}
2026-03-06 08:14:04 +08:00
if strings . Contains ( normalized , "gpt-5.4" ) || strings . Contains ( normalized , "gpt 5.4" ) {
return "gpt-5.4"
}
2026-01-09 18:35:58 +08:00
if strings . Contains ( normalized , "gpt-5.2" ) || strings . Contains ( normalized , "gpt 5.2" ) {
return "gpt-5.2"
}
2026-04-20 22:01:09 +08:00
if strings . Contains ( normalized , "gpt-5.3-codex-spark" ) || strings . Contains ( normalized , "gpt 5.3 codex spark" ) {
return "gpt-5.3-codex-spark"
}
2026-02-06 07:14:46 +08:00
if strings . Contains ( normalized , "gpt-5.3-codex" ) || strings . Contains ( normalized , "gpt 5.3 codex" ) {
return "gpt-5.3-codex"
}
if strings . Contains ( normalized , "gpt-5.3" ) || strings . Contains ( normalized , "gpt 5.3" ) {
2026-02-13 09:28:07 +08:00
return "gpt-5.3-codex"
2026-02-06 07:14:46 +08:00
}
2026-01-09 18:35:58 +08:00
if strings . Contains ( normalized , "codex" ) {
2026-04-20 22:01:09 +08:00
return "gpt-5.3-codex"
2026-01-09 18:35:58 +08:00
}
if strings . Contains ( normalized , "gpt-5" ) || strings . Contains ( normalized , "gpt 5" ) {
2026-04-20 22:01:09 +08:00
return "gpt-5.4"
2026-01-09 18:35:58 +08:00
}
2026-04-20 22:01:09 +08:00
return "gpt-5.4"
2026-01-09 18:35:58 +08:00
}
2026-04-24 07:42:31 +00:00
func isCodexSparkModel ( model string ) bool {
return normalizeCodexModel ( model ) == "gpt-5.3-codex-spark"
}
2026-04-23 09:53:57 +08:00
func hasOpenAIImageGenerationTool ( reqBody map [ string ] any ) bool {
rawTools , ok := reqBody [ "tools" ]
if ! ok || rawTools == nil {
return false
}
tools , ok := rawTools .([] any )
if ! ok {
return false
}
for _ , rawTool := range tools {
toolMap , ok := rawTool .( map [ string ] any )
if ! ok {
continue
}
if strings . TrimSpace ( firstNonEmptyString ( toolMap [ "type" ])) == "image_generation" {
return true
}
}
return false
}
2026-04-24 07:42:31 +00:00
func hasOpenAIInputImage ( reqBody map [ string ] any ) bool {
if reqBody == nil {
return false
}
return hasOpenAIInputImageValue ( reqBody [ "input" ]) || hasOpenAIInputImageValue ( reqBody [ "messages" ])
}
func hasOpenAIInputImageValue ( value any ) bool {
switch v := value .( type ) {
case [] any :
for _ , item := range v {
if hasOpenAIInputImageValue ( item ) {
return true
}
}
case map [ string ] any :
if strings . TrimSpace ( firstNonEmptyString ( v [ "type" ])) == "input_image" {
return true
}
if _ , ok := v [ "image_url" ]; ok {
return true
}
return hasOpenAIInputImageValue ( v [ "content" ])
}
return false
}
func validateCodexSparkInput ( reqBody map [ string ] any , model string ) error {
if ! isCodexSparkModel ( model ) || ! hasOpenAIInputImage ( reqBody ) {
return nil
}
return fmt . Errorf ( "model %q does not support image input" , strings . TrimSpace ( model ))
}
2026-04-23 09:53:57 +08:00
func normalizeOpenAIResponsesImageGenerationTools ( reqBody map [ string ] any ) bool {
rawTools , ok := reqBody [ "tools" ]
if ! ok || rawTools == nil {
return false
}
tools , ok := rawTools .([] any )
if ! ok {
return false
}
modified := false
for _ , rawTool := range tools {
toolMap , ok := rawTool .( map [ string ] any )
if ! ok || strings . TrimSpace ( firstNonEmptyString ( toolMap [ "type" ])) != "image_generation" {
continue
}
if _ , ok := toolMap [ "output_format" ]; ! ok {
if value := strings . TrimSpace ( firstNonEmptyString ( toolMap [ "format" ])); value != "" {
toolMap [ "output_format" ] = value
modified = true
}
}
if _ , ok := toolMap [ "output_compression" ]; ! ok {
if value , exists := toolMap [ "compression" ]; exists && value != nil {
toolMap [ "output_compression" ] = value
modified = true
}
}
if _ , ok := toolMap [ "format" ]; ok {
delete ( toolMap , "format" )
modified = true
}
if _ , ok := toolMap [ "compression" ]; ok {
delete ( toolMap , "compression" )
modified = true
}
}
return modified
}
2026-04-23 15:13:57 +00:00
func ensureOpenAIResponsesImageGenerationTool ( reqBody map [ string ] any ) bool {
if len ( reqBody ) == 0 {
return false
}
2026-04-24 07:42:31 +00:00
if isCodexSparkModel ( firstNonEmptyString ( reqBody [ "model" ])) {
return false
}
2026-04-23 15:13:57 +00:00
tool := map [ string ] any {
"type" : "image_generation" ,
"output_format" : "png" ,
}
rawTools , ok := reqBody [ "tools" ]
if ! ok || rawTools == nil {
reqBody [ "tools" ] = [] any { tool }
return true
}
tools , ok := rawTools .([] any )
if ! ok {
reqBody [ "tools" ] = [] any { tool }
return true
}
for _ , rawTool := range tools {
toolMap , ok := rawTool .( map [ string ] any )
if ! ok {
continue
}
if strings . TrimSpace ( firstNonEmptyString ( toolMap [ "type" ])) == "image_generation" {
return false
}
}
reqBody [ "tools" ] = append ( tools , tool )
return true
}
func applyCodexImageGenerationBridgeInstructions ( reqBody map [ string ] any ) bool {
if len ( reqBody ) == 0 || ! hasOpenAIImageGenerationTool ( reqBody ) {
return false
}
2026-04-24 07:42:31 +00:00
if isCodexSparkModel ( firstNonEmptyString ( reqBody [ "model" ])) {
return false
}
2026-04-23 15:13:57 +00:00
existing , _ := reqBody [ "instructions" ].( string )
if strings . Contains ( existing , codexImageGenerationBridgeMarker ) {
return false
}
existing = strings . TrimRight ( existing , " \t\r\n" )
if strings . TrimSpace ( existing ) == "" {
reqBody [ "instructions" ] = codexImageGenerationBridgeText
return true
}
reqBody [ "instructions" ] = existing + "\n\n" + codexImageGenerationBridgeText
return true
}
2026-04-24 07:42:31 +00:00
func applyCodexSparkImageUnsupportedInstructions ( reqBody map [ string ] any ) bool {
if len ( reqBody ) == 0 {
return false
}
existing , _ := reqBody [ "instructions" ].( string )
if strings . Contains ( existing , codexSparkImageUnsupportedMarker ) {
return false
}
existing = strings . TrimRight ( existing , " \t\r\n" )
if strings . TrimSpace ( existing ) == "" {
reqBody [ "instructions" ] = codexSparkImageUnsupportedText
return true
}
reqBody [ "instructions" ] = existing + "\n\n" + codexSparkImageUnsupportedText
return true
}
2026-04-23 09:53:57 +08:00
func validateOpenAIResponsesImageModel ( reqBody map [ string ] any , model string ) error {
if ! hasOpenAIImageGenerationTool ( reqBody ) {
return nil
}
model = strings . TrimSpace ( model )
if ! isOpenAIImageGenerationModel ( model ) {
return nil
}
return fmt . Errorf ( "/v1/responses image_generation requests require a Responses-capable text model; image-only model %q is not allowed" , model )
}
2026-04-23 15:13:57 +00:00
func normalizeOpenAIResponsesImageOnlyModel ( reqBody map [ string ] any ) bool {
if len ( reqBody ) == 0 {
return false
}
imageModel := strings . TrimSpace ( firstNonEmptyString ( reqBody [ "model" ]))
if ! isOpenAIImageGenerationModel ( imageModel ) {
return false
}
modified := false
tools , _ := reqBody [ "tools" ].([] any )
imageToolIndex := - 1
for i , rawTool := range tools {
toolMap , ok := rawTool .( map [ string ] any )
if ! ok {
continue
}
if strings . TrimSpace ( firstNonEmptyString ( toolMap [ "type" ])) == "image_generation" {
imageToolIndex = i
break
}
}
if imageToolIndex < 0 {
tools = append ( tools , map [ string ] any {
"type" : "image_generation" ,
"model" : imageModel ,
})
imageToolIndex = len ( tools ) - 1
reqBody [ "tools" ] = tools
modified = true
}
if toolMap , ok := tools [ imageToolIndex ].( map [ string ] any ); ok {
if strings . TrimSpace ( firstNonEmptyString ( toolMap [ "model" ])) == "" {
toolMap [ "model" ] = imageModel
modified = true
}
for _ , key := range [] string {
"size" ,
"quality" ,
"background" ,
"output_format" ,
"output_compression" ,
"moderation" ,
"style" ,
"partial_images" ,
} {
if value , exists := reqBody [ key ]; exists && value != nil {
if _ , toolHas := toolMap [ key ]; ! toolHas {
toolMap [ key ] = value
}
delete ( reqBody , key )
modified = true
}
}
}
if prompt := strings . TrimSpace ( firstNonEmptyString ( reqBody [ "prompt" ])); prompt != "" {
if _ , hasInput := reqBody [ "input" ]; ! hasInput {
reqBody [ "input" ] = prompt
}
delete ( reqBody , "prompt" )
modified = true
}
if _ , ok := reqBody [ "tool_choice" ]; ! ok {
reqBody [ "tool_choice" ] = map [ string ] any { "type" : "image_generation" }
modified = true
}
if imageModel != openAIImagesResponsesMainModel {
modified = true
}
reqBody [ "model" ] = openAIImagesResponsesMainModel
return modified
}
2026-04-07 11:27:57 +03:00
func normalizeOpenAIModelForUpstream ( account * Account , model string ) string {
if account == nil || account . Type == AccountTypeOAuth {
return normalizeCodexModel ( model )
}
return strings . TrimSpace ( model )
}
2026-03-11 21:12:07 +08:00
func SupportsVerbosity ( model string ) bool {
if ! strings . HasPrefix ( model , "gpt-" ) {
return true
}
var major , minor int
n , _ := fmt . Sscanf ( model , "gpt-%d.%d" , & major , & minor )
if major > 5 {
return true
}
if major < 5 {
return false
}
// gpt-5
if n == 1 {
return true
}
return minor >= 3
}
2026-01-09 18:35:58 +08:00
func getNormalizedCodexModel ( modelID string ) string {
if modelID == "" {
return ""
}
if mapped , ok := codexModelMap [ modelID ]; ok {
return mapped
}
lower := strings . ToLower ( modelID )
for key , value := range codexModelMap {
if strings . ToLower ( key ) == lower {
return value
}
}
return ""
}
2026-03-16 21:20:46 +08:00
// extractTextFromContent extracts plain text from a content value that is either
// a Go string or a []any of content-part maps with type:"text".
func extractTextFromContent ( content any ) string {
switch v := content .( type ) {
case string :
return v
case [] any :
var parts [] string
for _ , part := range v {
m , ok := part .( map [ string ] any )
if ! ok {
continue
}
if t , _ := m [ "type" ].( string ); t == "text" {
if text , ok := m [ "text" ].( string ); ok {
parts = append ( parts , text )
}
}
}
return strings . Join ( parts , "" )
default :
return ""
}
}
// extractSystemMessagesFromInput scans the input array for items with role=="system",
// removes them, and merges their content into reqBody["instructions"].
// If instructions is already non-empty, extracted content is prepended with "\n\n".
// Returns true if any system messages were extracted.
func extractSystemMessagesFromInput ( reqBody map [ string ] any ) bool {
input , ok := reqBody [ "input" ].([] any )
if ! ok || len ( input ) == 0 {
return false
}
var systemTexts [] string
remaining := make ([] any , 0 , len ( input ))
for _ , item := range input {
m , ok := item .( map [ string ] any )
if ! ok {
remaining = append ( remaining , item )
continue
}
if role , _ := m [ "role" ].( string ); role != "system" {
remaining = append ( remaining , item )
continue
}
if text := extractTextFromContent ( m [ "content" ]); text != "" {
systemTexts = append ( systemTexts , text )
}
}
if len ( systemTexts ) == 0 {
return false
}
extracted := strings . Join ( systemTexts , "\n\n" )
if existing , ok := reqBody [ "instructions" ].( string ); ok && strings . TrimSpace ( existing ) != "" {
reqBody [ "instructions" ] = extracted + "\n\n" + existing
} else {
reqBody [ "instructions" ] = extracted
}
reqBody [ "input" ] = remaining
return true
}
2026-03-07 13:39:47 +08:00
// applyInstructions 处理 instructions 字段:仅在 instructions 为空时填充默认值。
2026-02-03 21:22:33 +08:00
func applyInstructions ( reqBody map [ string ] any , isCodexCLI bool ) bool {
if ! isInstructionsEmpty ( reqBody ) {
2026-03-07 13:39:47 +08:00
return false
2026-02-03 21:22:33 +08:00
}
2026-03-07 13:39:47 +08:00
reqBody [ "instructions" ] = "You are a helpful coding assistant."
return true
2026-02-03 21:22:33 +08:00
}
// isInstructionsEmpty 检查 instructions 字段是否为空
// 处理以下情况:字段不存在、nil、空字符串、纯空白字符串
func isInstructionsEmpty ( reqBody map [ string ] any ) bool {
val , exists := reqBody [ "instructions" ]
if ! exists {
return true
}
if val == nil {
return true
}
str , ok := val .( string )
if ! ok {
return true
}
return strings . TrimSpace ( str ) == ""
}
2026-01-13 16:47:35 +08:00
// filterCodexInput 按需过滤 item_reference 与 id。
// preserveReferences 为 true 时保持引用与 id,以满足续链请求对上下文的依赖。
func filterCodexInput ( input [] any , preserveReferences bool ) [] any {
2026-01-09 18:35:58 +08:00
filtered := make ([] any , 0 , len ( input ))
for _ , item := range input {
m , ok := item .( map [ string ] any )
if ! ok {
filtered = append ( filtered , item )
continue
}
2026-01-12 20:18:53 -08:00
typ , _ := m [ "type" ].( string )
2026-03-12 20:52:35 +08:00
2026-03-14 13:47:01 +08:00
// 仅修正真正的 tool/function call 标识,避免误改普通 message/reasoning id;
// 若 item_reference 指向 legacy call_* 标识,则仅修正该引用本身。
fixCallIDPrefix := func ( id string ) string {
2026-03-12 20:52:35 +08:00
if id == "" || strings . HasPrefix ( id , "fc" ) {
return id
}
if strings . HasPrefix ( id , "call_" ) {
return "fc" + strings . TrimPrefix ( id , "call_" )
}
return "fc_" + id
}
2026-01-12 20:18:53 -08:00
if typ == "item_reference" {
2026-01-13 16:47:35 +08:00
if ! preserveReferences {
continue
}
2026-01-13 17:01:21 +08:00
newItem := make ( map [ string ] any , len ( m ))
for key , value := range m {
newItem [ key ] = value
}
2026-03-14 13:47:01 +08:00
if id , ok := newItem [ "id" ].( string ); ok && strings . HasPrefix ( id , "call_" ) {
newItem [ "id" ] = fixCallIDPrefix ( id )
2026-03-12 20:52:35 +08:00
}
2026-01-13 17:01:21 +08:00
filtered = append ( filtered , newItem )
2026-01-09 18:35:58 +08:00
continue
}
2026-01-13 17:01:21 +08:00
2026-01-13 16:47:35 +08:00
newItem := m
2026-01-13 17:01:21 +08:00
copied := false
// 仅在需要修改字段时创建副本,避免直接改写原始输入。
ensureCopy := func () {
if copied {
return
}
2026-01-13 16:47:35 +08:00
newItem = make ( map [ string ] any , len ( m ))
for key , value := range m {
newItem [ key ] = value
}
2026-01-13 17:01:21 +08:00
copied = true
2026-01-13 16:47:35 +08:00
}
2026-01-13 17:01:21 +08:00
2026-01-12 20:18:53 -08:00
if isCodexToolCallItemType ( typ ) {
2026-03-12 20:52:35 +08:00
callID , ok := m [ "call_id" ].( string )
if ! ok || strings . TrimSpace ( callID ) == "" {
2026-01-12 20:18:53 -08:00
if id , ok := m [ "id" ].( string ); ok && strings . TrimSpace ( id ) != "" {
2026-03-12 20:52:35 +08:00
callID = id
2026-01-13 17:01:21 +08:00
ensureCopy ()
2026-03-12 20:52:35 +08:00
newItem [ "call_id" ] = callID
}
}
if callID != "" {
2026-03-14 13:47:01 +08:00
fixedCallID := fixCallIDPrefix ( callID )
2026-03-12 20:52:35 +08:00
if fixedCallID != callID {
ensureCopy ()
newItem [ "call_id" ] = fixedCallID
2026-01-12 20:18:53 -08:00
}
}
}
2026-01-13 17:01:21 +08:00
2026-04-24 17:15:42 +08:00
if ! isCodexToolCallItemType ( typ ) {
ensureCopy ()
delete ( newItem , "call_id" )
}
2026-04-24 12:03:19 +00:00
if codexInputItemRequiresName ( typ ) {
if strings . TrimSpace ( firstNonEmptyString ( m [ "name" ])) == "" {
name := firstNonEmptyString ( m [ "tool_name" ])
if name == "" {
if function , ok := m [ "function" ].( map [ string ] any ); ok {
name = firstNonEmptyString ( function [ "name" ])
}
}
if name == "" {
name = "tool"
}
ensureCopy ()
newItem [ "name" ] = name
}
}
2026-01-13 17:01:21 +08:00
if ! preserveReferences {
ensureCopy ()
delete ( newItem , "id" )
2026-01-12 20:18:53 -08:00
}
2026-01-13 17:01:21 +08:00
2026-01-13 16:47:35 +08:00
filtered = append ( filtered , newItem )
2026-01-09 18:35:58 +08:00
}
return filtered
}
2026-01-12 20:18:53 -08:00
func isCodexToolCallItemType ( typ string ) bool {
2026-04-24 17:15:42 +08:00
switch typ {
case "function_call" ,
"tool_call" ,
"local_shell_call" ,
"tool_search_call" ,
"custom_tool_call" ,
"function_call_output" ,
"mcp_tool_call_output" ,
"custom_tool_call_output" ,
"tool_search_output" :
return true
default :
2026-01-12 20:18:53 -08:00
return false
}
}
2026-04-24 12:03:19 +00:00
func codexInputItemRequiresName ( typ string ) bool {
switch strings . TrimSpace ( typ ) {
case "function_call" , "custom_tool_call" , "mcp_tool_call" :
return true
default :
return false
}
}
2026-01-09 18:35:58 +08:00
func normalizeCodexTools ( reqBody map [ string ] any ) bool {
rawTools , ok := reqBody [ "tools" ]
if ! ok || rawTools == nil {
return false
}
tools , ok := rawTools .([] any )
if ! ok {
return false
}
modified := false
2026-01-17 11:00:07 +08:00
validTools := make ([] any , 0 , len ( tools ))
for _ , tool := range tools {
2026-01-09 18:35:58 +08:00
toolMap , ok := tool .( map [ string ] any )
if ! ok {
2026-01-17 11:00:07 +08:00
// Keep unknown structure as-is to avoid breaking upstream behavior.
validTools = append ( validTools , tool )
2026-01-09 18:35:58 +08:00
continue
}
toolType , _ := toolMap [ "type" ].( string )
2026-01-17 11:00:07 +08:00
toolType = strings . TrimSpace ( toolType )
if toolType != "function" {
validTools = append ( validTools , toolMap )
2026-01-09 18:35:58 +08:00
continue
}
2026-01-17 11:00:07 +08:00
// OpenAI Responses-style tools use top-level name/parameters.
if name , ok := toolMap [ "name" ].( string ); ok && strings . TrimSpace ( name ) != "" {
validTools = append ( validTools , toolMap )
continue
}
// ChatCompletions-style tools use {type:"function", function:{...}}.
functionValue , hasFunction := toolMap [ "function" ]
function , ok := functionValue .( map [ string ] any )
if ! hasFunction || functionValue == nil || ! ok || function == nil {
// Drop invalid function tools.
modified = true
2026-01-09 18:35:58 +08:00
continue
}
if _ , ok := toolMap [ "name" ]; ! ok {
if name , ok := function [ "name" ].( string ); ok && strings . TrimSpace ( name ) != "" {
toolMap [ "name" ] = name
modified = true
}
}
if _ , ok := toolMap [ "description" ]; ! ok {
if desc , ok := function [ "description" ].( string ); ok && strings . TrimSpace ( desc ) != "" {
toolMap [ "description" ] = desc
modified = true
}
}
if _ , ok := toolMap [ "parameters" ]; ! ok {
if params , ok := function [ "parameters" ]; ok {
toolMap [ "parameters" ] = params
modified = true
}
}
if _ , ok := toolMap [ "strict" ]; ! ok {
if strict , ok := function [ "strict" ]; ok {
toolMap [ "strict" ] = strict
modified = true
}
}
2026-01-17 11:00:07 +08:00
validTools = append ( validTools , toolMap )
2026-01-09 18:35:58 +08:00
}
if modified {
2026-01-17 11:00:07 +08:00
reqBody [ "tools" ] = validTools
2026-01-09 18:35:58 +08:00
}
return modified
}