From 33521a4aa8cb202d1e08cff6e010fc375fafd315 Mon Sep 17 00:00:00 2001 From: Stan Date: Tue, 27 Oct 2026 14:41:08 +0500 Subject: [PATCH 1/5] Drop the legacy completions path from the AI/ML provider The AI/ML gateway does not serve the legacy /v1/completions endpoint (it answers 404), so the built-in provider should not advertise it. Declare only the chat completions path, matching the other built-in providers that are chat-only (NVIDIA NIM, Mistral, Google, xAI). No preset routes AI/ML requests to the completion endpoint today, so this does not change request behavior; it keeps the registry accurate. --- src/services/apis/provider-registry.mjs | 1 - .../services/apis/provider-registry.test.mjs | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/services/apis/provider-registry.mjs b/src/services/apis/provider-registry.mjs index 206cc4481..2f01db530 100644 --- a/src/services/apis/provider-registry.mjs +++ b/src/services/apis/provider-registry.mjs @@ -65,7 +65,6 @@ const BUILTIN_PROVIDER_TEMPLATE = [ name: 'AI/ML', baseUrl: 'https://api.aimlapi.com/v1', chatCompletionsPath: '/chat/completions', - completionsPath: '/completions', builtin: true, enabled: true, }, diff --git a/tests/unit/services/apis/provider-registry.test.mjs b/tests/unit/services/apis/provider-registry.test.mjs index 722db6a67..ecdc12674 100644 --- a/tests/unit/services/apis/provider-registry.test.mjs +++ b/tests/unit/services/apis/provider-registry.test.mjs @@ -100,6 +100,28 @@ test('resolveProviderIdForSession does not treat raw Gemini model IDs as legacy assert.equal(resolveProviderIdForSession({ modelName: 'gemini-2.5-flash' }), null) }) +test('AI/ML built-in provider only declares the chat completions endpoint', () => { + const config = { + providerSecrets: { + aiml: 'aiml-key', + }, + } + + assert.equal(getProviderById(config, 'aiml').completionsPath, undefined) + + const resolved = resolveOpenAICompatibleRequest(config, { + apiMode: { + groupName: 'aimlModelKeys', + itemName: 'aiml_openai_gpt_5_5', + providerId: '', + }, + }) + + assert.equal(resolved.providerId, 'aiml') + assert.equal(resolved.endpointType, 'chat') + assert.equal(resolved.requestUrl, 'https://api.aimlapi.com/v1/chat/completions') +}) + test('resolveOpenAICompatibleRequest resolves custom provider from normalized session provider id', () => { const config = { customOpenAIProviders: [ From 5f3936d39b60215d4fb492ebe2a4fc0dced354fe Mon Sep 17 00:00:00 2001 From: Stan Date: Tue, 27 Oct 2026 14:42:12 +0500 Subject: [PATCH 2/5] Add AI/ML API attribution headers Send X-AIMLAPI-Source and X-AIMLAPI-Partner-ID on requests whose hostname is exactly api.aimlapi.com, next to the existing OpenRouter app attribution. This covers the built-in AI/ML provider and custom providers pointed at the same host. Requests to any other host get no AI/ML headers, including proxies copied from the AI/ML provider, look-alike hosts, subdomains, and URLs that only mention the host in their path. --- src/services/apis/openai-api.mjs | 23 ++- .../services/apis/aiml-attribution.test.mjs | 158 ++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 tests/unit/services/apis/aiml-attribution.test.mjs diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 86ae062d8..76a87828c 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -43,6 +43,11 @@ const OPENROUTER_ATTRIBUTION_HEADERS = { 'X-OpenRouter-Title': 'ChatGPTBox', 'X-OpenRouter-Categories': 'general-chat,writing-assistant', } +const AIML_API_HOSTNAME = 'api.aimlapi.com' +const AIML_ATTRIBUTION_HEADERS = { + 'X-AIMLAPI-Source': 'agent/chatgptbox', + 'X-AIMLAPI-Partner-ID': 'part_suAsqzeIpC2a5yS8K1leCNtf', +} function hasOpenAICompatibleRuntimeConfig(config) { if (!config || typeof config !== 'object') return false @@ -115,6 +120,22 @@ function getOpenRouterAttributionHeaders(requestUrl) { return OPENROUTER_ATTRIBUTION_HEADERS } +function getAimlAttributionHeaders(requestUrl) { + try { + if (new URL(requestUrl).hostname !== AIML_API_HOSTNAME) return {} + } catch { + return {} + } + return AIML_ATTRIBUTION_HEADERS +} + +function getAppAttributionHeaders(requestUrl) { + return { + ...getOpenRouterAttributionHeaders(requestUrl), + ...getAimlAttributionHeaders(requestUrl), + } +} + function resolveOllamaKeepAliveBaseUrl(request) { const requestUrl = normalizeBaseUrl(request?.requestUrl) if (requestUrl) { @@ -329,7 +350,7 @@ export async function generateAnswersWithOpenAICompatibleApi(port, question, ses apiKey: request.apiKey, config: runtimeConfig, provider: providerRequestShapingId, - extraHeaders: getOpenRouterAttributionHeaders(request.requestUrl), + extraHeaders: getAppAttributionHeaders(request.requestUrl), allowLegacyResponseField: request.provider.allowLegacyResponseField, }) diff --git a/tests/unit/services/apis/aiml-attribution.test.mjs b/tests/unit/services/apis/aiml-attribution.test.mjs new file mode 100644 index 000000000..44126cc47 --- /dev/null +++ b/tests/unit/services/apis/aiml-attribution.test.mjs @@ -0,0 +1,158 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { generateAnswersWithOpenAICompatibleApi } from '../../../../src/services/apis/openai-api.mjs' +import { createFakePort } from '../../helpers/port.mjs' +import { createMockSseResponse } from '../../helpers/sse-response.mjs' + +const ATTRIBUTION_HEADER_NAMES = ['X-AIMLAPI-Source', 'X-AIMLAPI-Partner-ID'] + +function createConfig(overrides = {}) { + return { + maxConversationContextLength: 3, + maxResponseTokenLength: 256, + temperatureOverrideEnabled: false, + temperature: 1, + ...overrides, + } +} + +function createCustomProviderSession(providerId) { + return { + modelName: 'customModel', + conversationRecords: [], + isRetry: false, + apiMode: { + groupName: 'customApiModelKeys', + itemName: 'customModel', + isCustom: true, + providerId, + customName: 'openai/gpt-5-5', + customUrl: '', + apiKey: '', + active: true, + }, + } +} + +function createCustomProviderConfig(providerId, baseUrl, extraProviderFields = {}) { + return createConfig({ + customOpenAIProviders: [ + { + id: providerId, + name: providerId, + baseUrl, + chatCompletionsPath: '/chat/completions', + completionsPath: '/completions', + enabled: true, + ...extraProviderFields, + }, + ], + providerSecrets: { + [providerId]: 'custom-key', + }, + }) +} + +async function captureRequest(t, config, session) { + let capturedInput + let capturedInit + t.mock.method(globalThis, 'fetch', async (input, init) => { + capturedInput = input + capturedInit = init + return createMockSseResponse([ + 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}\n\n', + ]) + }) + + await generateAnswersWithOpenAICompatibleApi(createFakePort(), 'CurrentQ', session, config) + + return { capturedInput, capturedInit } +} + +function assertAimlAttribution(headers) { + assert.equal(headers['X-AIMLAPI-Source'], 'agent/chatgptbox') + assert.equal(headers['X-AIMLAPI-Partner-ID'], 'part_suAsqzeIpC2a5yS8K1leCNtf') +} + +function assertNoAimlAttribution(headers) { + for (const headerName of ATTRIBUTION_HEADER_NAMES) { + assert.equal(Object.hasOwn(headers, headerName), false, headerName) + } +} + +test('adds attribution headers for built-in AI/ML requests', async (t) => { + const config = createConfig({ + providerSecrets: { + aiml: 'aiml-test-key', + }, + }) + const session = { + modelName: 'aiml_openai_gpt_5_5', + conversationRecords: [], + isRetry: false, + } + + const { capturedInput, capturedInit } = await captureRequest(t, config, session) + + assert.equal(capturedInput, 'https://api.aimlapi.com/v1/chat/completions') + assert.equal(capturedInit.headers.Authorization, 'Bearer aiml-test-key') + assertAimlAttribution(capturedInit.headers) + assert.equal(Object.hasOwn(capturedInit.headers, 'X-OpenRouter-Title'), false) + + const body = JSON.parse(capturedInit.body) + for (const headerName of ATTRIBUTION_HEADER_NAMES) { + assert.equal(Object.hasOwn(body, headerName), false) + } +}) + +test('adds attribution headers for custom providers that call AI/ML directly', async (t) => { + const config = createCustomProviderConfig('direct-aiml', 'https://api.aimlapi.com/v1') + const session = createCustomProviderSession('direct-aiml') + + const { capturedInput, capturedInit } = await captureRequest(t, config, session) + + assert.equal(capturedInput, 'https://api.aimlapi.com/v1/chat/completions') + assert.equal(capturedInit.headers.Authorization, 'Bearer custom-key') + assertAimlAttribution(capturedInit.headers) +}) + +const nonAimlBaseUrls = [ + ['third-party proxy', 'https://proxy.example.com/v1'], + ['look-alike host', 'https://api.aimlapi.com.example.com/v1'], + ['look-alike prefix', 'https://notapi.aimlapi.com/v1'], + ['subdomain', 'https://eu.api.aimlapi.com/v1'], + ['parent domain', 'https://aimlapi.com/v1'], + ['host in path', 'https://proxy.example.com/api.aimlapi.com/v1'], +] + +for (const [label, baseUrl] of nonAimlBaseUrls) { + test(`omits AI/ML attribution headers for a ${label} copied from the AI/ML provider`, async (t) => { + const config = createCustomProviderConfig('aiml-proxy', baseUrl, { sourceProviderId: 'aiml' }) + const session = createCustomProviderSession('aiml-proxy') + + const { capturedInput, capturedInit } = await captureRequest(t, config, session) + + assert.equal(capturedInput, `${baseUrl}/chat/completions`) + assert.equal(capturedInit.headers.Authorization, 'Bearer custom-key') + assertNoAimlAttribution(capturedInit.headers) + }) +} + +test('omits AI/ML attribution headers for other built-in providers', async (t) => { + const config = createConfig({ + providerSecrets: { + openrouter: 'sk-or-test', + }, + }) + const session = { + modelName: 'openRouter_auto', + conversationRecords: [], + isRetry: false, + } + + const { capturedInput, capturedInit } = await captureRequest(t, config, session) + + assert.equal(capturedInput, 'https://openrouter.ai/api/v1/chat/completions') + assertNoAimlAttribution(capturedInit.headers) + assert.equal(capturedInit.headers['X-OpenRouter-Title'], 'ChatGPTBox') +}) From 2908dd1fd1d86e4384a8337abc0e0710b9907112 Mon Sep 17 00:00:00 2001 From: Stan Date: Tue, 27 Oct 2026 14:43:16 +0500 Subject: [PATCH 3/5] Use the AI/ML provider label in AI/ML model names The provider group and the provider registry call this provider "AI/ML", but its model entries were labelled "AIML (...)". Use the same "AI/ML (...)" prefix for the model entries so the picker shows one consistent provider name. Only display labels change; model keys and model IDs are unchanged. --- src/config/index.mjs | 32 ++++++++++---------- tests/unit/config/config-predicates.test.mjs | 6 ++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/config/index.mjs b/src/config/index.mjs index 0a043c38c..7c5ef0f65 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -645,67 +645,67 @@ export const Models = { }, aiml_openai_gpt_5_5: { value: 'openai/gpt-5-5', - desc: 'AIML (GPT-5.5)', + desc: 'AI/ML (GPT-5.5)', }, aiml_openai_gpt_5_4: { value: 'openai/gpt-5-4', - desc: 'AIML (GPT-5.4)', + desc: 'AI/ML (GPT-5.4)', }, aiml_openai_gpt_5_1: { value: 'openai/gpt-5-1', - desc: 'AIML (GPT-5.1)', + desc: 'AI/ML (GPT-5.1)', }, aiml_openai_gpt_5: { value: 'openai/gpt-5', - desc: 'AIML (GPT-5)', + desc: 'AI/ML (GPT-5)', }, aiml_claude_opus_4_8: { value: 'anthropic/claude-opus-4-8', - desc: 'AIML (Claude Opus 4.8)', + desc: 'AI/ML (Claude Opus 4.8)', }, aiml_claude_haiku_4_5: { value: 'anthropic/claude-haiku-4.5', - desc: 'AIML (Claude Haiku 4.5)', + desc: 'AI/ML (Claude Haiku 4.5)', }, aiml_claude_sonnet_4_6_20260218: { value: 'anthropic/claude-sonnet-4-6-20260218', - desc: 'AIML (Claude Sonnet 4.6)', + desc: 'AI/ML (Claude Sonnet 4.6)', }, aiml_openai_gpt_5_2: { value: 'openai/gpt-5-2', - desc: 'AIML (GPT-5.2)', + desc: 'AI/ML (GPT-5.2)', }, aiml_google_gemini_3_5_flash: { value: 'google/gemini-3-5-flash', - desc: 'AIML (Gemini 3.5 Flash)', + desc: 'AI/ML (Gemini 3.5 Flash)', }, aiml_google_gemini_3_flash_preview: { value: 'google/gemini-3-flash-preview', - desc: 'AIML (Gemini 3 Flash)', + desc: 'AI/ML (Gemini 3 Flash)', }, aiml_google_gemini_3_1_pro_preview: { value: 'google/gemini-3-1-pro-preview', - desc: 'AIML (Gemini 3.1 Pro)', + desc: 'AI/ML (Gemini 3.1 Pro)', }, aiml_google_gemini_2_5_pro: { value: 'google/gemini-2.5-pro', - desc: 'AIML (Gemini 2.5 Pro)', + desc: 'AI/ML (Gemini 2.5 Pro)', }, aiml_google_gemini_2_5_flash: { value: 'google/gemini-2.5-flash', - desc: 'AIML (Gemini 2.5 Flash)', + desc: 'AI/ML (Gemini 2.5 Flash)', }, aiml_moonshot_kimi_k2_5: { value: 'moonshot/kimi-k2-5', - desc: 'AIML (Kimi K2.5)', + desc: 'AI/ML (Kimi K2.5)', }, aiml_deepseek_v4_pro: { value: 'deepseek/deepseek-v4-pro', - desc: 'AIML (DeepSeek V4 Pro)', + desc: 'AI/ML (DeepSeek V4 Pro)', }, aiml_deepseek_v4_flash: { value: 'deepseek/deepseek-v4-flash', - desc: 'AIML (DeepSeek V4 Flash)', + desc: 'AI/ML (DeepSeek V4 Flash)', }, googleGemini3_1Pro: { value: 'gemini-3.1-pro-preview', diff --git a/tests/unit/config/config-predicates.test.mjs b/tests/unit/config/config-predicates.test.mjs index 221bb6b0c..b1a3363aa 100644 --- a/tests/unit/config/config-predicates.test.mjs +++ b/tests/unit/config/config-predicates.test.mjs @@ -312,6 +312,12 @@ test('aimlApiModelKeys does not expose duplicate picker entries', () => { assert.equal(new Set(signatures).size, signatures.length) }) +test('aimlApiModelKeys use the AI/ML provider label', () => { + for (const modelName of aimlApiModelKeys) { + assert.match(Models[modelName].desc, /^AI\/ML \(.+\)$/, modelName) + } +}) + test('isUsingChatGLMApiModel detects ChatGLM models', () => { assert.equal(isUsingChatGLMApiModel({ modelName: 'chatglm52' }), true) assert.equal(isUsingChatGLMApiModel({ modelName: 'chatglm47' }), true) From 4ebbce9caddc35bcb99063dae3143d42907a9907 Mon Sep 17 00:00:00 2001 From: Stan Date: Tue, 27 Oct 2026 14:44:30 +0500 Subject: [PATCH 4/5] Refresh AI/ML model presets Bring the AI/ML presets in line with the current gateway catalog (https://api.aimlapi.com/v1/models, checked 2026-10-27). Every preset value is now a model ID listed with an openai/chat-completions entry. Add current models from the main vendors on the gateway: - OpenAI: GPT-6.1 Sol, GPT-6 Astra, GPT-6 Luna - Anthropic: Claude Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5 - Google: Gemini 4 Argon, Gemini 3.8 Flash - Moonshot: Kimi K3 Fix presets whose IDs are no longer listed as models: - Kimi K2.5 now uses moonshotai/kimi-k2.5; moonshot/kimi-k2-5 is gone from the catalog. - Gemini 3.1 Pro now uses google/gemini-3.1-pro-preview instead of the alias google/gemini-3-1-pro-preview. - Claude Sonnet 4.6 now uses anthropic/claude-sonnet-4.6 instead of the dated alias. Its key becomes aiml_claude_sonnet_4_6, and the old dated key is added to the legacy model key migrations so saved selections, modes and sessions move to the same model. Existing presets that are still listed are kept, so no saved selection is moved to a different model. --- src/config/index.mjs | 60 +++++++++++++++++-- src/config/model-key-migrations.mjs | 7 ++- tests/unit/config/config-predicates.test.mjs | 7 ++- .../unit/config/migrate-user-config.test.mjs | 40 +++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/config/index.mjs b/src/config/index.mjs index 7c5ef0f65..0a4d75edc 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -179,7 +179,17 @@ export const openRouterApiModelKeys = [ 'openRouter_deepseek_v4_flash', ] export const aimlApiModelKeys = [ - 'aiml_claude_sonnet_4_6_20260218', + 'aiml_openai_gpt_6_1_sol', + 'aiml_openai_gpt_6_astra', + 'aiml_openai_gpt_6_luna', + 'aiml_claude_fable_5_1', + 'aiml_claude_opus_5_5', + 'aiml_claude_sonnet_5_5', + 'aiml_claude_haiku_5_5', + 'aiml_google_gemini_4_argon', + 'aiml_google_gemini_3_8_flash', + 'aiml_moonshot_kimi_k3', + 'aiml_claude_sonnet_4_6', 'aiml_openai_gpt_5_2', 'aiml_google_gemini_3_flash_preview', 'aiml_google_gemini_3_1_pro_preview', @@ -643,6 +653,18 @@ export const Models = { value: 'openai/gpt-4.1-mini', desc: 'OpenRouter (GPT-4.1 Mini)', }, + aiml_openai_gpt_6_1_sol: { + value: 'openai/gpt-6.1-sol', + desc: 'AI/ML (GPT-6.1 Sol)', + }, + aiml_openai_gpt_6_astra: { + value: 'openai/gpt-6-astra', + desc: 'AI/ML (GPT-6 Astra)', + }, + aiml_openai_gpt_6_luna: { + value: 'openai/gpt-6-luna', + desc: 'AI/ML (GPT-6 Luna)', + }, aiml_openai_gpt_5_5: { value: 'openai/gpt-5-5', desc: 'AI/ML (GPT-5.5)', @@ -659,6 +681,22 @@ export const Models = { value: 'openai/gpt-5', desc: 'AI/ML (GPT-5)', }, + aiml_claude_fable_5_1: { + value: 'anthropic/claude-fable-5.1', + desc: 'AI/ML (Claude Fable 5.1)', + }, + aiml_claude_opus_5_5: { + value: 'anthropic/claude-opus-5.5', + desc: 'AI/ML (Claude Opus 5.5)', + }, + aiml_claude_sonnet_5_5: { + value: 'anthropic/claude-sonnet-5.5', + desc: 'AI/ML (Claude Sonnet 5.5)', + }, + aiml_claude_haiku_5_5: { + value: 'anthropic/claude-haiku-5.5', + desc: 'AI/ML (Claude Haiku 5.5)', + }, aiml_claude_opus_4_8: { value: 'anthropic/claude-opus-4-8', desc: 'AI/ML (Claude Opus 4.8)', @@ -667,14 +705,22 @@ export const Models = { value: 'anthropic/claude-haiku-4.5', desc: 'AI/ML (Claude Haiku 4.5)', }, - aiml_claude_sonnet_4_6_20260218: { - value: 'anthropic/claude-sonnet-4-6-20260218', + aiml_claude_sonnet_4_6: { + value: 'anthropic/claude-sonnet-4.6', desc: 'AI/ML (Claude Sonnet 4.6)', }, aiml_openai_gpt_5_2: { value: 'openai/gpt-5-2', desc: 'AI/ML (GPT-5.2)', }, + aiml_google_gemini_4_argon: { + value: 'google/gemini-4-argon', + desc: 'AI/ML (Gemini 4 Argon)', + }, + aiml_google_gemini_3_8_flash: { + value: 'google/gemini-3.8-flash', + desc: 'AI/ML (Gemini 3.8 Flash)', + }, aiml_google_gemini_3_5_flash: { value: 'google/gemini-3-5-flash', desc: 'AI/ML (Gemini 3.5 Flash)', @@ -684,7 +730,7 @@ export const Models = { desc: 'AI/ML (Gemini 3 Flash)', }, aiml_google_gemini_3_1_pro_preview: { - value: 'google/gemini-3-1-pro-preview', + value: 'google/gemini-3.1-pro-preview', desc: 'AI/ML (Gemini 3.1 Pro)', }, aiml_google_gemini_2_5_pro: { @@ -695,8 +741,12 @@ export const Models = { value: 'google/gemini-2.5-flash', desc: 'AI/ML (Gemini 2.5 Flash)', }, + aiml_moonshot_kimi_k3: { + value: 'moonshot/kimi-k3', + desc: 'AI/ML (Kimi K3)', + }, aiml_moonshot_kimi_k2_5: { - value: 'moonshot/kimi-k2-5', + value: 'moonshotai/kimi-k2.5', desc: 'AI/ML (Kimi K2.5)', }, aiml_deepseek_v4_pro: { diff --git a/src/config/model-key-migrations.mjs b/src/config/model-key-migrations.mjs index c2995495a..97fa066f3 100644 --- a/src/config/model-key-migrations.mjs +++ b/src/config/model-key-migrations.mjs @@ -41,8 +41,9 @@ export const LEGACY_MODEL_KEY_MIGRATIONS = { openRouter_deepseek_deepseek_chat_v3_0324_free: 'openRouter_deepseek_v4_flash', aiml_anthropic_claude_opus_4: 'aiml_claude_opus_4_8', - aiml_anthropic_claude_sonnet_4: 'aiml_claude_sonnet_4_6_20260218', - aiml_claude_3_7_sonnet_20250219: 'aiml_claude_sonnet_4_6_20260218', + aiml_anthropic_claude_sonnet_4: 'aiml_claude_sonnet_4_6', + aiml_claude_3_7_sonnet_20250219: 'aiml_claude_sonnet_4_6', + aiml_claude_sonnet_4_6_20260218: 'aiml_claude_sonnet_4_6', aiml_google_gemini_2_5_pro_preview_05_06: 'aiml_google_gemini_2_5_pro', aiml_google_gemini_2_5_flash_preview: 'aiml_google_gemini_2_5_flash', aiml_openai_o3_2025_04_16: 'aiml_openai_gpt_5_5', @@ -75,7 +76,7 @@ const MODEL_KEY_GROUP_OVERRIDES = { openRouter_deepseek_v4_flash: 'openRouterApiModelKeys', aiml_claude_opus_4_8: 'aimlModelKeys', - aiml_claude_sonnet_4_6_20260218: 'aimlModelKeys', + aiml_claude_sonnet_4_6: 'aimlModelKeys', aiml_google_gemini_2_5_pro: 'aimlModelKeys', aiml_google_gemini_2_5_flash: 'aimlModelKeys', aiml_openai_gpt_5_5: 'aimlModelKeys', diff --git a/tests/unit/config/config-predicates.test.mjs b/tests/unit/config/config-predicates.test.mjs index b1a3363aa..92a81d7b1 100644 --- a/tests/unit/config/config-predicates.test.mjs +++ b/tests/unit/config/config-predicates.test.mjs @@ -67,7 +67,11 @@ const representativeOpenRouterApiModelNames = [ 'openRouter_anthropic_claude_sonnet4_6', 'openRouter_openai_gpt_5_5', ] -const representativeAimlApiModelNames = ['aiml_claude_sonnet_4_6_20260218', 'aiml_openai_gpt_5_5'] +const representativeAimlApiModelNames = [ + 'aiml_claude_sonnet_4_6', + 'aiml_openai_gpt_5_5', + 'aiml_openai_gpt_6_1_sol', +] const originalNavigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator') @@ -128,6 +132,7 @@ test('canonicalized legacy model keys still match their provider predicates', () ['moonshot_k2', isUsingMoonshotApiModel], ['openRouter_deepseek_deepseek_chat_v3_0324_free', isUsingOpenRouterApiModel], ['aiml_openai_o3_2025_04_16', isUsingAimlApiModel], + ['aiml_claude_sonnet_4_6_20260218', isUsingAimlApiModel], ] for (const [legacyModelKey, predicate] of cases) { diff --git a/tests/unit/config/migrate-user-config.test.mjs b/tests/unit/config/migrate-user-config.test.mjs index e4104b02d..58d23da49 100644 --- a/tests/unit/config/migrate-user-config.test.mjs +++ b/tests/unit/config/migrate-user-config.test.mjs @@ -118,6 +118,46 @@ test('getUserConfig migrates legacy model keys in selected config fields', async assert.equal(storage.customApiModes.at(-1).itemName, 'aiml_openai_gpt_5_5') }) +test('getUserConfig moves the dated AI/ML Claude Sonnet 4.6 key to the canonical preset', async () => { + const datedSonnetMode = { + groupName: 'aimlModelKeys', + itemName: 'aiml_claude_sonnet_4_6_20260218', + isCustom: false, + customName: '', + customUrl: '', + apiKey: '', + providerId: '', + active: true, + } + globalThis.__TEST_BROWSER_SHIM__.replaceStorage({ + configSchemaVersion: 2, + knownApiModeDefaultIds: [...defaultApiModeIds], + modelName: 'aiml_claude_sonnet_4_6_20260218', + apiMode: datedSonnetMode, + customApiModes: [datedSonnetMode], + providerSecrets: { + aiml: 'aiml-key', + }, + }) + + const config = await getUserConfig() + const storage = globalThis.__TEST_BROWSER_SHIM__.getStorage() + + assert.equal(config.modelName, 'aiml_claude_sonnet_4_6') + assert.equal(storage.modelName, 'aiml_claude_sonnet_4_6') + assert.equal(config.apiMode.itemName, 'aiml_claude_sonnet_4_6') + assert.equal(storage.apiMode.itemName, 'aiml_claude_sonnet_4_6') + assert.equal(config.customApiModes[0].itemName, 'aiml_claude_sonnet_4_6') + assert.equal(storage.customApiModes[0].itemName, 'aiml_claude_sonnet_4_6') + + const request = resolveOpenAICompatibleRequest(config, { + modelName: config.modelName, + apiMode: config.apiMode, + }) + assert.equal(request.providerId, 'aiml') + assert.equal(request.requestUrl, 'https://api.aimlapi.com/v1/chat/completions') +}) + test('getUserConfig reuses custom mode key promoted earlier in the same migration pass', async () => { const customUrl = 'https://proxy.example.com/v1/chat/completions' globalThis.__TEST_BROWSER_SHIM__.replaceStorage({ From 09c504eaaeaa8f45c58ff72d4f223a6e19935b81 Mon Sep 17 00:00:00 2001 From: Stan Date: Tue, 27 Oct 2026 14:44:58 +0500 Subject: [PATCH 5/5] Add an AI/ML preset to the default API modes AI/ML had built-in presets but none in the default API modes, so users had to enable it by hand before it appeared in the model picker. Add AI/ML (GPT-6.1 Sol) next to the other aggregator defaults, the same way OpenRouter and NVIDIA NIM are exposed. The mode still needs an AI/ML API key before it sends any request. --- src/config/index.mjs | 1 + tests/unit/config/config-predicates.test.mjs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/config/index.mjs b/src/config/index.mjs index 0a4d75edc..86e364c07 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -846,6 +846,7 @@ export const defaultApiModeIds = [ 'mistralMediumLatest', 'openRouter_auto', 'openRouter_free', + 'aiml_openai_gpt_6_1_sol', 'nvidiaNim_nemotron_3_super', ] diff --git a/tests/unit/config/config-predicates.test.mjs b/tests/unit/config/config-predicates.test.mjs index 92a81d7b1..11e4a2ab7 100644 --- a/tests/unit/config/config-predicates.test.mjs +++ b/tests/unit/config/config-predicates.test.mjs @@ -108,6 +108,12 @@ test('Claude defaults use Haiku 5.5 and keep Haiku 4.5 selectable', () => { assert.ok(claudeApiModelKeys.includes('claudeHaiku45Api')) }) +test('AI/ML defaults expose one current AI/ML preset', () => { + const aimlDefaults = defaultApiModeIds.filter((modelName) => aimlApiModelKeys.includes(modelName)) + assert.deepEqual(aimlDefaults, ['aiml_openai_gpt_6_1_sol']) + assert.equal(isUsingAimlApiModel({ modelName: aimlDefaults[0] }), true) +}) + test('getNavigatorLanguage returns zh-Hant for zh-TW style locales', () => { setNavigatorLanguage('zh-TW') assert.equal(getNavigatorLanguage(), 'zh-Hant')