2026-01-09 18:35:58 +08:00
package service
import (
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-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
}
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-09 18:35:58 +08:00
}
2026-01-13 16:47:35 +08:00
// 续链场景保留 item_reference 与 id, 避免 call_id 上下文丢失。
2026-01-10 03:12:56 +08:00
if input , ok := reqBody [ "input" ] . ( [ ] any ) ; ok {
2026-01-13 16:47:35 +08:00
input = filterCodexInput ( input , needsToolContinuation )
2026-01-10 03:12:56 +08:00
reqBody [ "input" ] = input
2026-01-09 18:35:58 +08:00
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
}
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-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
}
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
}
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
}
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-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 16:47:35 +08:00
}
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-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
ensureCopy ( )
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 {
2026-01-13 17:01:21 +08:00
ensureCopy ( )
2026-03-12 20:52:35 +08:00
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-01-13 17:01:21 +08:00
if ! preserveReferences {
ensureCopy ( )
2026-01-13 16:47:35 +08:00
delete ( newItem , "id" )
2026-01-09 18:35:58 +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-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
}