505 lines
19 KiB
Go
505 lines
19 KiB
Go
package executor
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
const (
|
|
codexUserAgent = "codex-tui/0.146.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.146.0)"
|
|
codexOriginator = "codex-tui"
|
|
codexDefaultImageToolModel = "gpt-image-2"
|
|
codexResponsesLiteHeader = "X-OpenAI-Internal-Codex-Responses-Lite"
|
|
codexResponsesLiteMetadata = "client_metadata.ws_request_header_x_openai_internal_codex_responses_lite"
|
|
)
|
|
|
|
var dataTag = []byte("data:")
|
|
|
|
func translateCodexRequestPair(from, to sdktranslator.Format, model string, originalPayload, payload []byte, stream bool, preserveEmptyThinkingBlocks ...bool) ([]byte, []byte) {
|
|
isCompat := len(preserveEmptyThinkingBlocks) > 0 && preserveEmptyThinkingBlocks[0]
|
|
translate := func(raw []byte) []byte {
|
|
if isCompat && from == sdktranslator.FormatClaude && to == sdktranslator.FormatCodex {
|
|
return helps.TranslateRequestWithAPIKeyModelCompatibility(context.Background(), nil, nil, from, to, model, raw, stream, true)
|
|
}
|
|
return sdktranslator.TranslateRequest(from, to, model, raw, stream)
|
|
}
|
|
if bytes.Equal(originalPayload, payload) {
|
|
body := translate(payload)
|
|
return body, body
|
|
}
|
|
originalTranslated := translate(originalPayload)
|
|
body := translate(payload)
|
|
return originalTranslated, body
|
|
}
|
|
|
|
// PrepareRequest injects Codex credentials into the outgoing HTTP request.
|
|
func (e *CodexExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error {
|
|
if req == nil {
|
|
return nil
|
|
}
|
|
apiKey, _ := codexCreds(auth)
|
|
if strings.TrimSpace(apiKey) != "" {
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
} else {
|
|
req.Header.Del("Authorization")
|
|
}
|
|
var attrs map[string]string
|
|
if auth != nil {
|
|
attrs = auth.Attributes
|
|
}
|
|
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
|
return nil
|
|
}
|
|
|
|
// HttpRequest injects Codex credentials into the request and executes it.
|
|
func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) {
|
|
if req == nil {
|
|
return nil, fmt.Errorf("codex executor: request is nil")
|
|
}
|
|
if ctx == nil {
|
|
ctx = req.Context()
|
|
}
|
|
httpReq := req.WithContext(ctx)
|
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
|
return nil, err
|
|
}
|
|
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
|
|
return httpClient.Do(httpReq)
|
|
}
|
|
|
|
type codexIdentityConfuseState struct {
|
|
enabled bool
|
|
authID string
|
|
originalPromptCacheKey string
|
|
promptCacheKey string
|
|
turnIDs []codexIdentityReplacement
|
|
}
|
|
|
|
type codexIdentityReplacement struct {
|
|
original string
|
|
confused string
|
|
}
|
|
|
|
func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, userPayload []byte, rawJSON []byte, headerSets ...http.Header) (*http.Request, []byte, codexIdentityConfuseState, error) {
|
|
var headers http.Header
|
|
if len(headerSets) > 0 {
|
|
headers = headerSets[0]
|
|
}
|
|
var cache helps.CodexCache
|
|
if sourceFormatEqual(from, sdktranslator.FormatClaude) {
|
|
modelName := strings.TrimSpace(gjson.GetBytes(rawJSON, "model").String())
|
|
if modelName == "" {
|
|
modelName = thinking.ParseSuffix(req.Model).ModelName
|
|
}
|
|
cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, modelName, req.Payload, headers)
|
|
if errCache != nil {
|
|
return nil, nil, codexIdentityConfuseState{}, errCache
|
|
}
|
|
if ok {
|
|
cache = cached
|
|
}
|
|
} else if sourceFormatEqual(from, sdktranslator.FormatOpenAIResponse) {
|
|
promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key")
|
|
if promptCacheKey.Exists() {
|
|
cache.ID = promptCacheKey.String()
|
|
}
|
|
} else if sourceFormatEqual(from, sdktranslator.FormatOpenAI) {
|
|
if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() {
|
|
cache.ID = strings.TrimSpace(promptCacheKey.String())
|
|
}
|
|
if cache.ID == "" {
|
|
cache.ID = helps.ProviderSessionUUID("codex", req.Metadata)
|
|
}
|
|
if cache.ID == "" {
|
|
if apiKey := strings.TrimSpace(helps.APIKeyFromContext(ctx)); apiKey != "" {
|
|
cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String()
|
|
}
|
|
}
|
|
}
|
|
if cache.ID == "" {
|
|
cache.ID = helps.ProviderSessionUUID("codex", req.Metadata)
|
|
}
|
|
|
|
if cache.ID != "" {
|
|
rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", cache.ID)
|
|
}
|
|
rawJSON = helps.SanitizeCodexInputItemIDs(rawJSON)
|
|
var identityState codexIdentityConfuseState
|
|
rawJSON, identityState = applyCodexIdentityConfuseBody(e.cfg, auth, userPayload, rawJSON)
|
|
if identityState.promptCacheKey != "" {
|
|
cache.ID = identityState.promptCacheKey
|
|
}
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawJSON))
|
|
if err != nil {
|
|
return nil, nil, codexIdentityConfuseState{}, err
|
|
}
|
|
if cache.ID != "" {
|
|
httpReq.Header.Set("Session-Id", cache.ID)
|
|
}
|
|
return httpReq, rawJSON, identityState, nil
|
|
}
|
|
|
|
func applyCodexIdentityConfuseBody(cfg *config.Config, auth *cliproxyauth.Auth, userPayload []byte, rawJSON []byte) ([]byte, codexIdentityConfuseState) {
|
|
if !codexIdentityConfuseEnabled(cfg) || auth == nil || strings.TrimSpace(auth.ID) == "" || len(rawJSON) == 0 {
|
|
return rawJSON, codexIdentityConfuseState{}
|
|
}
|
|
|
|
state := codexIdentityConfuseState{enabled: true, authID: strings.TrimSpace(auth.ID)}
|
|
if promptCacheKey := strings.TrimSpace(gjson.GetBytes(userPayload, "prompt_cache_key").String()); promptCacheKey != "" {
|
|
state.originalPromptCacheKey = promptCacheKey
|
|
state.promptCacheKey = codexIdentityConfuseUUID(auth.ID, "prompt-cache", promptCacheKey)
|
|
rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", state.promptCacheKey)
|
|
}
|
|
if installationID := strings.TrimSpace(gjson.GetBytes(userPayload, "client_metadata.x-codex-installation-id").String()); installationID != "" {
|
|
rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-installation-id", codexIdentityConfuseUUID(auth.ID, "installation", installationID))
|
|
}
|
|
if turnMetadata := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-turn-metadata").String()); turnMetadata != "" {
|
|
rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-turn-metadata", applyCodexTurnMetadataIdentityConfuse(turnMetadata, &state))
|
|
}
|
|
if state.promptCacheKey != "" {
|
|
if windowID := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-window-id").String()); windowID != "" {
|
|
rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-window-id", state.promptCacheKey+":0")
|
|
}
|
|
}
|
|
|
|
return rawJSON, state
|
|
}
|
|
|
|
func applyCodexIdentityConfuseHeaders(headers http.Header, state *codexIdentityConfuseState) {
|
|
if headers == nil {
|
|
return
|
|
}
|
|
if state == nil || !state.enabled {
|
|
return
|
|
}
|
|
|
|
if rawTurnMetadata := strings.TrimSpace(headers.Get("X-Codex-Turn-Metadata")); rawTurnMetadata != "" {
|
|
headers.Set("X-Codex-Turn-Metadata", applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata, state))
|
|
}
|
|
if state.promptCacheKey == "" {
|
|
return
|
|
}
|
|
|
|
setCodexSessionHeaderCasePreserved(headers, "Session-Id", state.promptCacheKey)
|
|
if headerValueCaseInsensitive(headers, "Conversation_id") != "" {
|
|
setHeaderCasePreserved(headers, "Conversation_id", state.promptCacheKey)
|
|
}
|
|
headers.Set("X-Client-Request-Id", state.promptCacheKey)
|
|
headers.Set("Thread-Id", state.promptCacheKey)
|
|
headers.Set("X-Codex-Window-Id", state.promptCacheKey+":0")
|
|
}
|
|
|
|
func applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata string, state *codexIdentityConfuseState) string {
|
|
updatedTurnMetadata := rawTurnMetadata
|
|
if state == nil || !state.enabled {
|
|
return updatedTurnMetadata
|
|
}
|
|
if state.promptCacheKey != "" && gjson.Get(rawTurnMetadata, "prompt_cache_key").Exists() {
|
|
updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "prompt_cache_key", state.promptCacheKey)
|
|
} else if state.promptCacheKey != "" && state.originalPromptCacheKey != "" {
|
|
updatedTurnMetadata = strings.ReplaceAll(updatedTurnMetadata, state.originalPromptCacheKey, state.promptCacheKey)
|
|
}
|
|
if turnID := strings.TrimSpace(gjson.Get(rawTurnMetadata, "turn_id").String()); turnID != "" {
|
|
updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "turn_id", state.confuseTurnID(turnID))
|
|
}
|
|
if state.promptCacheKey != "" && gjson.Get(rawTurnMetadata, "window_id").Exists() {
|
|
updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "window_id", state.promptCacheKey+":0")
|
|
}
|
|
return updatedTurnMetadata
|
|
}
|
|
|
|
func applyCodexIdentityConfuseResponsePayload(payload []byte, state codexIdentityConfuseState) []byte {
|
|
payload = replaceCodexIdentityResponsePayload(payload, state.originalPromptCacheKey, state.promptCacheKey)
|
|
for _, turnID := range state.turnIDs {
|
|
payload = replaceCodexIdentityResponsePayload(payload, turnID.original, turnID.confused)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func applyCodexIdentityExposeResponsePayload(payload []byte, state codexIdentityConfuseState) []byte {
|
|
payload = replaceCodexIdentityResponsePayload(payload, state.promptCacheKey, state.originalPromptCacheKey)
|
|
for _, turnID := range state.turnIDs {
|
|
payload = replaceCodexIdentityResponsePayload(payload, turnID.confused, turnID.original)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func (state *codexIdentityConfuseState) confuseTurnID(turnID string) string {
|
|
turnID = strings.TrimSpace(turnID)
|
|
if state == nil || !state.enabled || strings.TrimSpace(state.authID) == "" || turnID == "" {
|
|
return turnID
|
|
}
|
|
for _, replacement := range state.turnIDs {
|
|
if replacement.original == turnID || replacement.confused == turnID {
|
|
return replacement.confused
|
|
}
|
|
}
|
|
confusedTurnID := codexIdentityConfuseUUID(state.authID, "turn", turnID)
|
|
state.turnIDs = append(state.turnIDs, codexIdentityReplacement{original: turnID, confused: confusedTurnID})
|
|
return confusedTurnID
|
|
}
|
|
|
|
func replaceCodexIdentityResponsePayload(payload []byte, from string, to string) []byte {
|
|
from = strings.TrimSpace(from)
|
|
to = strings.TrimSpace(to)
|
|
if len(payload) == 0 || from == "" || to == "" || from == to || !bytes.Contains(payload, []byte(from)) {
|
|
return payload
|
|
}
|
|
return bytes.ReplaceAll(payload, []byte(from), []byte(to))
|
|
}
|
|
|
|
func codexIdentityConfuseEnabled(cfg *config.Config) bool {
|
|
if cfg == nil || !cfg.Codex.IdentityConfuse {
|
|
return false
|
|
}
|
|
strategy := strings.ToLower(strings.TrimSpace(cfg.Routing.Strategy))
|
|
return cfg.Routing.SessionAffinity || strategy == "fill-first" || strategy == "fillfirst" || strategy == "ff"
|
|
}
|
|
|
|
func codexIdentityConfuseUUID(authID string, kind string, value string) string {
|
|
name := strings.Join([]string{"cli-proxy-api", "codex", "identity-confuse", kind, strings.TrimSpace(authID), strings.TrimSpace(value)}, ":")
|
|
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)).String()
|
|
}
|
|
|
|
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config, clientHeaders ...http.Header) {
|
|
var ginHeaders http.Header
|
|
if len(clientHeaders) > 0 && clientHeaders[0] != nil {
|
|
ginHeaders = clientHeaders[0]
|
|
} else if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
|
|
ginHeaders = ginCtx.Request.Header
|
|
}
|
|
applyCodexHeadersFromSources(r, auth, token, stream, cfg, ginHeaders)
|
|
}
|
|
|
|
// applyModelHeaderOverrides forces models.json config.override_header onto upstream headers.
|
|
func applyModelHeaderOverrides(headers http.Header, modelName string) {
|
|
if headers == nil {
|
|
return
|
|
}
|
|
overrides := registry.ModelOverrideHeaders(modelName)
|
|
if len(overrides) == 0 {
|
|
return
|
|
}
|
|
for key, value := range overrides {
|
|
headers.Set(key, value)
|
|
}
|
|
if strings.Contains(headers.Get("User-Agent"), "Mac OS") && codexSessionHeaderValue(headers) == "" {
|
|
headers.Set("Session_id", uuid.NewString())
|
|
}
|
|
}
|
|
|
|
// applyCodexDirectImageHeaders sets Codex upstream headers for direct /images/* calls.
|
|
// Downstream client User-Agent values are not forwarded to reduce Cloudflare 1010 blocks.
|
|
func applyCodexDirectImageHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config, clientHeaders ...http.Header) {
|
|
var ginHeaders http.Header
|
|
if len(clientHeaders) > 0 && clientHeaders[0] != nil {
|
|
ginHeaders = clientHeaders[0].Clone()
|
|
ginHeaders.Del("User-Agent")
|
|
} else if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
|
|
ginHeaders = ginCtx.Request.Header.Clone()
|
|
ginHeaders.Del("User-Agent")
|
|
}
|
|
applyCodexHeadersFromSources(r, auth, token, stream, cfg, ginHeaders)
|
|
}
|
|
|
|
func applyCodexHeadersFromSources(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config, ginHeaders http.Header) {
|
|
r.Header.Set("Content-Type", "application/json")
|
|
if strings.TrimSpace(token) != "" {
|
|
r.Header.Set("Authorization", "Bearer "+token)
|
|
} else {
|
|
r.Header.Del("Authorization")
|
|
}
|
|
|
|
if ginHeaders != nil && ginHeaders.Get("X-Codex-Beta-Features") != "" {
|
|
r.Header.Set("X-Codex-Beta-Features", ginHeaders.Get("X-Codex-Beta-Features"))
|
|
}
|
|
misc.EnsureHeader(r.Header, ginHeaders, "Version", "")
|
|
misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Turn-Metadata", "")
|
|
misc.EnsureHeader(r.Header, ginHeaders, "X-Client-Request-Id", "")
|
|
misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Window-Id", "")
|
|
misc.EnsureHeader(r.Header, ginHeaders, "Thread-Id", "")
|
|
misc.EnsureHeader(r.Header, ginHeaders, "Session-Id", "")
|
|
misc.EnsureHeader(r.Header, ginHeaders, "X-Openai-Internal-Codex-Responses-Lite", "")
|
|
|
|
cfgUserAgent, _ := codexHeaderDefaults(cfg, auth)
|
|
ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
|
|
|
|
if stream {
|
|
r.Header.Set("Accept", "text/event-stream")
|
|
} else {
|
|
r.Header.Set("Accept", "application/json")
|
|
}
|
|
r.Header.Set("Connection", "Keep-Alive")
|
|
|
|
isAPIKey := codexAuthUsesAPIKey(auth)
|
|
if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" {
|
|
r.Header.Set("Originator", originator)
|
|
} else if !isAPIKey {
|
|
r.Header.Set("Originator", codexOriginator)
|
|
}
|
|
if !isAPIKey {
|
|
if auth != nil && auth.Metadata != nil {
|
|
if accountID, ok := auth.Metadata["account_id"].(string); ok {
|
|
r.Header.Set("Chatgpt-Account-Id", accountID)
|
|
}
|
|
}
|
|
}
|
|
var attrs map[string]string
|
|
if auth != nil {
|
|
attrs = auth.Attributes
|
|
}
|
|
util.ApplyCustomHeadersFromAttrs(r, attrs, ginHeaders)
|
|
applyCodexCloakingHeaders(r.Header, cfg)
|
|
}
|
|
|
|
func applyCodexCloakingHeaders(headers http.Header, cfg *config.Config) {
|
|
if headers == nil || cfg == nil || cfg.Codex.DisableCodexCloaking {
|
|
return
|
|
}
|
|
headers.Set("User-Agent", codexUserAgent)
|
|
headers.Set("Originator", codexOriginator)
|
|
}
|
|
|
|
func normalizeCodexInstructions(body []byte) []byte {
|
|
instructions := gjson.GetBytes(body, "instructions")
|
|
if !instructions.Exists() || instructions.Type == gjson.Null {
|
|
body, _ = sjson.SetBytes(body, "instructions", "")
|
|
}
|
|
return body
|
|
}
|
|
|
|
var imageGenToolJSON = []byte(`{"type":"image_generation","output_format":"png"}`)
|
|
var imageGenToolArrayJSON = []byte(`[{"type":"image_generation","output_format":"png"}]`)
|
|
|
|
func isCodexFreePlanAuth(auth *cliproxyauth.Auth) bool {
|
|
if auth == nil || auth.Attributes == nil {
|
|
return false
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(auth.Provider), "codex") {
|
|
return false
|
|
}
|
|
return strings.EqualFold(strings.TrimSpace(auth.Attributes["plan_type"]), "free")
|
|
}
|
|
|
|
func isImageGenerationFunctionTool(tool gjson.Result) bool {
|
|
switch tool.Get("type").String() {
|
|
case "function":
|
|
return tool.Get("name").String() == "image_gen.imagegen"
|
|
case "namespace":
|
|
if tool.Get("name").String() != "image_gen" {
|
|
return false
|
|
}
|
|
tools := tool.Get("tools")
|
|
if !tools.IsArray() {
|
|
return false
|
|
}
|
|
for _, nestedTool := range tools.Array() {
|
|
if nestedTool.Get("type").String() == "function" && nestedTool.Get("name").String() == "imagegen" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isCodexResponsesLiteRequest(body []byte, headers http.Header) bool {
|
|
if strings.EqualFold(strings.TrimSpace(headers.Get(codexResponsesLiteHeader)), "true") {
|
|
return true
|
|
}
|
|
// Codex Desktop mirrors websocket-only request headers into client_metadata.
|
|
value := gjson.GetBytes(body, codexResponsesLiteMetadata)
|
|
if !value.Exists() {
|
|
return false
|
|
}
|
|
return value.Type == gjson.True || value.Type == gjson.String && strings.EqualFold(strings.TrimSpace(value.String()), "true")
|
|
}
|
|
|
|
func ensureImageGenerationTool(body []byte, baseModel string, auth *cliproxyauth.Auth, headers http.Header) []byte {
|
|
if isCodexResponsesLiteRequest(body, headers) {
|
|
return body
|
|
}
|
|
if strings.HasSuffix(baseModel, "spark") {
|
|
return body
|
|
}
|
|
if isCodexFreePlanAuth(auth) {
|
|
return body
|
|
}
|
|
|
|
tools := gjson.GetBytes(body, "tools")
|
|
if !tools.Exists() || !tools.IsArray() {
|
|
body, _ = sjson.SetRawBytes(body, "tools", imageGenToolArrayJSON)
|
|
return body
|
|
}
|
|
for _, t := range tools.Array() {
|
|
if t.Get("type").String() == "image_generation" || isImageGenerationFunctionTool(t) {
|
|
return body
|
|
}
|
|
}
|
|
body, _ = sjson.SetRawBytes(body, "tools.-1", imageGenToolJSON)
|
|
return body
|
|
}
|
|
|
|
func normalizeCodexParallelToolCalls(body []byte, headers http.Header) []byte {
|
|
if isCodexResponsesLiteRequest(body, headers) {
|
|
body = helps.SetBoolIfDifferent(body, "parallel_tool_calls", false)
|
|
return body
|
|
}
|
|
return normalizeCodexParallelToolCallsForTools(body)
|
|
}
|
|
|
|
func normalizeCodexParallelToolCallsForTools(body []byte) []byte {
|
|
if !gjson.GetBytes(body, "parallel_tool_calls").Exists() {
|
|
return body
|
|
}
|
|
|
|
tools := gjson.GetBytes(body, "tools")
|
|
hasTools := tools.Exists() && tools.IsArray() && len(tools.Array()) > 0
|
|
if hasTools {
|
|
return body
|
|
}
|
|
|
|
body, _ = sjson.DeleteBytes(body, "parallel_tool_calls")
|
|
return body
|
|
}
|
|
|
|
func publishCodexImageToolUsage(ctx context.Context, reporter *helps.UsageReporter, body []byte, completedData []byte) {
|
|
detail, ok := helps.ParseCodexImageToolUsage(completedData)
|
|
if !ok {
|
|
return
|
|
}
|
|
reporter.EnsurePublished(ctx)
|
|
reporter.PublishAdditionalModel(ctx, codexImageGenerationToolModel(body), detail)
|
|
}
|
|
|
|
func codexImageGenerationToolModel(body []byte) string {
|
|
tools := gjson.GetBytes(body, "tools")
|
|
if tools.IsArray() {
|
|
for _, tool := range tools.Array() {
|
|
if tool.Get("type").String() != "image_generation" {
|
|
continue
|
|
}
|
|
if model := strings.TrimSpace(tool.Get("model").String()); model != "" {
|
|
return model
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return codexDefaultImageToolModel
|
|
}
|