From 6862d59cdeb81d7d461bbcb77ee42267bf3f45f9 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 16:23:50 +0100 Subject: [PATCH 01/28] fix(ai-proxy): validate model tool support at Router init (fail fast) BREAKING CHANGE: isModelSupportingTools is no longer exported from ai-proxy - Add AIModelNotSupportedError for descriptive error messages - Move model validation from agent.addAi() to Router constructor - Make isModelSupportingTools internal (not exported from index) - Error is thrown immediately at Router init if model doesn't support tools This is a bug fix: validation should happen at proxy initialization, not at the agent level. This ensures consistent behavior regardless of how the Router is instantiated. Co-Authored-By: Claude Opus 4.5 --- packages/agent/src/agent.ts | 8 ---- packages/agent/test/agent.test.ts | 16 ------- packages/ai-proxy/src/errors.ts | 9 ++++ packages/ai-proxy/src/index.ts | 14 +++++- packages/ai-proxy/src/router.ts | 13 +++++- .../ai-proxy/test/provider-dispatcher.test.ts | 3 +- packages/ai-proxy/test/router.test.ts | 46 ++++++++++++++++++- 7 files changed, 80 insertions(+), 29 deletions(-) diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index e39b29f08..6a611aeb0 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -17,7 +17,6 @@ import type { import type { DataSource, DataSourceFactory } from '@forestadmin/datasource-toolkit'; import type { ForestSchema } from '@forestadmin/forestadmin-client'; -import { isModelSupportingTools } from '@forestadmin/ai-proxy'; import { DataSourceCustomizer } from '@forestadmin/datasource-customizer'; import bodyParser from '@koa/bodyparser'; import cors from '@koa/cors'; @@ -246,13 +245,6 @@ export default class Agent extends FrameworkMounter ); } - if (!isModelSupportingTools(configuration.model)) { - throw new Error( - `Model '${configuration.model}' does not support function calling (tools). ` + - 'Please use a compatible model like gpt-4o, gpt-4o-mini, or gpt-4-turbo.', - ); - } - this.options.logger( 'Warn', `AI configuration added with model '${configuration.model}'. ` + diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index dadc9d566..577f1295f 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -439,22 +439,6 @@ describe('Agent', () => { ).toThrow('addAi can only be called once. Multiple AI configurations are not supported yet.'); }); - test('should throw an error when model does not support tools', () => { - const agent = new Agent(options); - - expect(() => - agent.addAi({ - name: 'gpt4-base', - provider: 'openai', - apiKey: 'test-key', - model: 'gpt-4', - }), - ).toThrow( - "Model 'gpt-4' does not support function calling (tools). " + - 'Please use a compatible model like gpt-4o, gpt-4o-mini, or gpt-4-turbo.', - ); - }); - test('should include ai_llms in schema meta when AI is configured', async () => { const agent = new Agent(options); agent.addAi({ diff --git a/packages/ai-proxy/src/errors.ts b/packages/ai-proxy/src/errors.ts index 2e2c15609..fc1e5e250 100644 --- a/packages/ai-proxy/src/errors.ts +++ b/packages/ai-proxy/src/errors.ts @@ -32,6 +32,15 @@ export class AIBadRequestError extends AIError { } } +export class AIModelNotSupportedError extends AIBadRequestError { + constructor(model: string) { + super( + `Model '${model}' does not support tools. Please use a model that supports function calling.`, + ); + this.name = 'AIModelNotSupportedError'; + } +} + export class AINotFoundError extends AIError { constructor(message: string) { super(message, 404); diff --git a/packages/ai-proxy/src/index.ts b/packages/ai-proxy/src/index.ts index 75948196d..dc19a1289 100644 --- a/packages/ai-proxy/src/index.ts +++ b/packages/ai-proxy/src/index.ts @@ -2,7 +2,19 @@ import type { McpConfiguration } from './mcp-client'; import McpConfigChecker from './mcp-config-checker'; -export * from './provider-dispatcher'; +// Re-export from provider-dispatcher (excluding isModelSupportingTools - internal only) +export { ProviderDispatcher } from './provider-dispatcher'; +export type { + AiConfiguration, + AiProvider, + BaseAiConfiguration, + ChatCompletionMessage, + ChatCompletionResponse, + ChatCompletionTool, + ChatCompletionToolChoice, + DispatchBody, + OpenAiConfiguration, +} from './provider-dispatcher'; export * from './remote-tools'; export * from './router'; export * from './mcp-client'; diff --git a/packages/ai-proxy/src/router.ts b/packages/ai-proxy/src/router.ts index 63115a0da..4432bddec 100644 --- a/packages/ai-proxy/src/router.ts +++ b/packages/ai-proxy/src/router.ts @@ -5,8 +5,9 @@ import type { RouteArgs } from './schemas/route'; import type { Logger } from '@forestadmin/datasource-toolkit'; import type { z } from 'zod'; -import { AIBadRequestError, ProviderDispatcher } from './index'; +import { AIBadRequestError, AIModelNotSupportedError } from './errors'; import McpClient from './mcp-client'; +import { ProviderDispatcher, isModelSupportingTools } from './provider-dispatcher'; import { RemoteTools } from './remote-tools'; import { routeArgsSchema } from './schemas/route'; @@ -37,6 +38,16 @@ export class Router { this.aiConfigurations = params?.aiConfigurations ?? []; this.localToolsApiKeys = params?.localToolsApiKeys; this.logger = params?.logger; + + this.validateConfigurations(); + } + + private validateConfigurations(): void { + for (const config of this.aiConfigurations) { + if (!isModelSupportingTools(config.model)) { + throw new AIModelNotSupportedError(config.model); + } + } } /** diff --git a/packages/ai-proxy/test/provider-dispatcher.test.ts b/packages/ai-proxy/test/provider-dispatcher.test.ts index e75b96ff4..6977b9e51 100644 --- a/packages/ai-proxy/test/provider-dispatcher.test.ts +++ b/packages/ai-proxy/test/provider-dispatcher.test.ts @@ -2,7 +2,8 @@ import type { DispatchBody } from '../src'; import { convertToOpenAIFunction } from '@langchain/core/utils/function_calling'; -import { AINotConfiguredError, isModelSupportingTools, ProviderDispatcher, RemoteTools } from '../src'; +import { AINotConfiguredError, ProviderDispatcher, RemoteTools } from '../src'; +import { isModelSupportingTools } from '../src/provider-dispatcher'; // Mock raw OpenAI response (returned via __includeRawResponse: true) const mockOpenAIResponse = { diff --git a/packages/ai-proxy/test/router.test.ts b/packages/ai-proxy/test/router.test.ts index 4a2a6cba9..b5162a844 100644 --- a/packages/ai-proxy/test/router.test.ts +++ b/packages/ai-proxy/test/router.test.ts @@ -1,7 +1,7 @@ import type { DispatchBody, InvokeRemoteToolArgs, Route } from '../src'; import type { Logger } from '@forestadmin/datasource-toolkit'; -import { AIBadRequestError, Router } from '../src'; +import { AIBadRequestError, AIModelNotSupportedError, Router } from '../src'; import McpClient from '../src/mcp-client'; const invokeToolMock = jest.fn(); @@ -23,13 +23,17 @@ jest.mock('../src/provider-dispatcher', () => { ProviderDispatcher: jest.fn().mockImplementation(() => ({ dispatch: dispatchMock, })), + isModelSupportingTools: jest.fn().mockReturnValue(true), }; }); // eslint-disable-next-line import/first -import { ProviderDispatcher } from '../src/provider-dispatcher'; +import { isModelSupportingTools, ProviderDispatcher } from '../src/provider-dispatcher'; const ProviderDispatcherMock = ProviderDispatcher as jest.MockedClass; +const isModelSupportingToolsMock = isModelSupportingTools as jest.MockedFunction< + typeof isModelSupportingTools +>; jest.mock('../src/mcp-client', () => { return jest.fn().mockImplementation(() => ({ @@ -386,4 +390,42 @@ describe('route', () => { ); }); }); + + describe('Model validation', () => { + it('throws AIModelNotSupportedError when model does not support tools', () => { + isModelSupportingToolsMock.mockReturnValueOnce(false); + + expect( + () => + new Router({ + aiConfigurations: [ + { + name: 'test', + provider: 'openai', + apiKey: 'dev', + model: 'unsupported-model', + }, + ], + }), + ).toThrow(AIModelNotSupportedError); + }); + + it('throws with helpful error message including model name', () => { + isModelSupportingToolsMock.mockReturnValueOnce(false); + + expect( + () => + new Router({ + aiConfigurations: [ + { + name: 'test', + provider: 'openai', + apiKey: 'dev', + model: 'text-davinci-003', + }, + ], + }), + ).toThrow("Model 'text-davinci-003' does not support tools"); + }); + }); }); From 9f06d04dc8d4af8cd2382b5e3a250537ceb00db7 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 17:06:30 +0100 Subject: [PATCH 02/28] refactor(ai-proxy): move isModelSupportingTools to router.ts The function is only used in Router, so it makes sense to keep it there. This simplifies the provider-dispatcher module and keeps related code together. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/index.ts | 1 - packages/ai-proxy/src/provider-dispatcher.ts | 35 -------- packages/ai-proxy/src/router.ts | 37 +++++++- .../ai-proxy/test/provider-dispatcher.test.ts | 50 ----------- packages/ai-proxy/test/router.test.ts | 86 ++++++++++++++----- 5 files changed, 102 insertions(+), 107 deletions(-) diff --git a/packages/ai-proxy/src/index.ts b/packages/ai-proxy/src/index.ts index dc19a1289..5a614c6f3 100644 --- a/packages/ai-proxy/src/index.ts +++ b/packages/ai-proxy/src/index.ts @@ -2,7 +2,6 @@ import type { McpConfiguration } from './mcp-client'; import McpConfigChecker from './mcp-config-checker'; -// Re-export from provider-dispatcher (excluding isModelSupportingTools - internal only) export { ProviderDispatcher } from './provider-dispatcher'; export type { AiConfiguration, diff --git a/packages/ai-proxy/src/provider-dispatcher.ts b/packages/ai-proxy/src/provider-dispatcher.ts index b83eb80b3..ac324d01f 100644 --- a/packages/ai-proxy/src/provider-dispatcher.ts +++ b/packages/ai-proxy/src/provider-dispatcher.ts @@ -21,41 +21,6 @@ export type { } from './provider'; export type { DispatchBody } from './schemas/route'; -/** - * OpenAI model prefixes that do NOT support function calling (tools). - * Unknown models are allowed. - * @see https://platform.openai.com/docs/guides/function-calling - */ -const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ - 'gpt-4', - 'gpt-3.5-turbo', - 'gpt-3.5', - 'text-davinci', - 'davinci', - 'curie', - 'babbage', - 'ada', -]; - -/** - * Exceptions to the unsupported list - these models DO support tools - * even though they start with an unsupported prefix. - */ -const OPENAI_MODELS_EXCEPTIONS = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; - -export function isModelSupportingTools(model: string): boolean { - const isException = OPENAI_MODELS_EXCEPTIONS.some( - exception => model === exception || model.startsWith(`${exception}-`), - ); - if (isException) return true; - - const isKnownUnsupported = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT.some( - unsupported => model === unsupported || model.startsWith(`${unsupported}-`), - ); - - return !isKnownUnsupported; -} - export class ProviderDispatcher { private readonly chatModel: ChatOpenAI | null = null; diff --git a/packages/ai-proxy/src/router.ts b/packages/ai-proxy/src/router.ts index 4432bddec..d7da25897 100644 --- a/packages/ai-proxy/src/router.ts +++ b/packages/ai-proxy/src/router.ts @@ -7,10 +7,45 @@ import type { z } from 'zod'; import { AIBadRequestError, AIModelNotSupportedError } from './errors'; import McpClient from './mcp-client'; -import { ProviderDispatcher, isModelSupportingTools } from './provider-dispatcher'; +import { ProviderDispatcher } from './provider-dispatcher'; import { RemoteTools } from './remote-tools'; import { routeArgsSchema } from './schemas/route'; +/** + * OpenAI model prefixes that do NOT support function calling (tools). + * Unknown models are allowed. + * @see https://platform.openai.com/docs/guides/function-calling + */ +const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ + 'gpt-4', + 'gpt-3.5-turbo', + 'gpt-3.5', + 'text-davinci', + 'davinci', + 'curie', + 'babbage', + 'ada', +]; + +/** + * Exceptions to the unsupported list - these models DO support tools + * even though they start with an unsupported prefix. + */ +const OPENAI_MODELS_EXCEPTIONS = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; + +function isModelSupportingTools(model: string): boolean { + const isException = OPENAI_MODELS_EXCEPTIONS.some( + exception => model === exception || model.startsWith(`${exception}-`), + ); + if (isException) return true; + + const isKnownUnsupported = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT.some( + unsupported => model === unsupported || model.startsWith(`${unsupported}-`), + ); + + return !isKnownUnsupported; +} + export type { AiQueryArgs, Body, diff --git a/packages/ai-proxy/test/provider-dispatcher.test.ts b/packages/ai-proxy/test/provider-dispatcher.test.ts index 6977b9e51..b1b8afb97 100644 --- a/packages/ai-proxy/test/provider-dispatcher.test.ts +++ b/packages/ai-proxy/test/provider-dispatcher.test.ts @@ -3,7 +3,6 @@ import type { DispatchBody } from '../src'; import { convertToOpenAIFunction } from '@langchain/core/utils/function_calling'; import { AINotConfiguredError, ProviderDispatcher, RemoteTools } from '../src'; -import { isModelSupportingTools } from '../src/provider-dispatcher'; // Mock raw OpenAI response (returned via __includeRawResponse: true) const mockOpenAIResponse = { @@ -298,52 +297,3 @@ describe('ProviderDispatcher', () => { }); }); }); - -describe('isModelSupportingTools', () => { - describe('should return true for supported and unknown models', () => { - const supportedModels = [ - 'gpt-4o', - 'gpt-4o-mini', - 'gpt-4o-2024-08-06', - 'gpt-4-turbo', - 'gpt-4-turbo-2024-04-09', - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - 'gpt-5', - 'gpt-5-mini', - 'gpt-5-nano', - 'gpt-5.2', - 'o1', - 'o3', - 'o3-mini', - 'o3-pro', - 'o4-mini', - 'unknown-model', - 'future-gpt-model', - ]; - - it.each(supportedModels)('%s', model => { - expect(isModelSupportingTools(model)).toBe(true); - }); - }); - - describe('should return false for known unsupported models', () => { - const unsupportedModels = [ - 'gpt-4', - 'gpt-4-0613', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0125', - 'gpt-3.5', - 'text-davinci-003', - 'davinci', - 'curie', - 'babbage', - 'ada', - ]; - - it.each(unsupportedModels)('%s', model => { - expect(isModelSupportingTools(model)).toBe(false); - }); - }); -}); diff --git a/packages/ai-proxy/test/router.test.ts b/packages/ai-proxy/test/router.test.ts index b5162a844..b537b29b0 100644 --- a/packages/ai-proxy/test/router.test.ts +++ b/packages/ai-proxy/test/router.test.ts @@ -23,17 +23,13 @@ jest.mock('../src/provider-dispatcher', () => { ProviderDispatcher: jest.fn().mockImplementation(() => ({ dispatch: dispatchMock, })), - isModelSupportingTools: jest.fn().mockReturnValue(true), }; }); // eslint-disable-next-line import/first -import { isModelSupportingTools, ProviderDispatcher } from '../src/provider-dispatcher'; +import { ProviderDispatcher } from '../src/provider-dispatcher'; const ProviderDispatcherMock = ProviderDispatcher as jest.MockedClass; -const isModelSupportingToolsMock = isModelSupportingTools as jest.MockedFunction< - typeof isModelSupportingTools ->; jest.mock('../src/mcp-client', () => { return jest.fn().mockImplementation(() => ({ @@ -81,23 +77,23 @@ describe('route', () => { apiKey: 'dev', model: 'gpt-4o', }; - const gpt3Config = { - name: 'gpt3', + const gpt4MiniConfig = { + name: 'gpt4mini', provider: 'openai' as const, apiKey: 'dev', - model: 'gpt-3.5-turbo', + model: 'gpt-4o-mini', }; const router = new Router({ - aiConfigurations: [gpt4Config, gpt3Config], + aiConfigurations: [gpt4Config, gpt4MiniConfig], }); await router.route({ route: 'ai-query', - query: { 'ai-name': 'gpt3' }, + query: { 'ai-name': 'gpt4mini' }, body: { tools: [], tool_choice: 'required', messages: [] } as unknown as DispatchBody, }); - expect(ProviderDispatcherMock).toHaveBeenCalledWith(gpt3Config, expect.anything()); + expect(ProviderDispatcherMock).toHaveBeenCalledWith(gpt4MiniConfig, expect.anything()); }); it('uses first configuration when ai-name query is not provided', async () => { @@ -107,14 +103,14 @@ describe('route', () => { apiKey: 'dev', model: 'gpt-4o', }; - const gpt3Config = { - name: 'gpt3', + const gpt4MiniConfig = { + name: 'gpt4mini', provider: 'openai' as const, apiKey: 'dev', - model: 'gpt-3.5-turbo', + model: 'gpt-4o-mini', }; const router = new Router({ - aiConfigurations: [gpt4Config, gpt3Config], + aiConfigurations: [gpt4Config, gpt4MiniConfig], }); await router.route({ @@ -393,8 +389,6 @@ describe('route', () => { describe('Model validation', () => { it('throws AIModelNotSupportedError when model does not support tools', () => { - isModelSupportingToolsMock.mockReturnValueOnce(false); - expect( () => new Router({ @@ -403,7 +397,7 @@ describe('route', () => { name: 'test', provider: 'openai', apiKey: 'dev', - model: 'unsupported-model', + model: 'gpt-4', // Known unsupported model }, ], }), @@ -411,8 +405,6 @@ describe('route', () => { }); it('throws with helpful error message including model name', () => { - isModelSupportingToolsMock.mockReturnValueOnce(false); - expect( () => new Router({ @@ -427,5 +419,59 @@ describe('route', () => { }), ).toThrow("Model 'text-davinci-003' does not support tools"); }); + + describe('should accept supported models', () => { + const supportedModels = [ + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4o-2024-08-06', + 'gpt-4-turbo', + 'gpt-4-turbo-2024-04-09', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-5', + 'o1', + 'o3-mini', + 'unknown-model', + 'future-gpt-model', + ]; + + it.each(supportedModels)('%s', model => { + expect( + () => + new Router({ + aiConfigurations: [ + { name: 'test', provider: 'openai', apiKey: 'dev', model }, + ], + }), + ).not.toThrow(); + }); + }); + + describe('should reject known unsupported models', () => { + const unsupportedModels = [ + 'gpt-4', + 'gpt-4-0613', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-0125', + 'gpt-3.5', + 'text-davinci-003', + 'davinci', + 'curie', + 'babbage', + 'ada', + ]; + + it.each(unsupportedModels)('%s', model => { + expect( + () => + new Router({ + aiConfigurations: [ + { name: 'test', provider: 'openai', apiKey: 'dev', model }, + ], + }), + ).toThrow(AIModelNotSupportedError); + }); + }); }); }); From e4ab3a15fc2744c351fba315f50dadcb0ca488c3 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 17:16:53 +0100 Subject: [PATCH 03/28] refactor(ai-proxy): extract isModelSupportingTools to dedicated file Move model support checking logic to supported-models.ts for better organization and separation of concerns. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/router.ts | 36 +---------------------- packages/ai-proxy/src/supported-models.ts | 34 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 packages/ai-proxy/src/supported-models.ts diff --git a/packages/ai-proxy/src/router.ts b/packages/ai-proxy/src/router.ts index d7da25897..5812df10b 100644 --- a/packages/ai-proxy/src/router.ts +++ b/packages/ai-proxy/src/router.ts @@ -10,41 +10,7 @@ import McpClient from './mcp-client'; import { ProviderDispatcher } from './provider-dispatcher'; import { RemoteTools } from './remote-tools'; import { routeArgsSchema } from './schemas/route'; - -/** - * OpenAI model prefixes that do NOT support function calling (tools). - * Unknown models are allowed. - * @see https://platform.openai.com/docs/guides/function-calling - */ -const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ - 'gpt-4', - 'gpt-3.5-turbo', - 'gpt-3.5', - 'text-davinci', - 'davinci', - 'curie', - 'babbage', - 'ada', -]; - -/** - * Exceptions to the unsupported list - these models DO support tools - * even though they start with an unsupported prefix. - */ -const OPENAI_MODELS_EXCEPTIONS = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; - -function isModelSupportingTools(model: string): boolean { - const isException = OPENAI_MODELS_EXCEPTIONS.some( - exception => model === exception || model.startsWith(`${exception}-`), - ); - if (isException) return true; - - const isKnownUnsupported = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT.some( - unsupported => model === unsupported || model.startsWith(`${unsupported}-`), - ); - - return !isKnownUnsupported; -} +import isModelSupportingTools from './supported-models'; export type { AiQueryArgs, diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts new file mode 100644 index 000000000..5c98f7f89 --- /dev/null +++ b/packages/ai-proxy/src/supported-models.ts @@ -0,0 +1,34 @@ +/** + * OpenAI model prefixes that do NOT support function calling (tools). + * Unknown models are allowed. + * @see https://platform.openai.com/docs/guides/function-calling + */ +const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ + 'gpt-4', + 'gpt-3.5-turbo', + 'gpt-3.5', + 'text-davinci', + 'davinci', + 'curie', + 'babbage', + 'ada', +]; + +/** + * Exceptions to the unsupported list - these models DO support tools + * even though they start with an unsupported prefix. + */ +const OPENAI_MODELS_EXCEPTIONS = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; + +export default function isModelSupportingTools(model: string): boolean { + const isException = OPENAI_MODELS_EXCEPTIONS.some( + exception => model === exception || model.startsWith(`${exception}-`), + ); + if (isException) return true; + + const isKnownUnsupported = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT.some( + unsupported => model === unsupported || model.startsWith(`${unsupported}-`), + ); + + return !isKnownUnsupported; +} From 80301dd60524bd6f803745626de4dd07f2826298 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 17:27:31 +0100 Subject: [PATCH 04/28] refactor(ai-proxy): extract isModelSupportingTools tests to dedicated file - Create supported-models.test.ts with direct function tests - Remove duplicate model list tests from router.test.ts - Simplify index.ts exports using export * pattern Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/index.ts | 14 +---- packages/ai-proxy/test/router.test.ts | 54 ------------------- .../ai-proxy/test/supported-models.test.ts | 43 +++++++++++++++ 3 files changed, 44 insertions(+), 67 deletions(-) create mode 100644 packages/ai-proxy/test/supported-models.test.ts diff --git a/packages/ai-proxy/src/index.ts b/packages/ai-proxy/src/index.ts index 5a614c6f3..bee805b44 100644 --- a/packages/ai-proxy/src/index.ts +++ b/packages/ai-proxy/src/index.ts @@ -2,23 +2,11 @@ import type { McpConfiguration } from './mcp-client'; import McpConfigChecker from './mcp-config-checker'; -export { ProviderDispatcher } from './provider-dispatcher'; -export type { - AiConfiguration, - AiProvider, - BaseAiConfiguration, - ChatCompletionMessage, - ChatCompletionResponse, - ChatCompletionTool, - ChatCompletionToolChoice, - DispatchBody, - OpenAiConfiguration, -} from './provider-dispatcher'; +export * from './provider-dispatcher'; export * from './remote-tools'; export * from './router'; export * from './mcp-client'; export * from './oauth-token-injector'; - export * from './errors'; export function validMcpConfigurationOrThrow(mcpConfig: McpConfiguration) { diff --git a/packages/ai-proxy/test/router.test.ts b/packages/ai-proxy/test/router.test.ts index b537b29b0..63febc861 100644 --- a/packages/ai-proxy/test/router.test.ts +++ b/packages/ai-proxy/test/router.test.ts @@ -419,59 +419,5 @@ describe('route', () => { }), ).toThrow("Model 'text-davinci-003' does not support tools"); }); - - describe('should accept supported models', () => { - const supportedModels = [ - 'gpt-4o', - 'gpt-4o-mini', - 'gpt-4o-2024-08-06', - 'gpt-4-turbo', - 'gpt-4-turbo-2024-04-09', - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-5', - 'o1', - 'o3-mini', - 'unknown-model', - 'future-gpt-model', - ]; - - it.each(supportedModels)('%s', model => { - expect( - () => - new Router({ - aiConfigurations: [ - { name: 'test', provider: 'openai', apiKey: 'dev', model }, - ], - }), - ).not.toThrow(); - }); - }); - - describe('should reject known unsupported models', () => { - const unsupportedModels = [ - 'gpt-4', - 'gpt-4-0613', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0125', - 'gpt-3.5', - 'text-davinci-003', - 'davinci', - 'curie', - 'babbage', - 'ada', - ]; - - it.each(unsupportedModels)('%s', model => { - expect( - () => - new Router({ - aiConfigurations: [ - { name: 'test', provider: 'openai', apiKey: 'dev', model }, - ], - }), - ).toThrow(AIModelNotSupportedError); - }); - }); }); }); diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts new file mode 100644 index 000000000..b7ca91611 --- /dev/null +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -0,0 +1,43 @@ +import isModelSupportingTools from '../src/supported-models'; + +describe('isModelSupportingTools', () => { + describe('should return true for supported models', () => { + const supportedModels = [ + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4o-2024-08-06', + 'gpt-4-turbo', + 'gpt-4-turbo-2024-04-09', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-5', + 'o1', + 'o3-mini', + 'unknown-model', + 'future-gpt-model', + ]; + + it.each(supportedModels)('%s', model => { + expect(isModelSupportingTools(model)).toBe(true); + }); + }); + + describe('should return false for unsupported models', () => { + const unsupportedModels = [ + 'gpt-4', + 'gpt-4-0613', + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-0125', + 'gpt-3.5', + 'text-davinci-003', + 'davinci', + 'curie', + 'babbage', + 'ada', + ]; + + it.each(unsupportedModels)('%s', model => { + expect(isModelSupportingTools(model)).toBe(false); + }); + }); +}); From ae0354d0c0f28bd8eca3d46302532b769275d535 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 18:10:17 +0100 Subject: [PATCH 05/28] fix: test --- packages/ai-proxy/src/index.ts | 1 + packages/ai-proxy/src/supported-models.ts | 13 +++++++++++++ packages/ai-proxy/test/supported-models.test.ts | 14 +++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/ai-proxy/src/index.ts b/packages/ai-proxy/src/index.ts index bee805b44..bd42d8a6b 100644 --- a/packages/ai-proxy/src/index.ts +++ b/packages/ai-proxy/src/index.ts @@ -8,6 +8,7 @@ export * from './router'; export * from './mcp-client'; export * from './oauth-token-injector'; export * from './errors'; +export { validateModelSupportsTools } from './supported-models'; export function validMcpConfigurationOrThrow(mcpConfig: McpConfiguration) { return McpConfigChecker.check(mcpConfig); diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 5c98f7f89..4d6e58d26 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -32,3 +32,16 @@ export default function isModelSupportingTools(model: string): boolean { return !isKnownUnsupported; } + +/** + * Validates that a model supports tool calling. + * @throws {Error} with descriptive message if the model doesn't support tools. + * @internal Used by Router and Agent for early validation. + */ +export function validateModelSupportsTools(model: string): void { + if (!isModelSupportingTools(model)) { + throw new Error( + `Model '${model}' does not support tools. Please use a model that supports function calling.`, + ); + } +} diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index b7ca91611..b0d33840e 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -1,4 +1,4 @@ -import isModelSupportingTools from '../src/supported-models'; +import isModelSupportingTools, { validateModelSupportsTools } from '../src/supported-models'; describe('isModelSupportingTools', () => { describe('should return true for supported models', () => { @@ -41,3 +41,15 @@ describe('isModelSupportingTools', () => { }); }); }); + +describe('validateModelSupportsTools', () => { + it('should not throw for supported models', () => { + expect(() => validateModelSupportsTools('gpt-4o')).not.toThrow(); + }); + + it('should throw for unsupported models', () => { + expect(() => validateModelSupportsTools('gpt-4')).toThrow( + "Model 'gpt-4' does not support tools. Please use a model that supports function calling.", + ); + }); +}); From cc316da7bb51dce17f7f5c01d3eb5bd946c4d793 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 20:06:32 +0100 Subject: [PATCH 06/28] fix(ai-proxy): remove gpt-3.5-turbo from unsupported models blocklist gpt-3.5-turbo actually supports function calling (tools), verified with real OpenAI API integration tests. - Remove gpt-3.5-turbo and gpt-3.5 from blocklist - Add model-tools-support.integration.test.ts to verify tool support - Update unit tests to reflect correct model support Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 6 +- .../model-tools-support.integration.test.ts | 77 +++++++++++++++++++ .../ai-proxy/test/supported-models.test.ts | 7 +- 3 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 packages/ai-proxy/test/model-tools-support.integration.test.ts diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 4d6e58d26..237fd550f 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -1,12 +1,12 @@ /** * OpenAI model prefixes that do NOT support function calling (tools). * Unknown models are allowed. + * + * Verified with real API tests - see model-tools-support.integration.test.ts * @see https://platform.openai.com/docs/guides/function-calling */ const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ - 'gpt-4', - 'gpt-3.5-turbo', - 'gpt-3.5', + 'gpt-4', // Base gpt-4 doesn't honor tool_choice: required 'text-davinci', 'davinci', 'curie', diff --git a/packages/ai-proxy/test/model-tools-support.integration.test.ts b/packages/ai-proxy/test/model-tools-support.integration.test.ts new file mode 100644 index 000000000..7e6326913 --- /dev/null +++ b/packages/ai-proxy/test/model-tools-support.integration.test.ts @@ -0,0 +1,77 @@ +/** + * Integration test to verify which OpenAI models actually support tool calling. + * + * Run with: OPENAI_API_KEY=xxx yarn workspace @forestadmin/ai-proxy test model-tools-support.integration + * + * This test helps maintain an accurate blocklist by testing against the real API. + * It bypasses the Router validation to test directly with OpenAI. + */ +import OpenAI from 'openai'; + +const { OPENAI_API_KEY } = process.env; +const describeWithOpenAI = OPENAI_API_KEY ? describe : describe.skip; + +// Models to test - includes both potentially supported and unsupported +const MODELS_TO_TEST = [ + // GPT-4o family (should support tools) + 'gpt-4o', + 'gpt-4o-mini', + + // GPT-4.1 family (should support tools) + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + + // GPT-4 turbo (should support tools) + 'gpt-4-turbo', + + // GPT-4 base (uncertain - test will tell us) + 'gpt-4', + + // GPT-3.5 turbo (uncertain - test will tell us) + 'gpt-3.5-turbo', +]; + +describeWithOpenAI('Model Tool Support Integration (real API)', () => { + const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); + + const toolDefinition: OpenAI.ChatCompletionTool = { + type: 'function', + function: { + name: 'calculate', + description: 'Calculate a math expression', + parameters: { + type: 'object', + properties: { expression: { type: 'string' } }, + required: ['expression'], + }, + }, + }; + + describe.each(MODELS_TO_TEST)('Model: %s', model => { + it('should support tool calling', async () => { + try { + const response = await openai.chat.completions.create({ + model, + messages: [{ role: 'user', content: 'What is 2+2? Use the calculator.' }], + tools: [toolDefinition], + tool_choice: 'required', + }); + + // If we get here, the model supports tools + expect(response.choices[0].finish_reason).toBe('tool_calls'); + expect(response.choices[0].message.tool_calls).toBeDefined(); + expect(response.choices[0].message.tool_calls!.length).toBeGreaterThan(0); + + // eslint-disable-next-line no-console + console.log(`✅ ${model}: SUPPORTS tools`); + } catch (error: any) { + // eslint-disable-next-line no-console + console.log(`❌ ${model}: FAILED - ${error.message}`); + + // Re-throw to fail the test - we want to see which models fail + throw error; + } + }, 30000); + }); +}); diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index b0d33840e..605ae5913 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -2,6 +2,7 @@ import isModelSupportingTools, { validateModelSupportsTools } from '../src/suppo describe('isModelSupportingTools', () => { describe('should return true for supported models', () => { + // Verified with real API tests - see model-tools-support.integration.test.ts const supportedModels = [ 'gpt-4o', 'gpt-4o-mini', @@ -10,6 +11,8 @@ describe('isModelSupportingTools', () => { 'gpt-4-turbo-2024-04-09', 'gpt-4.1', 'gpt-4.1-mini', + 'gpt-3.5-turbo', // Verified: supports tools + 'gpt-3.5-turbo-0125', // Verified: supports tools 'gpt-5', 'o1', 'o3-mini', @@ -23,12 +26,10 @@ describe('isModelSupportingTools', () => { }); describe('should return false for unsupported models', () => { + // Base gpt-4 doesn't honor tool_choice: required (verified with real API) const unsupportedModels = [ 'gpt-4', 'gpt-4-0613', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0125', - 'gpt-3.5', 'text-davinci-003', 'davinci', 'curie', From acbe27bd42b08c7bc7dab591cfcb36e46c7f26f6 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 20:57:17 +0100 Subject: [PATCH 07/28] chore(ai-proxy): remove model tools support integration test Test was only used to investigate which models support tools. Co-Authored-By: Claude Opus 4.5 --- .../model-tools-support.integration.test.ts | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 packages/ai-proxy/test/model-tools-support.integration.test.ts diff --git a/packages/ai-proxy/test/model-tools-support.integration.test.ts b/packages/ai-proxy/test/model-tools-support.integration.test.ts deleted file mode 100644 index 7e6326913..000000000 --- a/packages/ai-proxy/test/model-tools-support.integration.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Integration test to verify which OpenAI models actually support tool calling. - * - * Run with: OPENAI_API_KEY=xxx yarn workspace @forestadmin/ai-proxy test model-tools-support.integration - * - * This test helps maintain an accurate blocklist by testing against the real API. - * It bypasses the Router validation to test directly with OpenAI. - */ -import OpenAI from 'openai'; - -const { OPENAI_API_KEY } = process.env; -const describeWithOpenAI = OPENAI_API_KEY ? describe : describe.skip; - -// Models to test - includes both potentially supported and unsupported -const MODELS_TO_TEST = [ - // GPT-4o family (should support tools) - 'gpt-4o', - 'gpt-4o-mini', - - // GPT-4.1 family (should support tools) - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - - // GPT-4 turbo (should support tools) - 'gpt-4-turbo', - - // GPT-4 base (uncertain - test will tell us) - 'gpt-4', - - // GPT-3.5 turbo (uncertain - test will tell us) - 'gpt-3.5-turbo', -]; - -describeWithOpenAI('Model Tool Support Integration (real API)', () => { - const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); - - const toolDefinition: OpenAI.ChatCompletionTool = { - type: 'function', - function: { - name: 'calculate', - description: 'Calculate a math expression', - parameters: { - type: 'object', - properties: { expression: { type: 'string' } }, - required: ['expression'], - }, - }, - }; - - describe.each(MODELS_TO_TEST)('Model: %s', model => { - it('should support tool calling', async () => { - try { - const response = await openai.chat.completions.create({ - model, - messages: [{ role: 'user', content: 'What is 2+2? Use the calculator.' }], - tools: [toolDefinition], - tool_choice: 'required', - }); - - // If we get here, the model supports tools - expect(response.choices[0].finish_reason).toBe('tool_calls'); - expect(response.choices[0].message.tool_calls).toBeDefined(); - expect(response.choices[0].message.tool_calls!.length).toBeGreaterThan(0); - - // eslint-disable-next-line no-console - console.log(`✅ ${model}: SUPPORTS tools`); - } catch (error: any) { - // eslint-disable-next-line no-console - console.log(`❌ ${model}: FAILED - ${error.message}`); - - // Re-throw to fail the test - we want to see which models fail - throw error; - } - }, 30000); - }); -}); From f62081f9c89df007bd66f694d9a576f8124a8237 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 21:03:33 +0100 Subject: [PATCH 08/28] fix(agent): validate model tool support in addAi() for early failure Add validation in addAi() to fail fast when an unsupported model is configured, rather than waiting until route creation. Co-Authored-By: Claude Opus 4.5 --- packages/agent/src/agent.ts | 3 +++ packages/agent/test/agent.test.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 6a611aeb0..d1033c017 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -17,6 +17,7 @@ import type { import type { DataSource, DataSourceFactory } from '@forestadmin/datasource-toolkit'; import type { ForestSchema } from '@forestadmin/forestadmin-client'; +import { validateModelSupportsTools } from '@forestadmin/ai-proxy'; import { DataSourceCustomizer } from '@forestadmin/datasource-customizer'; import bodyParser from '@koa/bodyparser'; import cors from '@koa/cors'; @@ -245,6 +246,8 @@ export default class Agent extends FrameworkMounter ); } + validateModelSupportsTools(configuration.model); + this.options.logger( 'Warn', `AI configuration added with model '${configuration.model}'. ` + diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 577f1295f..2a141d6f8 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -419,6 +419,19 @@ describe('Agent', () => { expect(result).toBe(agent); }); + test('should throw an error when model does not support tools', () => { + const agent = new Agent(options); + + expect(() => + agent.addAi({ + name: 'gpt4-base', + provider: 'openai', + apiKey: 'test-key', + model: 'gpt-4', + }), + ).toThrow("Model 'gpt-4' does not support tools. Please use a model that supports function calling."); + }); + test('should throw an error when addAi is called more than once', () => { const agent = new Agent(options); From 3ff397f4d6a9cf6405d12f235d50f6897556d7d8 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 21:08:05 +0100 Subject: [PATCH 09/28] test(agent): reorder addAi tests Co-Authored-By: Claude Opus 4.5 --- packages/agent/test/agent.test.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 2a141d6f8..afb203b95 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -419,19 +419,6 @@ describe('Agent', () => { expect(result).toBe(agent); }); - test('should throw an error when model does not support tools', () => { - const agent = new Agent(options); - - expect(() => - agent.addAi({ - name: 'gpt4-base', - provider: 'openai', - apiKey: 'test-key', - model: 'gpt-4', - }), - ).toThrow("Model 'gpt-4' does not support tools. Please use a model that supports function calling."); - }); - test('should throw an error when addAi is called more than once', () => { const agent = new Agent(options); @@ -452,6 +439,19 @@ describe('Agent', () => { ).toThrow('addAi can only be called once. Multiple AI configurations are not supported yet.'); }); + test('should throw an error when model does not support tools', () => { + const agent = new Agent(options); + + expect(() => + agent.addAi({ + name: 'gpt4-base', + provider: 'openai', + apiKey: 'test-key', + model: 'gpt-4', + }), + ).toThrow("Model 'gpt-4' does not support tools. Please use a model that supports function calling."); + }); + test('should include ai_llms in schema meta when AI is configured', async () => { const agent = new Agent(options); agent.addAi({ From 394e5c8fcbc3cfdb33dafdd2b1ccd720105507f2 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 21:39:55 +0100 Subject: [PATCH 10/28] style(agent): fix prettier formatting in test Co-Authored-By: Claude Opus 4.5 --- packages/agent/test/agent.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index afb203b95..c3ccaed0e 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -449,7 +449,9 @@ describe('Agent', () => { apiKey: 'test-key', model: 'gpt-4', }), - ).toThrow("Model 'gpt-4' does not support tools. Please use a model that supports function calling."); + ).toThrow( + "Model 'gpt-4' does not support tools. Please use a model that supports function calling.", + ); }); test('should include ai_llms in schema meta when AI is configured', async () => { From ce09754b7cdbc7917df2c586f4f8909f5e2dcb56 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:15:28 +0100 Subject: [PATCH 11/28] chore(ai-proxy): remove integration test reference comments Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 2 -- packages/ai-proxy/test/supported-models.test.ts | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 237fd550f..fdef9db76 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -1,8 +1,6 @@ /** * OpenAI model prefixes that do NOT support function calling (tools). * Unknown models are allowed. - * - * Verified with real API tests - see model-tools-support.integration.test.ts * @see https://platform.openai.com/docs/guides/function-calling */ const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index 605ae5913..6bddd261b 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -2,7 +2,6 @@ import isModelSupportingTools, { validateModelSupportsTools } from '../src/suppo describe('isModelSupportingTools', () => { describe('should return true for supported models', () => { - // Verified with real API tests - see model-tools-support.integration.test.ts const supportedModels = [ 'gpt-4o', 'gpt-4o-mini', @@ -11,8 +10,8 @@ describe('isModelSupportingTools', () => { 'gpt-4-turbo-2024-04-09', 'gpt-4.1', 'gpt-4.1-mini', - 'gpt-3.5-turbo', // Verified: supports tools - 'gpt-3.5-turbo-0125', // Verified: supports tools + 'gpt-3.5-turbo', + 'gpt-3.5-turbo-0125', 'gpt-5', 'o1', 'o3-mini', @@ -26,7 +25,6 @@ describe('isModelSupportingTools', () => { }); describe('should return false for unsupported models', () => { - // Base gpt-4 doesn't honor tool_choice: required (verified with real API) const unsupportedModels = [ 'gpt-4', 'gpt-4-0613', From 4d5db255a27fa1197e70321de3d23f8d806e8911 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:16:41 +0100 Subject: [PATCH 12/28] chore(ai-proxy): remove @internal comment Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index fdef9db76..0b51132c0 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -34,7 +34,6 @@ export default function isModelSupportingTools(model: string): boolean { /** * Validates that a model supports tool calling. * @throws {Error} with descriptive message if the model doesn't support tools. - * @internal Used by Router and Agent for early validation. */ export function validateModelSupportsTools(model: string): void { if (!isModelSupportingTools(model)) { From a2f1e0ea854e40fe82c913da173cab3ef6340dd8 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:19:13 +0100 Subject: [PATCH 13/28] test(ai-proxy): add model tool support verification test Verify all allowed OpenAI models support tool calls with real API. Co-Authored-By: Claude Opus 4.5 --- .../ai-proxy/test/llm.integration.test.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index 238d22a53..639d6934e 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -688,4 +688,45 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { }, 10000); }); }); + + describe('Model tool support verification', () => { + const modelsToTest = [ + 'gpt-4o', + 'gpt-4o-mini', + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + 'gpt-4-turbo', + 'gpt-3.5-turbo', + ]; + + it.each(modelsToTest)('%s should support tool calls', async model => { + const modelRouter = new Router({ + aiConfigurations: [ + { name: 'test', provider: 'openai', model, apiKey: OPENAI_API_KEY! }, + ], + }); + + const response = (await modelRouter.route({ + route: 'ai-query', + body: { + messages: [{ role: 'user', content: 'What is 2+2?' }], + tools: [ + { + type: 'function', + function: { + name: 'calculate', + description: 'Calculate a math expression', + parameters: { type: 'object', properties: { result: { type: 'number' } } }, + }, + }, + ], + tool_choice: 'required', + }, + })) as ChatCompletionResponse; + + expect(response.choices[0].finish_reason).toBe('tool_calls'); + expect(response.choices[0].message.tool_calls).toBeDefined(); + }, 30000); + }); }); From fce3f952af51811e5f71fc8d14bbca355d80b45c Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:27:20 +0100 Subject: [PATCH 14/28] test(ai-proxy): add all OpenAI models from types to tests Include comprehensive model list from OpenAI types: - GPT-4o family (gpt-4o, gpt-4o-mini, audio/search previews) - GPT-4.1 family (gpt-4.1, mini, nano) - GPT-4 turbo variants - GPT-3.5 family - O-series reasoning models (o1, o3, o4-mini) - Future models (gpt-5.x) Co-Authored-By: Claude Opus 4.5 --- .../ai-proxy/test/llm.integration.test.ts | 7 +++++ .../ai-proxy/test/supported-models.test.ts | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index 639d6934e..8867288d8 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -691,13 +691,20 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { describe('Model tool support verification', () => { const modelsToTest = [ + // GPT-4o family 'gpt-4o', 'gpt-4o-mini', + // GPT-4.1 family 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano', + // GPT-4 turbo 'gpt-4-turbo', + // GPT-3.5 'gpt-3.5-turbo', + // O-series (reasoning models) + 'o1-mini', + 'o3-mini', ]; it.each(modelsToTest)('%s should support tool calls', async model => { diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index 6bddd261b..e7e62719a 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -3,20 +3,38 @@ import isModelSupportingTools, { validateModelSupportsTools } from '../src/suppo describe('isModelSupportingTools', () => { describe('should return true for supported models', () => { const supportedModels = [ + // GPT-4o family 'gpt-4o', 'gpt-4o-mini', 'gpt-4o-2024-08-06', - 'gpt-4-turbo', - 'gpt-4-turbo-2024-04-09', + 'gpt-4o-audio-preview', + 'gpt-4o-search-preview', + // GPT-4.1 family 'gpt-4.1', 'gpt-4.1-mini', + 'gpt-4.1-nano', + // GPT-4 turbo + 'gpt-4-turbo', + 'gpt-4-turbo-2024-04-09', + 'gpt-4-turbo-preview', + // GPT-3.5 family 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', - 'gpt-5', + 'gpt-3.5-turbo-16k', + // O-series (reasoning models) 'o1', + 'o1-mini', + 'o1-preview', + 'o3', 'o3-mini', + 'o4-mini', + // Future models + 'gpt-5', + 'gpt-5-mini', + 'gpt-5-nano', + 'gpt-5.1', + 'gpt-5.2', 'unknown-model', - 'future-gpt-model', ]; it.each(supportedModels)('%s', model => { From deaa0509628219209dbf005983f5abcf1efc39f0 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:30:10 +0100 Subject: [PATCH 15/28] test(ai-proxy): test all supported models and remove deprecated o1-mini All models verified with real OpenAI API: - gpt-4o, gpt-4o-mini - gpt-4.1, gpt-4.1-mini, gpt-4.1-nano - gpt-4-turbo - gpt-3.5-turbo - o1, o3, o3-mini, o4-mini Removed o1-mini and o1-preview (no longer available). Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/test/llm.integration.test.ts | 6 ++++-- packages/ai-proxy/test/supported-models.test.ts | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index 8867288d8..0fefbb4e1 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -700,11 +700,13 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { 'gpt-4.1-nano', // GPT-4 turbo 'gpt-4-turbo', - // GPT-3.5 + // GPT-3.5 family 'gpt-3.5-turbo', // O-series (reasoning models) - 'o1-mini', + 'o1', + 'o3', 'o3-mini', + 'o4-mini', ]; it.each(modelsToTest)('%s should support tool calls', async model => { diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index e7e62719a..bfcec247f 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -23,8 +23,6 @@ describe('isModelSupportingTools', () => { 'gpt-3.5-turbo-16k', // O-series (reasoning models) 'o1', - 'o1-mini', - 'o1-preview', 'o3', 'o3-mini', 'o4-mini', From 3baa37799a76f417ddb0538b055fea1b50adb592 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:33:16 +0100 Subject: [PATCH 16/28] refactor(ai-proxy): share SUPPORTED_OPENAI_MODELS between tests Export SUPPORTED_OPENAI_MODELS from supported-models.ts and use it in both unit tests and integration tests to ensure consistency. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 23 +++++++++++++++++++ .../ai-proxy/test/llm.integration.test.ts | 23 +++---------------- .../ai-proxy/test/supported-models.test.ts | 23 +++++-------------- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 0b51132c0..d81a00e31 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -1,3 +1,26 @@ +/** + * OpenAI models that support function calling (tools). + * This list is used for integration tests. + */ +export const SUPPORTED_OPENAI_MODELS = [ + // GPT-4o family + 'gpt-4o', + 'gpt-4o-mini', + // GPT-4.1 family + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + // GPT-4 turbo + 'gpt-4-turbo', + // GPT-3.5 family + 'gpt-3.5-turbo', + // O-series (reasoning models) + 'o1', + 'o3', + 'o3-mini', + 'o4-mini', +] as const; + /** * OpenAI model prefixes that do NOT support function calling (tools). * Unknown models are allowed. diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index 0fefbb4e1..c781552e9 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -15,6 +15,7 @@ import { z } from 'zod'; import { Router } from '../src'; import runMcpServer from '../src/examples/simple-mcp-server'; +import { SUPPORTED_OPENAI_MODELS } from '../src/supported-models'; const { OPENAI_API_KEY } = process.env; const describeWithOpenAI = OPENAI_API_KEY ? describe : describe.skip; @@ -690,26 +691,8 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { }); describe('Model tool support verification', () => { - const modelsToTest = [ - // GPT-4o family - 'gpt-4o', - 'gpt-4o-mini', - // GPT-4.1 family - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - // GPT-4 turbo - 'gpt-4-turbo', - // GPT-3.5 family - 'gpt-3.5-turbo', - // O-series (reasoning models) - 'o1', - 'o3', - 'o3-mini', - 'o4-mini', - ]; - - it.each(modelsToTest)('%s should support tool calls', async model => { + + it.each(SUPPORTED_OPENAI_MODELS)('%s should support tool calls', async model => { const modelRouter = new Router({ aiConfigurations: [ { name: 'test', provider: 'openai', model, apiKey: OPENAI_API_KEY! }, diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index bfcec247f..003be8744 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -1,31 +1,20 @@ -import isModelSupportingTools, { validateModelSupportsTools } from '../src/supported-models'; +import isModelSupportingTools, { + SUPPORTED_OPENAI_MODELS, + validateModelSupportsTools, +} from '../src/supported-models'; describe('isModelSupportingTools', () => { describe('should return true for supported models', () => { const supportedModels = [ - // GPT-4o family - 'gpt-4o', - 'gpt-4o-mini', + ...SUPPORTED_OPENAI_MODELS, + // Additional variants (dated versions) 'gpt-4o-2024-08-06', 'gpt-4o-audio-preview', 'gpt-4o-search-preview', - // GPT-4.1 family - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - // GPT-4 turbo - 'gpt-4-turbo', 'gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview', - // GPT-3.5 family - 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-16k', - // O-series (reasoning models) - 'o1', - 'o3', - 'o3-mini', - 'o4-mini', // Future models 'gpt-5', 'gpt-5-mini', From 7aa150aeb5693fcc50b43da2a0f5776f1b1627d4 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:36:52 +0100 Subject: [PATCH 17/28] feat(ai-proxy): add gpt-5 family to supported models Verified with real API - all gpt-5 variants exist and support tools: - gpt-5 - gpt-5-mini - gpt-5-nano - gpt-5.1 - gpt-5.2 Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 6 ++++++ packages/ai-proxy/test/supported-models.test.ts | 7 +------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index d81a00e31..28b25420a 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -3,6 +3,12 @@ * This list is used for integration tests. */ export const SUPPORTED_OPENAI_MODELS = [ + // GPT-5 family + 'gpt-5', + 'gpt-5-mini', + 'gpt-5-nano', + 'gpt-5.1', + 'gpt-5.2', // GPT-4o family 'gpt-4o', 'gpt-4o-mini', diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index 003be8744..5497a46b6 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -15,12 +15,7 @@ describe('isModelSupportingTools', () => { 'gpt-4-turbo-preview', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-16k', - // Future models - 'gpt-5', - 'gpt-5-mini', - 'gpt-5-nano', - 'gpt-5.1', - 'gpt-5.2', + // Unknown models are allowed by default 'unknown-model', ]; From 30f653e9f7d7bbb7b0493a16838d1627f9d56ac4 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 22:38:56 +0100 Subject: [PATCH 18/28] chore(ai-proxy): upgrade @langchain/openai to 1.2.5 and fix lint Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/package.json | 2 +- .../ai-proxy/test/llm.integration.test.ts | 53 ++++++++++--------- yarn.lock | 14 +++++ 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/packages/ai-proxy/package.json b/packages/ai-proxy/package.json index 6bf0c085d..e7e32f84f 100644 --- a/packages/ai-proxy/package.json +++ b/packages/ai-proxy/package.json @@ -17,7 +17,7 @@ "@langchain/core": "1.1.15", "@langchain/langgraph": "^1.1.0", "@langchain/mcp-adapters": "1.1.1", - "@langchain/openai": "1.2.2", + "@langchain/openai": "1.2.5", "zod": "^4.3.5" }, "devDependencies": { diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index c781552e9..fbceda374 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -691,34 +691,35 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { }); describe('Model tool support verification', () => { + it.each(SUPPORTED_OPENAI_MODELS)( + '%s should support tool calls', + async model => { + const modelRouter = new Router({ + aiConfigurations: [{ name: 'test', provider: 'openai', model, apiKey: OPENAI_API_KEY }], + }); - it.each(SUPPORTED_OPENAI_MODELS)('%s should support tool calls', async model => { - const modelRouter = new Router({ - aiConfigurations: [ - { name: 'test', provider: 'openai', model, apiKey: OPENAI_API_KEY! }, - ], - }); - - const response = (await modelRouter.route({ - route: 'ai-query', - body: { - messages: [{ role: 'user', content: 'What is 2+2?' }], - tools: [ - { - type: 'function', - function: { - name: 'calculate', - description: 'Calculate a math expression', - parameters: { type: 'object', properties: { result: { type: 'number' } } }, + const response = (await modelRouter.route({ + route: 'ai-query', + body: { + messages: [{ role: 'user', content: 'What is 2+2?' }], + tools: [ + { + type: 'function', + function: { + name: 'calculate', + description: 'Calculate a math expression', + parameters: { type: 'object', properties: { result: { type: 'number' } } }, + }, }, - }, - ], - tool_choice: 'required', - }, - })) as ChatCompletionResponse; + ], + tool_choice: 'required', + }, + })) as ChatCompletionResponse; - expect(response.choices[0].finish_reason).toBe('tool_calls'); - expect(response.choices[0].message.tool_calls).toBeDefined(); - }, 30000); + expect(response.choices[0].finish_reason).toBe('tool_calls'); + expect(response.choices[0].message.tool_calls).toBeDefined(); + }, + 30000, + ); }); }); diff --git a/yarn.lock b/yarn.lock index bba1ac377..434e55cc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2499,6 +2499,15 @@ openai "^6.10.0" zod "^3.25.76 || ^4" +"@langchain/openai@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-1.2.5.tgz#e99ebdc68224b895ef7c7f5cf47bc199388291ea" + integrity sha512-AtnzS0j8Kv7IIdXywp/N27ytsMIbmncvFckiaWyOGibgqQYZyyR3rFC6P4z2x4/yoMgU7nXcd8dTNf+mABzLUw== + dependencies: + js-tiktoken "^1.0.12" + openai "^6.18.0" + zod "^3.25.76 || ^4" + "@langchain/textsplitters@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@langchain/textsplitters/-/textsplitters-1.0.1.tgz#292f9c93239178c248b3338acf7b68aa47aa9830" @@ -14112,6 +14121,11 @@ openai@^6.10.0: resolved "https://registry.yarnpkg.com/openai/-/openai-6.16.0.tgz#53661d9f6307dc7523e89637ff7c6ccd0be16c67" integrity sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg== +openai@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-6.18.0.tgz#bd6c0bdb1aebf93375d324de51756280f7e85c6f" + integrity sha512-odLRYyz9rlzz6g8gKn61RM2oP5UUm428sE2zOxZqS9MzVfD5/XW8UoEjpnRkzTuScXP7ZbP/m7fC+bl8jCOZZw== + openapi-types@^12.1.3: version "12.1.3" resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" From f0c58bb2d5dfbd89666539df72ae5aab74a7bccb Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 23:18:30 +0100 Subject: [PATCH 19/28] feat(ai-proxy): fetch OpenAI models dynamically from API - Remove hardcoded SUPPORTED_OPENAI_MODELS list - Fetch models dynamically via openai.models.list() in integration tests - Filter models using isModelSupportingTools() blacklist - Add prefix and pattern-based blacklist for unsupported models - Add explicit error message when tests fail with instructions to update blacklist Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 95 +++++++++------- .../ai-proxy/test/llm.integration.test.ts | 102 +++++++++++++----- .../ai-proxy/test/supported-models.test.ts | 32 ++++-- 3 files changed, 158 insertions(+), 71 deletions(-) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 28b25420a..d558f9cfb 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -1,63 +1,82 @@ /** - * OpenAI models that support function calling (tools). - * This list is used for integration tests. - */ -export const SUPPORTED_OPENAI_MODELS = [ - // GPT-5 family - 'gpt-5', - 'gpt-5-mini', - 'gpt-5-nano', - 'gpt-5.1', - 'gpt-5.2', - // GPT-4o family - 'gpt-4o', - 'gpt-4o-mini', - // GPT-4.1 family - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - // GPT-4 turbo - 'gpt-4-turbo', - // GPT-3.5 family - 'gpt-3.5-turbo', - // O-series (reasoning models) - 'o1', - 'o3', - 'o3-mini', - 'o4-mini', -] as const; - -/** - * OpenAI model prefixes that do NOT support function calling (tools). - * Unknown models are allowed. + * OpenAI model prefixes that do NOT support function calling (tools) + * via the chat completions API. + * + * Uses prefix matching: model === prefix OR model.startsWith(prefix + '-') + * + * Unknown models are allowed by default. + * If a model fails the integration test, add it here. + * * @see https://platform.openai.com/docs/guides/function-calling */ -const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT = [ +const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES = [ + // Legacy models 'gpt-4', // Base gpt-4 doesn't honor tool_choice: required 'text-davinci', 'davinci', 'curie', 'babbage', 'ada', + // Non-chat model families + 'dall-e', + 'whisper', + 'tts', + 'text-embedding', + 'omni-moderation', + 'chatgpt', // chatgpt-4o-latest, chatgpt-image-latest + 'computer-use', // computer-use-preview + 'gpt-image', // gpt-image-1, gpt-image-1.5 + 'gpt-realtime', // gpt-realtime, gpt-realtime-mini + 'gpt-audio', // gpt-audio + 'sora', // sora-2, sora-2-pro + 'codex', // codex-mini-latest +]; + +/** + * OpenAI model patterns that do NOT support function calling (tools). + * Uses contains matching: model.includes(pattern) + */ +const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS = [ + // Non-chat model variants (can appear in the middle of model names) + '-realtime', + '-audio', + '-transcribe', + '-tts', + '-search', + '-codex', + '-instruct', + // Models that only support v1/responses, not v1/chat/completions + '-pro', + '-deep-research', ]; /** * Exceptions to the unsupported list - these models DO support tools - * even though they start with an unsupported prefix. + * even though they match an unsupported pattern. */ const OPENAI_MODELS_EXCEPTIONS = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; export default function isModelSupportingTools(model: string): boolean { + // Check pattern matches first (contains) - these NEVER support tools + const matchesPattern = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS.some(pattern => + model.includes(pattern), + ); + if (matchesPattern) return false; + + // Check prefix blacklist + const matchesPrefix = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES.some( + prefix => model === prefix || model.startsWith(`${prefix}-`), + ); + + // Check exceptions (whitelist specific models from the prefix blacklist) const isException = OPENAI_MODELS_EXCEPTIONS.some( exception => model === exception || model.startsWith(`${exception}-`), ); - if (isException) return true; - const isKnownUnsupported = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT.some( - unsupported => model === unsupported || model.startsWith(`${unsupported}-`), - ); + // If it matches a prefix but is an exception, it's supported + if (matchesPrefix && !isException) return false; - return !isKnownUnsupported; + return true; } /** diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index fbceda374..c06877ca2 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -11,15 +11,32 @@ import type { Server } from 'http'; // eslint-disable-next-line import/extensions import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import OpenAI from 'openai'; import { z } from 'zod'; import { Router } from '../src'; import runMcpServer from '../src/examples/simple-mcp-server'; -import { SUPPORTED_OPENAI_MODELS } from '../src/supported-models'; +import isModelSupportingTools from '../src/supported-models'; const { OPENAI_API_KEY } = process.env; const describeWithOpenAI = OPENAI_API_KEY ? describe : describe.skip; +/** + * Fetches available models from OpenAI API. + * Returns all models that pass `isModelSupportingTools`. + * + * If a model fails the integration test, update the blacklist in supported-models.ts. + */ +async function fetchChatModelsFromOpenAI(): Promise { + const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); + const models = await openai.models.list(); + + return models.data + .map(m => m.id) + .filter(id => isModelSupportingTools(id)) + .sort(); +} + describeWithOpenAI('OpenAI Integration (real API)', () => { const router = new Router({ aiConfigurations: [ @@ -691,35 +708,70 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { }); describe('Model tool support verification', () => { - it.each(SUPPORTED_OPENAI_MODELS)( - '%s should support tool calls', - async model => { + let modelsToTest: string[]; + + beforeAll(async () => { + modelsToTest = await fetchChatModelsFromOpenAI(); + }); + + it('should have found chat models from OpenAI API', () => { + expect(modelsToTest.length).toBeGreaterThan(0); + // eslint-disable-next-line no-console + console.log(`Testing ${modelsToTest.length} models:`, modelsToTest); + }); + + it('all chat models should support tool calls', async () => { + const results: { model: string; success: boolean; error?: string }[] = []; + + for (const model of modelsToTest) { const modelRouter = new Router({ aiConfigurations: [{ name: 'test', provider: 'openai', model, apiKey: OPENAI_API_KEY }], }); - const response = (await modelRouter.route({ - route: 'ai-query', - body: { - messages: [{ role: 'user', content: 'What is 2+2?' }], - tools: [ - { - type: 'function', - function: { - name: 'calculate', - description: 'Calculate a math expression', - parameters: { type: 'object', properties: { result: { type: 'number' } } }, + try { + const response = (await modelRouter.route({ + route: 'ai-query', + body: { + messages: [{ role: 'user', content: 'What is 2+2?' }], + tools: [ + { + type: 'function', + function: { + name: 'calculate', + description: 'Calculate a math expression', + parameters: { type: 'object', properties: { result: { type: 'number' } } }, + }, }, - }, - ], - tool_choice: 'required', - }, - })) as ChatCompletionResponse; + ], + tool_choice: 'required', + }, + })) as ChatCompletionResponse; - expect(response.choices[0].finish_reason).toBe('tool_calls'); - expect(response.choices[0].message.tool_calls).toBeDefined(); - }, - 30000, - ); + const success = + response.choices[0].finish_reason === 'tool_calls' && + response.choices[0].message.tool_calls !== undefined; + + results.push({ model, success }); + } catch (error) { + results.push({ model, success: false, error: String(error) }); + } + } + + const failures = results.filter(r => !r.success); + if (failures.length > 0) { + const failedModelNames = failures.map(f => f.model).join(', '); + // eslint-disable-next-line no-console + console.error( + `\n❌ ${failures.length} model(s) failed: ${failedModelNames}\n\n` + + `To fix this, add the failing model(s) to the blacklist in:\n` + + ` packages/ai-proxy/src/supported-models.ts\n\n` + + `Add to OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES (for prefix match)\n` + + `or OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS (for contains match)\n`, + failures, + ); + } + + expect(failures).toEqual([]); + }, 300000); // 5 minutes for all models }); }); diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index 5497a46b6..049608c54 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -1,20 +1,36 @@ -import isModelSupportingTools, { - SUPPORTED_OPENAI_MODELS, - validateModelSupportsTools, -} from '../src/supported-models'; +import isModelSupportingTools, { validateModelSupportsTools } from '../src/supported-models'; describe('isModelSupportingTools', () => { describe('should return true for supported models', () => { + // Static list for unit tests (no API call required) const supportedModels = [ - ...SUPPORTED_OPENAI_MODELS, - // Additional variants (dated versions) + // GPT-5 family + 'gpt-5', + 'gpt-5-mini', + 'gpt-5-nano', + 'gpt-5.1', + 'gpt-5.2', + // GPT-4o family + 'gpt-4o', + 'gpt-4o-mini', 'gpt-4o-2024-08-06', - 'gpt-4o-audio-preview', - 'gpt-4o-search-preview', + // GPT-4.1 family + 'gpt-4.1', + 'gpt-4.1-mini', + 'gpt-4.1-nano', + // GPT-4 turbo + 'gpt-4-turbo', 'gpt-4-turbo-2024-04-09', 'gpt-4-turbo-preview', + // GPT-3.5 family + 'gpt-3.5-turbo', 'gpt-3.5-turbo-0125', 'gpt-3.5-turbo-16k', + // O-series (reasoning models) + 'o1', + 'o3', + 'o3-mini', + 'o4-mini', // Unknown models are allowed by default 'unknown-model', ]; From 094895e20e6538e43662f8811024f8d72e219f25 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 23:21:22 +0100 Subject: [PATCH 20/28] refactor(ai-proxy): simplify unit tests for isModelSupportingTools Keep only essential test cases: - Known supported model - Unknown model (allowed by default) - Blacklisted model Co-Authored-By: Claude Opus 4.5 --- .../ai-proxy/test/supported-models.test.ts | 57 +++---------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index 049608c54..fd051069d 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -1,59 +1,16 @@ import isModelSupportingTools, { validateModelSupportsTools } from '../src/supported-models'; describe('isModelSupportingTools', () => { - describe('should return true for supported models', () => { - // Static list for unit tests (no API call required) - const supportedModels = [ - // GPT-5 family - 'gpt-5', - 'gpt-5-mini', - 'gpt-5-nano', - 'gpt-5.1', - 'gpt-5.2', - // GPT-4o family - 'gpt-4o', - 'gpt-4o-mini', - 'gpt-4o-2024-08-06', - // GPT-4.1 family - 'gpt-4.1', - 'gpt-4.1-mini', - 'gpt-4.1-nano', - // GPT-4 turbo - 'gpt-4-turbo', - 'gpt-4-turbo-2024-04-09', - 'gpt-4-turbo-preview', - // GPT-3.5 family - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-0125', - 'gpt-3.5-turbo-16k', - // O-series (reasoning models) - 'o1', - 'o3', - 'o3-mini', - 'o4-mini', - // Unknown models are allowed by default - 'unknown-model', - ]; - - it.each(supportedModels)('%s', model => { - expect(isModelSupportingTools(model)).toBe(true); - }); + it('should return true for a known supported model', () => { + expect(isModelSupportingTools('gpt-4o')).toBe(true); }); - describe('should return false for unsupported models', () => { - const unsupportedModels = [ - 'gpt-4', - 'gpt-4-0613', - 'text-davinci-003', - 'davinci', - 'curie', - 'babbage', - 'ada', - ]; + it('should return true for an unknown model (allowed by default)', () => { + expect(isModelSupportingTools('unknown-future-model')).toBe(true); + }); - it.each(unsupportedModels)('%s', model => { - expect(isModelSupportingTools(model)).toBe(false); - }); + it('should return false for a blacklisted model', () => { + expect(isModelSupportingTools('gpt-4')).toBe(false); }); }); From 8fd7416b714476fb48258360f1b320be10a15c4e Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Thu, 5 Feb 2026 23:33:56 +0100 Subject: [PATCH 21/28] refactor(agent): move model validation to Router instantiation - Remove validateModelSupportsTools call from Agent.addAi() - Validation now happens when AiProxyRouter is instantiated during start() - Update test to use real makeRoutes to trigger validation Co-Authored-By: Claude Opus 4.5 --- packages/agent/src/agent.ts | 3 --- packages/agent/test/agent.test.ts | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index d1033c017..6a611aeb0 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -17,7 +17,6 @@ import type { import type { DataSource, DataSourceFactory } from '@forestadmin/datasource-toolkit'; import type { ForestSchema } from '@forestadmin/forestadmin-client'; -import { validateModelSupportsTools } from '@forestadmin/ai-proxy'; import { DataSourceCustomizer } from '@forestadmin/datasource-customizer'; import bodyParser from '@koa/bodyparser'; import cors from '@koa/cors'; @@ -246,8 +245,6 @@ export default class Agent extends FrameworkMounter ); } - validateModelSupportsTools(configuration.model); - this.options.logger( 'Warn', `AI configuration added with model '${configuration.model}'. ` + diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index c3ccaed0e..5874f2686 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -439,17 +439,21 @@ describe('Agent', () => { ).toThrow('addAi can only be called once. Multiple AI configurations are not supported yet.'); }); - test('should throw an error when model does not support tools', () => { + test('should throw an error on start when model does not support tools', async () => { + // Use the real makeRoutes to trigger validation in AiProxyRouter + const realMakeRoutes = jest.requireActual('../src/routes').default; + mockMakeRoutes.mockImplementation(realMakeRoutes); + const agent = new Agent(options); - expect(() => - agent.addAi({ - name: 'gpt4-base', - provider: 'openai', - apiKey: 'test-key', - model: 'gpt-4', - }), - ).toThrow( + agent.addAi({ + name: 'gpt4-base', + provider: 'openai', + apiKey: 'test-key', + model: 'gpt-4', + }); + + await expect(agent.start()).rejects.toThrow( "Model 'gpt-4' does not support tools. Please use a model that supports function calling.", ); }); From ba88d44d12bd1fc3e3cfeac70e6084b158ee0c0b Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 09:21:36 +0100 Subject: [PATCH 22/28] fix(ai-proxy): improve model validation robustness - Remove -pro pattern that incorrectly blocked o3-pro - Add error handling for OpenAI models.list() API call - Distinguish infrastructure errors from model compatibility errors in tests - Remove unused validateModelSupportsTools export - Add test for multiple AI configurations validation Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/index.ts | 1 - packages/ai-proxy/src/supported-models.ts | 13 -------- .../ai-proxy/test/llm.integration.test.ts | 30 +++++++++++++++++-- packages/ai-proxy/test/router.test.ts | 12 ++++++++ .../ai-proxy/test/supported-models.test.ts | 14 +-------- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/packages/ai-proxy/src/index.ts b/packages/ai-proxy/src/index.ts index bd42d8a6b..bee805b44 100644 --- a/packages/ai-proxy/src/index.ts +++ b/packages/ai-proxy/src/index.ts @@ -8,7 +8,6 @@ export * from './router'; export * from './mcp-client'; export * from './oauth-token-injector'; export * from './errors'; -export { validateModelSupportsTools } from './supported-models'; export function validMcpConfigurationOrThrow(mcpConfig: McpConfiguration) { return McpConfigChecker.check(mcpConfig); diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index d558f9cfb..1c980dd77 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -46,7 +46,6 @@ const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS = [ '-codex', '-instruct', // Models that only support v1/responses, not v1/chat/completions - '-pro', '-deep-research', ]; @@ -78,15 +77,3 @@ export default function isModelSupportingTools(model: string): boolean { return true; } - -/** - * Validates that a model supports tool calling. - * @throws {Error} with descriptive message if the model doesn't support tools. - */ -export function validateModelSupportsTools(model: string): void { - if (!isModelSupportingTools(model)) { - throw new Error( - `Model '${model}' does not support tools. Please use a model that supports function calling.`, - ); - } -} diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index c06877ca2..6d13ee295 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -29,7 +29,17 @@ const describeWithOpenAI = OPENAI_API_KEY ? describe : describe.skip; */ async function fetchChatModelsFromOpenAI(): Promise { const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); - const models = await openai.models.list(); + + let models; + try { + models = await openai.models.list(); + } catch (error) { + throw new Error( + `Failed to fetch models from OpenAI API. ` + + `Ensure OPENAI_API_KEY is valid and network is available. ` + + `Original error: ${error}`, + ); + } return models.data .map(m => m.id) @@ -753,7 +763,23 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { results.push({ model, success }); } catch (error) { - results.push({ model, success: false, error: String(error) }); + const errorMessage = String(error); + + // Infrastructure errors should fail the test immediately + const isInfrastructureError = + errorMessage.includes('rate limit') || + errorMessage.includes('429') || + errorMessage.includes('401') || + errorMessage.includes('Authentication') || + errorMessage.includes('ECONNREFUSED') || + errorMessage.includes('ETIMEDOUT') || + errorMessage.includes('getaddrinfo'); + + if (isInfrastructureError) { + throw new Error(`Infrastructure error testing model ${model}: ${errorMessage}`); + } + + results.push({ model, success: false, error: errorMessage }); } } diff --git a/packages/ai-proxy/test/router.test.ts b/packages/ai-proxy/test/router.test.ts index 63febc861..82abb1dbf 100644 --- a/packages/ai-proxy/test/router.test.ts +++ b/packages/ai-proxy/test/router.test.ts @@ -419,5 +419,17 @@ describe('route', () => { }), ).toThrow("Model 'text-davinci-003' does not support tools"); }); + + it('validates all configurations, not just the first', () => { + expect( + () => + new Router({ + aiConfigurations: [ + { name: 'valid', provider: 'openai', apiKey: 'dev', model: 'gpt-4o' }, + { name: 'invalid', provider: 'openai', apiKey: 'dev', model: 'gpt-4' }, + ], + }), + ).toThrow("Model 'gpt-4' does not support tools"); + }); }); }); diff --git a/packages/ai-proxy/test/supported-models.test.ts b/packages/ai-proxy/test/supported-models.test.ts index fd051069d..7c5297b9b 100644 --- a/packages/ai-proxy/test/supported-models.test.ts +++ b/packages/ai-proxy/test/supported-models.test.ts @@ -1,4 +1,4 @@ -import isModelSupportingTools, { validateModelSupportsTools } from '../src/supported-models'; +import isModelSupportingTools from '../src/supported-models'; describe('isModelSupportingTools', () => { it('should return true for a known supported model', () => { @@ -13,15 +13,3 @@ describe('isModelSupportingTools', () => { expect(isModelSupportingTools('gpt-4')).toBe(false); }); }); - -describe('validateModelSupportsTools', () => { - it('should not throw for supported models', () => { - expect(() => validateModelSupportsTools('gpt-4o')).not.toThrow(); - }); - - it('should throw for unsupported models', () => { - expect(() => validateModelSupportsTools('gpt-4')).toThrow( - "Model 'gpt-4' does not support tools. Please use a model that supports function calling.", - ); - }); -}); From b9d831409b428f872e54442652d5be4a1a60e4ff Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 09:26:29 +0100 Subject: [PATCH 23/28] fix(ai-proxy): restore -pro pattern for models not supporting chat completions Models like gpt-5-pro, o1-pro only support v1/responses, not v1/chat/completions. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 1c980dd77..42bda7945 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -46,6 +46,7 @@ const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS = [ '-codex', '-instruct', // Models that only support v1/responses, not v1/chat/completions + '-pro', '-deep-research', ]; From 2533ae79ad3e61a1cbb110804c36a82336eebabd Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 10:20:19 +0100 Subject: [PATCH 24/28] refactor(ai-proxy): rename model lists for clarity - UNSUPPORTED_MODEL_PREFIXES (was OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES) - UNSUPPORTED_MODEL_PATTERNS (was OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS) - SUPPORTED_MODEL_OVERRIDES (was OPENAI_MODELS_EXCEPTIONS) Clearer naming reflecting that models must support tool calls. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 33 +++++++++++------------ 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 42bda7945..948198747 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -1,6 +1,5 @@ /** - * OpenAI model prefixes that do NOT support function calling (tools) - * via the chat completions API. + * OpenAI model prefixes that do NOT support tool calls via the chat completions API. * * Uses prefix matching: model === prefix OR model.startsWith(prefix + '-') * @@ -9,7 +8,7 @@ * * @see https://platform.openai.com/docs/guides/function-calling */ -const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES = [ +const UNSUPPORTED_MODEL_PREFIXES = [ // Legacy models 'gpt-4', // Base gpt-4 doesn't honor tool_choice: required 'text-davinci', @@ -33,10 +32,10 @@ const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES = [ ]; /** - * OpenAI model patterns that do NOT support function calling (tools). + * OpenAI model patterns that do NOT support tool calls. * Uses contains matching: model.includes(pattern) */ -const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS = [ +const UNSUPPORTED_MODEL_PATTERNS = [ // Non-chat model variants (can appear in the middle of model names) '-realtime', '-audio', @@ -51,30 +50,30 @@ const OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS = [ ]; /** - * Exceptions to the unsupported list - these models DO support tools - * even though they match an unsupported pattern. + * Models that DO support tool calls even though they match an unsupported prefix. + * These override the UNSUPPORTED_MODEL_PREFIXES list. */ -const OPENAI_MODELS_EXCEPTIONS = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; +const SUPPORTED_MODEL_OVERRIDES = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; export default function isModelSupportingTools(model: string): boolean { // Check pattern matches first (contains) - these NEVER support tools - const matchesPattern = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS.some(pattern => + const matchesUnsupportedPattern = UNSUPPORTED_MODEL_PATTERNS.some(pattern => model.includes(pattern), ); - if (matchesPattern) return false; + if (matchesUnsupportedPattern) return false; - // Check prefix blacklist - const matchesPrefix = OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES.some( + // Check unsupported prefixes + const matchesUnsupportedPrefix = UNSUPPORTED_MODEL_PREFIXES.some( prefix => model === prefix || model.startsWith(`${prefix}-`), ); - // Check exceptions (whitelist specific models from the prefix blacklist) - const isException = OPENAI_MODELS_EXCEPTIONS.some( - exception => model === exception || model.startsWith(`${exception}-`), + // Check if model is in the supported overrides list + const isSupportedOverride = SUPPORTED_MODEL_OVERRIDES.some( + override => model === override || model.startsWith(`${override}-`), ); - // If it matches a prefix but is an exception, it's supported - if (matchesPrefix && !isException) return false; + // If it matches an unsupported prefix but is not in overrides, reject it + if (matchesUnsupportedPrefix && !isSupportedOverride) return false; return true; } From 4847e4e32d76f61302f891d7895fe8614d2e0587 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 10:21:59 +0100 Subject: [PATCH 25/28] test(ai-proxy): add parallel_tool_calls to model verification test This will identify models that don't support parallel_tool_calls: false. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/test/llm.integration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index 6d13ee295..3096bbf79 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -754,6 +754,7 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { }, ], tool_choice: 'required', + parallel_tool_calls: false, }, })) as ChatCompletionResponse; From a8df84c7f7a2dc1388519c7da2b8d56c8990fdd5 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 10:28:52 +0100 Subject: [PATCH 26/28] fix(ai-proxy): block o-series models that don't support parallel_tool_calls O-series reasoning models (o1, o3, o4) don't support parallel_tool_calls parameter which is required for our use case. Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 948198747..91e4ab4c1 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -16,6 +16,10 @@ const UNSUPPORTED_MODEL_PREFIXES = [ 'curie', 'babbage', 'ada', + // O-series reasoning models - don't support parallel_tool_calls + 'o1', + 'o3', + 'o4', // Non-chat model families 'dall-e', 'whisper', From 516706dadcf244a74345753f40773f3f19fe015e Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 10:36:32 +0100 Subject: [PATCH 27/28] docs(ai-proxy): add JSDoc to isModelSupportingTools Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/src/supported-models.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ai-proxy/src/supported-models.ts b/packages/ai-proxy/src/supported-models.ts index 91e4ab4c1..37647b94e 100644 --- a/packages/ai-proxy/src/supported-models.ts +++ b/packages/ai-proxy/src/supported-models.ts @@ -59,6 +59,11 @@ const UNSUPPORTED_MODEL_PATTERNS = [ */ const SUPPORTED_MODEL_OVERRIDES = ['gpt-4-turbo', 'gpt-4o', 'gpt-4.1']; +/** + * Checks if a model is compatible with Forest Admin AI. + * + * Supported models must handle tool calls and the parallel_tool_calls parameter. + */ export default function isModelSupportingTools(model: string): boolean { // Check pattern matches first (contains) - these NEVER support tools const matchesUnsupportedPattern = UNSUPPORTED_MODEL_PATTERNS.some(pattern => From f3ceff7d9ff9e79a706612498c2fbb1a85bce858 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Fri, 6 Feb 2026 15:17:27 +0100 Subject: [PATCH 28/28] fix(ai-proxy): update variable names in test error message Co-Authored-By: Claude Opus 4.5 --- packages/ai-proxy/test/llm.integration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai-proxy/test/llm.integration.test.ts b/packages/ai-proxy/test/llm.integration.test.ts index 3096bbf79..a3f65d55b 100644 --- a/packages/ai-proxy/test/llm.integration.test.ts +++ b/packages/ai-proxy/test/llm.integration.test.ts @@ -792,8 +792,8 @@ describeWithOpenAI('OpenAI Integration (real API)', () => { `\n❌ ${failures.length} model(s) failed: ${failedModelNames}\n\n` + `To fix this, add the failing model(s) to the blacklist in:\n` + ` packages/ai-proxy/src/supported-models.ts\n\n` + - `Add to OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PREFIXES (for prefix match)\n` + - `or OPENAI_MODELS_WITHOUT_TOOLS_SUPPORT_PATTERNS (for contains match)\n`, + `Add to UNSUPPORTED_MODEL_PREFIXES (for prefix match)\n` + + `or UNSUPPORTED_MODEL_PATTERNS (for contains match)\n`, failures, ); }