From e77240eccb16665d22bcd0ee4cfa35927d540db5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 03:14:42 +0000 Subject: [PATCH] fix(openai): omit temperature for o1/o1-mini/o1-preview reasoning models The o1 family of OpenAI reasoning models does not accept a temperature parameter (it is fixed at 1). Passing temperature to these models causes a 400 API error, which surfaces when a user runs: opencli run "..." --model o1-mini --temperature 0.5 o3 and o4 variants do support temperature and are unaffected. Adds a lacksTemperatureSupport() helper and 6 new tests covering: - temperature is passed for gpt-4o and o3-mini - temperature is omitted for o1, o1-mini, and o1-preview - temperature is omitted when not configured at all Part of #43 Co-Authored-By: Claude Sonnet 4.6 --- src/providers/openai.test.ts | 95 ++++++++++++++++++++++++++++++++++++ src/providers/openai.ts | 10 +++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/providers/openai.test.ts b/src/providers/openai.test.ts index 61c5dd6..9192ebd 100644 --- a/src/providers/openai.test.ts +++ b/src/providers/openai.test.ts @@ -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, + ); + 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, + ); + 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, + ); + 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, + ); + 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" }] }]), diff --git a/src/providers/openai.ts b/src/providers/openai.ts index 4a76659..33d5c9c 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -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; @@ -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();