mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 11:19:50 +08:00
feat(codex): align Search and execution protocol
This commit is contained in:
@@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
// 测试精确映射
|
||||
|
||||
+74
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user