2026-08-05 11:09:08 +08:00
|
|
|
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
|
|
|
|
|
2026-08-05 11:14:10 +08:00
|
|
|
|
function completionEndpoint(apiUrl) {
|
|
|
|
|
|
const value = String(apiUrl || "").replace(/\/+$/, "");
|
|
|
|
|
|
if (/\/chat\/completions$/i.test(value)) return value;
|
|
|
|
|
|
return `${value}/chat/completions`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 11:09:08 +08:00
|
|
|
|
function extractJson(text) {
|
|
|
|
|
|
const value = String(text || "").trim();
|
|
|
|
|
|
if (!value) throw new Error("model_response_empty");
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(value);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
const fenced = value.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
|
|
|
|
if (fenced) return JSON.parse(fenced[1]);
|
|
|
|
|
|
const first = value.indexOf("{");
|
|
|
|
|
|
const last = value.lastIndexOf("}");
|
|
|
|
|
|
if (first >= 0 && last > first) return JSON.parse(value.slice(first, last + 1));
|
|
|
|
|
|
throw new Error("model_response_not_json");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class DeepSeekJsonClient {
|
|
|
|
|
|
constructor({
|
|
|
|
|
|
apiKey = process.env.DEEPSEEK_API_KEY,
|
|
|
|
|
|
apiUrl = process.env.DEEPSEEK_API_URL || "https://api.deepseek.com/v1",
|
|
|
|
|
|
model = process.env.DEEPSEEK_MODEL || "deepseek-chat",
|
|
|
|
|
|
fetchImpl = globalThis.fetch,
|
|
|
|
|
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
|
|
|
|
} = {}) {
|
|
|
|
|
|
if (!apiKey) throw new Error("deepseek_api_key_missing");
|
|
|
|
|
|
if (!fetchImpl) throw new Error("fetch_unavailable");
|
|
|
|
|
|
this.apiKey = apiKey;
|
|
|
|
|
|
this.apiUrl = String(apiUrl).replace(/\/+$/, "");
|
|
|
|
|
|
this.model = model;
|
|
|
|
|
|
this.fetchImpl = fetchImpl;
|
|
|
|
|
|
this.timeoutMs = timeoutMs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async generate({ role, instruction, input, validatorError = null }) {
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
|
|
|
|
try {
|
2026-08-05 11:14:10 +08:00
|
|
|
|
const response = await this.fetchImpl(completionEndpoint(this.apiUrl), {
|
2026-08-05 11:09:08 +08:00
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
authorization: `Bearer ${this.apiKey}`,
|
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
model: this.model,
|
|
|
|
|
|
stream: false,
|
|
|
|
|
|
temperature: role === "controller_witness" ? 0.1 : 0.2,
|
|
|
|
|
|
response_format: { type: "json_object" },
|
|
|
|
|
|
messages: [
|
|
|
|
|
|
{
|
|
|
|
|
|
role: "system",
|
|
|
|
|
|
content:
|
|
|
|
|
|
"你是光湖语言世界中的有界模型运行位。只输出一个JSON对象,不输出思维过程、Markdown或额外说明。不得伪造现实成功、权限、冰朔的新意志或未提供的事实。",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
role: "user",
|
|
|
|
|
|
content: JSON.stringify({
|
|
|
|
|
|
role,
|
|
|
|
|
|
instruction,
|
|
|
|
|
|
validator_error_from_previous_attempt: validatorError,
|
|
|
|
|
|
runtime_input: input,
|
|
|
|
|
|
}),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`model_http_${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = await response.json();
|
|
|
|
|
|
const text = payload?.choices?.[0]?.message?.content;
|
|
|
|
|
|
return extractJson(text);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
clearTimeout(timer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 11:14:10 +08:00
|
|
|
|
export { completionEndpoint, extractJson };
|