diff --git a/package.json b/package.json index f8dc822..d8d3e83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@missionsquad/rosetta-ai", - "version": "1.12.0", + "version": "1.12.1", "description": "Unified TypeScript SDK for interacting with multiple AI providers (Anthropic, Google, Groq, OpenAI).", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/core/mapping/gpt5.support.ts b/src/core/mapping/gpt5.support.ts index 2f9fdeb..09e904f 100644 --- a/src/core/mapping/gpt5.support.ts +++ b/src/core/mapping/gpt5.support.ts @@ -7,6 +7,7 @@ export interface Gpt5Support { fixedReasoningEffort?: ReasoningEffort supportsVerbosity: boolean supportsSampling: 'never' | 'always' | 'only_with_reasoning_none' + supportsFunctionToolsWithReasoning?: boolean } // Chat Completions-supported GPT-5 models verified for this implementation. @@ -16,62 +17,71 @@ const GPT5_CHAT_COMPLETIONS_SUPPORT: Record = { allowedReasoningEfforts: ['minimal', 'low', 'medium', 'high'], defaultReasoningEffort: 'medium', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: true }, 'gpt-5-mini': { chatCompletionsSupported: true, allowedReasoningEfforts: ['minimal', 'low', 'medium', 'high'], defaultReasoningEffort: 'medium', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: true }, 'gpt-5-nano': { chatCompletionsSupported: true, allowedReasoningEfforts: ['minimal', 'low', 'medium', 'high'], defaultReasoningEffort: 'medium', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: true }, 'gpt-5.1': { chatCompletionsSupported: true, allowedReasoningEfforts: ['none', 'low', 'medium', 'high'], defaultReasoningEffort: 'none', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: true }, 'gpt-5.2': { chatCompletionsSupported: true, allowedReasoningEfforts: ['none', 'low', 'medium', 'high', 'xhigh'], defaultReasoningEffort: 'none', supportsVerbosity: true, - supportsSampling: 'only_with_reasoning_none' + supportsSampling: 'only_with_reasoning_none', + supportsFunctionToolsWithReasoning: true }, 'gpt-5.4': { chatCompletionsSupported: true, allowedReasoningEfforts: ['none', 'low', 'medium', 'high', 'xhigh'], defaultReasoningEffort: 'none', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: false }, 'gpt-5-chat-latest': { chatCompletionsSupported: true, allowedReasoningEfforts: [], supportsVerbosity: true, - supportsSampling: 'always' + supportsSampling: 'always', + supportsFunctionToolsWithReasoning: true }, 'gpt-5.1-chat-latest': { chatCompletionsSupported: true, allowedReasoningEfforts: ['medium'], fixedReasoningEffort: 'medium', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: true }, 'gpt-5.2-chat-latest': { chatCompletionsSupported: true, allowedReasoningEfforts: ['medium'], fixedReasoningEffort: 'medium', supportsVerbosity: true, - supportsSampling: 'never' + supportsSampling: 'never', + supportsFunctionToolsWithReasoning: true } } diff --git a/src/core/mapping/openai.mapper.ts b/src/core/mapping/openai.mapper.ts index 8bdb085..6135d46 100644 --- a/src/core/mapping/openai.mapper.ts +++ b/src/core/mapping/openai.mapper.ts @@ -191,6 +191,7 @@ export class OpenAIMapper implements IProviderMapper { const isThinking = isThinkingModel(params.model!) const isGPT5 = isGPT5Model(params.model!) const hasEffectiveLimit = baseMappedParams.maxTokens !== undefined + const hasFunctionTools = Array.isArray(tools) && tools.length > 0 if (!isThinking) { // Non-thinking OpenAI models must use max_completion_tokens only. @@ -235,7 +236,11 @@ export class OpenAIMapper implements IProviderMapper { } } - if (effectiveEffort !== undefined) { + const shouldOmitReasoningForFunctionTools = + hasFunctionTools && + support.supportsFunctionToolsWithReasoning === false + + if (effectiveEffort !== undefined && !shouldOmitReasoningForFunctionTools) { basePayload.reasoning_effort = effectiveEffort } else { delete basePayload.reasoning_effort diff --git a/tests/unit/core/mapping/openai.mapper.spec.ts b/tests/unit/core/mapping/openai.mapper.spec.ts index f67b33e..edd705f 100644 --- a/tests/unit/core/mapping/openai.mapper.spec.ts +++ b/tests/unit/core/mapping/openai.mapper.spec.ts @@ -522,6 +522,28 @@ describe('OpenAI Mapper', () => { expect(result.max_completion_tokens).toBe(321) }) + it('[Medium] should omit reasoning_effort for gpt-5.4 when function tools are present', () => { + const params: GenerateParams = { + ...baseParams, + model: 'gpt-5.4', + messages: [{ role: 'user', content: 'Generate.' }], + reasoningEffort: 'high', + tools: [ + { + type: 'function', + function: { + name: 'myFunc', + parameters: { type: 'object', properties: {} }, + zodSchema: z.object({}) + } + } + ] + } + const result = mapper.mapToProviderParams(params) as any + expect(result.reasoning_effort).toBeUndefined() + expect(result.tools).toHaveLength(1) + }) + it('[Medium] should map toolChoice required and none', () => { const paramsRequired: GenerateParams = { ...baseParams, messages: [], toolChoice: 'required' } const paramsNone: GenerateParams = { ...baseParams, messages: [], toolChoice: 'none' }