fix(codex): advertise search capability for bridged routes

This commit is contained in:
wucm667
2026-09-04 16:43:15 +08:00
parent b1748c4ea9
commit c7538cd256
2 changed files with 145 additions and 3 deletions
@@ -93,6 +93,83 @@ func TestBuildCodexModelsManifestForGroupUsesNoneForExplicitNonReasoningMetadata
require.Equal(t, []string{"none"}, effortsFromManifestModel(t, models[0]))
}
func TestBuildCodexModelsManifestForGroupAdvertisesSearchOnlyForChatBridgeRoutes(t *testing.T) {
t.Parallel()
newAccount := func(id int64, nativeResponses bool) Account {
return Account{
ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
Credentials: map[string]any{
"base_url": "https://provider.example/v1",
"model_mapping": map[string]any{"company-coding-model": "company-coding-model"},
},
Extra: map[string]any{"openai_responses_supported": nativeResponses},
}
}
for _, tc := range []struct {
name string
accounts []Account
want bool
}{
{name: "chat bridge", accounts: []Account{newAccount(80, false)}, want: true},
{name: "native responses", accounts: []Account{newAccount(81, true)}, want: false},
{name: "mixed routes", accounts: []Account{newAccount(82, false), newAccount(83, true)}, want: false},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
body, err := buildCodexModelsManifestForAccounts(
PlatformOpenAI, []string{"company-coding-model"}, tc.accounts, nil, true,
)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Len(t, models, 1)
require.Equal(t, tc.want, models[0]["supports_search_tool"])
})
}
}
func TestCompleteAPIKeyCodexManifestSearchCapabilityPreservesUpstreamAndFailsClosed(t *testing.T) {
t.Parallel()
nativeAccount := &Account{
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"base_url": "https://provider.example/v1",
},
Extra: map[string]any{"openai_responses_supported": true},
}
body, err := completeAPIKeyCodexModelsManifestMetadata([]byte(`{"models":[
{"slug":"explicit","supports_search_tool":true},
{"slug":"missing"}
]}`), true, nativeAccount)
require.NoError(t, err)
models := decodeCodexManifestModels(t, body)
require.Equal(t, true, models[0]["supports_search_tool"])
require.Equal(t, false, models[1]["supports_search_tool"])
chatAccount := &Account{
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"base_url": "https://provider.example/v1",
},
Extra: map[string]any{"openai_responses_supported": false},
}
body, err = completeAPIKeyCodexModelsManifestMetadata(
[]byte(`{"models":[
{"slug":"explicit-disabled","supports_search_tool":false},
{"slug":"generated"}
]}`), true, chatAccount,
)
require.NoError(t, err)
models = decodeCodexManifestModels(t, body)
require.Equal(t, false, models[0]["supports_search_tool"])
require.Equal(t, true, models[1]["supports_search_tool"])
}
// Scenario: multiple schedulable accounts advertise only their shared capabilities.
func TestBuildCodexModelsManifestForGroupIntersectsSyncedAccountMetadata(t *testing.T) {
t.Parallel()
@@ -734,7 +734,7 @@ func claudeCodexDisplayName(modelID string) string {
// routed through a custom provider. The response is also suitable for saving
// as model_catalog_json in clients that do not refresh custom-provider catalogs.
func BuildCodexModelsManifest(modelIDs []string) ([]byte, error) {
return buildCodexModelsManifest(modelIDs, nil, nil, nil)
return buildCodexModelsManifest(modelIDs, nil, nil, nil, nil)
}
// BuildCodexModelsManifestForGroup derives input capabilities from the
@@ -789,6 +789,7 @@ func buildCodexModelsManifestForAccounts(
compositeRoutesAvailable bool,
) ([]byte, error) {
imageInputModels := make(map[string]bool, len(modelIDs))
searchToolModels := make(map[string]bool, len(modelIDs))
metadataModels := codexCatalogMetadataModels(
effectivePlatform,
modelIDs,
@@ -808,6 +809,15 @@ func buildCodexModelsManifestForAccounts(
) {
imageInputModels[modelID] = true
}
if groupCodexModelSupportsSearchTool(
effectivePlatform,
modelID,
accounts,
compositeRoutes,
compositeRoutesAvailable,
) {
searchToolModels[modelID] = true
}
if metadata, ok := groupCodexModelMetadata(
effectivePlatform,
modelID,
@@ -818,12 +828,13 @@ func buildCodexModelsManifestForAccounts(
modelMetadata[modelID] = metadata
}
}
return buildCodexModelsManifest(modelIDs, imageInputModels, metadataModels, modelMetadata)
return buildCodexModelsManifest(modelIDs, imageInputModels, searchToolModels, metadataModels, modelMetadata)
}
func buildCodexModelsManifest(
modelIDs []string,
imageInputModels map[string]bool,
searchToolModels map[string]bool,
metadataModels map[string]string,
modelMetadata map[string]codexModelMetadataOverride,
) ([]byte, error) {
@@ -850,6 +861,7 @@ func buildCodexModelsManifest(
if imageInputModels[modelID] {
descriptor.InputModalities = []string{"text", "image"}
}
descriptor.SupportsSearchTool = searchToolModels[modelID]
if metadata, ok := modelMetadata[modelID]; ok {
applyUpstreamModelMetadataToCodexDescriptor(&descriptor, metadata)
}
@@ -1005,6 +1017,52 @@ func groupCodexModelSupportsImageInput(
return candidates > 0
}
// groupCodexModelSupportsSearchTool advertises client-side tool discovery only
// when every account that may serve the model uses the gateway's Responses to
// Chat Completions bridge. Native Responses routes must declare the capability
// in their upstream Codex manifest instead of having it inferred here.
func groupCodexModelSupportsSearchTool(
platform string,
modelID string,
accounts []Account,
compositeRoutes []CompositeModelRoute,
compositeRoutesAvailable bool,
) bool {
modelID = strings.TrimSpace(modelID)
if modelID == "" {
return false
}
upstreamModel := modelID
if platform == PlatformComposite {
var resolved bool
platform, upstreamModel, resolved = resolveCodexCompositeModelTarget(
modelID,
accounts,
compositeRoutes,
compositeRoutesAvailable,
)
if !resolved {
return false
}
}
if platform != PlatformOpenAI {
return false
}
candidates := 0
for i := range accounts {
account := &accounts[i]
if account.Platform != platform || !account.IsModelSupported(upstreamModel) {
continue
}
candidates++
if !shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
return false
}
}
return candidates > 0
}
func resolveCodexCompositeModelTarget(
modelID string,
accounts []Account,
@@ -1939,7 +1997,13 @@ func convertOpenAIModelListToCodexManifestForAccount(body []byte, account *Accou
imageInputModels[modelID] = true
}
}
converted, err := buildCodexModelsManifest(modelIDs, imageInputModels, nil, nil)
searchToolModels := make(map[string]bool, len(modelIDs))
if shouldForwardOpenAIResponsesViaRawChatCompletions(account) {
for _, modelID := range modelIDs {
searchToolModels[modelID] = true
}
}
converted, err := buildCodexModelsManifest(modelIDs, imageInputModels, searchToolModels, nil, nil)
if err != nil {
return body
}
@@ -2141,6 +2205,7 @@ func completeAPIKeyCodexModelsManifestMetadata(body []byte, completeAll bool, ac
}
descriptor := newConfiguredCodexModelDescriptor(slug)
descriptor.SupportsSearchTool = shouldForwardOpenAIResponsesViaRawChatCompletions(account)
if accountCodexModelSupportsImageInput(account, slug) {
descriptor.InputModalities = []string{"text", "image"}
}