Skip to content
Merged
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
30 changes: 28 additions & 2 deletions src/provider/deepseek-v4-effort.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { describe, test, expect } from "bun:test";
import {
DEEPSEEK_V4_EFFORTS,
DEEPSEEK_V4_MODEL_CARD,
isDeepSeekV4Model,
mapV4Effort,
V4_STREAM_OPTIONS,
v4Thinking,
} from "./deepseek-v4-effort.js";

describe("isDeepSeekV4Model", () => {
Expand Down Expand Up @@ -44,10 +47,33 @@ describe("mapV4Effort", () => {
test("maps none to null (drop the wire effort)", () => {
expect(mapV4Effort("none")).toBeNull();
});
test.each(["low", "medium", "high"] as const)(
"maps off-ladder %s to undefined",
test.each(["low", "medium", "high", "ultra", "minimal"] as const)(
"leaves off-ladder %s untouched (undefined)",
(effort) => {
expect(mapV4Effort(effort)).toBeUndefined();
},
);
});

describe("v4Thinking", () => {
test("thinking off for none", () => {
expect(v4Thinking("none")).toEqual({ thinking: false });
});
test.each(["xhigh", "max"] as const)("thinking on for %s", (effort) => {
expect(v4Thinking(effort)).toEqual({ thinking: true });
});
});

describe("DEEPSEEK_V4_MODEL_CARD", () => {
test("defaults both temperature 1.0 and top_p 0.95", () => {
expect(DEEPSEEK_V4_MODEL_CARD).toEqual({ temperature: 1.0, topP: 0.95 });
});
});

describe("V4_STREAM_OPTIONS", () => {
test("requests include_usage on stream chunks", () => {
expect(V4_STREAM_OPTIONS).toEqual({
stream_options: { include_usage: true },
});
});
});
48 changes: 38 additions & 10 deletions src/provider/deepseek-v4-effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,56 @@ import type { ReasoningEffort } from "../agent/profile-types.js";
// DeepSeek V4 effort translator shared by the adapter and the effort picker.
// V4 ships under several ids across two catalogs — deepseek-v4-pro/-flash/
// -flash-vision-exp — so the family is matched by prefix rather than an exact
// id list, mirroring the grok/kimi/muse-spark prefix predicates elsewhere.
// id list; the `^` anchor keeps hypothetical spaced/pre-V4 ids out. This shape
// mirrors the grok/kimi/muse-spark prefix predicates elsewhere in the tree.
export function isDeepSeekV4Model(model: string): boolean {
return /^deepseek-v4/i.test(model.trim());
}

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

// Translate a V4 effort to its raw wire value: `none` maps to null (the caller
// drops `reasoning_effort`, since `reasoning_effort:"none"` is illegal on the
// wire), `xhigh`/`max` pass through verbatim, and any off-ladder value returns
// undefined so the caller leaves effort untouched.
/**
* Translate a V4 effort to its raw wire value: `xhigh`/`max` pass through
* verbatim, `none` maps to null (the caller drops `reasoning_effort`, since
* `reasoning_effort:"none"` is illegal on the wire). Off-ladder values return
* undefined — the caller leaves `reasoning_effort` untouched rather than
* coercing it onto a V4 rung.
*/
export function mapV4Effort(
effort: ReasoningEffort,
): "xhigh" | "max" | null | undefined {
if (effort === "none") return null;
if (effort === "xhigh" || effort === "max") return effort;
return undefined;
return effort === "xhigh" || effort === "max" ? effort : undefined;
}

/**
* chat_template_kwargs toggle for a V4 effort: thinking off for `none`,
* thinking on for any wire effort. When the caller drops `reasoning_effort`
* for `none` it supplies `{thinking:false}` so the encoder still knows to
* bypass the reasoning pass.
*/
export function v4Thinking(effort: ReasoningEffort): { thinking: boolean } {
return { thinking: effort !== "none" };
}

// Operator-confirmed V4 agentic model-card defaults: BOTH temperature 1.0 and
// top_p 0.95 (deliberate bypass of their temperature/topP mutual-exclusion).
// Absent-field-only — provider-set values always win.
export const DEEPSEEK_V4_MODEL_CARD = {
temperature: 1.0,
topP: 0.95,
} as const;

// Ask every V4 stream for per-chunk usage so vendor usage accounting can read
// it (`vendor/intx-inference/.../openai.ts` only emits inference.usage when the
// chunk carries it). Hoisted so the adapter reuses one object per build.
export const V4_STREAM_OPTIONS = {
stream_options: { include_usage: true },
} as const;
165 changes: 139 additions & 26 deletions src/provider/openai-compatible-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defined } from "../../testkit/defined.js";
import { describe, test, expect } from "bun:test";
import { ProtocolMismatchError } from "@intx/inference";
import { createOpenAIAdapter } from "@intx/inference/providers";
import type { ConversationTurn, InferenceOptions } from "@intx/types/runtime";
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";

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

test("none drops reasoning_effort and sets thinking off", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: { reasoning_effort: "none" },
} as InferenceOptions);
expect("reasoning_effort" in body).toBe(false);
expect(body["chat_template_kwargs"]).toEqual({ thinking: false });
});

test.each(["xhigh", "max"] as const)(
"maps ladder %s through verbatim",
"wires V4 effort %s through raw and turns thinking on",
(effort) => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: { reasoning_effort: effort },
} as InferenceOptions);
expect(body["reasoning_effort"]).toBe(effort);
expect(body["chat_template_kwargs"]).toEqual({ thinking: true });
},
);

