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
11 changes: 8 additions & 3 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import {
GROK_SESSION_ID_OPTION,
} 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 { isOllamaProviderId, ollamaOpenAIBaseURL } from "../provider/ollama.js";
import { selectableGoModelIds } from "../provider/model-catalogs.js";
import {
Expand Down Expand Up @@ -265,8 +267,9 @@ export function resolveMcpServers(
}

// Build the OpenAI-compatible InferenceSource the runtime consumes. `id` is the
// user-facing name for this source (e.g. "zen"); `provider` is always
// "openai-compatible" so the inference registry routes it to the right adapter.
// user-facing name for this source (e.g. "zen"); `provider` routes to the
// openai-compatible adapter, or the deepseek-v4 per-family adapter for V4
// models, so the inference registry picks the right adapter.
export function buildOpenAISource(fields: {
id: string;
baseURL: string;
Expand All @@ -287,7 +290,9 @@ export function buildOpenAISource(fields: {
registerSourceSecret(fields.id, fields.apiKey);
return {
id: fields.id,
provider: "openai-compatible",
provider: isDeepSeekV4Model(fields.model)
? DEEPSEEK_V4_PROVIDER
: "openai-compatible",
baseURL: isOllamaProviderId(fields.id)
? ollamaOpenAIBaseURL(fields.baseURL)
: normalizeOpenAICompatibleBaseURL(fields.baseURL),
Expand Down
30 changes: 30 additions & 0 deletions src/config/inference-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,3 +794,33 @@ describe("session source bundles", () => {
expect(bundle.defaultSource).toBe("openai");
});
});

describe("deepseek-v4 provider stamping (PR A)", () => {
function v4SourceFor(model: string) {
return buildInferenceSourceForRef(
{ provider: "ds", model },
{
sessionId: "s1",
catalog: [
{
name: "ds",
baseURL: "https://ds.example/v1",
apiKey: "k",
models: [model],
},
],
},
undefined,
);
}

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

test("non-V4 custom models stay provider 'openai-compatible'", () => {
const source = v4SourceFor("gpt-5");
expect(source?.provider).toBe("openai-compatible");
});
});
226 changes: 226 additions & 0 deletions src/provider/deepseek-v4-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { describe, test, expect } from "bun:test";
import type { ConversationTurn, InferenceOptions } from "@intx/types/runtime";
import {
createDeepSeekV4Adapter,
DEEPSEEK_V4_PROVIDER,
} from "./deepseek-v4-adapter.js";
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";

const source = {
id: "test",
provider: DEEPSEEK_V4_PROVIDER,
baseURL: "https://example.test",
apiKey: "sk-test",
model: "deepseek-v4-pro",
} as unknown as Parameters<typeof createDeepSeekV4Adapter>[0];

const messages: ConversationTurn[] = [
{
role: "user",
content: [{ type: "text", text: "hi" }],
} as unknown as ConversationTurn,
];

function bodyFor(options: InferenceOptions): Record<string, unknown> {
const adapter = createDeepSeekV4Adapter(source);
const built = adapter.buildRequest(messages, "deepseek-v4-pro", options);
return JSON.parse(built.body) as Record<string, unknown>;
}

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

test("none → chat_template_kwargs.thinking false and NO reasoning_effort", () => {
const body = bodyFor({
providerOptions: { reasoning_effort: "none" },
} as InferenceOptions);
expect(body["chat_template_kwargs"]).toEqual({ thinking: false });
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("unset effort does not inject empty chat_template_kwargs", () => {
const body = bodyFor({} as InferenceOptions);
expect("chat_template_kwargs" in body).toBe(false);
expect(body["reasoning_effort"]).toBeUndefined();
});
});

describe("deepseek-v4 adapter request settings", () => {
test("stream_options include_usage is true", () => {
const body = bodyFor({} as InferenceOptions);
expect(body["stream_options"]).toEqual({ include_usage: true });
});

test("top_p 0.95 and temperature 1.0 defaulted when absent", () => {
const body = bodyFor({} as InferenceOptions);
expect(body["top_p"]).toBe(0.95);
expect(body["temperature"]).toBe(1.0);
});

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

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

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

test("provider-set thinking is preserved (absent-only thinking)", () => {
const noneThenTrue = bodyFor({
providerOptions: {
reasoning_effort: "none",
chat_template_kwargs: { thinking: true },
},
} as InferenceOptions);
expect(noneThenTrue["chat_template_kwargs"]).toEqual({ thinking: true });

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

describe("deepseek-v4 adapter inherits base quirks", () => {
const withThinking: ConversationTurn[] = [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{
role: "assistant",
model: "deepseek-v4",
content: [
{ type: "thinking", thinking: "ponder" },
{ type: "text", text: "hello" },
],
},
{ role: "user", content: [{ type: "text", text: "again" }] },
] as unknown as ConversationTurn[];

test("keeps reasoning_content on replayed assistant turns (inherited base)", () => {
const adapter = createDeepSeekV4Adapter(source);
const built = adapter.buildRequest(
withThinking,
"deepseek-v4",
{} as InferenceOptions,
);
const assistant = (
JSON.parse(built.body) as { messages: Record<string, unknown>[] }
).messages.find((m) => m["role"] === "assistant");
expect(assistant?.["reasoning_content"]).toBe("ponder");
});

test("vendor-qualified V4 id keeps reasoning_content on replays (org/ prefix)", () => {
const adapter = createDeepSeekV4Adapter(source);
const built = adapter.buildRequest(
withThinking,
"deepseek-ai/DeepSeek-V4-Flash-0731",
{
providerOptions: { reasoning_effort: "xhigh" },
} as InferenceOptions,
);
const body = JSON.parse(built.body) as {
messages: Record<string, unknown>[];
reasoning_effort?: unknown;
};
const assistant = body.messages.find((m) => m["role"] === "assistant");
expect(assistant?.["reasoning_content"]).toBe("ponder");
expect(body.reasoning_effort).toBe("xhigh");
});

test("non-V4 select a generic body build (base short-circuits, no V4 fields)", () => {
const adapter = createDeepSeekV4Adapter({
...source,
model: "gpt-5",
} as typeof source);
const built = adapter.buildRequest(
messages,
"gpt-5",
{} as InferenceOptions,
);
const body = JSON.parse(built.body) as Record<string, unknown>;
expect("top_p" in body).toBe(false);
expect("temperature" in body).toBe(false);
expect("stream_options" in body).toBe(false);
expect("chat_template_kwargs" in body).toBe(false);
expect(body["model"]).toBe("gpt-5");
});

test("keeps reasoning_content for non-DeepSeek models", () => {
const adapter = createDeepSeekV4Adapter({
...source,
model: "kimi-k2",
} as typeof source);
const built = adapter.buildRequest(
withThinking,
"kimi-k2",
{} as InferenceOptions,
);
const assistant = (
JSON.parse(built.body) as { messages: Record<string, unknown>[] }
).messages.find((m) => m["role"] === "assistant");
expect(assistant?.["reasoning_content"]).toBe("ponder");
});
});

describe("deepseek-v4 adapter byte-identical to base for non-V4", () => {
test("non-V4 body matches the patched generic base byte-for-byte", () => {
const baseAdapter = createOpenAICompatibleAdapter({
...source,
model: "gpt-5",
} as typeof source);
const baseBuilt = baseAdapter.buildRequest(messages, "gpt-5", {
providerOptions: { reasoning_effort: "none" },
} as InferenceOptions);

const adapter = createDeepSeekV4Adapter({
...source,
model: "gpt-5",
} as typeof source);
const built = adapter.buildRequest(messages, "gpt-5", {
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);
});
});
71 changes: 71 additions & 0 deletions src/provider/deepseek-v4-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { type ProviderAdapter } from "@intx/inference";
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";
import {
DEEPSEEK_V4_MODEL_CARD,
isDeepSeekV4Model,
mapV4Effort,
V4_STREAM_OPTIONS,
} from "./deepseek-v4-effort.js";

export const DEEPSEEK_V4_PROVIDER = "deepseek-v4";

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.
export function createDeepSeekV4Adapter(
source: AdapterSource,
): ProviderAdapter {
const base = createOpenAICompatibleAdapter(source);

const buildRequest: ProviderAdapter["buildRequest"] = (
messages,
model,
options,
) => {
const req = base.buildRequest(messages, model, options);
if (!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.
if ("reasoning_effort" in body) {
const effort = body["reasoning_effort"] as Parameters<
typeof mapV4Effort
>[0];
const wire = mapV4Effort(effort);
if (wire === null) {
delete body["reasoning_effort"];
mergeThinking(body, false);
} else if (wire !== undefined) {
body["reasoning_effort"] = wire;
mergeThinking(body, true);
}
}

if (body["top_p"] === undefined)
body["top_p"] = DEEPSEEK_V4_MODEL_CARD.topP;
if (body["temperature"] === undefined)
body["temperature"] = DEEPSEEK_V4_MODEL_CARD.temperature;
body["stream_options"] ??= V4_STREAM_OPTIONS.stream_options;

return { ...req, body: JSON.stringify(body) };
};

return { ...base, buildRequest };
}

// chat_template_kwargs clone with an absent-only thinking toggle (never mutate
// the caller's providerOptions; provider-set thinking wins).
function mergeThinking(body: Record<string, unknown>, thinking: boolean): void {
const existing =
body["chat_template_kwargs"] !== null &&
typeof body["chat_template_kwargs"] === "object"
? (body["chat_template_kwargs"] as Record<string, unknown>)
: {};
const merged = { ...existing };
merged.thinking ??= thinking;
Object.assign(body, { chat_template_kwargs: merged });
}
5 changes: 5 additions & 0 deletions src/provider/deepseek-v4-effort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ describe("isDeepSeekV4Model", () => {
"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",
])("true for V4 id %s", (model) => {
expect(isDeepSeekV4Model(model)).toBe(true);
});
test.each([
"deepseek-v3",
"deepseek-r1",
"deepseek-coder",
"deepseek-chat",
"gpt-5",
"kimi-k2",
"claude-sonnet-4.5",
Expand Down
Loading
Loading