@world-engines/project-host 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 作者侧真实模型连接。认证材料仅存在于 host process 内存和请求 header 中,
|
|
3
|
+
* 错误对象、usage 与返回值都不会携带 token、底层响应正文或请求 URL。
|
|
4
|
+
*/
|
|
5
|
+
export const AUTHORING_EMBEDDING_MODEL_ALLOWLIST = Object.freeze([
|
|
6
|
+
"qwen/qwen3-embedding-8b",
|
|
7
|
+
"qwen/qwen3-embedding-4b",
|
|
8
|
+
"openai/text-embedding-3-small",
|
|
9
|
+
]);
|
|
10
|
+
const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
|
|
11
|
+
const DEFAULT_BASE_URL_ALLOWLIST = Object.freeze([
|
|
12
|
+
DEFAULT_BASE_URL,
|
|
13
|
+
]);
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
+
const MAX_TIMEOUT_MS = 900_000;
|
|
16
|
+
export const EMBEDDING_BATCH_SIZE = 128;
|
|
17
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 4_096;
|
|
18
|
+
const DEFAULT_STRUCTURED_TEMPERATURE = 0;
|
|
19
|
+
const MAX_DEEPSEEK_OUTPUT_TOKENS = 384_000;
|
|
20
|
+
const OPENROUTER_PRODUCTION_BASE_URL = "https://openrouter.ai/api/v1";
|
|
21
|
+
const DEEPSEEK_PRODUCTION_BASE_URL = "https://api.deepseek.com";
|
|
22
|
+
const DEEPSEEK_V4_FLASH_MODEL = "deepseek-v4-flash";
|
|
23
|
+
// 仅已实际遇到且审核过的有限字段名可进入错误路径;不回显任意 provider key。
|
|
24
|
+
const SAFE_ADDITIONAL_PROPERTY_DIAGNOSTIC_KEYS = new Set(["labels"]);
|
|
25
|
+
/** 所有失败都是可穷举、已脱敏的 terminal result;本模块绝不静默降级。 */
|
|
26
|
+
export class AuthoringModelConnectionError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
retryable;
|
|
29
|
+
status;
|
|
30
|
+
responseMetadata;
|
|
31
|
+
schemaIssue;
|
|
32
|
+
validationStage;
|
|
33
|
+
parseIssue;
|
|
34
|
+
name = "AuthoringModelConnectionError";
|
|
35
|
+
terminal = true;
|
|
36
|
+
validationIssues;
|
|
37
|
+
constructor(code, retryable, status, responseMetadata, schemaIssue, validationStage, parseIssue) {
|
|
38
|
+
super(`authoring_model_connection:${code}`);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.retryable = retryable;
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.responseMetadata = responseMetadata;
|
|
43
|
+
this.schemaIssue = schemaIssue;
|
|
44
|
+
this.validationStage = validationStage;
|
|
45
|
+
this.parseIssue = parseIssue;
|
|
46
|
+
this.validationIssues = Object.freeze(schemaIssue === undefined ? [] : [schemaIssue]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function fail(code, stage) {
|
|
50
|
+
throw new AuthoringModelConnectionError(code, false, undefined, undefined, undefined, stage);
|
|
51
|
+
}
|
|
52
|
+
function plainRecord(value) {
|
|
53
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
54
|
+
return undefined;
|
|
55
|
+
const prototype = Object.getPrototypeOf(value);
|
|
56
|
+
return prototype === Object.prototype || prototype === null ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
function finishReason(value) {
|
|
59
|
+
switch (value) {
|
|
60
|
+
case "stop":
|
|
61
|
+
case "length":
|
|
62
|
+
case "tool_calls":
|
|
63
|
+
case "content_filter":
|
|
64
|
+
case "error":
|
|
65
|
+
return value;
|
|
66
|
+
case undefined:
|
|
67
|
+
case null:
|
|
68
|
+
return "missing";
|
|
69
|
+
default:
|
|
70
|
+
return "other";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function responseMetadata(finishReasonValue, content) {
|
|
74
|
+
let contentType;
|
|
75
|
+
if (content === null)
|
|
76
|
+
contentType = "null";
|
|
77
|
+
else if (Array.isArray(content))
|
|
78
|
+
contentType = "array";
|
|
79
|
+
else if (typeof content === "string")
|
|
80
|
+
contentType = "string";
|
|
81
|
+
else if (typeof content === "number")
|
|
82
|
+
contentType = "number";
|
|
83
|
+
else if (typeof content === "boolean")
|
|
84
|
+
contentType = "boolean";
|
|
85
|
+
else if (typeof content === "undefined")
|
|
86
|
+
contentType = "undefined";
|
|
87
|
+
else
|
|
88
|
+
contentType = "object";
|
|
89
|
+
const contentLength = typeof content === "string" || Array.isArray(content) ? content.length : null;
|
|
90
|
+
return Object.freeze({ finishReason: finishReason(finishReasonValue), contentType, contentLength });
|
|
91
|
+
}
|
|
92
|
+
function parseProviderJson(text, layer, metadata) {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// 不暴露 SyntaxError.message(V8 会在其中回显 provider 正文)。
|
|
98
|
+
const positionText = error instanceof Error ? /position (\d+)/u.exec(error.message)?.[1] : undefined;
|
|
99
|
+
const position = positionText === undefined ? null : Number(positionText);
|
|
100
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, undefined, metadata, undefined, "response_parse", Object.freeze({ layer, position }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function structuredJsonFrame(text) {
|
|
104
|
+
// 仅解包完整单围栏,不抽取子串、不修 JSON,也不容忍围栏外说明或多个文档。
|
|
105
|
+
const match = /^[\t\r\n ]*```json\r?\n([\s\S]*?)\r?\n```[\t\r\n ]*$/u.exec(text);
|
|
106
|
+
return Object.freeze(match === null ? { text, framing: "bare_json" } : { text: match[1], framing: "single_json_fence" });
|
|
107
|
+
}
|
|
108
|
+
function requireSafeText(value, maximum) {
|
|
109
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maximum || value.trim() !== value
|
|
110
|
+
|| value.normalize("NFC") !== value || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value)) {
|
|
111
|
+
fail("invalid_request");
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function canonicalBaseUrl(value) {
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(value);
|
|
118
|
+
if (url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "") {
|
|
119
|
+
throw new Error("unsafe URL");
|
|
120
|
+
}
|
|
121
|
+
return url.toString().replace(/\/$/u, "");
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function requireUsage(value, outputRequired) {
|
|
128
|
+
const usage = plainRecord(value);
|
|
129
|
+
const inputTokens = usage?.prompt_tokens;
|
|
130
|
+
const outputTokens = usage?.completion_tokens ?? (outputRequired ? undefined : 0);
|
|
131
|
+
const totalTokens = usage?.total_tokens;
|
|
132
|
+
if (!Number.isSafeInteger(inputTokens) || inputTokens < 0
|
|
133
|
+
|| !Number.isSafeInteger(outputTokens) || outputTokens < 0
|
|
134
|
+
|| !Number.isSafeInteger(totalTokens) || totalTokens < 0
|
|
135
|
+
|| totalTokens < inputTokens + outputTokens) {
|
|
136
|
+
fail("provider_response_invalid", "usage");
|
|
137
|
+
}
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
inputTokens: inputTokens,
|
|
140
|
+
outputTokens: outputTokens,
|
|
141
|
+
totalTokens: totalTokens,
|
|
142
|
+
requests: 1,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function addUsage(left, right) {
|
|
146
|
+
const inputTokens = left.inputTokens + right.inputTokens;
|
|
147
|
+
const outputTokens = left.outputTokens + right.outputTokens;
|
|
148
|
+
const totalTokens = left.totalTokens + right.totalTokens;
|
|
149
|
+
const requests = left.requests + right.requests;
|
|
150
|
+
if (![inputTokens, outputTokens, totalTokens, requests].every(Number.isSafeInteger)) {
|
|
151
|
+
fail("provider_response_invalid");
|
|
152
|
+
}
|
|
153
|
+
return Object.freeze({ inputTokens, outputTokens, totalTokens, requests });
|
|
154
|
+
}
|
|
155
|
+
function combineAbort(signal, timeoutMs) {
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
let timeout = false;
|
|
158
|
+
const abort = () => controller.abort();
|
|
159
|
+
if (signal?.aborted)
|
|
160
|
+
controller.abort();
|
|
161
|
+
else
|
|
162
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
timeout = true;
|
|
165
|
+
controller.abort();
|
|
166
|
+
}, timeoutMs);
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
timedOut: () => timeout,
|
|
170
|
+
dispose: () => {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
signal?.removeEventListener("abort", abort);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function requireApiKey(apiKey) {
|
|
177
|
+
if (typeof apiKey !== "string" || apiKey.length === 0 || apiKey.trim() !== apiKey || /[\r\n]/u.test(apiKey)) {
|
|
178
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function optionalApiKey(apiKey) {
|
|
182
|
+
if (apiKey === "")
|
|
183
|
+
return false;
|
|
184
|
+
requireApiKey(apiKey);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
function retryPolicy(input) {
|
|
188
|
+
const policy = Object.freeze({
|
|
189
|
+
maxAttempts: input?.maxAttempts ?? 3,
|
|
190
|
+
baseDelayMs: input?.baseDelayMs ?? 250,
|
|
191
|
+
maxDelayMs: input?.maxDelayMs ?? 10_000,
|
|
192
|
+
});
|
|
193
|
+
if (!Number.isSafeInteger(policy.maxAttempts) || policy.maxAttempts < 1 || policy.maxAttempts > 5
|
|
194
|
+
|| !Number.isSafeInteger(policy.baseDelayMs) || policy.baseDelayMs < 0 || policy.baseDelayMs > 60_000
|
|
195
|
+
|| !Number.isSafeInteger(policy.maxDelayMs) || policy.maxDelayMs < policy.baseDelayMs || policy.maxDelayMs > 120_000) {
|
|
196
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
197
|
+
}
|
|
198
|
+
return policy;
|
|
199
|
+
}
|
|
200
|
+
function retryDelay(response, attempt, policy) {
|
|
201
|
+
const header = response?.headers.get("retry-after") ?? null;
|
|
202
|
+
let requested;
|
|
203
|
+
if (header !== null && /^\d+(?:\.\d+)?$/u.test(header.trim()))
|
|
204
|
+
requested = Math.ceil(Number(header) * 1_000);
|
|
205
|
+
else if (header !== null) {
|
|
206
|
+
const date = Date.parse(header);
|
|
207
|
+
if (Number.isFinite(date))
|
|
208
|
+
requested = Math.max(0, date - Date.now());
|
|
209
|
+
}
|
|
210
|
+
const base = Math.min(policy.maxDelayMs, requested ?? policy.baseDelayMs * (2 ** Math.max(0, attempt - 1)));
|
|
211
|
+
const jitterCeiling = Math.min(250, Math.floor(base * 0.2));
|
|
212
|
+
const jitter = jitterCeiling === 0 ? 0 : Math.floor(Math.random() * (jitterCeiling + 1));
|
|
213
|
+
return Math.min(policy.maxDelayMs, base + jitter);
|
|
214
|
+
}
|
|
215
|
+
async function waitForRetry(delayMs, signal) {
|
|
216
|
+
if (signal?.aborted)
|
|
217
|
+
throw new AuthoringModelConnectionError("aborted", false);
|
|
218
|
+
if (delayMs === 0)
|
|
219
|
+
return;
|
|
220
|
+
await new Promise((resolve, reject) => {
|
|
221
|
+
const done = () => {
|
|
222
|
+
signal?.removeEventListener("abort", aborted);
|
|
223
|
+
resolve();
|
|
224
|
+
};
|
|
225
|
+
const aborted = () => {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
signal?.removeEventListener("abort", aborted);
|
|
228
|
+
reject(new AuthoringModelConnectionError("aborted", false));
|
|
229
|
+
};
|
|
230
|
+
const timer = setTimeout(done, delayMs);
|
|
231
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function createJsonPoster(options) {
|
|
235
|
+
return async (path, body, signal, timeoutOverrideMs) => {
|
|
236
|
+
let serialized;
|
|
237
|
+
try {
|
|
238
|
+
serialized = JSON.stringify(body);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
fail("invalid_request");
|
|
242
|
+
}
|
|
243
|
+
const timeoutMs = timeoutOverrideMs ?? options.timeoutMs;
|
|
244
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS)
|
|
245
|
+
fail("invalid_request");
|
|
246
|
+
for (let attempt = 1; attempt <= options.retry.maxAttempts; attempt += 1) {
|
|
247
|
+
if (signal?.aborted)
|
|
248
|
+
throw new AuthoringModelConnectionError("aborted", false);
|
|
249
|
+
const abort = combineAbort(signal, timeoutMs);
|
|
250
|
+
try {
|
|
251
|
+
options.onAttempt?.();
|
|
252
|
+
const response = await fetch(`${options.baseUrl}${path}`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: Object.freeze({ authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" }),
|
|
255
|
+
body: serialized,
|
|
256
|
+
signal: abort.signal,
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
260
|
+
if (retryable && attempt < options.retry.maxAttempts) {
|
|
261
|
+
const delayMs = retryDelay(response, attempt, options.retry);
|
|
262
|
+
await response.body?.cancel();
|
|
263
|
+
abort.dispose();
|
|
264
|
+
await waitForRetry(delayMs, signal);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
throw new AuthoringModelConnectionError("provider_rejected", retryable, response.status);
|
|
268
|
+
}
|
|
269
|
+
// 读流错误交给 transport retry;完整但非法的 HTTP JSON 保留独立 parse layer。
|
|
270
|
+
const decoded = parseProviderJson(await response.text(), "http_envelope");
|
|
271
|
+
const record = plainRecord(decoded);
|
|
272
|
+
if (record === undefined)
|
|
273
|
+
fail("provider_response_invalid", "response_shape");
|
|
274
|
+
return record;
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (signal?.aborted)
|
|
278
|
+
throw new AuthoringModelConnectionError("aborted", false);
|
|
279
|
+
if (error instanceof AuthoringModelConnectionError)
|
|
280
|
+
throw error;
|
|
281
|
+
const code = abort.timedOut() ? "timeout" : "transport_failed";
|
|
282
|
+
if (attempt < options.retry.maxAttempts) {
|
|
283
|
+
// 响应丢失时远端可能已处理;只有限重发原 serialized body,不推测其 token 消耗。
|
|
284
|
+
abort.dispose();
|
|
285
|
+
await waitForRetry(retryDelay(undefined, attempt, options.retry), signal);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
throw new AuthoringModelConnectionError(code, true);
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
abort.dispose();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw new AuthoringModelConnectionError("provider_rejected", true);
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function assertClosedJsonSchema(value, depth = 0) {
|
|
298
|
+
const schema = plainRecord(value);
|
|
299
|
+
if (schema === undefined || depth > 32)
|
|
300
|
+
fail("invalid_request");
|
|
301
|
+
const type = schema.type;
|
|
302
|
+
if (type !== "object" && type !== "array" && type !== "string" && type !== "number"
|
|
303
|
+
&& type !== "integer" && type !== "boolean" && type !== "null")
|
|
304
|
+
fail("invalid_request");
|
|
305
|
+
if (schema.enum !== undefined && !Array.isArray(schema.enum))
|
|
306
|
+
fail("invalid_request");
|
|
307
|
+
if (type === "object") {
|
|
308
|
+
const properties = plainRecord(schema.properties);
|
|
309
|
+
if (properties === undefined || schema.additionalProperties !== false || !Array.isArray(schema.required)
|
|
310
|
+
|| !schema.required.every((key) => typeof key === "string" && Object.hasOwn(properties, key)))
|
|
311
|
+
fail("invalid_request");
|
|
312
|
+
Object.values(properties).forEach((property) => assertClosedJsonSchema(property, depth + 1));
|
|
313
|
+
}
|
|
314
|
+
else if (type === "array") {
|
|
315
|
+
assertClosedJsonSchema(schema.items, depth + 1);
|
|
316
|
+
}
|
|
317
|
+
else if (type === "string" && schema.pattern !== undefined) {
|
|
318
|
+
if (typeof schema.pattern !== "string")
|
|
319
|
+
fail("invalid_request");
|
|
320
|
+
try {
|
|
321
|
+
new RegExp(schema.pattern, "u");
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
fail("invalid_request");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function schemaPath(path, key) {
|
|
329
|
+
return `${path}/${String(key).replace(/~/gu, "~0").replace(/\//gu, "~1")}`;
|
|
330
|
+
}
|
|
331
|
+
function closedJsonSchemaIssue(value, schema, path = "$", depth = 0) {
|
|
332
|
+
const issue = (keyword, issuePath = path) => Object.freeze({ path: issuePath, keyword });
|
|
333
|
+
if (depth > 32)
|
|
334
|
+
return issue("type");
|
|
335
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => Object.is(candidate, value)))
|
|
336
|
+
return issue("enum");
|
|
337
|
+
switch (schema.type) {
|
|
338
|
+
case "null": return value === null ? undefined : issue("type");
|
|
339
|
+
case "boolean": return typeof value === "boolean" ? undefined : issue("type");
|
|
340
|
+
case "number":
|
|
341
|
+
case "integer": {
|
|
342
|
+
if (typeof value !== "number" || !Number.isFinite(value) || (schema.type === "integer" && !Number.isSafeInteger(value)))
|
|
343
|
+
return issue("type");
|
|
344
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
345
|
+
return issue("minimum");
|
|
346
|
+
return typeof schema.maximum === "number" && value > schema.maximum ? issue("maximum") : undefined;
|
|
347
|
+
}
|
|
348
|
+
case "string": {
|
|
349
|
+
if (typeof value !== "string")
|
|
350
|
+
return issue("type");
|
|
351
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength)
|
|
352
|
+
return issue("minLength");
|
|
353
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength)
|
|
354
|
+
return issue("maxLength");
|
|
355
|
+
return typeof schema.pattern === "string" && !new RegExp(schema.pattern, "u").test(value) ? issue("pattern") : undefined;
|
|
356
|
+
}
|
|
357
|
+
case "array": {
|
|
358
|
+
if (!Array.isArray(value))
|
|
359
|
+
return issue("type");
|
|
360
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems)
|
|
361
|
+
return issue("minItems");
|
|
362
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems)
|
|
363
|
+
return issue("maxItems");
|
|
364
|
+
const items = plainRecord(schema.items);
|
|
365
|
+
if (items === undefined)
|
|
366
|
+
return issue("type");
|
|
367
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
368
|
+
const nested = closedJsonSchemaIssue(value[index], items, schemaPath(path, index), depth + 1);
|
|
369
|
+
if (nested !== undefined)
|
|
370
|
+
return nested;
|
|
371
|
+
}
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
case "object": {
|
|
375
|
+
const object = plainRecord(value);
|
|
376
|
+
const properties = plainRecord(schema.properties);
|
|
377
|
+
if (object === undefined || properties === undefined)
|
|
378
|
+
return issue("type");
|
|
379
|
+
const required = schema.required;
|
|
380
|
+
const missing = required.find((key) => !Object.hasOwn(object, key));
|
|
381
|
+
if (missing !== undefined)
|
|
382
|
+
return issue("required", schemaPath(path, missing));
|
|
383
|
+
const extraKey = Object.keys(object).find((key) => !Object.hasOwn(properties, key));
|
|
384
|
+
if (schema.additionalProperties === false && extraKey !== undefined) {
|
|
385
|
+
return issue("additionalProperties", SAFE_ADDITIONAL_PROPERTY_DIAGNOSTIC_KEYS.has(extraKey) ? schemaPath(path, extraKey) : path);
|
|
386
|
+
}
|
|
387
|
+
for (const [key, item] of Object.entries(object)) {
|
|
388
|
+
const property = plainRecord(properties[key]);
|
|
389
|
+
if (property === undefined)
|
|
390
|
+
return issue("additionalProperties");
|
|
391
|
+
const nested = closedJsonSchemaIssue(item, property, schemaPath(path, key), depth + 1);
|
|
392
|
+
if (nested !== undefined)
|
|
393
|
+
return nested;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
default: return issue("type");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function createEmbeddingPort(options) {
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
providerId: options.providerId,
|
|
403
|
+
modelId: options.modelId,
|
|
404
|
+
dimensions: options.dimensions,
|
|
405
|
+
origin: options.origin,
|
|
406
|
+
async embed(texts, requestOptions) {
|
|
407
|
+
if (options.available === false) {
|
|
408
|
+
throw new AuthoringModelConnectionError("embedding_credentials_missing", false);
|
|
409
|
+
}
|
|
410
|
+
if (!Array.isArray(texts) || texts.length === 0)
|
|
411
|
+
fail("invalid_request");
|
|
412
|
+
const inputKind = requestOptions?.inputKind ?? "document";
|
|
413
|
+
const input = texts.map((text) => requireSafeText(inputKind === "query"
|
|
414
|
+
? `Instruct: 检索虚构世界中能够回答问题的事实。\nQuery: ${text}` : text, 1_000_000));
|
|
415
|
+
const complete = [];
|
|
416
|
+
for (let offset = 0; offset < input.length; offset += EMBEDDING_BATCH_SIZE) {
|
|
417
|
+
const batch = input.slice(offset, offset + EMBEDDING_BATCH_SIZE);
|
|
418
|
+
const response = await options.post("/embeddings", {
|
|
419
|
+
model: options.modelId,
|
|
420
|
+
input: batch,
|
|
421
|
+
dimensions: options.dimensions,
|
|
422
|
+
encoding_format: "float",
|
|
423
|
+
}, requestOptions?.signal);
|
|
424
|
+
if (!Array.isArray(response.data) || response.data.length !== batch.length)
|
|
425
|
+
fail("provider_response_invalid");
|
|
426
|
+
const ordered = new Array(batch.length);
|
|
427
|
+
for (const itemValue of response.data) {
|
|
428
|
+
const item = plainRecord(itemValue);
|
|
429
|
+
if (item === undefined || !Number.isSafeInteger(item.index) || item.index < 0
|
|
430
|
+
|| item.index >= batch.length || ordered[item.index] !== undefined
|
|
431
|
+
|| !Array.isArray(item.embedding) || item.embedding.length !== options.dimensions
|
|
432
|
+
|| !item.embedding.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
433
|
+
fail("provider_response_invalid");
|
|
434
|
+
}
|
|
435
|
+
ordered[item.index] = Object.freeze([...item.embedding]);
|
|
436
|
+
}
|
|
437
|
+
if (ordered.some((vector) => vector === undefined))
|
|
438
|
+
fail("provider_response_invalid");
|
|
439
|
+
const usage = requireUsage(response.usage, false);
|
|
440
|
+
options.recordUsage(usage);
|
|
441
|
+
complete.push(...ordered);
|
|
442
|
+
}
|
|
443
|
+
return Object.freeze(complete);
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
export function createOpenAiCompatibleAuthoringModelConnection(config) {
|
|
448
|
+
requireApiKey(config.apiKey);
|
|
449
|
+
if (!AUTHORING_EMBEDDING_MODEL_ALLOWLIST.includes(config.embeddingModel)) {
|
|
450
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
451
|
+
}
|
|
452
|
+
if (!Number.isSafeInteger(config.embeddingDimensions) || config.embeddingDimensions <= 0) {
|
|
453
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
454
|
+
}
|
|
455
|
+
if (typeof config.structuredChatModel !== "string" || config.structuredChatModel.length === 0
|
|
456
|
+
|| !config.structuredChatModelAllowlist.includes(config.structuredChatModel)) {
|
|
457
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
458
|
+
}
|
|
459
|
+
const baseUrl = canonicalBaseUrl(config.baseUrl ?? DEFAULT_BASE_URL);
|
|
460
|
+
const baseUrlAllowlist = (config.baseUrlAllowlist ?? DEFAULT_BASE_URL_ALLOWLIST).map(canonicalBaseUrl);
|
|
461
|
+
if (!baseUrlAllowlist.includes(baseUrl)) {
|
|
462
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
463
|
+
}
|
|
464
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
465
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
466
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
467
|
+
}
|
|
468
|
+
let aggregate = Object.freeze({ inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 });
|
|
469
|
+
const post = createJsonPoster({ apiKey: config.apiKey, baseUrl, timeoutMs, retry: retryPolicy(config.retry) });
|
|
470
|
+
const embedding = createEmbeddingPort({
|
|
471
|
+
providerId: "openai-compatible",
|
|
472
|
+
modelId: config.embeddingModel,
|
|
473
|
+
dimensions: config.embeddingDimensions,
|
|
474
|
+
origin: new URL(baseUrl).origin,
|
|
475
|
+
post,
|
|
476
|
+
recordUsage: (usage) => { aggregate = addUsage(aggregate, usage); },
|
|
477
|
+
});
|
|
478
|
+
const structuredJson = Object.freeze({
|
|
479
|
+
providerId: "openai-compatible",
|
|
480
|
+
modelId: config.structuredChatModel,
|
|
481
|
+
async generate(request) {
|
|
482
|
+
if (request.reasoningEffort !== undefined)
|
|
483
|
+
fail("invalid_request");
|
|
484
|
+
const schemaName = requireSafeText(request.schemaName, 64);
|
|
485
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(schemaName) || plainRecord(request.jsonSchema) === undefined
|
|
486
|
+
|| !Array.isArray(request.messages) || request.messages.length === 0 || request.messages.length > 128) {
|
|
487
|
+
fail("invalid_request");
|
|
488
|
+
}
|
|
489
|
+
const messages = request.messages.map((message) => {
|
|
490
|
+
if (message.role !== "system" && message.role !== "user" && message.role !== "assistant")
|
|
491
|
+
fail("invalid_request");
|
|
492
|
+
return Object.freeze({ role: message.role, content: requireSafeText(message.content, 1_000_000) });
|
|
493
|
+
});
|
|
494
|
+
const response = await post("/chat/completions", {
|
|
495
|
+
model: config.structuredChatModel,
|
|
496
|
+
messages,
|
|
497
|
+
response_format: {
|
|
498
|
+
type: "json_schema",
|
|
499
|
+
json_schema: { name: schemaName, strict: true, schema: request.jsonSchema },
|
|
500
|
+
},
|
|
501
|
+
}, request.signal);
|
|
502
|
+
if (!Array.isArray(response.choices) || response.choices.length !== 1)
|
|
503
|
+
fail("provider_response_invalid");
|
|
504
|
+
const choice = plainRecord(response.choices[0]);
|
|
505
|
+
const message = plainRecord(choice?.message);
|
|
506
|
+
const metadata = responseMetadata(choice?.finish_reason, message?.content);
|
|
507
|
+
const usage = requireUsage(response.usage, true);
|
|
508
|
+
aggregate = addUsage(aggregate, usage);
|
|
509
|
+
if (metadata.finishReason === "length") {
|
|
510
|
+
throw new AuthoringModelConnectionError("provider_output_truncated", false, undefined, metadata);
|
|
511
|
+
}
|
|
512
|
+
if (typeof message?.content !== "string") {
|
|
513
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, undefined, metadata);
|
|
514
|
+
}
|
|
515
|
+
let value;
|
|
516
|
+
try {
|
|
517
|
+
value = JSON.parse(message.content);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, undefined, metadata);
|
|
521
|
+
}
|
|
522
|
+
return Object.freeze({ value: value, usage });
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
return Object.freeze({ embedding, structuredJson, usage: () => aggregate });
|
|
526
|
+
}
|
|
527
|
+
function requireDeepSeekUsage(value) {
|
|
528
|
+
const usage = plainRecord(value);
|
|
529
|
+
const details = plainRecord(usage?.input_tokens_details);
|
|
530
|
+
const inputTokens = usage?.input_tokens;
|
|
531
|
+
const outputTokens = usage?.output_tokens;
|
|
532
|
+
const totalTokens = usage?.total_tokens;
|
|
533
|
+
const cachedInputTokens = details?.cached_tokens;
|
|
534
|
+
if (!Number.isSafeInteger(inputTokens) || inputTokens < 0
|
|
535
|
+
|| !Number.isSafeInteger(outputTokens) || outputTokens < 0
|
|
536
|
+
|| !Number.isSafeInteger(totalTokens) || totalTokens < 0
|
|
537
|
+
|| totalTokens < inputTokens + outputTokens
|
|
538
|
+
|| (cachedInputTokens !== undefined && (!Number.isSafeInteger(cachedInputTokens)
|
|
539
|
+
|| cachedInputTokens < 0 || cachedInputTokens > inputTokens))) {
|
|
540
|
+
fail("provider_response_invalid", "usage");
|
|
541
|
+
}
|
|
542
|
+
return Object.freeze({
|
|
543
|
+
inputTokens: inputTokens,
|
|
544
|
+
outputTokens: outputTokens,
|
|
545
|
+
totalTokens: totalTokens,
|
|
546
|
+
requests: 1,
|
|
547
|
+
providerId: "deepseek",
|
|
548
|
+
modelId: DEEPSEEK_V4_FLASH_MODEL,
|
|
549
|
+
...(cachedInputTokens === undefined ? {} : { cachedInputTokens: cachedInputTokens }),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function addProviderUsage(left, right) {
|
|
553
|
+
const totals = addUsage(left, right);
|
|
554
|
+
const cachedInputTokens = (left.cachedInputTokens ?? 0) + (right.cachedInputTokens ?? 0);
|
|
555
|
+
if (!Number.isSafeInteger(cachedInputTokens))
|
|
556
|
+
fail("provider_response_invalid");
|
|
557
|
+
return Object.freeze({
|
|
558
|
+
...totals,
|
|
559
|
+
...(right.providerId === undefined ? {} : { providerId: right.providerId }),
|
|
560
|
+
...(right.modelId === undefined ? {} : { modelId: right.modelId }),
|
|
561
|
+
cachedInputTokens,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function taskInstruction(mode) {
|
|
565
|
+
switch (mode) {
|
|
566
|
+
case "extraction": return "执行结构化事实抽取;不得省略、猜测或添加 schema 外字段。";
|
|
567
|
+
case "simulation_retrieval": return "执行模拟检索与结构化排序;只依据输入候选,不得虚构候选。";
|
|
568
|
+
case "full_book_planning": return "对完整长文执行结构化规划;覆盖全局结构并保持可追溯,不得输出 schema 外字段。";
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function isTaskMode(value) {
|
|
572
|
+
return value === "extraction" || value === "simulation_retrieval" || value === "full_book_planning";
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 生产作者模型连接:embedding 固定走 OpenRouter/Qwen3,structured JSON 固定走
|
|
576
|
+
* DeepSeek 官方 Responses API/V4 Flash;两个 credential 永远位于不同 poster closure。
|
|
577
|
+
*/
|
|
578
|
+
export function createProductionAuthoringModelConnection(config) {
|
|
579
|
+
// deferred ingestion 只消费 structured port;另一份凭据在真正被消费前验证。
|
|
580
|
+
const embeddingAvailable = optionalApiKey(config.embedding.apiKey);
|
|
581
|
+
const structuredAvailable = optionalApiKey(config.structured.apiKey);
|
|
582
|
+
if ((config.embedding.baseUrl !== undefined && config.embedding.baseUrl !== OPENROUTER_PRODUCTION_BASE_URL)
|
|
583
|
+
|| (config.embedding.model !== undefined && config.embedding.model !== "qwen/qwen3-embedding-8b")
|
|
584
|
+
|| (config.embedding.dimensions !== undefined && config.embedding.dimensions !== 1024)
|
|
585
|
+
|| (config.structured.baseUrl !== undefined && config.structured.baseUrl !== DEEPSEEK_PRODUCTION_BASE_URL)
|
|
586
|
+
|| (config.structured.model !== undefined && config.structured.model !== DEEPSEEK_V4_FLASH_MODEL)
|
|
587
|
+
|| (config.structured.thinking !== undefined && config.structured.thinking !== "disabled")
|
|
588
|
+
|| (config.structured.responseFormat !== undefined && config.structured.responseFormat !== "json_schema")
|
|
589
|
+
|| (config.structured.defaultTaskMode !== undefined && !isTaskMode(config.structured.defaultTaskMode))) {
|
|
590
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
591
|
+
}
|
|
592
|
+
const embeddingTimeoutMs = config.embedding.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
593
|
+
const structuredTimeoutMs = config.structured.timeoutMs ?? 120_000;
|
|
594
|
+
const defaultMaxOutputTokens = config.structured.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
595
|
+
const temperature = config.structured.temperature === undefined ? DEFAULT_STRUCTURED_TEMPERATURE : config.structured.temperature;
|
|
596
|
+
if (!Number.isSafeInteger(embeddingTimeoutMs) || embeddingTimeoutMs < 1 || embeddingTimeoutMs > MAX_TIMEOUT_MS
|
|
597
|
+
|| !Number.isSafeInteger(structuredTimeoutMs) || structuredTimeoutMs < 1 || structuredTimeoutMs > MAX_TIMEOUT_MS
|
|
598
|
+
|| !Number.isSafeInteger(defaultMaxOutputTokens) || defaultMaxOutputTokens < 1
|
|
599
|
+
|| defaultMaxOutputTokens > MAX_DEEPSEEK_OUTPUT_TOKENS
|
|
600
|
+
|| !Number.isFinite(temperature) || temperature < 0 || temperature > 2) {
|
|
601
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
602
|
+
}
|
|
603
|
+
const policy = retryPolicy(config.retry);
|
|
604
|
+
let embeddingAggregate = Object.freeze({ inputTokens: 0, outputTokens: 0, totalTokens: 0, requests: 0 });
|
|
605
|
+
let embeddingRequestAttempts = 0;
|
|
606
|
+
let structuredRequestAttempts = 0;
|
|
607
|
+
const embeddingPost = createJsonPoster({
|
|
608
|
+
apiKey: config.embedding.apiKey,
|
|
609
|
+
baseUrl: OPENROUTER_PRODUCTION_BASE_URL,
|
|
610
|
+
timeoutMs: embeddingTimeoutMs,
|
|
611
|
+
retry: policy,
|
|
612
|
+
onAttempt: () => { embeddingRequestAttempts += 1; },
|
|
613
|
+
});
|
|
614
|
+
const embedding = createEmbeddingPort({
|
|
615
|
+
providerId: "openrouter",
|
|
616
|
+
modelId: "qwen/qwen3-embedding-8b",
|
|
617
|
+
dimensions: 1024,
|
|
618
|
+
origin: new URL(OPENROUTER_PRODUCTION_BASE_URL).origin,
|
|
619
|
+
post: embeddingPost,
|
|
620
|
+
available: embeddingAvailable,
|
|
621
|
+
recordUsage: (usage) => { embeddingAggregate = addUsage(embeddingAggregate, usage); },
|
|
622
|
+
});
|
|
623
|
+
const deepSeekPost = createJsonPoster({
|
|
624
|
+
apiKey: config.structured.apiKey,
|
|
625
|
+
baseUrl: DEEPSEEK_PRODUCTION_BASE_URL,
|
|
626
|
+
timeoutMs: structuredTimeoutMs,
|
|
627
|
+
retry: policy,
|
|
628
|
+
onAttempt: () => { structuredRequestAttempts += 1; },
|
|
629
|
+
});
|
|
630
|
+
let structuredAggregate = Object.freeze({
|
|
631
|
+
inputTokens: 0,
|
|
632
|
+
outputTokens: 0,
|
|
633
|
+
totalTokens: 0,
|
|
634
|
+
requests: 0,
|
|
635
|
+
providerId: "deepseek",
|
|
636
|
+
modelId: DEEPSEEK_V4_FLASH_MODEL,
|
|
637
|
+
cachedInputTokens: 0,
|
|
638
|
+
});
|
|
639
|
+
const structuredJson = Object.freeze({
|
|
640
|
+
providerId: "deepseek",
|
|
641
|
+
modelId: DEEPSEEK_V4_FLASH_MODEL,
|
|
642
|
+
async generate(request) {
|
|
643
|
+
if (!structuredAvailable) {
|
|
644
|
+
throw new AuthoringModelConnectionError("structured_credentials_missing", false);
|
|
645
|
+
}
|
|
646
|
+
const schemaName = requireSafeText(request.schemaName, 64);
|
|
647
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(schemaName)
|
|
648
|
+
|| !Array.isArray(request.messages) || request.messages.length === 0 || request.messages.length > 128) {
|
|
649
|
+
fail("invalid_request");
|
|
650
|
+
}
|
|
651
|
+
assertClosedJsonSchema(request.jsonSchema);
|
|
652
|
+
const reasoningEffort = request.reasoningEffort ?? "none";
|
|
653
|
+
if ((request.reasoningEffort !== undefined && request.reasoningEffort !== "none" && request.reasoningEffort !== "low")
|
|
654
|
+
|| (config.structured.thinking === "disabled" && reasoningEffort !== "none"))
|
|
655
|
+
fail("invalid_request");
|
|
656
|
+
const messages = request.messages.map((message) => {
|
|
657
|
+
if (message.role !== "system" && message.role !== "user" && message.role !== "assistant")
|
|
658
|
+
fail("invalid_request");
|
|
659
|
+
return Object.freeze({ role: message.role, content: requireSafeText(message.content, 1_000_000) });
|
|
660
|
+
});
|
|
661
|
+
const mode = request.taskMode ?? config.structured.defaultTaskMode ?? "extraction";
|
|
662
|
+
if (!isTaskMode(mode))
|
|
663
|
+
fail("invalid_request");
|
|
664
|
+
const maxOutputTokens = request.maxOutputTokens ?? defaultMaxOutputTokens;
|
|
665
|
+
if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens < 1 || maxOutputTokens > MAX_DEEPSEEK_OUTPUT_TOKENS) {
|
|
666
|
+
fail("invalid_request");
|
|
667
|
+
}
|
|
668
|
+
// 只派生根字段的类型提示;完整规则仍仅由同一 text.format.schema 提供。
|
|
669
|
+
const properties = plainRecord(request.jsonSchema.properties);
|
|
670
|
+
const requiredFieldTypes = request.jsonSchema.type === "object"
|
|
671
|
+
? Object.fromEntries(request.jsonSchema.required.map((key) => [key, plainRecord(properties?.[key])?.type]))
|
|
672
|
+
: undefined;
|
|
673
|
+
const response = await deepSeekPost("/responses", {
|
|
674
|
+
model: DEEPSEEK_V4_FLASH_MODEL,
|
|
675
|
+
reasoning: { effort: reasoningEffort },
|
|
676
|
+
temperature,
|
|
677
|
+
max_output_tokens: maxOutputTokens,
|
|
678
|
+
instructions: `${taskInstruction(mode)}\n直接输出单个裸 JSON 文本,禁止 Markdown 代码围栏、前言、解释和后记。输出数据实例,不是 Schema;响应格式中的 JSON Schema 是验证规则,不是待输出内容,不得复述其定义。根值类型为 ${String(request.jsonSchema.type)}。${requiredFieldTypes === undefined ? "" : `根对象必需字段及各自值类型为 ${JSON.stringify(requiredFieldTypes)};这些字段必须填写根据任务得出的实际值,而不是类型描述对象或类型名称。`}所有字段和嵌套值仍须符合响应格式中的同一 Schema;不要输出示例、占位值或 Schema 本身。`,
|
|
679
|
+
input: messages,
|
|
680
|
+
text: { format: { type: "json_schema", name: schemaName, schema: request.jsonSchema } },
|
|
681
|
+
}, request.signal, request.timeoutMs);
|
|
682
|
+
const usage = requireDeepSeekUsage(response.usage);
|
|
683
|
+
structuredAggregate = addProviderUsage(structuredAggregate, usage);
|
|
684
|
+
if (response.model !== DEEPSEEK_V4_FLASH_MODEL) {
|
|
685
|
+
throw new AuthoringModelConnectionError("provider_model_mismatch", false);
|
|
686
|
+
}
|
|
687
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
688
|
+
const messagesOutput = output.map(plainRecord).filter((item) => item?.type === "message");
|
|
689
|
+
const content = messagesOutput.length === 1 && Array.isArray(messagesOutput[0]?.content)
|
|
690
|
+
? messagesOutput[0].content
|
|
691
|
+
: [];
|
|
692
|
+
const textParts = content.map(plainRecord).filter((item) => item?.type === "output_text");
|
|
693
|
+
const text = textParts.length === 1 ? textParts[0]?.text : undefined;
|
|
694
|
+
const incompleteDetails = plainRecord(response.incomplete_details);
|
|
695
|
+
const metadata = responseMetadata(response.status === "incomplete" && incompleteDetails?.reason === "max_output_tokens" ? "length" : response.status, text);
|
|
696
|
+
if (response.status === "incomplete" && incompleteDetails?.reason === "max_output_tokens") {
|
|
697
|
+
throw new AuthoringModelConnectionError("provider_output_truncated", false, undefined, metadata);
|
|
698
|
+
}
|
|
699
|
+
if (response.status !== "completed" || typeof text !== "string") {
|
|
700
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, undefined, metadata, undefined, "response_shape");
|
|
701
|
+
}
|
|
702
|
+
const framed = structuredJsonFrame(text);
|
|
703
|
+
const framedMetadata = Object.freeze({ ...metadata, framing: framed.framing });
|
|
704
|
+
const value = parseProviderJson(framed.text, "model_output", framedMetadata);
|
|
705
|
+
const schemaIssue = closedJsonSchemaIssue(value, request.jsonSchema);
|
|
706
|
+
if (schemaIssue !== undefined) {
|
|
707
|
+
throw new AuthoringModelConnectionError("provider_response_invalid", false, undefined, framedMetadata, schemaIssue, "closed_schema");
|
|
708
|
+
}
|
|
709
|
+
return Object.freeze({ value: value, usage, framing: framed.framing });
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
return Object.freeze({
|
|
713
|
+
embedding,
|
|
714
|
+
structuredJson,
|
|
715
|
+
assertAvailable(ports) {
|
|
716
|
+
for (const port of ports) {
|
|
717
|
+
if (port === "embedding") {
|
|
718
|
+
if (!embeddingAvailable)
|
|
719
|
+
throw new AuthoringModelConnectionError("embedding_credentials_missing", false);
|
|
720
|
+
}
|
|
721
|
+
else if (port === "structured") {
|
|
722
|
+
if (!structuredAvailable)
|
|
723
|
+
throw new AuthoringModelConnectionError("structured_credentials_missing", false);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
throw new AuthoringModelConnectionError("invalid_configuration", false);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
usage: () => {
|
|
731
|
+
const total = addUsage(embeddingAggregate, structuredAggregate);
|
|
732
|
+
const byProviderModel = Object.freeze([
|
|
733
|
+
Object.freeze({
|
|
734
|
+
providerId: "openrouter",
|
|
735
|
+
modelId: "qwen/qwen3-embedding-8b",
|
|
736
|
+
inputTokens: embeddingAggregate.inputTokens,
|
|
737
|
+
outputTokens: embeddingAggregate.outputTokens,
|
|
738
|
+
totalTokens: embeddingAggregate.totalTokens,
|
|
739
|
+
requests: embeddingAggregate.requests,
|
|
740
|
+
requestAttempts: embeddingRequestAttempts,
|
|
741
|
+
}),
|
|
742
|
+
Object.freeze({
|
|
743
|
+
providerId: "deepseek",
|
|
744
|
+
modelId: DEEPSEEK_V4_FLASH_MODEL,
|
|
745
|
+
inputTokens: structuredAggregate.inputTokens,
|
|
746
|
+
outputTokens: structuredAggregate.outputTokens,
|
|
747
|
+
totalTokens: structuredAggregate.totalTokens,
|
|
748
|
+
cachedInputTokens: structuredAggregate.cachedInputTokens ?? 0,
|
|
749
|
+
requests: structuredAggregate.requests,
|
|
750
|
+
requestAttempts: structuredRequestAttempts,
|
|
751
|
+
}),
|
|
752
|
+
]);
|
|
753
|
+
return Object.freeze({ ...total, requestAttempts: embeddingRequestAttempts + structuredRequestAttempts,
|
|
754
|
+
cachedInputTokens: structuredAggregate.cachedInputTokens ?? 0, byProviderModel });
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
}
|