From 8ac560d2c90638a6a5523f02fe23c5dddf328614 Mon Sep 17 00:00:00 2001 From: krishdef7 <157892833+krishdef7@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:41:38 +0530 Subject: [PATCH] fix(core): handle partial llm_request in BeforeModel hook override (#22326) --- .../core/src/hooks/hookTranslator.test.ts | 50 +++++++++++++++++++ packages/core/src/hooks/hookTranslator.ts | 35 +++++++------ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/core/src/hooks/hookTranslator.test.ts b/packages/core/src/hooks/hookTranslator.test.ts index 785cadfc4e..8755049aa9 100644 --- a/packages/core/src/hooks/hookTranslator.test.ts +++ b/packages/core/src/hooks/hookTranslator.test.ts @@ -121,6 +121,56 @@ describe('HookTranslator', () => { }, ]); }); + + it('should apply model override when hook returns only model field', () => { + const baseRequest: GenerateContentParameters = { + model: 'gemini-2.5-flash-lite', + contents: [ + { + role: 'user', + parts: [{ text: 'Hello' }], + }, + ], + } as unknown as GenerateContentParameters; + + // Simulate a hook that only overrides the model — no messages field + const hookRequest = { + model: 'gemini-2.5-flash', + } as unknown as LLMRequest; + + const sdkRequest = translator.fromHookLLMRequest( + hookRequest, + baseRequest, + ); + + // Model should be overridden + expect(sdkRequest.model).toBe('gemini-2.5-flash'); + // Original conversation contents should be preserved + expect(sdkRequest.contents).toEqual(baseRequest.contents); + }); + + it('should preserve base request contents when hook messages is undefined', () => { + const baseRequest: GenerateContentParameters = { + model: 'gemini-1.5-flash', + contents: [ + { role: 'user', parts: [{ text: 'original message' }] }, + { role: 'model', parts: [{ text: 'original reply' }] }, + ], + } as unknown as GenerateContentParameters; + + const hookRequest = { + model: 'gemini-1.5-pro', + // messages intentionally omitted + } as unknown as LLMRequest; + + const sdkRequest = translator.fromHookLLMRequest( + hookRequest, + baseRequest, + ); + + expect(sdkRequest.model).toBe('gemini-1.5-pro'); + expect(sdkRequest.contents).toEqual(baseRequest.contents); + }); }); describe('LLM Response Translation', () => { diff --git a/packages/core/src/hooks/hookTranslator.ts b/packages/core/src/hooks/hookTranslator.ts index 82cd1a5850..a733168089 100644 --- a/packages/core/src/hooks/hookTranslator.ts +++ b/packages/core/src/hooks/hookTranslator.ts @@ -225,23 +225,30 @@ export class HookTranslatorGenAIv1 extends HookTranslator { hookRequest: LLMRequest, baseRequest?: GenerateContentParameters, ): GenerateContentParameters { - // Convert hook messages back to SDK Content format - const contents = hookRequest.messages.map((message) => ({ - role: message.role === 'model' ? 'model' : message.role, - parts: [ - { - text: - typeof message.content === 'string' - ? message.content - : String(message.content), - }, - ], - })); + // Convert hook messages back to SDK Content format. + // If the hook returned a partial request without messages (e.g. only + // overriding `model`), fall back to the base request's contents so the + // conversation is preserved. + const contents = hookRequest.messages + ? hookRequest.messages.map((message) => ({ + role: message.role === 'model' ? 'model' : message.role, + parts: [ + { + text: + typeof message.content === 'string' + ? message.content + : String(message.content), + }, + ], + })) + : (baseRequest?.contents ?? []); - // Build the result with proper typing + // Build the result with proper typing. + // Use nullish coalescing so a hook that only sets `model` still works -- + // fall back to the base request's model rather than overwriting with undefined. const result: GenerateContentParameters = { ...baseRequest, - model: hookRequest.model, + model: hookRequest.model ?? baseRequest?.model ?? '', contents, };