From 735739d7eb3520a94c5004b8145e666c034ca9be Mon Sep 17 00:00:00 2001 From: cdxd Date: Tue, 3 Mar 2026 09:42:09 +0800 Subject: [PATCH 1/2] fix(core): skip stream_options for providers that reject it (Cerebras 422) Some OpenAI-compatible providers (e.g. Cerebras) strictly validate request parameters and return HTTP 422 for unknown fields like stream_options. Add shouldIncludeStreamOptions() to conditionally exclude stream_options based on the provider's hostname. Fixes iOfficeAI/AionUi#1038 --- .../core/src/core/openaiContentGenerator.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/core/src/core/openaiContentGenerator.ts b/packages/core/src/core/openaiContentGenerator.ts index 202527134eb..07f3a1456c6 100644 --- a/packages/core/src/core/openaiContentGenerator.ts +++ b/packages/core/src/core/openaiContentGenerator.ts @@ -164,6 +164,27 @@ export class OpenAIContentGenerator implements ContentGenerator { return false; // Default behavior: never suppress error logging } + /** + * Check if stream_options should be included in streaming requests. + * Some providers (e.g. Cerebras) strictly validate request parameters + * and return 422 for unknown fields like stream_options. + * Default: include stream_options (most OpenAI-compatible providers support it). + */ + private shouldIncludeStreamOptions(): boolean { + const baseURL = this.client?.baseURL || ''; + let hostname: string | undefined; + try { + hostname = new URL(baseURL).hostname; + } catch (_e) { + return true; // Default to including stream_options + } + // Providers known to reject stream_options with 422 + const strictProviders = ['api.cerebras.ai']; + return !strictProviders.some( + (h) => hostname === h || hostname!.endsWith('.' + h), + ); + } + /** * Check if metadata should be included in the request * Only include metadata for specific providers that support it @@ -431,7 +452,9 @@ export class OpenAIContentGenerator implements ContentGenerator { messages, ...samplingParams, stream: true, - stream_options: { include_usage: true }, + ...(this.shouldIncludeStreamOptions() && { + stream_options: { include_usage: true }, + }), ...(metadata && { metadata }), }; From 45f92016a7c214a7aaebc3b0afdbe380056acdc4 Mon Sep 17 00:00:00 2001 From: cdxd Date: Thu, 5 Mar 2026 18:32:42 +0800 Subject: [PATCH 2/2] fix(core): skip unsupported store field for strict providers like Cerebras --- .../src/core/openaiContentGenerator.test.ts | 149 +++++++++++++++++- .../core/src/core/openaiContentGenerator.ts | 28 +++- 2 files changed, 174 insertions(+), 3 deletions(-) diff --git a/packages/core/src/core/openaiContentGenerator.test.ts b/packages/core/src/core/openaiContentGenerator.test.ts index 9cee8202422..ca5fe2c3da7 100644 --- a/packages/core/src/core/openaiContentGenerator.test.ts +++ b/packages/core/src/core/openaiContentGenerator.test.ts @@ -388,9 +388,73 @@ describe('OpenAIContentGenerator', () => { temperature: 0.7, // From config sampling params (higher priority) max_tokens: 1000, // From config sampling params (higher priority) top_p: 0.9, - }), + }), ); }); + + it('should omit store for strict providers like Cerebras', async () => { + vi.stubEnv('OPENAI_BASE_URL', 'https://api.cerebras.ai/v1'); + generator = new OpenAIContentGenerator('test-key', 'gpt-4', mockConfig); + mockOpenAIClient.baseURL = 'https://api.cerebras.ai/v1'; + + const mockResponse = { + id: 'chatcmpl-123', + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'Response' }, + finish_reason: 'stop', + }, + ], + created: 1677652288, + model: 'gpt-4', + }; + mockOpenAIClient.chat.completions.create.mockResolvedValue(mockResponse); + + const request: GenerateContentParameters = { + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + model: 'gpt-4', + }; + + await generator.generateContent(request, 'test-prompt-id'); + + const createCall = + mockOpenAIClient.chat.completions.create.mock.calls[0]?.[0]; + expect(createCall).toBeDefined(); + expect(createCall).not.toHaveProperty('store'); + }); + + it('should include store for regular OpenAI providers on GPT models', async () => { + vi.stubEnv('OPENAI_BASE_URL', 'https://api.openai.com/v1'); + generator = new OpenAIContentGenerator('test-key', 'gpt-4', mockConfig); + mockOpenAIClient.baseURL = 'https://api.openai.com/v1'; + + const mockResponse = { + id: 'chatcmpl-123', + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'Response' }, + finish_reason: 'stop', + }, + ], + created: 1677652288, + model: 'gpt-4', + }; + mockOpenAIClient.chat.completions.create.mockResolvedValue(mockResponse); + + const request: GenerateContentParameters = { + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + model: 'gpt-4', + }; + + await generator.generateContent(request, 'test-prompt-id'); + + const createCall = + mockOpenAIClient.chat.completions.create.mock.calls[0]?.[0]; + expect(createCall).toBeDefined(); + expect(createCall?.store).toBe(true); + }); }); describe('generateContentStream', () => { @@ -570,6 +634,89 @@ describe('OpenAIContentGenerator', () => { } } }); + + it('should omit stream_options and store for strict providers like Cerebras', async () => { + vi.stubEnv('OPENAI_BASE_URL', 'https://api.cerebras.ai/v1'); + generator = new OpenAIContentGenerator('test-key', 'gpt-4', mockConfig); + mockOpenAIClient.baseURL = 'https://api.cerebras.ai/v1'; + + mockOpenAIClient.chat.completions.create.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + id: 'chatcmpl-123', + choices: [ + { + index: 0, + delta: { content: 'ok' }, + finish_reason: 'stop', + }, + ], + created: 1677652288, + }; + }, + }); + + const request: GenerateContentParameters = { + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + model: 'gpt-4', + }; + + const stream = await generator.generateContentStream( + request, + 'test-prompt-id', + ); + for await (const _response of stream) { + // Exhaust stream + } + + const createCall = + mockOpenAIClient.chat.completions.create.mock.calls[0]?.[0]; + expect(createCall).toBeDefined(); + expect(createCall).not.toHaveProperty('stream_options'); + expect(createCall).not.toHaveProperty('store'); + }); + + it('should include stream_options and store for regular OpenAI providers', async () => { + vi.stubEnv('OPENAI_BASE_URL', 'https://api.openai.com/v1'); + generator = new OpenAIContentGenerator('test-key', 'gpt-4', mockConfig); + mockOpenAIClient.baseURL = 'https://api.openai.com/v1'; + + mockOpenAIClient.chat.completions.create.mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + id: 'chatcmpl-123', + choices: [ + { + index: 0, + delta: { content: 'ok' }, + finish_reason: 'stop', + }, + ], + created: 1677652288, + }; + }, + }); + + const request: GenerateContentParameters = { + contents: [{ role: 'user', parts: [{ text: 'Hello' }] }], + model: 'gpt-4', + }; + + const stream = await generator.generateContentStream( + request, + 'test-prompt-id', + ); + for await (const _response of stream) { + // Exhaust stream + } + + const createCall = + mockOpenAIClient.chat.completions.create.mock.calls[0]?.[0]; + expect(createCall).toBeDefined(); + expect(createCall).toHaveProperty('stream_options'); + expect(createCall?.stream_options).toEqual({ include_usage: true }); + expect(createCall?.store).toBe(true); + }); }); describe('countTokens', () => { diff --git a/packages/core/src/core/openaiContentGenerator.ts b/packages/core/src/core/openaiContentGenerator.ts index 07f3a1456c6..7cf8583d646 100644 --- a/packages/core/src/core/openaiContentGenerator.ts +++ b/packages/core/src/core/openaiContentGenerator.ts @@ -185,6 +185,26 @@ export class OpenAIContentGenerator implements ContentGenerator { ); } + /** + * Check if store should be included in requests. + * Some providers (e.g. Cerebras) reject unsupported fields like store with 422. + * Default: include store for GPT models unless provider is known strict. + */ + private shouldIncludeStore(): boolean { + const baseURL = this.client?.baseURL || ''; + let hostname: string | undefined; + try { + hostname = new URL(baseURL).hostname; + } catch (_e) { + return true; // Default to including store + } + // Providers known to reject store with 422 + const strictProviders = ['api.cerebras.ai']; + return !strictProviders.some( + (h) => hostname === h || hostname!.endsWith('.' + h), + ); + } + /** * Check if metadata should be included in the request * Only include metadata for specific providers that support it @@ -285,7 +305,9 @@ export class OpenAIContentGenerator implements ContentGenerator { modelName.includes('gpt5') || modelName.includes('gpt4') ) { - createParams.store = true; + if (this.shouldIncludeStore()) { + createParams.store = true; + } } // Handle JSON schema requests (for generateJson calls) @@ -465,7 +487,9 @@ export class OpenAIContentGenerator implements ContentGenerator { modelNameStream.includes('gpt5') || modelNameStream.includes('gpt4') ) { - createParams.store = true; + if (this.shouldIncludeStore()) { + createParams.store = true; + } } // Handle JSON schema requests (for generateJson calls) - same as non-streaming