test.each(["low", "medium", "high"] as const)(
"leaves off-ladder %s untouched",
(effort) => {
describe("V4 request settings (PR4)", () => {
test("none → chat_template_kwargs.thinking false and NO reasoning_effort", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: { reasoning_effort: effort },
providerOptions: { reasoning_effort: "none" },
} as InferenceOptions);
expect(body["reasoning_effort"]).toBe(effort);
},
);
expect(body["chat_template_kwargs"]).toEqual({ thinking: false });
expect("reasoning_effort" in body).toBe(false);
});

test("stream_options include_usage is true", () => {
const body = bodyForModel("deepseek-v4-pro", {} as InferenceOptions);
expect(body["stream_options"]).toEqual({ include_usage: true });
// An unset effort must not force the encoder's thinking pass on nor
// inject an empty chat_template_kwargs object.
expect(body["chat_template_kwargs"]).toBeUndefined();
});

test("top_p 0.95 and temperature 1.0 defaulted when absent", () => {
const body = bodyForModel("deepseek-v4-pro", {} as InferenceOptions);
expect(body["top_p"]).toBe(0.95);
expect(body["temperature"]).toBe(1.0);
// An unset effort must not force the encoder's thinking pass on nor
// inject an empty chat_template_kwargs object.
expect(body["chat_template_kwargs"]).toBeUndefined();
});

test("top_p and temperature preserved when providerOptions set them", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: { temperature: 0.7, top_p: 0.9 },
} as InferenceOptions);
expect(body["top_p"]).toBe(0.9);
expect(body["temperature"]).toBe(0.7);
});

test("chat_template_kwargs merge preserves existing keys", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: {
reasoning_effort: "xhigh",
chat_template_kwargs: { some: "existing" },
},
} as InferenceOptions);
expect(body["chat_template_kwargs"]).toEqual({
thinking: true,
some: "existing",
});
});

test("off-ladder effort is left untouched (no high→xhigh coercion)", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: { reasoning_effort: "high" },
} as InferenceOptions);
expect(body["reasoning_effort"]).toBe("high");
expect("chat_template_kwargs" in body).toBe(false);
});

test("none merges thinking off into, never replaces, existing kwargs", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: {
reasoning_effort: "none",
chat_template_kwargs: { some: "existing" },
},
} as InferenceOptions);
expect(body["chat_template_kwargs"]).toEqual({
thinking: false,
some: "existing",
});
});

test("provider-set thinking is preserved (absent-only thinking)", () => {
// A provider-supplied thinking value beats the V4 effort's implied one:
// `none` normally forces thinking off, but an explicit thinking:true
// must be kept (never force-stamped), and vice versa.
const noneThenTrue = bodyForModel("deepseek-v4-pro", {
providerOptions: {
reasoning_effort: "none",
chat_template_kwargs: { thinking: true },
},
} as InferenceOptions);
expect(noneThenTrue["chat_template_kwargs"]).toEqual({ thinking: true });

const xhighThenFalse = bodyForModel("deepseek-v4-pro", {
providerOptions: {
reasoning_effort: "xhigh",
chat_template_kwargs: { thinking: false },
},
} as InferenceOptions);
expect(xhighThenFalse["chat_template_kwargs"]).toEqual({
thinking: false,
});
});

test("unset effort does not inject empty chat_template_kwargs", () => {
const body = bodyForModel("deepseek-v4-pro", {} as InferenceOptions);
expect("chat_template_kwargs" in body).toBe(false);
expect(body["reasoning_effort"]).toBeUndefined();
});

test("provider-set stream_options is preserved (absent-only)", () => {
const body = bodyForModel("deepseek-v4-pro", {
providerOptions: {
stream_options: { include_usage: false },
},
} as InferenceOptions);
expect(body["stream_options"]).toEqual({ include_usage: false });
});
});

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

test.each(models)(
"merged body for %s is byte-identical to a manual base+merge (V4 step no-op)",
(model) => {
test.each(
models.flatMap((model) =>
(["high", "none", "max"] as const).map((effort) => [model, effort]),
),
)(
"merged body for %s with effort %s is byte-identical to a manual base+merge (V4 step no-op)",
(model, effort) => {
const base = bodyForModel(model, {} as InferenceOptions);
const withEffort = bodyForModel(model, {
providerOptions: { reasoning_effort: "high" },
providerOptions: { reasoning_effort: effort },
} as InferenceOptions);
// For a non-V4 model the V4 step must not run: the output body equals
// the base body shallow-merged with providerOptions verbatim (no
// effort mapping, no chat_template_kwargs injection) — including V4
// ladder values like none (delete + thinking:false) and max (verbatim),
// so a regression routing any V4 rung through the V4 branch is caught.
const expected = {
...base,
...({ reasoning_effort: "high" } as const),
...({ reasoning_effort: effort } as const),
};
expect(withEffort).toEqual(expected);
},
);

test.each(models)(
"merged body for %s carries reasoning_effort verbatim",
"V4 settings (top_p/temperature/stream_options/chat_template_kwargs) not injected for %s",
(model) => {
const body = bodyForModel(model, {
providerOptions: { reasoning_effort: "high" },
} as InferenceOptions);
expect(body["reasoning_effort"]).toBe("high");
const body = bodyForModel(model, {} as InferenceOptions);
// Full-body equality against the raw base OpenAI adapter output: for a
// non-V4 model the wrapper must short-circuit the V4 step entirely, so
// the body matches the base build byte-for-byte (a regression that
// injects a 5th field or mutates an existing one would fail here).
const baseAdapter = createOpenAIAdapter({
...source,
model,
} as typeof source);
const baseBuilt = baseAdapter.buildRequest(
messages,
model,
{} as InferenceOptions,
);
expect(body).toEqual(
JSON.parse(baseBuilt.body) as Record<string, unknown>,
);
},
);
});
Expand Down
Loading
Loading