diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts index eaec7a7bdb..00a3d8c6bf 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts @@ -6154,8 +6154,14 @@ describe('ContentGenerationPipeline', () => { expect(err).toBeInstanceOf(StreamInactivityTimeoutError); expect((err as Error).message).toBe( 'No stream activity for 1000ms after 0 chunks ' + - '(stream lifetime: 1000ms). Set QWEN_STREAM_IDLE_TIMEOUT_MS ' + - 'to increase this window (or 0 to disable it).', + '(stream lifetime: 1000ms). For provider-backed models, ' + + 'increase modelProviders[providerId][].generationConfig.streamIdleTimeoutMs; ' + + 'provider configuration takes precedence, so model.generationConfig is ' + + 'ignored for those models. For runtime models, increase ' + + 'model.generationConfig.streamIdleTimeoutMs. Built-in Qwen OAuth models ' + + 'cannot be overridden via settings. Use QWEN_STREAM_IDLE_TIMEOUT_MS ' + + 'for them or whenever no explicit value is active. ' + + 'Set the active value to 0 to disable it.', ); expect(err).toMatchObject({ code: 'ETIMEDOUT' }); expect((err as StreamInactivityTimeoutError).chunksReceived).toBe(0); @@ -6164,7 +6170,7 @@ describe('ContentGenerationPipeline', () => { expect(mockErrorHandler.handle).not.toHaveBeenCalled(); }); - it('includes the idle detail and env override hint in timeout errors', async () => { + it('includes settings and environment override hints in timeout errors', async () => { const gated = gatedStream(); // never push/end → silent (mockClient.chat.completions.create as Mock).mockResolvedValue( gated.stream, @@ -6184,6 +6190,10 @@ describe('ContentGenerationPipeline', () => { expect(err).toBeInstanceOf(StreamInactivityTimeoutError); const message = (err as Error).message; expect(message).toContain('No stream activity for 1000ms after 0 chunks'); + expect(message).toContain('model.generationConfig.streamIdleTimeoutMs'); + expect(message).toContain( + 'modelProviders[providerId][].generationConfig.streamIdleTimeoutMs', + ); expect(message).toContain('QWEN_STREAM_IDLE_TIMEOUT_MS'); }); diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index f3f8d89146..599d8234f6 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -191,9 +191,14 @@ export class StreamInactivityTimeoutError extends Error { ) { super( `No stream activity for ${idleMs}ms after ${chunksReceived} chunks ` + - `(stream lifetime: ${streamLifetimeMs}ms). Set ` + - `${QWEN_STREAM_IDLE_TIMEOUT_MS_ENV} to increase this window ` + - `(or 0 to disable it).`, + `(stream lifetime: ${streamLifetimeMs}ms). For provider-backed models, ` + + `increase modelProviders[providerId][].generationConfig.streamIdleTimeoutMs; ` + + `provider configuration takes precedence, so model.generationConfig is ` + + `ignored for those models. For runtime models, increase ` + + `model.generationConfig.streamIdleTimeoutMs. Built-in Qwen OAuth models ` + + `cannot be overridden via settings. Use ${QWEN_STREAM_IDLE_TIMEOUT_MS_ENV} ` + + `for them or whenever no explicit value is active. ` + + `Set the active value to 0 to disable it.`, ); this.name = 'StreamInactivityTimeoutError'; } diff --git a/packages/core/src/models/content-generator-config.test.ts b/packages/core/src/models/content-generator-config.test.ts index da4d2f3f17..5f3a81f5df 100644 --- a/packages/core/src/models/content-generator-config.test.ts +++ b/packages/core/src/models/content-generator-config.test.ts @@ -193,6 +193,24 @@ describe('buildAgentContentGeneratorConfig', () => { expect(result.extra_body).toBeUndefined(); }); + it('should preserve a zero stream idle timeout from the registry', () => { + const config = createMockConfig(parentConfig, { + ...resolvedModel, + generationConfig: { + ...resolvedModel.generationConfig, + streamIdleTimeoutMs: 0, + }, + }); + + const result = buildAgentContentGeneratorConfig( + config, + 'registry-model-id', + { authType: 'anthropic' }, + ); + + expect(result.streamIdleTimeoutMs).toBe(0); + }); + it('should prefer explicit auth overrides over registry values', () => { const config = createMockConfig(parentConfig, resolvedModel);