2026-08-21 16:34:59 +08:00

112 lines
3.2 KiB
TypeScript

import type { Extension, ExtensionConfig } from "../../../kernel";
import { ExtensionId, Hook } from "../../catalog";
import type { ModelProvider } from "../agent";
export interface DeepSeekOptions {
apiKey?: string;
baseUrl?: string;
model?: string;
request?: (url: string, init: RequestInit) => Promise<Response>;
}
export function createDeepSeekExtension(options: ExtensionConfig = {}): Extension {
if (options.apiKey !== undefined && typeof options.apiKey !== "string") {
throw new Error("deepseek.apiKey must be a string.");
}
if (options.baseUrl !== undefined && typeof options.baseUrl !== "string") {
throw new Error("deepseek.baseUrl must be a string.");
}
if (options.model !== undefined && typeof options.model !== "string") {
throw new Error("deepseek.model must be a string.");
}
if (options.request !== undefined && typeof options.request !== "function") {
throw new Error("deepseek.request must be a function.");
}
const config = options as DeepSeekOptions;
const apiKey = process.env.DEEPSEEK_API_KEY ?? config.apiKey;
const baseUrl = (
process.env.DEEPSEEK_BASE_URL ??
config.baseUrl ??
"https://api.deepseek.com"
).replace(/\/+$/, "");
const model =
process.env.DEEPSEEK_MODEL ?? config.model ?? "deepseek-v4-flash";
const request = config.request ?? fetch;
const provider: ModelProvider = {
id: "deepseek",
async *chat(messages, signal) {
const response = await request(`${baseUrl}/chat/completions`, {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
model,
messages: messages.map(({ role, content }) => ({ role, content })),
thinking: { type: "disabled" },
stream: true,
}),
signal,
});
if (!response.ok) {
throw new Error(
`DeepSeek request failed (${response.status}): ${await response.text()}`,
);
}
if (!response.body) {
throw new Error("DeepSeek returned an empty response body.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
buffer += decoder.decode(value, { stream: !done });
const events = buffer.split(/\r?\n\r?\n/);
buffer = events.pop() ?? "";
for (const entry of events) {
for (const line of entry.split(/\r?\n/)) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") return;
if (!data) continue;
const chunk = JSON.parse(data);
const content = chunk.choices?.[0]?.delta?.content;
if (content) yield content;
}
}
if (done) break;
}
} finally {
reader.releaseLock();
}
},
};
return {
setup(context) {
if (!apiKey) {
throw new Error("DEEPSEEK_API_KEY is required.");
}
context.add(Hook.ModelProviders, provider);
},
};
}
createDeepSeekExtension.id = ExtensionId.DeepSeek;