diff --git a/src/config.test.ts b/src/config.test.ts index 32d488de0..4b86d86f3 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -1467,6 +1467,25 @@ describe("buildOpenAISource", () => { }); }); + test("flows temperature and top_p together when both are present", () => { + // Settings load rejects both knobs. This builder still forwards a pair + // it is given. DeepSeek V4 model-card defaults reach the wire because + // the adapter writes top_p 0.95 and temperature 1.0 when each body + // field is absent, not because this merge supplies those defaults. + const source = buildOpenAISource({ + id: "fp", + baseURL: "https://fp/v1", + apiKey: "k", + model: "m", + temperature: 0.7, + topP: 0.9, + }); + expect(source.defaults).toEqual({ + maxTokens: SOURCE_MAX_TOKENS, + providerOptions: { temperature: 0.7, top_p: 0.9 }, + }); + }); + test("coexists temperature with reasoning_effort in one providerOptions", () => { const source = buildOpenAISource({ id: "fp", diff --git a/src/config/settings.test.ts b/src/config/settings.test.ts index 03526dbf5..6ae6eb026 100644 --- a/src/config/settings.test.ts +++ b/src/config/settings.test.ts @@ -72,7 +72,7 @@ test("invalid token settings cannot be loaded or overwrite a saved provider", as } }); -test("provider settings reject simultaneous sampling fields but preserve zero", () => { +test("provider settings reject simultaneous sampling fields but preserve zero", async () => { const provider = { baseURL: "https://custom.example/v1", keyless: true, @@ -85,6 +85,19 @@ test("provider settings reject simultaneous sampling fields but preserve zero", expect(isSettings({ providers: { custom: { ...provider, topP: 0 } } })).toBe( false, ); + // The exclusion is enforced at load, not just by isSettings — a persisted + // file with both knobs fails to load. + const cwd = await mkdtemp(join(tmpdir(), "both-sampling-knobs-")); + try { + const path = join(cwd, "settings.json"); + await writeFile( + path, + JSON.stringify({ providers: { custom: { ...provider, topP: 0 } } }), + ); + await expect(loadSettings(path)).rejects.toThrow(); + } finally { + await rm(cwd, { recursive: true, force: true }); + } }); afterEach(() => { diff --git a/src/config/settings.ts b/src/config/settings.ts index f099cdc29..f58363b83 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -43,7 +43,10 @@ export interface ProviderSettings { // Per-call output cap, overriding SOURCE_MAX_TOKENS when set. maxTokens?: number; // Sampling temperature (0..2, OpenAI-compatible); mutually exclusive with - // topP — never send both on the wire. + // topP in provider settings — load rejects both (see + // ProviderInferenceOptionsSchema). The wire may still carry both when a + // provider adapter mandates it (DeepSeek V4 backfills absent fields from + // its model-card defaults). temperature?: number; // Sampling top-p (0..1, OpenAI-compatible); mutually exclusive with // temperature.