feat(codex): align Search and execution protocol

This commit is contained in:
MMEXA
2026-07-12 03:04:15 +08:00
parent 59d37ae1dd
commit 2316df5c9a
121 changed files with 5141 additions and 543 deletions
@@ -366,12 +366,12 @@ import { type EndpointAPIKey } from '@/api/endpoints/keys'
import { updateModel } from '@/api/endpoints/models'
import { parseApiError } from '@/utils/errorParser'
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
import { normalizeApiFormatAlias } from '@/api/endpoints/types/api-format'
import {
buildDefaultModelTestRequestHeaders,
buildDefaultModelTestRequestBody,
isModelTestableApiFormat,
isModelTestableEndpoint,
modelTestMappingScopeMatchesEndpoint,
parseModelTestRequestHeadersDraft,
parseModelTestRequestBodyDraft,
selectPreferredModelTestEndpoint,
@@ -852,16 +852,11 @@ function scopedMappingEndpoints(item: CombinedMapping): ProviderEndpoint[] {
const group = item.group
if (!group) return activeEndpoints.value
const apiFormats = new Set(normalizeStringList(group.apiFormats).map(normalizeApiFormatAlias))
const endpointIds = new Set(normalizeStringList(group.endpointIds))
const matched = activeEndpoints.value.filter((endpoint) => {
const apiFormatMatched = apiFormats.size === 0
|| apiFormats.has(normalizeApiFormatAlias(endpoint.api_format))
const endpointMatched = endpointIds.size === 0 || endpointIds.has(endpoint.id)
return apiFormatMatched && endpointMatched
})
return matched.length > 0 ? matched : activeEndpoints.value
return activeEndpoints.value.filter(endpoint => modelTestMappingScopeMatchesEndpoint(
group.apiFormats,
group.endpointIds,
endpoint,
))
}
// 测试精确映射
@@ -10,6 +10,7 @@ import {
isModelTestableEndpoint,
isModelTestableApiFormat,
listModelTestMappedModelOptions,
modelTestMappingScopeMatchesEndpoint,
normalizeModelTestMappedModelSelection,
selectPreferredModelTestEndpoint,
setModelTestRequestBodyModel,
@@ -97,6 +98,20 @@ describe('buildDefaultModelTestRequestBody', () => {
expect(body.messages).toBeUndefined()
})
it('uses the synchronous OpenAI Search request contract', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody('gpt-5.6-sol', 'openai:search'))
expect(body).toEqual({
id: expect.stringMatching(/^aether-model-test-/),
model: 'gpt-5.6-sol',
input: 'Hello! This is a test message.',
commands: {
search_query: [{ q: 'Hello! This is a test message.' }],
},
max_output_tokens: 256,
})
})
it('uses image generation tools for image models on OpenAI Responses endpoints', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody(
'gpt-image-2',
@@ -177,6 +192,52 @@ describe('buildDefaultModelTestRequestBody', () => {
expect(options).toEqual([])
})
it('uses one-way format permissions for Search mapping options', () => {
const responsesOptions = listModelTestMappedModelOptions({
provider_model_name: 'gpt-5.6-luna',
provider_model_mappings: [{
name: 'gpt-5.6-luna',
priority: 1,
api_formats: ['openai:responses'],
}],
}, {
id: 'search-endpoint',
api_format: 'openai:search',
})
const searchOptions = listModelTestMappedModelOptions({
provider_model_name: 'gpt-5.6-luna',
provider_model_mappings: [{
name: 'gpt-5.6-luna',
priority: 1,
api_formats: ['openai:search'],
}],
}, {
id: 'responses-endpoint',
api_format: 'openai:responses',
})
expect(responsesOptions).toEqual([{ name: 'gpt-5.6-luna', priority: 1 }])
expect(searchOptions).toEqual([])
})
it('keeps explicit mapping scopes strict when no endpoint matches', () => {
expect(modelTestMappingScopeMatchesEndpoint(
['openai:responses'],
['search-endpoint'],
{ id: 'search-endpoint', api_format: 'openai:search' },
)).toBe(true)
expect(modelTestMappingScopeMatchesEndpoint(
['openai:responses'],
['another-endpoint'],
{ id: 'search-endpoint', api_format: 'openai:search' },
)).toBe(false)
expect(modelTestMappingScopeMatchesEndpoint(
['openai:search'],
undefined,
{ id: 'responses-endpoint', api_format: 'openai:responses' },
)).toBe(false)
})
it('keeps the current model selected by default until a mapped model is chosen', () => {
const options = [
{ name: 'MiniMax-M2.7-highspeed', priority: 1 },
@@ -259,7 +320,7 @@ describe('isModelTestableApiFormat', () => {
'gemini:video',
'gemini:files',
' OPENAI:VIDEO ',
])('excludes task and file endpoint formats from model tests: %s', (apiFormat) => {
])('excludes endpoint formats without a model-test request contract: %s', (apiFormat) => {
expect(isModelTestableApiFormat(apiFormat)).toBe(false)
})
@@ -267,6 +328,7 @@ describe('isModelTestableApiFormat', () => {
'openai:chat',
'openai:responses',
'openai:responses:compact',
'openai:search',
'openai:image',
'claude:messages',
'gemini:generate_content',
@@ -379,6 +441,17 @@ describe('isModelTestableEndpoint', () => {
}, keys)).toBe(true)
})
it('uses one-way format permissions for Search endpoint keys', () => {
expect(isModelTestableEndpoint({
api_format: 'openai:search',
is_active: true,
}, [{ api_formats: ['openai:responses'], is_active: true }])).toBe(true)
expect(isModelTestableEndpoint({
api_format: 'openai:responses',
is_active: true,
}, [{ api_formats: ['openai:search'], is_active: true }])).toBe(false)
})
it('lets fixed provider OAuth keys inherit testable endpoint formats', () => {
const keys = [{
api_formats: ['legacy:mismatch'],
@@ -1,4 +1,7 @@
import { normalizeApiFormatAlias } from '@/api/endpoints/types/api-format'
import {
apiFormatPermissionCovers,
normalizeApiFormatAlias,
} from '@/api/endpoints/types/api-format'
import type { ModelTestCapabilities, OpenAiImageModelTestCapability } from '@/api/endpoints/types'
export type ModelTestEndpointSource = {
@@ -71,7 +74,7 @@ export function modelTestKeySupportsEndpoint(
const keyFormats = normalizeModelTestStringList(key.api_formats)
if (keyFormats.length === 0) return true
return keyFormats.some(format => normalizeApiFormatAlias(format) === endpointFormat)
return keyFormats.some(format => apiFormatPermissionCovers(format, endpointFormat))
}
export function isModelTestableEndpoint(
@@ -1,4 +1,7 @@
import { normalizeApiFormatAlias } from '@/api/endpoints/types/api-format'
import {
apiFormatPermissionCovers,
normalizeApiFormatAlias,
} from '@/api/endpoints/types/api-format'
import type { ProviderModelMapping } from '@/api/endpoints/types'
import type { TestModelRequest } from '@/api/endpoints/providers'
import {
@@ -29,12 +32,18 @@ export type { ModelTestImagePreview } from './model-test-preview'
const DEFAULT_MODEL_TEST_MESSAGE = 'Hello! This is a test message.'
function buildModelTestSearchSessionId(): string {
const id = globalThis.crypto?.randomUUID?.()
?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
return `aether-model-test-${id}`
}
type ModelTestMappingSource = {
provider_model_name: string
provider_model_mappings?: ProviderModelMapping[] | null
}
type ModelTestMappingEndpoint = {
export type ModelTestMappingEndpoint = {
id: string
api_format: string
}
@@ -44,17 +53,19 @@ export type ModelTestMappedModelOption = {
priority: number
}
function mappingApiFormatMatches(mapping: ProviderModelMapping, endpoint: ModelTestMappingEndpoint): boolean {
const apiFormats = normalizeModelTestStringList(mapping.api_formats)
if (apiFormats.length === 0) return true
export function modelTestMappingScopeMatchesEndpoint(
apiFormats: string[] | null | undefined,
endpointIds: string[] | null | undefined,
endpoint: ModelTestMappingEndpoint,
): boolean {
const normalizedApiFormats = normalizeModelTestStringList(apiFormats)
const endpointFormat = normalizeApiFormatAlias(endpoint.api_format)
return apiFormats.some(format => normalizeApiFormatAlias(format) === endpointFormat)
}
function mappingEndpointMatches(mapping: ProviderModelMapping, endpoint: ModelTestMappingEndpoint): boolean {
const endpointIds = normalizeModelTestStringList(mapping.endpoint_ids)
if (endpointIds.length === 0) return true
return endpointIds.includes(endpoint.id)
const apiFormatMatches = normalizedApiFormats.length === 0
|| normalizedApiFormats.some(format => apiFormatPermissionCovers(format, endpointFormat))
const normalizedEndpointIds = normalizeModelTestStringList(endpointIds)
const endpointMatches = normalizedEndpointIds.length === 0
|| normalizedEndpointIds.includes(endpoint.id)
return apiFormatMatches && endpointMatches
}
export function listModelTestMappedModelOptions(
@@ -65,8 +76,11 @@ export function listModelTestMappedModelOptions(
const matchedMappings = model.provider_model_mappings
.filter(mapping => mapping.name.trim())
.filter(mapping => mappingApiFormatMatches(mapping, endpoint))
.filter(mapping => mappingEndpointMatches(mapping, endpoint))
.filter(mapping => modelTestMappingScopeMatchesEndpoint(
mapping.api_formats,
mapping.endpoint_ids,
endpoint,
))
.sort((left, right) => {
const leftPriority = Number.isFinite(left.priority) ? left.priority : 1
const rightPriority = Number.isFinite(right.priority) ? right.priority : 1
@@ -204,6 +218,18 @@ export function buildDefaultModelTestRequestBody(
}, null, 2)
}
if (normalizedApiFormat === 'openai:search') {
return JSON.stringify({
id: buildModelTestSearchSessionId(),
model: modelName,
input: DEFAULT_MODEL_TEST_MESSAGE,
commands: {
search_query: [{ q: DEFAULT_MODEL_TEST_MESSAGE }],
},
max_output_tokens: 256,
}, null, 2)
}
if (normalizedApiFormat === 'openai:responses' && modelSupportsImageGeneration(model)) {
return JSON.stringify({
model: modelName,