Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 148 additions & 1 deletion packages/core/src/core/openaiContentGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
53 changes: 50 additions & 3 deletions packages/core/src/core/openaiContentGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,47 @@ 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 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
Expand Down Expand Up @@ -264,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)
Expand Down Expand Up @@ -431,7 +474,9 @@ export class OpenAIContentGenerator implements ContentGenerator {
messages,
...samplingParams,
stream: true,
stream_options: { include_usage: true },
...(this.shouldIncludeStreamOptions() && {
stream_options: { include_usage: true },
}),
...(metadata && { metadata }),
};

Expand All @@ -442,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
Expand Down