Skip to content
Closed
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
10 changes: 5 additions & 5 deletions src/agent/model-family-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,39 +411,39 @@ describe("resolveModelFamilyPolicy", () => {

test("orchestrators get the d1 primary residual, no leaf role body", () => {
const p = v4({ orchestrator: true });
expect(p.family).toBe("deepseek-v4");
expect(p.family).toBe("deepseek");
expect(p.promptResidual).toBeDefined();
expect(p.promptResidual).toContain("DeepSeek V4 Flash");
expect(p.leafRoleBody).toBeUndefined();
});

test("coder leaves get the tuned slim coder body, no residual", () => {
const p = v4({ orchestrator: false, directorId: "coder" });
expect(p.family).toBe("deepseek-v4");
expect(p.family).toBe("deepseek");
expect(p.leafRoleBody).toBeDefined();
expect(p.leafRoleBody).toContain("Coder");
expect(p.promptResidual).toBeUndefined();
});

test("reviewer leaves get the tuned slim reviewer body", () => {
const p = v4({ orchestrator: false, directorId: "reviewer" });
expect(p.family).toBe("deepseek-v4");
expect(p.family).toBe("deepseek");
expect(p.leafRoleBody).toBeDefined();
expect(p.leafRoleBody).toContain("Reviewer");
expect(p.promptResidual).toBeUndefined();
});

test("explorer leaves get the g2 leaf residual only", () => {
const p = v4({ orchestrator: false, directorId: "explorer" });
expect(p.family).toBe("deepseek-v4");
expect(p.family).toBe("deepseek");
expect(p.promptResidual).toBeDefined();
expect(p.promptResidual).toContain("DeepSeek V4 Flash");
expect(p.leafRoleBody).toBeUndefined();
});

test("a leaf without a tuned director id gets the default policy, no body/residual", () => {
const p = v4({ orchestrator: false });
expect(p.family).toBe("deepseek-v4");
expect(p.family).toBe("deepseek");
expect(p.leafRoleBody).toBeUndefined();
expect(p.promptResidual).toBeUndefined();
});
Expand Down
2 changes: 1 addition & 1 deletion src/agent/model-family-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export function resolveModelFamilyPolicy(input: {
// Generic gpt prompts are byte-identical — only astra carries the
// residual.
return { family, ...ASTRA_POLICY };
case "deepseek-v4": {
case "deepseek": {
// Winning d1+slim config: d1 primary residual on orchestrators, g2 leaf
// residual on explorer leaves, tuned slim coder/reviewer bodies.
const directorId = input.directorId;
Expand Down
4 changes: 2 additions & 2 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
} from "../provider/grok-responses.js";
import { BIFROST_PROVIDER } from "../provider/bifrost-adapter.js";
import { DEEPSEEK_V4_PROVIDER } from "../provider/deepseek-v4-adapter.js";
import { isDeepSeekV4Model } from "../provider/deepseek-v4-effort.js";
import { isDeepSeekModel } from "../provider/deepseek-v4-effort.js";
import { isOllamaProviderId, ollamaOpenAIBaseURL } from "../provider/ollama.js";
import { selectableGoModelIds } from "../provider/model-catalogs.js";
import {
Expand Down Expand Up @@ -290,7 +290,7 @@ export function buildOpenAISource(fields: {
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: isDeepSeekV4Model(fields.model)
provider: isDeepSeekModel(fields.model)
? DEEPSEEK_V4_PROVIDER
: "openai-compatible",
baseURL: isOllamaProviderId(fields.id)
Expand Down
7 changes: 5 additions & 2 deletions src/config/inference-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,12 +814,15 @@ describe("deepseek-v4 provider stamping (PR A)", () => {
);
}

test("a deepseek-v4-* model stamps provider 'deepseek-v4'", () => {
test("a deepseek-* model stamps provider 'deepseek-v4' (family routing)", () => {
const source = v4SourceFor("deepseek-v4-pro");
expect(source?.provider).toBe("deepseek-v4");

const preV4 = v4SourceFor("deepseek-v3");
expect(preV4?.provider).toBe("deepseek-v4");
});

test("non-V4 custom models stay provider 'openai-compatible'", () => {
test("non-deepseek custom models stay provider 'openai-compatible'", () => {
const source = v4SourceFor("gpt-5");
expect(source?.provider).toBe("openai-compatible");
});
Expand Down
37 changes: 31 additions & 6 deletions src/provider/deepseek-v4-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ describe("deepseek-v4 adapter effort wiring", () => {
expect("reasoning_effort" in body).toBe(false);
});

test("off-ladder effort is left untouched (no high→xhigh coercion)", () => {
const body = bodyFor({
providerOptions: { reasoning_effort: "high" },
} as InferenceOptions);
expect(body["reasoning_effort"]).toBe("high");
expect("chat_template_kwargs" in body).toBe(false);
test("off-ladder effort is rejected (no pass-through to the wire)", () => {
expect(() =>
bodyFor({
providerOptions: { reasoning_effort: "high" },
} as InferenceOptions),
).toThrow(/does not support reasoning effort "high"/);
});

test("unset effort does not inject empty chat_template_kwargs", () => {
Expand Down Expand Up @@ -223,4 +223,29 @@ describe("deepseek-v4 adapter byte-identical to base for non-V4", () => {
const body = JSON.parse(built.body) as Record<string, unknown>;
expect(body).toEqual(baseBody);
});

test("pre-V4 deepseek gets no V4 quirks (family route, V4-variant gate)", () => {
const baseAdapter = createOpenAICompatibleAdapter({
...source,
model: "deepseek-v3",
} as typeof source);
const baseBuilt = baseAdapter.buildRequest(messages, "deepseek-v3", {
providerOptions: { reasoning_effort: "none" },
} as InferenceOptions);

const adapter = createDeepSeekV4Adapter({
...source,
model: "deepseek-v3",
} as typeof source);
const built = adapter.buildRequest(messages, "deepseek-v3", {
providerOptions: { reasoning_effort: "none" },
} as InferenceOptions);

const baseBody = JSON.parse(baseBuilt.body) as Record<string, unknown>;
const body = JSON.parse(built.body) as Record<string, unknown>;
expect(body).toEqual(baseBody);
expect("top_p" in body).toBe(false);
expect("temperature" in body).toBe(false);
expect("stream_options" in body).toBe(false);
});
});
13 changes: 8 additions & 5 deletions src/provider/deepseek-v4-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type ProviderAdapter } from "@intx/inference";
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";
import {
DEEPSEEK_V4_MODEL_CARD,
isDeepSeekModel,
isDeepSeekV4Model,
mapV4Effort,
V4_STREAM_OPTIONS,
Expand All @@ -13,7 +14,9 @@ type AdapterSource = Parameters<typeof createOpenAICompatibleAdapter>[0];

// Wraps the patched openai-compatible adapter so V4 requests inherit the
// providerOptions merge, NIM null-delta patch, and Accept header; this layer
// only applies the V4 effort/sampling/stream quirks to the built body.
// routes the whole DeepSeek family through this family adapter and applies the
// V4 effort/sampling/stream quirks to the built body only for the V4 variant
// (pre-V4 deepseek stays on the generic openai-compatible body, byte-identical).
export function createDeepSeekV4Adapter(
source: AdapterSource,
): ProviderAdapter {
Expand All @@ -25,12 +28,12 @@ export function createDeepSeekV4Adapter(
options,
) => {
const req = base.buildRequest(messages, model, options);
if (!isDeepSeekV4Model(model)) return req;
if (!isDeepSeekModel(model) || !isDeepSeekV4Model(model)) return req;
const body = JSON.parse(req.body) as Record<string, unknown>;

// V4 effort ladder (none/xhigh/max): none drops reasoning_effort and
// turns thinking off; xhigh/max go out raw with thinking on; off-ladder
// or absent efforts are left untouched.
// turns thinking off; xhigh/max go out raw with thinking on. Off-ladder
// efforts throw inside mapV4Effort (rejected, never passed to the wire).
if ("reasoning_effort" in body) {
const effort = body["reasoning_effort"] as Parameters<
typeof mapV4Effort
Expand All @@ -39,7 +42,7 @@ export function createDeepSeekV4Adapter(
if (wire === null) {
delete body["reasoning_effort"];
mergeThinking(body, false);
} else if (wire !== undefined) {
} else {
body["reasoning_effort"] = wire;
mergeThinking(body, true);
}
Expand Down
60 changes: 58 additions & 2 deletions src/provider/deepseek-v4-effort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,43 @@ import { describe, test, expect } from "bun:test";
import {
DEEPSEEK_V4_EFFORTS,
DEEPSEEK_V4_MODEL_CARD,
DEEPSEEK_V4_ROLE_EFFORT,
isDeepSeekModel,
isDeepSeekV4Model,
mapV4Effort,
V4_STREAM_OPTIONS,
v4Thinking,
} from "./deepseek-v4-effort.js";

describe("isDeepSeekModel", () => {
test.each([
"deepseek-v4",
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-v4.1-flash",
"deepseek-v4-flash-vision-exp",
"deepseek-ai/DeepSeek-V4-Flash-0731",
"deepseek-ai/deepseek-v4-flash",
"deepseek-v3",
"deepseek-r1",
"deepseek-coder",
"deepseek-chat",
"deepseek-ai/DeepSeek-Chat",
])("true for deepseek family id %s", (model) => {
expect(isDeepSeekModel(model)).toBe(true);
});
test.each([
"gpt-5",
"kimi-k2",
"claude-sonnet-4.5",
"glm-5.3",
"grok-4.6",
"some-proxy-deepseek-pro",
])("false for non-deepseek id %s", (model) => {
expect(isDeepSeekModel(model)).toBe(false);
});
});

describe("isDeepSeekV4Model", () => {
test.each([
"deepseek-v4",
Expand All @@ -25,6 +56,7 @@ describe("isDeepSeekV4Model", () => {
"deepseek-r1",
"deepseek-coder",
"deepseek-chat",
"deepseek-ai/DeepSeek-Chat",
"gpt-5",
"kimi-k2",
"claude-sonnet-4.5",
Expand All @@ -42,6 +74,28 @@ describe("DEEPSEEK_V4_EFFORTS", () => {
});
});

describe("DEEPSEEK_V4_ROLE_EFFORT", () => {
test("coder defaults to max", () => {
expect(DEEPSEEK_V4_ROLE_EFFORT.coder).toBe("max");
});
test("dispatch/planner/reviewer are max and other leaves xhigh", () => {
expect(DEEPSEEK_V4_ROLE_EFFORT.dispatch).toBe("max");
expect(DEEPSEEK_V4_ROLE_EFFORT.planner).toBe("max");
expect(DEEPSEEK_V4_ROLE_EFFORT.reviewer).toBe("max");
for (const role of [
"explorer",
"artist",
"qa-lead",
"prober",
"designer",
"shakespeare",
"warden",
] as const) {
expect(DEEPSEEK_V4_ROLE_EFFORT[role]).toBe("xhigh");
}
});
});

describe("mapV4Effort", () => {
test.each([
["xhigh", "xhigh"],
Expand All @@ -53,9 +107,11 @@ describe("mapV4Effort", () => {
expect(mapV4Effort("none")).toBeNull();
});
test.each(["low", "medium", "high", "ultra", "minimal"] as const)(
"leaves off-ladder %s untouched (undefined)",
"rejects off-ladder %s with a clear error (no pass-through)",
(effort) => {
expect(mapV4Effort(effort)).toBeUndefined();
expect(() => mapV4Effort(effort)).toThrow(
`DeepSeek V4 does not support reasoning effort "${effort}"`,
);
},
);
});
Expand Down
48 changes: 30 additions & 18 deletions src/provider/deepseek-v4-effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import type { ReasoningEffort } from "../agent/profile-types.js";
import type { DirectorId } from "../agent/directors/types.js";

/**
* Per-role V4 effort defaults. coder is the cheapest spawn: 'low' clamps to
* the V4 'none' wire rung (thinking off), per the operator's "simplest
* implementer = none" intent. Every other role takes its rung from the map.
* Per-role V4 effort defaults. dispatch/planner/reviewer dispatch and judge —
* max. coder defaults to max (operator-confirmed). Every other leaf reasons at
* xhigh.
*/
export const DEEPSEEK_V4_ROLE_EFFORT: Record<DirectorId, ReasoningEffort> = {
dispatch: "max",
planner: "max",
reviewer: "max",
coder: "low",
coder: "max",
explorer: "xhigh",
artist: "xhigh",
"qa-lead": "xhigh",
Expand All @@ -20,13 +20,23 @@ export const DEEPSEEK_V4_ROLE_EFFORT: Record<DirectorId, ReasoningEffort> = {
warden: "xhigh",
};

// 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 — and under a vendor-qualified host id like
// deepseek-ai/DeepSeek-V4-Flash-0731. Match the `deepseek-v4` segment at the
// start or after a `/`, so both bare and qualified ids resolve while pre-V4
// ids (deepseek-coder, deepseek-chat) do not. This mirrors the grok/kimi/astra
// leaf predicates, which also accept an org-qualified segment.
// DeepSeek family predicate shared by the adapter router and the family policy.
// The family ships under several ids across two catalogs — deepseek-v4-pro/
// -flash/-flash-vision-exp and deepseek-chat/deepseek-coder/deepseek-v3 — and
// under a vendor-qualified host id like deepseek-ai/DeepSeek-V4-Flash-0731.
// Match the `deepseek` segment at the start or after a `/`, so both bare and
// qualified ids resolve across the whole family (including future versions).
// This mirrors the grok/kimi/astra leaf predicates, which also accept an
// org-qualified segment.
export function isDeepSeekModel(model: string): boolean {
return /(^|\/)deepseek/i.test(model.trim());
}

// V4-variant detection: distinguishes the V4 model line from pre-V4 DeepSeek
// (deepseek-chat, deepseek-coder, deepseek-v3). Only where the wire semantics
// genuinely differ between V4 and pre-V4 deepseek — the V4 adapter's model-card
// and effort quirks, the reasoning_content keep-vs-strip behavior, and the
// none/xhigh/max effort ladder — do callers gate on this variant predicate.
export function isDeepSeekV4Model(model: string): boolean {
return /(^|\/)deepseek-v4/i.test(model.trim());
}
Expand All @@ -43,15 +53,17 @@ export const DEEPSEEK_V4_EFFORTS: readonly ReasoningEffort[] = [
/**
* 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.
* `reasoning_effort:"none"` is illegal on the wire). The V4 ladder is strictly
* none/xhigh/max: any off-ladder effort arriving on a manual/exec/config path
* is rejected with a clear error rather than coerced or passed through, so no
* off-ladder value can reach the wire for a V4 model.
*/
export function mapV4Effort(
effort: ReasoningEffort,
): "xhigh" | "max" | null | undefined {
export function mapV4Effort(effort: ReasoningEffort): "xhigh" | "max" | null {
if (effort === "none") return null;
return effort === "xhigh" || effort === "max" ? effort : undefined;
if (effort === "xhigh" || effort === "max") return effort;
throw new Error(
`DeepSeek V4 does not support reasoning effort "${effort}" (supported: none, xhigh, max).`,
);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/provider/openai-compatible-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type BuiltRequest, type ProviderAdapter } from "@intx/inference";
import { createOpenAIAdapter } from "@intx/inference/providers";
import { normalizeNullDeltaFields } from "./null-delta-fields.js";
import { isDeepSeekV4Model } from "./deepseek-v4-effort.js";
import { isDeepSeekModel, isDeepSeekV4Model } from "./deepseek-v4-effort.js";

// The stock OpenAI adapter builds the request body from a fixed set of fields
// (max_tokens, temperature, tools, messages, response_format) and ignores
Expand Down Expand Up @@ -51,7 +51,7 @@ export function createOpenAICompatibleAdapter(
// keeps each turn's reasoning on replay (handled by its own adapter), so only
// pre-V4 DeepSeek strips it here.
const m = model.toLowerCase().trim();
const isDeepSeek = m.includes("deepseek");
const isDeepSeek = isDeepSeekModel(m);
const stripReasoning = isDeepSeek && !isDeepSeekV4Model(m);
needsDeepSeekPatch = isDeepSeek;
if (!hasProviderOptions && !stripReasoning) return ensureAccept(built);
Expand Down
11 changes: 5 additions & 6 deletions src/subagent/agent-fleet-effort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,15 +390,14 @@ describe("deepseek-v4 per-role effort policy (CL-10294)", () => {
return worker.provider.reasoningEffort as ReasoningEffort;
}

test("planner/reviewer max, coder cheapest, other leaves xhigh", async () => {
// V4's native ladder is ["none","xhigh","max"]. coder's low clamps to the
// cheapest wire rung (none, thinking off) per the operator's "simplest
// implementer = none" intent; planner/reviewer resolve max. dispatch is the
// primary session (not a spawned worker) and already defaults to max for
test("planner/reviewer/coder max, other leaves xhigh", async () => {
// V4's native ladder is ["none","xhigh","max"]. coder defaults to max
// (operator-confirmed, PR #1377); planner/reviewer resolve max. dispatch is
// the primary session (not a spawned worker) and already defaults to max for
// V4 via defaultEffortForModel.
expect(await captureEffort("planner")).toBe("max");
expect(await captureEffort("reviewer")).toBe("max");
expect(await captureEffort("coder")).toBe("none");
expect(await captureEffort("coder")).toBe("max");
expect(await captureEffort("explorer")).toBe("xhigh");
expect(await captureEffort("artist")).toBe("xhigh");
expect(await captureEffort("qa-lead")).toBe("xhigh");
Expand Down
Loading
Loading