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