mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-29 08:11:32 +00:00
fix(codex): advertise search capability for bridged routes
This commit is contained in:
@@ -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"}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user