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
95 changes: 95 additions & 0 deletions src/providers/openai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,101 @@ describe("OpenAIClient message format", () => {
expect(callArgs[0].max_tokens).toBe(16384);
});

it("passes temperature to the API call for non-o1 models", async () => {
const tempClient = new OpenAIClient("fake-key", "gpt-4o", { temperature: 0.5 });
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
);
for await (const e of tempClient.stream([], "sys", [])) {
void e;
}
const [callArgs] = mockCreate.mock.calls;
expect(callArgs[0].temperature).toBe(0.5);
});

it("passes temperature to the API call for o3-mini (supports temperature)", async () => {
vi.mocked(OpenAI).mockImplementation(
() =>
({
chat: { completions: { create: mockCreate } },
}) as unknown as InstanceType<typeof OpenAI>,
);
const tempClient = new OpenAIClient("fake-key", "o3-mini", { temperature: 0.7 });
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
);
for await (const e of tempClient.stream([], "sys", [])) {
void e;
}
const [callArgs] = mockCreate.mock.calls;
expect(callArgs[0].temperature).toBe(0.7);
});

it("omits temperature for o1 (temperature is fixed at 1)", async () => {
vi.mocked(OpenAI).mockImplementation(
() =>
({
chat: { completions: { create: mockCreate } },
}) as unknown as InstanceType<typeof OpenAI>,
);
const o1Client = new OpenAIClient("fake-key", "o1", { temperature: 0.5 });
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
);
for await (const e of o1Client.stream([], "sys", [])) {
void e;
}
const [callArgs] = mockCreate.mock.calls;
expect(callArgs[0].temperature).toBeUndefined();
});

it("omits temperature for o1-mini (temperature is fixed at 1)", async () => {
vi.mocked(OpenAI).mockImplementation(
() =>
({
chat: { completions: { create: mockCreate } },
}) as unknown as InstanceType<typeof OpenAI>,
);
const o1MiniClient = new OpenAIClient("fake-key", "o1-mini", { temperature: 0.5 });
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
);
for await (const e of o1MiniClient.stream([], "sys", [])) {
void e;
}
const [callArgs] = mockCreate.mock.calls;
expect(callArgs[0].temperature).toBeUndefined();
});

it("omits temperature for o1-preview (temperature is fixed at 1)", async () => {
vi.mocked(OpenAI).mockImplementation(
() =>
({
chat: { completions: { create: mockCreate } },
}) as unknown as InstanceType<typeof OpenAI>,
);
const o1PreviewClient = new OpenAIClient("fake-key", "o1-preview", { temperature: 0.5 });
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
);
for await (const e of o1PreviewClient.stream([], "sys", [])) {
void e;
}
const [callArgs] = mockCreate.mock.calls;
expect(callArgs[0].temperature).toBeUndefined();
});

it("omits temperature when not configured regardless of model", async () => {
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
);
for await (const e of client.stream([], "sys", [])) {
void e;
}
const [callArgs] = mockCreate.mock.calls;
expect(callArgs[0].temperature).toBeUndefined();
});

it("injects system instruction as first message", async () => {
mockCreate.mockResolvedValue(
makeStream([{ choices: [{ delta: { content: "hi" }, finish_reason: "stop" }] }]),
Expand Down
10 changes: 9 additions & 1 deletion src/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ function isReasoningModel(model: string): boolean {
return /^o[134](-|$)/.test(model);
}

// o1/o1-mini/o1-preview do not accept a temperature parameter (it is fixed at 1).
// o3 and o4 variants do support temperature; only the original o1 family is excluded.
function lacksTemperatureSupport(model: string): boolean {
return /^o1(-|$)/.test(model);
}

export class OpenAIClient implements LLMClient {
private client: OpenAI;
private model: string;
Expand Down Expand Up @@ -74,7 +80,9 @@ export class OpenAIClient implements LLMClient {
tools: openaiTools.length > 0 ? openaiTools : undefined,
stream: true,
stream_options: this.includeUsage ? { include_usage: true } : undefined,
...(this.temperature !== undefined ? { temperature: this.temperature } : {}),
...(this.temperature !== undefined && !lacksTemperatureSupport(this.model)
? { temperature: this.temperature }
: {}),
});

const pendingCalls = new Map<number, { id: string; name: string; args: string }>();
Expand Down