From dfc1905321a3547ca5fb47d9c16856b065ff7969 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:41:08 +0000 Subject: [PATCH 1/4] feat(ai-gateway): add Eden AI direct BYOK Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../direct-byok/direct-byok-definitions.ts | 2 ++ .../providers/direct-byok/direct-byok-meta.ts | 1 + .../providers/direct-byok/edenai.test.ts | 31 +++++++++++++++++++ .../providers/direct-byok/edenai.ts | 29 +++++++++++++++++ .../direct-byok/sync-direct-byok.test.ts | 12 +++++++ .../providers/direct-byok/sync-direct-byok.ts | 3 +- .../ai-gateway/providers/model-settings.ts | 2 +- .../openrouter/inference-provider-id.ts | 2 ++ 8 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts create mode 100644 apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-definitions.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-definitions.ts index b4ffcdc478..dd7d1e5c7b 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-definitions.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-definitions.ts @@ -3,6 +3,7 @@ import alibabaTokenPlan from './alibaba-token-plan'; import byteplusCoding from './byteplus-coding'; import chutesByok from './chutes-byok'; import crofai from './crofai'; +import edenai from './edenai'; import inceptronByok from './inceptron-byok'; import kimiCoding from './kimi-coding'; import martian from './martian'; @@ -22,6 +23,7 @@ export default [ byteplusCoding, chutesByok, crofai, + edenai, inceptronByok, kimiCoding, martian, diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-meta.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-meta.ts index 5860086fc2..012c88efe0 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-meta.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/direct-byok-meta.ts @@ -6,6 +6,7 @@ export const DIRECT_BYOK_PROVIDERS_META = { 'byteplus-coding': 'BytePlus Coding Plan', 'chutes-byok': 'Chutes BYOK', crofai: 'CrofAI', + edenai: 'Eden AI', 'kimi-coding': 'Kimi Code', 'inceptron-byok': 'Inceptron BYOK', martian: 'Martian', diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts new file mode 100644 index 0000000000..cc99c282cd --- /dev/null +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts @@ -0,0 +1,31 @@ +import { getAiSdkProvider } from '../model-settings'; +import type { TransformRequestContext } from '../types'; +import edenai from './edenai'; + +describe('Eden AI direct BYOK provider', () => { + test('uses the Eden AI v3 Chat Completions API', () => { + expect(edenai.base_url).toBe('https://api.edenai.run/v3'); + expect(edenai.supported_chat_apis).toEqual(['chat_completions']); + }); + + test.each(['edenai/openai/gpt-5.6-luna', 'edenai/anthropic/claude-sonnet-5'])( + 'uses OpenAI-compatible Chat Completions for %s', + model => { + expect(getAiSdkProvider(model, 'edenai')).toBe('openai-compatible'); + } + ); + + test.each([ + [{ reasoning: { effort: 'high' } }, 'high'], + [{ reasoning: { effort: 'low' }, reasoning_effort: 'medium' }, 'medium'], + ] as const)('translates reasoning settings for Chat Completions', (body, expectedEffort) => { + const context = { + request: { kind: 'chat_completions', body: { model: 'test', messages: [], ...body } }, + } as TransformRequestContext; + + edenai.transformRequest(context); + + expect(context.request.body).toMatchObject({ reasoning_effort: expectedEffort }); + expect(context.request.body).not.toHaveProperty('reasoning'); + }); +}); diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts new file mode 100644 index 0000000000..b24f56132f --- /dev/null +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts @@ -0,0 +1,29 @@ +import { cachedEnhancedDirectByokModelList } from '@/lib/ai-gateway/providers/direct-byok/model-list'; +import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/types'; + +export default { + id: 'edenai', + base_url: 'https://api.edenai.run/v3', + supported_chat_apis: ['chat_completions'], + default_ai_sdk_provider: 'openai-compatible', + transformRequest(context) { + const { request } = context; + if (request.kind !== 'chat_completions') { + return; + } + request.body.reasoning_effort ??= request.body.reasoning?.effort ?? undefined; + delete request.body.reasoning; + }, + models: cachedEnhancedDirectByokModelList({ + providerId: 'edenai', + recommendedModels: [ + { + id: 'openai/gpt-5.6-luna', + name: 'GPT-5.6 Luna', + flags: ['vision', 'reasoning'], + context_length: 1_050_000, + max_completion_tokens: 128_000, + }, + ], + }), +} satisfies DirectByokProvider; diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts index 0cf125b2ff..1aa7de5f91 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.test.ts @@ -20,6 +20,10 @@ describe('parseOpenAICompatibleProviderModels', () => { id: 'morph-minimax3-428b', max_model_len: 256000, }, + { + id: 'provider-with-null-context', + context_length: null, + }, ], }); @@ -40,6 +44,14 @@ describe('parseOpenAICompatibleProviderModels', () => { input_modalities: undefined, flags: ['reasoning'], }, + { + id: 'provider-with-null-context', + name: undefined, + context_length: undefined, + max_completion_tokens: undefined, + input_modalities: undefined, + flags: ['reasoning'], + }, ]); }); diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts index c34b39f496..1be277bc6b 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/sync-direct-byok.ts @@ -29,7 +29,7 @@ const OpenAICompatibleModelsResponseSchema = z.object({ z.object({ id: z.string(), name: z.string().optional(), - context_length: z.number().optional(), + context_length: z.number().nullish(), max_model_len: z.number().optional(), max_output_length: z.number().optional(), input_modalities: z.array(ModalitySchema).optional(), @@ -294,6 +294,7 @@ const FETCHERS: ReadonlyArray = [ url: 'https://www.morphllm.com/api/models/json', }), modelsDevFetcher('alibaba-token-plan', 'alibaba-token-plan'), + modelsDevFetcher('edenai', 'edenai', 'https://api.edenai.run/v3/models'), modelsDevFetcher('zai-coding', 'zai-coding-plan'), modelsDevFetcher('ollama-cloud', 'ollama-cloud', 'https://ollama.com/v1/models'), modelsDevFetcher('opencode-go', 'opencode-go', 'https://opencode.ai/zen/go/v1/models'), diff --git a/apps/web/src/lib/ai-gateway/providers/model-settings.ts b/apps/web/src/lib/ai-gateway/providers/model-settings.ts index 7c8487ea6f..15a96f1f58 100644 --- a/apps/web/src/lib/ai-gateway/providers/model-settings.ts +++ b/apps/web/src/lib/ai-gateway/providers/model-settings.ts @@ -62,7 +62,7 @@ export function getAiSdkProvider( model: string, directProviderId: DirectUserByokInferenceProviderId | null ): Exclude | undefined { - if (directProviderId === 'morph-byok') { + if (directProviderId === 'edenai' || directProviderId === 'morph-byok') { return 'openai-compatible'; } if (model === longcat_2_free_model.public_id) { diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts index fa19c1ecce..454051a20e 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/inference-provider-id.ts @@ -107,6 +107,7 @@ export const DirectUserByokInferenceProviderIdSchema = z.enum([ 'chutes-byok', 'codestral', 'crofai', + 'edenai', 'inceptron-byok', 'kimi-coding', 'martian', @@ -152,6 +153,7 @@ export const UserByokTestModels = { [DirectUserByokInferenceProviderIdSchema.enum['byteplus-coding']]: 'bytedance-seed-code', [DirectUserByokInferenceProviderIdSchema.enum['chutes-byok']]: 'Qwen/Qwen3-30B-A3B', [DirectUserByokInferenceProviderIdSchema.enum.codestral]: 'mistral/codestral', + [DirectUserByokInferenceProviderIdSchema.enum.edenai]: 'openai/gpt-5.6-luna', [DirectUserByokInferenceProviderIdSchema.enum['kimi-coding']]: 'kimi-for-coding', [DirectUserByokInferenceProviderIdSchema.enum['inceptron-byok']]: 'moonshotai/Kimi-K2.6', [DirectUserByokInferenceProviderIdSchema.enum.martian]: 'google/gemini-3.5-flash', From 24ca612e949f705b112a081dd9a1ced15115fb78 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:45:34 +0000 Subject: [PATCH 2/4] fix(ai-gateway): type Eden AI transform test request Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../providers/direct-byok/edenai.test.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts index cc99c282cd..9dc90c18bb 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts @@ -1,4 +1,5 @@ import { getAiSdkProvider } from '../model-settings'; +import type { GatewayRequest } from '../openrouter/types'; import type { TransformRequestContext } from '../types'; import edenai from './edenai'; @@ -19,13 +20,14 @@ describe('Eden AI direct BYOK provider', () => { [{ reasoning: { effort: 'high' } }, 'high'], [{ reasoning: { effort: 'low' }, reasoning_effort: 'medium' }, 'medium'], ] as const)('translates reasoning settings for Chat Completions', (body, expectedEffort) => { - const context = { - request: { kind: 'chat_completions', body: { model: 'test', messages: [], ...body } }, - } as TransformRequestContext; + const request: GatewayRequest = { + kind: 'chat_completions', + body: { model: 'test', messages: [], ...body }, + }; - edenai.transformRequest(context); + edenai.transformRequest({ request } as TransformRequestContext); - expect(context.request.body).toMatchObject({ reasoning_effort: expectedEffort }); - expect(context.request.body).not.toHaveProperty('reasoning'); + expect(request.body).toMatchObject({ reasoning_effort: expectedEffort }); + expect(request.body).not.toHaveProperty('reasoning'); }); }); From e42eebb6aeafd9985f3f1146602573c3a813f850 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:57:44 +0000 Subject: [PATCH 3/4] fix(ai-gateway): preserve disabled Eden AI reasoning Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../src/lib/ai-gateway/providers/direct-byok/edenai.test.ts | 1 + apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts index 9dc90c18bb..11558d963c 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.test.ts @@ -18,6 +18,7 @@ describe('Eden AI direct BYOK provider', () => { test.each([ [{ reasoning: { effort: 'high' } }, 'high'], + [{ reasoning: { enabled: false } }, 'none'], [{ reasoning: { effort: 'low' }, reasoning_effort: 'medium' }, 'medium'], ] as const)('translates reasoning settings for Chat Completions', (body, expectedEffort) => { const request: GatewayRequest = { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts index b24f56132f..1e49298de0 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts @@ -11,7 +11,9 @@ export default { if (request.kind !== 'chat_completions') { return; } - request.body.reasoning_effort ??= request.body.reasoning?.effort ?? undefined; + request.body.reasoning_effort ??= + request.body.reasoning?.effort ?? + (request.body.reasoning?.enabled === false ? 'none' : undefined); delete request.body.reasoning; }, models: cachedEnhancedDirectByokModelList({ From 5f44b34ef9e9ab9707d6314aa80bd0317fce8bf6 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 14 Aug 2026 14:07:40 +0200 Subject: [PATCH 4/4] feat(ai-gateway): support Eden AI protocol endpoints --- .../api/openrouter/[...path]/route.test.ts | 1 + .../src/app/api/openrouter/[...path]/route.ts | 1 + .../experiments/build-direct-provider.ts | 1 + .../direct-byok/alibaba-token-plan.ts | 1 + .../providers/direct-byok/byteplus-coding.ts | 1 + .../providers/direct-byok/chutes-byok.ts | 1 + .../providers/direct-byok/crofai.ts | 1 + .../providers/direct-byok/edenai.test.ts | 19 ++++---- .../providers/direct-byok/edenai.ts | 3 +- .../providers/direct-byok/inceptron-byok.ts | 1 + .../providers/direct-byok/index.test.ts | 2 + .../providers/direct-byok/kimi-coding.ts | 1 + .../providers/direct-byok/martian.ts | 1 + .../ai-gateway/providers/direct-byok/morph.ts | 1 + .../providers/direct-byok/neurowatt.ts | 1 + .../providers/direct-byok/nvidia-byok.ts | 1 + .../providers/direct-byok/ollama-cloud.ts | 1 + .../providers/direct-byok/opencode-go.ts | 1 + .../providers/direct-byok/orcarouter.ts | 1 + .../providers/direct-byok/synthetic.ts | 1 + .../ai-gateway/providers/direct-byok/types.ts | 7 ++- .../direct-byok/xiaomi-token-plan-ams.ts | 1 + .../direct-byok/xiaomi-token-plan-sgp.ts | 1 + .../providers/direct-byok/zai-coding.ts | 1 + .../lib/ai-gateway/providers/get-provider.ts | 1 + .../ai-gateway/providers/model-settings.ts | 2 +- .../providers/provider-definitions.ts | 8 ++++ .../web/src/lib/ai-gateway/providers/types.ts | 3 ++ .../upstream-request.generation.test.ts | 1 + .../ai-gateway/providers/upstream-request.ts | 9 ++-- .../src/tests/openrouterApi.timeout.test.ts | 48 +++++++++++++++++++ 31 files changed, 109 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/api/openrouter/[...path]/route.test.ts b/apps/web/src/app/api/openrouter/[...path]/route.test.ts index c7cfb56a94..24bd001496 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.test.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.test.ts @@ -143,6 +143,7 @@ const mockedGetEffectiveModelDecision = jest.mocked(getEffectiveModelDecision); const provider = { id: 'openrouter', apiUrl: 'https://openrouter.ai/api/v1', + apiUrlOverrides: {}, apiKey: 'test-key', supportedChatApis: ['chat_completions', 'responses', 'messages'], responseTransforms: null, diff --git a/apps/web/src/app/api/openrouter/[...path]/route.ts b/apps/web/src/app/api/openrouter/[...path]/route.ts index 7d30f07407..66eda6c941 100644 --- a/apps/web/src/app/api/openrouter/[...path]/route.ts +++ b/apps/web/src/app/api/openrouter/[...path]/route.ts @@ -936,6 +936,7 @@ export async function POST(request: NextRequest): Promise { - test('uses the Eden AI v3 Chat Completions API', () => { + test('supports all chat APIs with the nested Anthropic Messages base URL', () => { expect(edenai.base_url).toBe('https://api.edenai.run/v3'); - expect(edenai.supported_chat_apis).toEqual(['chat_completions']); + expect(edenai.base_url_overrides).toEqual({ messages: 'https://api.edenai.run/v3/v1' }); + expect(edenai.supported_chat_apis).toEqual(['chat_completions', 'messages', 'responses']); }); - test.each(['edenai/openai/gpt-5.6-luna', 'edenai/anthropic/claude-sonnet-5'])( - 'uses OpenAI-compatible Chat Completions for %s', - model => { - expect(getAiSdkProvider(model, 'edenai')).toBe('openai-compatible'); - } - ); + test.each([ + ['edenai/openai/gpt-5.6-luna', 'openai'], + ['edenai/anthropic/claude-sonnet-5', 'anthropic'], + ['edenai/xai/grok-4.6', 'openai'], + ['edenai/google/gemini-flash-latest', undefined], + ] as const)('selects the model-family API for %s', (model, expectedProvider) => { + expect(getAiSdkProvider(model, 'edenai')).toBe(expectedProvider); + }); test.each([ [{ reasoning: { effort: 'high' } }, 'high'], diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts index 1e49298de0..d620ac04be 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/edenai.ts @@ -4,7 +4,8 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'edenai', base_url: 'https://api.edenai.run/v3', - supported_chat_apis: ['chat_completions'], + base_url_overrides: { messages: 'https://api.edenai.run/v3/v1' }, + supported_chat_apis: ['chat_completions', 'messages', 'responses'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { const { request } = context; diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/inceptron-byok.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/inceptron-byok.ts index ad7b023c00..25524aba88 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/inceptron-byok.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/inceptron-byok.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'inceptron-byok', base_url: 'https://api.inceptron.io/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/index.test.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/index.test.ts index 25c0c05cd6..2de6296109 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/index.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/index.test.ts @@ -27,6 +27,7 @@ jest.mock('./direct-byok-definitions', () => ({ { id: 'chutes-byok', base_url: 'https://chutes.example.com/v1', + base_url_overrides: {}, models: jest.fn(async () => [ { id: 'supported-model', @@ -52,6 +53,7 @@ jest.mock('./direct-byok-definitions', () => ({ { id: 'crofai', base_url: 'https://crofai.example.com/v1', + base_url_overrides: {}, models: jest.fn(async () => [ { id: 'other-model', diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/kimi-coding.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/kimi-coding.ts index 5597832638..ee777c83c2 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/kimi-coding.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/kimi-coding.ts @@ -8,6 +8,7 @@ import { isRooCodeBasedClient } from '@/lib/utils'; export default { id: 'kimi-coding', base_url: 'https://api.kimi.com/coding/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/martian.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/martian.ts index 3ff1ba94f1..66e25627a7 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/martian.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/martian.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'martian', base_url: 'https://api.withmartian.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions', 'messages', 'responses'], default_ai_sdk_provider: 'openrouter', transformRequest() {}, diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/morph.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/morph.ts index 765eafdd54..64af16a9ab 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/morph.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/morph.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'morph-byok', base_url: 'https://api.morphllm.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest() {}, diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/neurowatt.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/neurowatt.ts index 1938c8046a..d0bdec3178 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/neurowatt.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/neurowatt.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'neuralwatt', base_url: 'https://api.neuralwatt.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(_context) {}, diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/nvidia-byok.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/nvidia-byok.ts index ad30bce1d7..11b6c01626 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/nvidia-byok.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/nvidia-byok.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'nvidia-byok', base_url: 'https://integrate.api.nvidia.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/ollama-cloud.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/ollama-cloud.ts index 363f59d697..443b925538 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/ollama-cloud.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/ollama-cloud.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'ollama-cloud', base_url: 'https://ollama.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/opencode-go.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/opencode-go.ts index ceb55335b2..9b114f1027 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/opencode-go.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/opencode-go.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'opencode-go', base_url: 'https://opencode.ai/zen/go/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions', 'messages', 'responses'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/orcarouter.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/orcarouter.ts index 9d242b0fa9..1d2c9e0c1e 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/orcarouter.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/orcarouter.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'orcarouter', base_url: 'https://api.orcarouter.ai/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions', 'messages', 'responses'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/synthetic.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/synthetic.ts index 9750f53486..ca46e0f189 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/synthetic.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/synthetic.ts @@ -4,6 +4,7 @@ import type { DirectByokProvider } from '@/lib/ai-gateway/providers/direct-byok/ export default { id: 'synthetic', base_url: 'https://api.synthetic.new/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/types.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/types.ts index e510641f64..8d043d0f7c 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/types.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/types.ts @@ -1,6 +1,10 @@ import * as z from 'zod'; import type { DirectByokProviderMetaId } from '@/lib/ai-gateway/providers/direct-byok/direct-byok-meta'; -import type { GatewayChatApiKind, TransformRequestContext } from '@/lib/ai-gateway/providers/types'; +import type { + GatewayChatApiKind, + ProviderApiUrlOverrides, + TransformRequestContext, +} from '@/lib/ai-gateway/providers/types'; import type { CustomLlmProvider } from '@kilocode/db'; import { OpenCodeVariantSchema } from '@kilocode/db/schema-types'; @@ -24,6 +28,7 @@ export type DirectByokModel = z.infer; export type DirectByokProvider = { id: DirectByokProviderMetaId; base_url: string; + base_url_overrides: ProviderApiUrlOverrides; models: () => Promise>; supported_chat_apis: ReadonlyArray; default_ai_sdk_provider: CustomLlmProvider; diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-ams.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-ams.ts index 264142af5e..23d4a2c73c 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-ams.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-ams.ts @@ -4,6 +4,7 @@ import { cachedEnhancedDirectByokModelList } from '@/lib/ai-gateway/providers/di export default { id: 'xiaomi-token-plan-ams', base_url: 'https://token-plan-ams.xiaomimimo.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest() {}, diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-sgp.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-sgp.ts index cb25ed7ce5..b92c293758 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-sgp.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/xiaomi-token-plan-sgp.ts @@ -4,6 +4,7 @@ import { cachedEnhancedDirectByokModelList } from '@/lib/ai-gateway/providers/di export default { id: 'xiaomi-token-plan-sgp', base_url: 'https://token-plan-sgp.xiaomimimo.com/v1', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest() {}, diff --git a/apps/web/src/lib/ai-gateway/providers/direct-byok/zai-coding.ts b/apps/web/src/lib/ai-gateway/providers/direct-byok/zai-coding.ts index 430407fc34..db78af9a83 100644 --- a/apps/web/src/lib/ai-gateway/providers/direct-byok/zai-coding.ts +++ b/apps/web/src/lib/ai-gateway/providers/direct-byok/zai-coding.ts @@ -5,6 +5,7 @@ import { cachedEnhancedDirectByokModelList } from '@/lib/ai-gateway/providers/di export default { id: 'zai-coding', base_url: 'https://api.z.ai/api/coding/paas/v4', + base_url_overrides: {}, supported_chat_apis: ['chat_completions'], default_ai_sdk_provider: 'openai-compatible', transformRequest(context) { diff --git a/apps/web/src/lib/ai-gateway/providers/get-provider.ts b/apps/web/src/lib/ai-gateway/providers/get-provider.ts index 4d5790417d..79b72dc401 100644 --- a/apps/web/src/lib/ai-gateway/providers/get-provider.ts +++ b/apps/web/src/lib/ai-gateway/providers/get-provider.ts @@ -81,6 +81,7 @@ async function checkDirectBYOK( provider: { id: 'direct-byok', apiUrl: directByok.base_url, + apiUrlOverrides: directByok.base_url_overrides, apiKey: userByok[0].decryptedAPIKey, supportedChatApis: directByok.supported_chat_apis, responseTransforms: null, diff --git a/apps/web/src/lib/ai-gateway/providers/model-settings.ts b/apps/web/src/lib/ai-gateway/providers/model-settings.ts index 15a96f1f58..7c8487ea6f 100644 --- a/apps/web/src/lib/ai-gateway/providers/model-settings.ts +++ b/apps/web/src/lib/ai-gateway/providers/model-settings.ts @@ -62,7 +62,7 @@ export function getAiSdkProvider( model: string, directProviderId: DirectUserByokInferenceProviderId | null ): Exclude | undefined { - if (directProviderId === 'edenai' || directProviderId === 'morph-byok') { + if (directProviderId === 'morph-byok') { return 'openai-compatible'; } if (model === longcat_2_free_model.public_id) { diff --git a/apps/web/src/lib/ai-gateway/providers/provider-definitions.ts b/apps/web/src/lib/ai-gateway/providers/provider-definitions.ts index 75ba3c57cd..fe60adf20b 100644 --- a/apps/web/src/lib/ai-gateway/providers/provider-definitions.ts +++ b/apps/web/src/lib/ai-gateway/providers/provider-definitions.ts @@ -7,6 +7,7 @@ export default { OPENROUTER: { id: 'openrouter', apiUrl: 'https://openrouter.ai/api/v1', + apiUrlOverrides: {}, apiKey: getEnvVariable('OPENROUTER_API_KEY'), supportedChatApis: ['chat_completions', 'messages', 'responses'], responseTransforms: null, @@ -15,6 +16,7 @@ export default { ALIBABA: { id: 'alibaba', apiUrl: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', + apiUrlOverrides: {}, apiKey: getEnvVariable('ALIBABA_API_KEY'), // Prompt caching is not supported on the responses API for Alibaba; enabling it is therefore dangerous. supportedChatApis: ['chat_completions' /*, 'responses'*/], @@ -26,6 +28,7 @@ export default { SEED: { id: 'seed', apiUrl: 'https://ark.ap-southeast.bytepluses.com/api/v3', + apiUrlOverrides: {}, apiKey: getEnvVariable('BYTEDANCE_API_KEY'), // Prompt caching is not supported on the responses API for Bytedance; enabling it is therefore dangerous. supportedChatApis: ['chat_completions' /*, 'responses'*/], @@ -50,6 +53,7 @@ export default { LONGCAT: { id: 'longcat', apiUrl: 'https://api.longcat.ai/openai/v1', + apiUrlOverrides: {}, apiKey: getEnvVariable('LONGCAT_API_KEY'), supportedChatApis: ['chat_completions'], responseTransforms: null, @@ -63,6 +67,7 @@ export default { MARTIAN: { id: 'martian', apiUrl: 'https://api.withmartian.com/v1', + apiUrlOverrides: {}, apiKey: getEnvVariable('MARTIAN_API_KEY'), supportedChatApis: ['chat_completions', 'responses', 'messages'], responseTransforms: null, @@ -73,6 +78,7 @@ export default { MISTRAL: { id: 'mistral', apiUrl: 'https://api.mistral.ai/v1', + apiUrlOverrides: {}, apiKey: getEnvVariable('MISTRAL_API_KEY'), supportedChatApis: [], responseTransforms: null, @@ -81,6 +87,7 @@ export default { STREAMLAKE: { id: 'streamlake', apiUrl: 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints', + apiUrlOverrides: {}, apiKey: getEnvVariable('STREAMLAKE_API_KEY'), supportedChatApis: ['chat_completions'], responseTransforms: null, @@ -91,6 +98,7 @@ export default { VERCEL_AI_GATEWAY: { id: 'vercel', apiUrl: 'https://ai-gateway.vercel.sh/v1', + apiUrlOverrides: {}, apiKey: getEnvVariable('VERCEL_AI_GATEWAY_API_KEY'), supportedChatApis: ['chat_completions', 'messages', 'responses'], responseTransforms: null, diff --git a/apps/web/src/lib/ai-gateway/providers/types.ts b/apps/web/src/lib/ai-gateway/providers/types.ts index 46db9a7a49..60fd782409 100644 --- a/apps/web/src/lib/ai-gateway/providers/types.ts +++ b/apps/web/src/lib/ai-gateway/providers/types.ts @@ -36,6 +36,8 @@ export type TransformRequestContext = { export type GatewayChatApiKind = GatewayRequest['kind']; +export type ProviderApiUrlOverrides = Readonly>>; + export type ProviderResponseTransforms = { mapGeminiThoughtContent: boolean; }; @@ -43,6 +45,7 @@ export type ProviderResponseTransforms = { export type Provider = { id: ProviderId; apiUrl: string; + apiUrlOverrides: ProviderApiUrlOverrides; apiKey: string; supportedChatApis: ReadonlyArray; responseTransforms: ProviderResponseTransforms | null; diff --git a/apps/web/src/lib/ai-gateway/providers/upstream-request.generation.test.ts b/apps/web/src/lib/ai-gateway/providers/upstream-request.generation.test.ts index 1f27b36433..5efbd2858b 100644 --- a/apps/web/src/lib/ai-gateway/providers/upstream-request.generation.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/upstream-request.generation.test.ts @@ -18,6 +18,7 @@ jest.mock('../../fetchWithBackoff', () => ({ const provider: Provider = { id: 'openrouter', apiUrl: 'https://openrouter.example/api/v1', + apiUrlOverrides: {}, apiKey: 'test-api-key', supportedChatApis: [], responseTransforms: null, diff --git a/apps/web/src/lib/ai-gateway/providers/upstream-request.ts b/apps/web/src/lib/ai-gateway/providers/upstream-request.ts index 69eee0db58..f43bc3ce8b 100644 --- a/apps/web/src/lib/ai-gateway/providers/upstream-request.ts +++ b/apps/web/src/lib/ai-gateway/providers/upstream-request.ts @@ -9,7 +9,7 @@ import type { GatewayMessagesRequest, } from '@/lib/ai-gateway/providers/openrouter/types'; import { ATTRIBUTION_HEADERS } from '@/lib/ai-gateway/providers/openrouter/attribution-headers'; -import type { Provider } from '@/lib/ai-gateway/providers/types'; +import type { GatewayChatApiKind, Provider } from '@/lib/ai-gateway/providers/types'; import { after, NextResponse } from 'next/server'; import { ProxyErrorType } from '@/lib/proxy-error-types'; import { withRequestId } from '@/lib/ai-gateway/request-id'; @@ -190,6 +190,7 @@ function upstreamFetchFailureResponse( } export async function upstreamRequest({ + chatApi, path, search, method, @@ -199,6 +200,7 @@ export async function upstreamRequest({ signal, vercelRequestId, }: { + chatApi: GatewayChatApiKind; path: string; search: string; method: string; @@ -220,7 +222,8 @@ export async function upstreamRequest({ headers.set(key, value); }); - const targetUrl = `${provider.apiUrl}${path}${search}`; + const apiUrl = provider.apiUrlOverrides[chatApi] ?? provider.apiUrl; + const targetUrl = `${apiUrl}${path}${search}`; const timeoutSignal = AbortSignal.timeout(TIMEOUT_MS); const onTimeoutAbort = () => { @@ -264,7 +267,7 @@ export async function upstreamRequest({ failureFamily = classifyUpstreamFetchFailure({ errorName, causeCode, causeName }); const failureMetadata = { providerId: provider.id, - targetHost: getProviderTargetHost(provider.apiUrl), + targetHost: getProviderTargetHost(apiUrl), path, failureFamily, errorName, diff --git a/apps/web/src/tests/openrouterApi.timeout.test.ts b/apps/web/src/tests/openrouterApi.timeout.test.ts index 90d49f6c9e..7e3c9414e7 100644 --- a/apps/web/src/tests/openrouterApi.timeout.test.ts +++ b/apps/web/src/tests/openrouterApi.timeout.test.ts @@ -30,11 +30,50 @@ describe('upstreamRequest timeout', () => { global.fetch = originalFetch; }); + test.each([ + { + chatApi: 'messages', + path: '/messages', + apiUrlOverrides: { messages: 'https://messages.example.test/v3/v1' }, + expectedUrl: 'https://messages.example.test/v3/v1/messages?beta=true', + }, + { + chatApi: 'responses', + path: '/responses', + apiUrlOverrides: {}, + expectedUrl: 'https://gateway.example.test/v3/responses?beta=true', + }, + ] as const)( + 'uses the $chatApi API URL override when provided', + async ({ chatApi, path, apiUrlOverrides, expectedUrl }) => { + const mockFetch = jest.fn().mockResolvedValue(new Response('{}')); + global.fetch = mockFetch; + + const result = await upstreamRequest({ + chatApi, + path, + search: '?beta=true', + method: 'POST', + body: { model: 'test-model', messages: [{ role: 'user', content: 'test' }] }, + extraHeaders: {}, + provider: { + ...PROVIDERS.OPENROUTER, + apiUrl: 'https://gateway.example.test/v3', + apiUrlOverrides, + }, + }); + + expect(result.type).toBe('success'); + expect(mockFetch).toHaveBeenCalledWith(expectedUrl, expect.any(Object)); + } + ); + it('reports a client disconnect instead of an upstream disconnect when the caller aborts', async () => { const controller = new AbortController(); controller.abort(); const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -68,6 +107,7 @@ describe('upstreamRequest timeout', () => { global.fetch = jest.fn().mockRejectedValue(timeoutError); const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -96,6 +136,7 @@ describe('upstreamRequest timeout', () => { .mockRejectedValue(new TypeError('fetch failed', { cause: resetCause })); const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -124,6 +165,7 @@ describe('upstreamRequest timeout', () => { .mockRejectedValue(new TypeError('fetch failed', { cause: resetCause })); const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -159,6 +201,7 @@ describe('upstreamRequest timeout', () => { controller.abort(); const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -190,6 +233,7 @@ describe('upstreamRequest timeout', () => { global.fetch = mockFetch; const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -226,6 +270,7 @@ describe('upstreamRequest timeout', () => { global.fetch = mockFetch; const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -257,6 +302,7 @@ describe('upstreamRequest timeout', () => { global.fetch = mockFetch; const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '?trace=search-secret', method: 'POST', @@ -306,6 +352,7 @@ describe('upstreamRequest timeout', () => { global.fetch = mockFetch; const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '', method: 'POST', @@ -343,6 +390,7 @@ describe('upstreamRequest timeout', () => { global.fetch = mockFetch; const result = await upstreamRequest({ + chatApi: 'chat_completions', path: '/chat/completions', search: '?trace=search-secret', method: 'POST',