112 lines
3.2 KiB
TypeScript
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;
|