diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index e0dad68431..fd20176632 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -236,6 +236,7 @@ "com_endpoint_assistant": "Assistant", "com_endpoint_assistant_model": "Assistant Model", "com_endpoint_assistant_placeholder": "Please select an Assistant from the right-hand Side Panel", + "com_endpoint_bedrock_reasoning_effort": "Controls the reasoning level for supported Bedrock models (e.g. Kimi K2.5, GLM). Higher levels produce more thorough reasoning at the cost of increased latency and tokens.", "com_endpoint_config_click_here": "Click Here", "com_endpoint_config_google_api_info": "To get your Generative Language API key (for Gemini),", "com_endpoint_config_google_api_key": "Google API Key", diff --git a/packages/api/src/endpoints/bedrock/initialize.spec.ts b/packages/api/src/endpoints/bedrock/initialize.spec.ts index 2b83c55937..158650017e 100644 --- a/packages/api/src/endpoints/bedrock/initialize.spec.ts +++ b/packages/api/src/endpoints/bedrock/initialize.spec.ts @@ -722,4 +722,66 @@ describe('initializeBedrock', () => { expect(amrf.effort).toBeUndefined(); }); }); + + describe('Bedrock reasoning_effort for Moonshot/ZAI models', () => { + it('should map reasoning_effort to reasoning_config for Moonshot Kimi K2.5', async () => { + const params = createMockParams({ + model_parameters: { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'high', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.reasoning_config).toBe('high'); + expect(amrf.reasoning_effort).toBeUndefined(); + expect(amrf.thinking).toBeUndefined(); + expect(amrf.anthropic_beta).toBeUndefined(); + }); + + it('should map reasoning_effort to reasoning_config for ZAI GLM', async () => { + const params = createMockParams({ + model_parameters: { + model: 'zai.glm-4.7', + reasoning_effort: 'medium', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.reasoning_config).toBe('medium'); + expect(amrf.reasoning_effort).toBeUndefined(); + }); + + it('should not include reasoning_config when reasoning_effort is unset', async () => { + const params = createMockParams({ + model_parameters: { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: '', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + + expect(result.llmConfig.additionalModelRequestFields).toBeUndefined(); + }); + + it('should not map reasoning_effort to reasoning_config for Anthropic models', async () => { + const params = createMockParams({ + model_parameters: { + model: 'anthropic.claude-opus-4-6-v1', + reasoning_effort: 'high', + }, + }); + + const result = (await initializeBedrock(params)) as BedrockLLMConfigResult; + const amrf = result.llmConfig.additionalModelRequestFields as Record; + + expect(amrf.reasoning_config).toBeUndefined(); + expect(amrf.thinking).toEqual({ type: 'adaptive' }); + }); + }); }); diff --git a/packages/data-provider/specs/bedrock.spec.ts b/packages/data-provider/specs/bedrock.spec.ts index 55bd0a2e08..1398b17b25 100644 --- a/packages/data-provider/specs/bedrock.spec.ts +++ b/packages/data-provider/specs/bedrock.spec.ts @@ -688,5 +688,175 @@ describe('bedrockInputParser', () => { expect(amrf.anthropic_beta).toBeDefined(); expect(Array.isArray(amrf.anthropic_beta)).toBe(true); }); + + test('should strip stale reasoning_config when switching to Anthropic model', () => { + const staleConversationData = { + model: 'anthropic.claude-sonnet-4-6', + additionalModelRequestFields: { + reasoning_config: 'high', + }, + }; + const result = bedrockInputParser.parse(staleConversationData) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBeUndefined(); + }); + + test('should strip stale reasoning_config when switching from Moonshot to Meta model', () => { + const staleData = { + model: 'meta.llama-3-1-70b', + additionalModelRequestFields: { + reasoning_config: 'high', + }, + }; + const result = bedrockInputParser.parse(staleData) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + + test('should strip stale reasoning_config when switching from ZAI to DeepSeek model', () => { + const staleData = { + model: 'deepseek.deepseek-r1', + additionalModelRequestFields: { + reasoning_config: 'medium', + }, + }; + const result = bedrockInputParser.parse(staleData) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + }); + + describe('Bedrock reasoning_effort → reasoning_config for Moonshot/ZAI models', () => { + test('should map reasoning_effort to reasoning_config for moonshotai.kimi-k2.5', () => { + const input = { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'high', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBe('high'); + expect(amrf.reasoning_effort).toBeUndefined(); + }); + + test('should map reasoning_effort to reasoning_config for moonshot.kimi-k2.5', () => { + const input = { + model: 'moonshot.kimi-k2.5', + reasoning_effort: 'medium', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBe('medium'); + }); + + test('should map reasoning_effort to reasoning_config for zai.glm-4.7', () => { + const input = { + model: 'zai.glm-4.7', + reasoning_effort: 'high', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBe('high'); + }); + + test('should map reasoning_effort "low" to reasoning_config for Moonshot model', () => { + const input = { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'low', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBe('low'); + }); + + test('should not include reasoning_config when reasoning_effort is unset (empty string)', () => { + const input = { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: '', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + + test('should not include reasoning_config when reasoning_effort is not provided', () => { + const input = { + model: 'moonshotai.kimi-k2.5', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + + test('should not forward reasoning_effort "none" to reasoning_config', () => { + const result = bedrockInputParser.parse({ + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'none', + }) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + + test('should not forward reasoning_effort "minimal" to reasoning_config', () => { + const result = bedrockInputParser.parse({ + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'minimal', + }) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + + test('should not forward reasoning_effort "xhigh" to reasoning_config', () => { + const result = bedrockInputParser.parse({ + model: 'zai.glm-4.7', + reasoning_effort: 'xhigh', + }) as Record; + const amrf = result.additionalModelRequestFields as Record | undefined; + expect(amrf?.reasoning_config).toBeUndefined(); + }); + + test('should not add reasoning_config to Anthropic models', () => { + const input = { + model: 'anthropic.claude-sonnet-4-6', + reasoning_effort: 'high', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBeUndefined(); + expect(amrf.reasoning_effort).toBeUndefined(); + }); + + test('should not add thinking or anthropic_beta to Moonshot models with reasoning_effort', () => { + const input = { + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'high', + }; + const result = bedrockInputParser.parse(input) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.thinking).toBeUndefined(); + expect(amrf.thinkingBudget).toBeUndefined(); + expect(amrf.anthropic_beta).toBeUndefined(); + }); + + test('should pass reasoning_config through bedrockOutputParser', () => { + const parsed = bedrockInputParser.parse({ + model: 'moonshotai.kimi-k2.5', + reasoning_effort: 'high', + }) as Record; + const output = bedrockOutputParser(parsed); + const amrf = output.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBe('high'); + }); + + test('should strip stale reasoning_config from additionalModelRequestFields for Anthropic models', () => { + const staleData = { + model: 'anthropic.claude-opus-4-6-v1', + additionalModelRequestFields: { + reasoning_config: 'high', + }, + }; + const result = bedrockInputParser.parse(staleData) as Record; + const amrf = result.additionalModelRequestFields as Record; + expect(amrf.reasoning_config).toBeUndefined(); + }); }); }); diff --git a/packages/data-provider/src/bedrock.ts b/packages/data-provider/src/bedrock.ts index 54f5c8b23f..bdf5e19605 100644 --- a/packages/data-provider/src/bedrock.ts +++ b/packages/data-provider/src/bedrock.ts @@ -4,6 +4,8 @@ import * as s from './schemas'; const DEFAULT_ENABLED_MAX_TOKENS = 8192; const DEFAULT_THINKING_BUDGET = 2000; +const bedrockReasoningConfigValues = new Set(Object.values(s.BedrockReasoningConfig)); + type ThinkingConfig = { type: 'enabled'; budget_tokens: number } | { type: 'adaptive' }; type AnthropicReasoning = { @@ -134,6 +136,7 @@ export const bedrockInputSchema = s.tConversationSchema thinking: true, thinkingBudget: true, effort: true, + reasoning_effort: true, promptCache: true, /* Catch-all fields */ topK: true, @@ -178,6 +181,7 @@ export const bedrockInputParser = s.tConversationSchema thinking: true, thinkingBudget: true, effort: true, + reasoning_effort: true, promptCache: true, /* Catch-all fields */ topK: true, @@ -256,6 +260,9 @@ export const bedrockInputParser = s.tConversationSchema delete additionalFields.effort; } + /** Anthropic uses 'effort' via output_config, not reasoning_config */ + delete additionalFields.reasoning_effort; + if ((typedData.model as string).includes('anthropic.')) { const betaHeaders = getBedrockAnthropicBetaHeaders(typedData.model as string); if (betaHeaders.length > 0) { @@ -268,23 +275,37 @@ export const bedrockInputParser = s.tConversationSchema delete additionalFields.effort; delete additionalFields.output_config; delete additionalFields.anthropic_beta; + + const reasoningEffort = additionalFields.reasoning_effort; + delete additionalFields.reasoning_effort; + if ( + typeof reasoningEffort === 'string' && + bedrockReasoningConfigValues.has(reasoningEffort) + ) { + additionalFields.reasoning_config = reasoningEffort; + } } const isAnthropicModel = typeof typedData.model === 'string' && typedData.model.includes('anthropic.'); - /** Strip stale anthropic_beta from previously-persisted additionalModelRequestFields */ + /** Strip stale fields from previously-persisted additionalModelRequestFields */ if ( - !isAnthropicModel && typeof typedData.additionalModelRequestFields === 'object' && typedData.additionalModelRequestFields != null ) { const amrf = typedData.additionalModelRequestFields as Record; - delete amrf.anthropic_beta; - delete amrf.thinking; - delete amrf.thinkingBudget; - delete amrf.effort; - delete amrf.output_config; + if (!isAnthropicModel) { + delete amrf.anthropic_beta; + delete amrf.thinking; + delete amrf.thinkingBudget; + delete amrf.effort; + delete amrf.output_config; + delete amrf.reasoning_config; + } else { + delete amrf.reasoning_config; + delete amrf.reasoning_effort; + } } /** Default promptCache for claude and nova models, if not defined */ diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index 0796efe773..229f970c7d 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -530,6 +530,30 @@ const bedrock: Record = { showDefault: false, columnSpan: 2, }, + reasoning_effort: { + key: 'reasoning_effort', + label: 'com_endpoint_reasoning_effort', + labelCode: true, + description: 'com_endpoint_bedrock_reasoning_effort', + descriptionCode: true, + type: 'enum', + default: ReasoningEffort.unset, + component: 'slider', + options: [ + ReasoningEffort.unset, + ReasoningEffort.low, + ReasoningEffort.medium, + ReasoningEffort.high, + ], + enumMappings: { + [ReasoningEffort.unset]: 'com_ui_off', + [ReasoningEffort.low]: 'com_ui_low', + [ReasoningEffort.medium]: 'com_ui_medium', + [ReasoningEffort.high]: 'com_ui_high', + }, + optionType: 'model', + columnSpan: 4, + }, }; const mistral: Record = { @@ -905,6 +929,34 @@ const bedrockGeneralCol2: SettingsConfiguration = [ librechat.fileTokenLimit, ]; +const bedrockZAI: SettingsConfiguration = [ + librechat.modelLabel, + librechat.promptPrefix, + librechat.maxContextTokens, + meta.temperature, + meta.topP, + librechat.resendFiles, + bedrock.region, + bedrock.reasoning_effort, + librechat.fileTokenLimit, +]; + +const bedrockZAICol1: SettingsConfiguration = [ + baseDefinitions.model as SettingDefinition, + librechat.modelLabel, + librechat.promptPrefix, +]; + +const bedrockZAICol2: SettingsConfiguration = [ + librechat.maxContextTokens, + meta.temperature, + meta.topP, + librechat.resendFiles, + bedrock.region, + bedrock.reasoning_effort, + librechat.fileTokenLimit, +]; + const bedrockMoonshot: SettingsConfiguration = [ librechat.modelLabel, bedrock.system, @@ -917,6 +969,7 @@ const bedrockMoonshot: SettingsConfiguration = [ baseDefinitions.stop, librechat.resendFiles, bedrock.region, + bedrock.reasoning_effort, librechat.fileTokenLimit, ]; @@ -936,6 +989,7 @@ const bedrockMoonshotCol2: SettingsConfiguration = [ bedrock.topP, librechat.resendFiles, bedrock.region, + bedrock.reasoning_effort, librechat.fileTokenLimit, ]; @@ -954,7 +1008,7 @@ export const paramSettings: Record = [`${EModelEndpoint.bedrock}-${BedrockProviders.Moonshot}`]: bedrockMoonshot, [`${EModelEndpoint.bedrock}-${BedrockProviders.MoonshotAI}`]: bedrockMoonshot, [`${EModelEndpoint.bedrock}-${BedrockProviders.OpenAI}`]: bedrockGeneral, - [`${EModelEndpoint.bedrock}-${BedrockProviders.ZAI}`]: bedrockGeneral, + [`${EModelEndpoint.bedrock}-${BedrockProviders.ZAI}`]: bedrockZAI, [EModelEndpoint.google]: googleConfig, }; @@ -1008,7 +1062,10 @@ export const presetSettings: Record< col2: bedrockMoonshotCol2, }, [`${EModelEndpoint.bedrock}-${BedrockProviders.OpenAI}`]: bedrockGeneralColumns, - [`${EModelEndpoint.bedrock}-${BedrockProviders.ZAI}`]: bedrockGeneralColumns, + [`${EModelEndpoint.bedrock}-${BedrockProviders.ZAI}`]: { + col1: bedrockZAICol1, + col2: bedrockZAICol2, + }, [EModelEndpoint.google]: { col1: googleCol1, col2: googleCol2, diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 90d5362273..02096cb0cf 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -185,6 +185,12 @@ export enum AnthropicEffort { max = 'max', } +export enum BedrockReasoningConfig { + low = 'low', + medium = 'medium', + high = 'high', +} + export enum ReasoningSummary { none = '', auto = 'auto',