Skip to content

Commit 19bc9e1

Browse files
feat(provider): add v4 request settings defaults (#1365)
1 parent b69db53 commit 19bc9e1

4 files changed

Lines changed: 245 additions & 57 deletions

File tree

‎src/provider/deepseek-v4-effort.test.ts‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { describe, test, expect } from "bun:test";
22
import {
33
DEEPSEEK_V4_EFFORTS,
4+
DEEPSEEK_V4_MODEL_CARD,
45
isDeepSeekV4Model,
56
mapV4Effort,
7+
V4_STREAM_OPTIONS,
8+
v4Thinking,
69
} from "./deepseek-v4-effort.js";
710

811
describe("isDeepSeekV4Model", () => {
@@ -44,10 +47,33 @@ describe("mapV4Effort", () => {
4447
test("maps none to null (drop the wire effort)", () => {
4548
expect(mapV4Effort("none")).toBeNull();
4649
});
47-
test.each(["low", "medium", "high"] as const)(
48-
"maps off-ladder %s to undefined",
50+
test.each(["low", "medium", "high", "ultra", "minimal"] as const)(
51+
"leaves off-ladder %s untouched (undefined)",
4952
(effort) => {
5053
expect(mapV4Effort(effort)).toBeUndefined();
5154
},
5255
);
5356
});
57+
58+
describe("v4Thinking", () => {
59+
test("thinking off for none", () => {
60+
expect(v4Thinking("none")).toEqual({ thinking: false });
61+
});
62+
test.each(["xhigh", "max"] as const)("thinking on for %s", (effort) => {
63+
expect(v4Thinking(effort)).toEqual({ thinking: true });
64+
});
65+
});
66+
67+
describe("DEEPSEEK_V4_MODEL_CARD", () => {
68+
test("defaults both temperature 1.0 and top_p 0.95", () => {
69+
expect(DEEPSEEK_V4_MODEL_CARD).toEqual({ temperature: 1.0, topP: 0.95 });
70+
});
71+
});
72+
73+
describe("V4_STREAM_OPTIONS", () => {
74+
test("requests include_usage on stream chunks", () => {
75+
expect(V4_STREAM_OPTIONS).toEqual({
76+
stream_options: { include_usage: true },
77+
});
78+
});
79+
});

‎src/provider/deepseek-v4-effort.ts‎

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,56 @@ import type { ReasoningEffort } from "../agent/profile-types.js";
33
// DeepSeek V4 effort translator shared by the adapter and the effort picker.
44
// V4 ships under several ids across two catalogs — deepseek-v4-pro/-flash/
55
// -flash-vision-exp — so the family is matched by prefix rather than an exact
6-
// id list, mirroring the grok/kimi/muse-spark prefix predicates elsewhere.
6+
// id list; the `^` anchor keeps hypothetical spaced/pre-V4 ids out. This shape
7+
// mirrors the grok/kimi/muse-spark prefix predicates elsewhere in the tree.
78
export function isDeepSeekV4Model(model: string): boolean {
89
return /^deepseek-v4/i.test(model.trim());
910
}
1011

11-
// The native V4 rungs. `none` is a toggle, never a wire effort; `xhigh`/`max`
12-
// are the wire rungs sent raw (the API docs' low/high/max is the gateway
13-
// normalization layer, not what the model/encoder honors).
12+
// The operator-confirmed native V4 rungs. `none` is a toggle, never a wire
13+
// effort; `xhigh`/`max` are the wire rungs sent raw (the API docs' low/high/max
14+
// is the gateway normalization layer, not what the model/encoder honors).
1415
export const DEEPSEEK_V4_EFFORTS: readonly ReasoningEffort[] = [
1516
"none",
1617
"xhigh",
1718
"max",
1819
];
1920

20-
// Translate a V4 effort to its raw wire value: `none` maps to null (the caller
21-
// drops `reasoning_effort`, since `reasoning_effort:"none"` is illegal on the
22-
// wire), `xhigh`/`max` pass through verbatim, and any off-ladder value returns
23-
// undefined so the caller leaves effort untouched.
21+
/**
22+
* Translate a V4 effort to its raw wire value: `xhigh`/`max` pass through
23+
* verbatim, `none` maps to null (the caller drops `reasoning_effort`, since
24+
* `reasoning_effort:"none"` is illegal on the wire). Off-ladder values return
25+
* undefined — the caller leaves `reasoning_effort` untouched rather than
26+
* coercing it onto a V4 rung.
27+
*/
2428
export function mapV4Effort(
2529
effort: ReasoningEffort,
2630
): "xhigh" | "max" | null | undefined {
2731
if (effort === "none") return null;
28-
if (effort === "xhigh" || effort === "max") return effort;
29-
return undefined;
32+
return effort === "xhigh" || effort === "max" ? effort : undefined;
3033
}
34+
35+
/**
36+
* chat_template_kwargs toggle for a V4 effort: thinking off for `none`,
37+
* thinking on for any wire effort. When the caller drops `reasoning_effort`
38+
* for `none` it supplies `{thinking:false}` so the encoder still knows to
39+
* bypass the reasoning pass.
40+
*/
41+
export function v4Thinking(effort: ReasoningEffort): { thinking: boolean } {
42+
return { thinking: effort !== "none" };
43+
}
44+
45+
// Operator-confirmed V4 agentic model-card defaults: BOTH temperature 1.0 and
46+
// top_p 0.95 (deliberate bypass of their temperature/topP mutual-exclusion).
47+
// Absent-field-only — provider-set values always win.
48+
export const DEEPSEEK_V4_MODEL_CARD = {
49+
temperature: 1.0,
50+
topP: 0.95,
51+
} as const;
52+
53+
// Ask every V4 stream for per-chunk usage so vendor usage accounting can read
54+
// it (`vendor/intx-inference/.../openai.ts` only emits inference.usage when the
55+
// chunk carries it). Hoisted so the adapter reuses one object per build.
56+
export const V4_STREAM_OPTIONS = {
57+
stream_options: { include_usage: true },
58+
} as const;

‎src/provider/openai-compatible-adapter.test.ts‎

Lines changed: 139 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { defined } from "../../testkit/defined.js";
22
import { describe, test, expect } from "bun:test";
33
import { ProtocolMismatchError } from "@intx/inference";
4+
import { createOpenAIAdapter } from "@intx/inference/providers";
45
import type { ConversationTurn, InferenceOptions } from "@intx/types/runtime";
56
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";
67

@@ -178,33 +179,123 @@ describe("openai-compatible adapter V4 effort wiring", () => {
178179
return JSON.parse(built.body) as Record<string, unknown>;
179180
}
180181

181-
test("none drops reasoning_effort and sets thinking off", () => {
182-
const body = bodyForModel("deepseek-v4-pro", {
183-
providerOptions: { reasoning_effort: "none" },
184-
} as InferenceOptions);
185-
expect("reasoning_effort" in body).toBe(false);
186-
expect(body["chat_template_kwargs"]).toEqual({ thinking: false });
187-
});
188-
189182
test.each(["xhigh", "max"] as const)(
190-
"maps ladder %s through verbatim",
183+
"wires V4 effort %s through raw and turns thinking on",
191184
(effort) => {
192185
const body = bodyForModel("deepseek-v4-pro", {
193186
providerOptions: { reasoning_effort: effort },
194187
} as InferenceOptions);
195188
expect(body["reasoning_effort"]).toBe(effort);
189+
expect(body["chat_template_kwargs"]).toEqual({ thinking: true });
196190
},
197191
);
198192

199-
test.each(["low", "medium", "high"] as const)(
200-
"leaves off-ladder %s untouched",
201-
(effort) => {
193+
describe("V4 request settings (PR4)", () => {
194+
test("none → chat_template_kwargs.thinking false and NO reasoning_effort", () => {
202195
const body = bodyForModel("deepseek-v4-pro", {
203-
providerOptions: { reasoning_effort: effort },
196+
providerOptions: { reasoning_effort: "none" },
204197
} as InferenceOptions);
205-
expect(body["reasoning_effort"]).toBe(effort);
206-
},
207-
);
198+
expect(body["chat_template_kwargs"]).toEqual({ thinking: false });
199+
expect("reasoning_effort" in body).toBe(false);
200+
});
201+
202+
test("stream_options include_usage is true", () => {
203+
const body = bodyForModel("deepseek-v4-pro", {} as InferenceOptions);
204+
expect(body["stream_options"]).toEqual({ include_usage: true });
205+
// An unset effort must not force the encoder's thinking pass on nor
206+
// inject an empty chat_template_kwargs object.
207+
expect(body["chat_template_kwargs"]).toBeUndefined();
208+
});
209+
210+
test("top_p 0.95 and temperature 1.0 defaulted when absent", () => {
211+
const body = bodyForModel("deepseek-v4-pro", {} as InferenceOptions);
212+
expect(body["top_p"]).toBe(0.95);
213+
expect(body["temperature"]).toBe(1.0);
214+
// An unset effort must not force the encoder's thinking pass on nor
215+
// inject an empty chat_template_kwargs object.
216+
expect(body["chat_template_kwargs"]).toBeUndefined();
217+
});
218+
219+
test("top_p and temperature preserved when providerOptions set them", () => {
220+
const body = bodyForModel("deepseek-v4-pro", {
221+
providerOptions: { temperature: 0.7, top_p: 0.9 },
222+
} as InferenceOptions);
223+
expect(body["top_p"]).toBe(0.9);
224+
expect(body["temperature"]).toBe(0.7);
225+
});
226+
227+
test("chat_template_kwargs merge preserves existing keys", () => {
228+
const body = bodyForModel("deepseek-v4-pro", {
229+
providerOptions: {
230+
reasoning_effort: "xhigh",
231+
chat_template_kwargs: { some: "existing" },
232+
},
233+
} as InferenceOptions);
234+
expect(body["chat_template_kwargs"]).toEqual({
235+
thinking: true,
236+
some: "existing",
237+
});
238+
});
239+
240+
test("off-ladder effort is left untouched (no high→xhigh coercion)", () => {
241+
const body = bodyForModel("deepseek-v4-pro", {
242+
providerOptions: { reasoning_effort: "high" },
243+
} as InferenceOptions);
244+
expect(body["reasoning_effort"]).toBe("high");
245+
expect("chat_template_kwargs" in body).toBe(false);
246+
});
247+
248+
test("none merges thinking off into, never replaces, existing kwargs", () => {
249+
const body = bodyForModel("deepseek-v4-pro", {
250+
providerOptions: {
251+
reasoning_effort: "none",
252+
chat_template_kwargs: { some: "existing" },
253+
},
254+
} as InferenceOptions);
255+
expect(body["chat_template_kwargs"]).toEqual({
256+
thinking: false,
257+
some: "existing",
258+
});
259+
});
260+
261+
test("provider-set thinking is preserved (absent-only thinking)", () => {
262+
// A provider-supplied thinking value beats the V4 effort's implied one:
263+
// `none` normally forces thinking off, but an explicit thinking:true
264+
// must be kept (never force-stamped), and vice versa.
265+
const noneThenTrue = bodyForModel("deepseek-v4-pro", {
266+
providerOptions: {
267+
reasoning_effort: "none",
268+
chat_template_kwargs: { thinking: true },
269+
},
270+
} as InferenceOptions);
271+
expect(noneThenTrue["chat_template_kwargs"]).toEqual({ thinking: true });
272+
273+
const xhighThenFalse = bodyForModel("deepseek-v4-pro", {
274+
providerOptions: {
275+
reasoning_effort: "xhigh",
276+
chat_template_kwargs: { thinking: false },
277+
},
278+
} as InferenceOptions);
279+
expect(xhighThenFalse["chat_template_kwargs"]).toEqual({
280+
thinking: false,
281+
});
282+
});
283+
284+
test("unset effort does not inject empty chat_template_kwargs", () => {
285+
const body = bodyForModel("deepseek-v4-pro", {} as InferenceOptions);
286+
expect("chat_template_kwargs" in body).toBe(false);
287+
expect(body["reasoning_effort"]).toBeUndefined();
288+
});
289+
290+
test("provider-set stream_options is preserved (absent-only)", () => {
291+
const body = bodyForModel("deepseek-v4-pro", {
292+
providerOptions: {
293+
stream_options: { include_usage: false },
294+
},
295+
} as InferenceOptions);
296+
expect(body["stream_options"]).toEqual({ include_usage: false });
297+
});
298+
});
208299

209300
describe("v4-effort is no-op for non-V4", () => {
210301
const models = [
@@ -219,28 +310,50 @@ describe("openai-compatible adapter V4 effort wiring", () => {
219310
"deepseek-v3",
220311
];
221312

222-
test.each(models)(
223-
"merged body for %s is byte-identical to a manual base+merge (V4 step no-op)",
224-
(model) => {
313+
test.each(
314+
models.flatMap((model) =>
315+
(["high", "none", "max"] as const).map((effort) => [model, effort]),
316+
),
317+
)(
318+
"merged body for %s with effort %s is byte-identical to a manual base+merge (V4 step no-op)",
319+
(model, effort) => {
225320
const base = bodyForModel(model, {} as InferenceOptions);
226321
const withEffort = bodyForModel(model, {
227-
providerOptions: { reasoning_effort: "high" },
322+
providerOptions: { reasoning_effort: effort },
228323
} as InferenceOptions);
324+
// For a non-V4 model the V4 step must not run: the output body equals
325+
// the base body shallow-merged with providerOptions verbatim (no
326+
// effort mapping, no chat_template_kwargs injection) — including V4
327+
// ladder values like none (delete + thinking:false) and max (verbatim),
328+
// so a regression routing any V4 rung through the V4 branch is caught.
229329
const expected = {
230330
...base,
231-
...({ reasoning_effort: "high" } as const),
331+
...({ reasoning_effort: effort } as const),
232332
};
233333
expect(withEffort).toEqual(expected);
234334
},
235335
);
236336

237337
test.each(models)(
238-
"merged body for %s carries reasoning_effort verbatim",
338+
"V4 settings (top_p/temperature/stream_options/chat_template_kwargs) not injected for %s",
239339
(model) => {
240-
const body = bodyForModel(model, {
241-
providerOptions: { reasoning_effort: "high" },
242-
} as InferenceOptions);
243-
expect(body["reasoning_effort"]).toBe("high");
340+
const body = bodyForModel(model, {} as InferenceOptions);
341+
// Full-body equality against the raw base OpenAI adapter output: for a
342+
// non-V4 model the wrapper must short-circuit the V4 step entirely, so
343+
// the body matches the base build byte-for-byte (a regression that
344+
// injects a 5th field or mutates an existing one would fail here).
345+
const baseAdapter = createOpenAIAdapter({
346+
...source,
347+
model,
348+
} as typeof source);
349+
const baseBuilt = baseAdapter.buildRequest(
350+
messages,
351+
model,
352+
{} as InferenceOptions,
353+
);
354+
expect(body).toEqual(
355+
JSON.parse(baseBuilt.body) as Record<string, unknown>,
356+
);
244357
},
245358
);
246359
});

0 commit comments

Comments
 (0)