fix(core): clarify stream idle timeout guidance (#9896)

* fix(core): clarify stream idle timeout guidance

* fix(core): order timeout guidance by precedence

* fix(core): document OAuth timeout fallback

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
This commit is contained in:
Stellar鱼 2026-08-27 12:46:53 +00:00 committed by GitHub
parent 6652fdc9f6
commit ff1b691bd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 39 additions and 6 deletions

View file

@ -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');
});

View file

@ -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';
}

View file

@ -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);