narraeon 0.1.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 +201 -0
- package/README.md +164 -0
- package/README.zh-CN.md +164 -0
- package/dist/node/cli/NarraeonCli.js +163 -0
- package/dist/node/cli/main.js +38 -0
- package/dist/node/protocol/appPreferences.js +5 -0
- package/dist/node/protocol/contentTree.js +14 -0
- package/dist/node/protocol/modelConnections.js +110 -0
- package/dist/node/protocol/playerViews.js +1 -0
- package/dist/node/protocol/v1.js +278 -0
- package/dist/node/runtime/V1Runtime.js +625 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
- package/dist/node/runtime/config/appPaths.js +19 -0
- package/dist/node/runtime/content/ContentLimits.js +1 -0
- package/dist/node/runtime/content/ContentTreeFile.js +1 -0
- package/dist/node/runtime/content/ContentWorkspace.js +601 -0
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
- package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
- package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
- package/dist/node/runtime/model/AiFailureLog.js +332 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
- package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
- package/dist/node/runtime/model/ModelHost.js +89 -0
- package/dist/node/runtime/model/ModelHostStream.js +367 -0
- package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
- package/dist/node/runtime/model/ProviderStream.js +90 -0
- package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
- package/dist/node/runtime/play/PlayCallChain.js +1280 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
- package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
- package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
- package/dist/node/runtime/world/WorldStateRevision.js +75 -0
- package/dist/node/server/createRuntime.js +6 -0
- package/dist/node/server/createServer.js +218 -0
- package/dist/node/server/main.js +25 -0
- package/dist/node/server/startWebServer.js +28 -0
- package/dist/node/shared/default-play-prompts.js +85 -0
- package/dist/node/shared/default-preset-host.js +538 -0
- package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
- package/dist/node/shared/first-party-action-choices.js +164 -0
- package/dist/node/shared/first-party-generic-panels.js +105 -0
- package/dist/node/shared/first-party-play-preset-templates.js +41 -0
- package/dist/node/shared/first-party-player-view.js +179 -0
- package/dist/node/shared/play-preset-prompt-roles.js +4 -0
- package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
- package/dist/web/assets/index-DDx1itzT.js +1192 -0
- package/dist/web/index.html +14 -0
- package/package.json +86 -0
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
5
|
+
import { ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
6
|
+
import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
|
|
7
|
+
import { aggregateAnthropicSettingStream, aggregateChatSettingStream, } from "./SettingAuthorStream.js";
|
|
8
|
+
import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
|
|
9
|
+
import { defaultRuntimeToolDefinitionStrategy, } from "../prompt/FileNativeToolRegistry.js";
|
|
10
|
+
import { AiExchangeCapture, errorDescription, } from "./AiFailureLog.js";
|
|
11
|
+
export class FileNativeModelHost {
|
|
12
|
+
#connection;
|
|
13
|
+
#fetch;
|
|
14
|
+
#failureLog;
|
|
15
|
+
constructor(connection, fetchImplementation = fetch, failureLog) {
|
|
16
|
+
this.#connection = structuredClone(connection);
|
|
17
|
+
this.#fetch = fetchImplementation;
|
|
18
|
+
this.#failureLog = failureLog;
|
|
19
|
+
}
|
|
20
|
+
binding() {
|
|
21
|
+
const { provider, modelId, contextWindowTokens, maxOutputTokens } = this.#connection;
|
|
22
|
+
return {
|
|
23
|
+
provider,
|
|
24
|
+
modelId,
|
|
25
|
+
contextWindowTokens,
|
|
26
|
+
maxOutputTokens,
|
|
27
|
+
endpointFingerprint: createHash("sha256")
|
|
28
|
+
.update(`${provider}\0${this.#connection.baseUrl}`)
|
|
29
|
+
.digest("hex"),
|
|
30
|
+
protocolConfigFingerprint: createHash("sha256")
|
|
31
|
+
.update(JSON.stringify({
|
|
32
|
+
provider,
|
|
33
|
+
stream: true,
|
|
34
|
+
toolChoice: "runtime_strategy",
|
|
35
|
+
responseStorage: provider === "openai_responses" ? "disabled" : "provider_default",
|
|
36
|
+
encryptedReasoningContinuation: provider === "openai_responses" ? "included" : "not_requested",
|
|
37
|
+
anthropicVersion: provider === "anthropic_messages" ? "2023-06-01" : null,
|
|
38
|
+
}))
|
|
39
|
+
.digest("hex"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async exchange(request, observer) {
|
|
43
|
+
await trace("model_host_exchange", modelHostTrace(request));
|
|
44
|
+
if (this.#connection.provider === "chat_completions")
|
|
45
|
+
return this.#chat(request, observer?.onDelta);
|
|
46
|
+
if (this.#connection.provider === "openai_responses")
|
|
47
|
+
return this.#responses(request, observer?.onDelta);
|
|
48
|
+
return this.#anthropic(request, observer?.onDelta);
|
|
49
|
+
}
|
|
50
|
+
async #responses(request, onDelta) {
|
|
51
|
+
return openAIResponsesRequest({
|
|
52
|
+
connection: this.#connection,
|
|
53
|
+
fetchImplementation: this.#fetch,
|
|
54
|
+
input: [
|
|
55
|
+
...request.bootstrap.provider.messages.map(responsesBootstrapMessage),
|
|
56
|
+
...responsesAppend(request),
|
|
57
|
+
],
|
|
58
|
+
tools: providerToolDefinitions(request).map((tool) => ({
|
|
59
|
+
name: tool.name,
|
|
60
|
+
description: tool.description,
|
|
61
|
+
parameters: tool.inputSchema,
|
|
62
|
+
})),
|
|
63
|
+
allowedTools: providerAllowedTools(request),
|
|
64
|
+
toolStrategy: providerToolStrategy(request, this.#connection.provider),
|
|
65
|
+
hasFrozenToolPolicy: hasFrozenToolPolicy(request),
|
|
66
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
67
|
+
tracePrefix: "responses",
|
|
68
|
+
...(this.#failureLog === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { failureLog: this.#failureLog }),
|
|
71
|
+
diagnosticContext: modelDiagnosticContext(request),
|
|
72
|
+
...(onDelta === undefined ? {} : { onDelta }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async #chat(request, onDelta) {
|
|
76
|
+
const messages = [
|
|
77
|
+
...request.bootstrap.provider.messages,
|
|
78
|
+
...chatAppend(request),
|
|
79
|
+
];
|
|
80
|
+
const tools = providerToolDefinitions(request);
|
|
81
|
+
const allowedTools = providerAllowedTools(request);
|
|
82
|
+
const hasPolicy = hasFrozenToolPolicy(request);
|
|
83
|
+
const body = {
|
|
84
|
+
model: this.#connection.modelId,
|
|
85
|
+
messages,
|
|
86
|
+
...(tools.length === 0
|
|
87
|
+
? {}
|
|
88
|
+
: {
|
|
89
|
+
tools: tools.map((tool) => ({
|
|
90
|
+
type: "function",
|
|
91
|
+
function: {
|
|
92
|
+
name: tool.name,
|
|
93
|
+
description: tool.description,
|
|
94
|
+
parameters: tool.inputSchema,
|
|
95
|
+
},
|
|
96
|
+
})),
|
|
97
|
+
}),
|
|
98
|
+
...(hasPolicy && allowedTools.length === 0
|
|
99
|
+
? { tool_choice: "none" }
|
|
100
|
+
: tools.length === 0
|
|
101
|
+
? {}
|
|
102
|
+
: { tool_choice: "auto" }),
|
|
103
|
+
max_tokens: request.maxOutputTokens,
|
|
104
|
+
stream: true,
|
|
105
|
+
stream_options: { include_usage: true },
|
|
106
|
+
};
|
|
107
|
+
const bodyJson = JSON.stringify(body);
|
|
108
|
+
await trace("chat_request", body);
|
|
109
|
+
const url = providerUrl(this.#connection);
|
|
110
|
+
const capture = providerCapture(this.#failureLog, this.#connection.provider, url, modelDiagnosticContext(request), bodyJson);
|
|
111
|
+
const diagnosticDelta = captureDelta(capture, onDelta);
|
|
112
|
+
return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
|
|
113
|
+
const response = observe(await dispatchProviderRequest(this.#fetch, url, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: {
|
|
116
|
+
Accept: "text/event-stream",
|
|
117
|
+
Authorization: `Bearer ${this.#connection.apiKey}`,
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
},
|
|
120
|
+
body: bodyJson,
|
|
121
|
+
}, "Chat Completions"));
|
|
122
|
+
if (!response.ok)
|
|
123
|
+
throw await providerError(response);
|
|
124
|
+
if (isProviderEventStream(response)) {
|
|
125
|
+
const streamed = await providerStreamResult(response, "Chat Completions", (body) => aggregateChatModelStream(body, diagnosticDelta));
|
|
126
|
+
capture?.setReasoning(streamed.reasoningContent);
|
|
127
|
+
await trace("chat_response", streamed);
|
|
128
|
+
return {
|
|
129
|
+
...(streamed.content === "" ? {} : { text: streamed.content }),
|
|
130
|
+
...(streamed.reasoningContent === ""
|
|
131
|
+
? {}
|
|
132
|
+
: { reasoningContent: streamed.reasoningContent }),
|
|
133
|
+
providerState: {
|
|
134
|
+
protocol: "chat_completions",
|
|
135
|
+
assistantMessage: cloneProviderValue(streamed.assistantMessage),
|
|
136
|
+
},
|
|
137
|
+
usage: normalizeChatUsage(streamed.usage),
|
|
138
|
+
toolCalls: streamed.toolCalls.map((call) => ({
|
|
139
|
+
id: call.id,
|
|
140
|
+
name: call.name,
|
|
141
|
+
arguments: parseArguments(call.arguments),
|
|
142
|
+
})),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const rawPayload = await providerJson(response, "Chat Completions");
|
|
146
|
+
if (!isRecord(rawPayload) ||
|
|
147
|
+
!Array.isArray(rawPayload.choices) ||
|
|
148
|
+
!isRecord(rawPayload.choices[0]) ||
|
|
149
|
+
!isRecord(rawPayload.choices[0].message))
|
|
150
|
+
throw unknownProviderResponse("Chat Completions");
|
|
151
|
+
const payload = rawPayload;
|
|
152
|
+
await trace("chat_response", payload);
|
|
153
|
+
const message = payload.choices?.[0]?.message;
|
|
154
|
+
if (message === undefined)
|
|
155
|
+
throw unknownProviderResponse("Chat Completions");
|
|
156
|
+
if ((message.content !== undefined &&
|
|
157
|
+
message.content !== null &&
|
|
158
|
+
typeof message.content !== "string") ||
|
|
159
|
+
(message.reasoning_content !== undefined &&
|
|
160
|
+
message.reasoning_content !== null &&
|
|
161
|
+
typeof message.reasoning_content !== "string") ||
|
|
162
|
+
(message.tool_calls !== undefined &&
|
|
163
|
+
(!Array.isArray(message.tool_calls) ||
|
|
164
|
+
message.tool_calls.some((call) => !isRecord(call) ||
|
|
165
|
+
typeof call.id !== "string" ||
|
|
166
|
+
!isRecord(call.function) ||
|
|
167
|
+
typeof call.function.name !== "string" ||
|
|
168
|
+
typeof call.function.arguments !== "string"))))
|
|
169
|
+
throw unknownProviderResponse("Chat Completions");
|
|
170
|
+
capture?.setReasoning(message.reasoning_content ?? undefined);
|
|
171
|
+
return {
|
|
172
|
+
...(message.content == null ? {} : { text: message.content }),
|
|
173
|
+
...(message.reasoning_content == null
|
|
174
|
+
? {}
|
|
175
|
+
: { reasoningContent: message.reasoning_content }),
|
|
176
|
+
providerState: {
|
|
177
|
+
protocol: "chat_completions",
|
|
178
|
+
assistantMessage: cloneProviderValue(message),
|
|
179
|
+
},
|
|
180
|
+
usage: normalizeChatUsage(payload.usage),
|
|
181
|
+
toolCalls: (message.tool_calls ?? []).map((call) => ({
|
|
182
|
+
id: call.id,
|
|
183
|
+
name: call.function.name,
|
|
184
|
+
arguments: parseArguments(call.function.arguments),
|
|
185
|
+
})),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async #anthropic(request, onDelta) {
|
|
190
|
+
const tools = providerToolDefinitions(request);
|
|
191
|
+
const body = {
|
|
192
|
+
model: this.#connection.modelId,
|
|
193
|
+
system: request.bootstrap.provider.system,
|
|
194
|
+
messages: [
|
|
195
|
+
...request.bootstrap.provider.messages.filter(({ role }) => role !== "system"),
|
|
196
|
+
...anthropicAppend(request),
|
|
197
|
+
],
|
|
198
|
+
...(tools.length === 0
|
|
199
|
+
? {}
|
|
200
|
+
: {
|
|
201
|
+
tools: tools.map((tool) => ({
|
|
202
|
+
name: tool.name,
|
|
203
|
+
description: tool.description,
|
|
204
|
+
input_schema: tool.inputSchema,
|
|
205
|
+
})),
|
|
206
|
+
}),
|
|
207
|
+
max_tokens: request.maxOutputTokens,
|
|
208
|
+
stream: true,
|
|
209
|
+
};
|
|
210
|
+
const bodyJson = JSON.stringify(body);
|
|
211
|
+
await trace("anthropic_request", body);
|
|
212
|
+
const url = providerUrl(this.#connection);
|
|
213
|
+
const capture = providerCapture(this.#failureLog, this.#connection.provider, url, modelDiagnosticContext(request), bodyJson);
|
|
214
|
+
const diagnosticDelta = captureDelta(capture, onDelta);
|
|
215
|
+
return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
|
|
216
|
+
const response = observe(await dispatchProviderRequest(this.#fetch, url, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: {
|
|
219
|
+
Accept: "text/event-stream",
|
|
220
|
+
"anthropic-version": "2023-06-01",
|
|
221
|
+
"x-api-key": this.#connection.apiKey,
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
},
|
|
224
|
+
body: bodyJson,
|
|
225
|
+
}, "Anthropic Messages"));
|
|
226
|
+
if (!response.ok)
|
|
227
|
+
throw await providerError(response);
|
|
228
|
+
if (isProviderEventStream(response)) {
|
|
229
|
+
const streamed = await providerStreamResult(response, "Anthropic Messages", (body) => aggregateAnthropicModelStream(body, diagnosticDelta));
|
|
230
|
+
capture?.setReasoning(streamed.reasoningContent);
|
|
231
|
+
await trace("anthropic_response", streamed);
|
|
232
|
+
return {
|
|
233
|
+
...(streamed.text === "" ? {} : { text: streamed.text }),
|
|
234
|
+
...(streamed.reasoningContent === ""
|
|
235
|
+
? {}
|
|
236
|
+
: { reasoningContent: streamed.reasoningContent }),
|
|
237
|
+
toolCalls: streamed.toolCalls.map((call) => ({
|
|
238
|
+
id: call.id,
|
|
239
|
+
name: call.name,
|
|
240
|
+
arguments: structuredClone(call.arguments),
|
|
241
|
+
})),
|
|
242
|
+
providerState: {
|
|
243
|
+
protocol: "anthropic_messages",
|
|
244
|
+
content: cloneProviderValue(streamed.content),
|
|
245
|
+
...(streamed.responseId === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { responseId: streamed.responseId }),
|
|
248
|
+
...(streamed.model === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { model: streamed.model }),
|
|
251
|
+
...(streamed.stopReason === undefined
|
|
252
|
+
? {}
|
|
253
|
+
: { stopReason: streamed.stopReason }),
|
|
254
|
+
},
|
|
255
|
+
usage: normalizeAnthropicUsage(streamed.usage),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const rawPayload = await providerJson(response, "Anthropic Messages");
|
|
259
|
+
if (!isRecord(rawPayload) || !Array.isArray(rawPayload.content))
|
|
260
|
+
throw unknownProviderResponse("Anthropic Messages");
|
|
261
|
+
const payload = rawPayload;
|
|
262
|
+
const content = payload.content;
|
|
263
|
+
if (content.some((item) => !isRecord(item) ||
|
|
264
|
+
typeof item.type !== "string" ||
|
|
265
|
+
(item.type === "text" && typeof item.text !== "string") ||
|
|
266
|
+
(item.type === "thinking" && typeof item.thinking !== "string") ||
|
|
267
|
+
(item.type === "tool_use" &&
|
|
268
|
+
(typeof item.id !== "string" ||
|
|
269
|
+
typeof item.name !== "string" ||
|
|
270
|
+
!("input" in item)))))
|
|
271
|
+
throw unknownProviderResponse("Anthropic Messages");
|
|
272
|
+
await trace("anthropic_response", payload);
|
|
273
|
+
const reasoningContent = content
|
|
274
|
+
.filter((item) => item.type === "thinking")
|
|
275
|
+
.map(({ thinking }) => thinking)
|
|
276
|
+
.join("");
|
|
277
|
+
capture?.setReasoning(reasoningContent);
|
|
278
|
+
return {
|
|
279
|
+
text: content
|
|
280
|
+
.filter((item) => item.type === "text")
|
|
281
|
+
.map(({ text }) => text)
|
|
282
|
+
.join(""),
|
|
283
|
+
...(reasoningContent === "" ? {} : { reasoningContent }),
|
|
284
|
+
toolCalls: content
|
|
285
|
+
.filter((item) => item.type === "tool_use")
|
|
286
|
+
.map((call) => ({
|
|
287
|
+
id: call.id,
|
|
288
|
+
name: call.name,
|
|
289
|
+
arguments: call.input,
|
|
290
|
+
})),
|
|
291
|
+
providerState: {
|
|
292
|
+
protocol: "anthropic_messages",
|
|
293
|
+
content: cloneProviderValue(content),
|
|
294
|
+
...(payload.id === undefined ? {} : { responseId: payload.id }),
|
|
295
|
+
...(payload.model === undefined ? {} : { model: payload.model }),
|
|
296
|
+
...(payload.stop_reason === undefined
|
|
297
|
+
? {}
|
|
298
|
+
: { stopReason: payload.stop_reason }),
|
|
299
|
+
},
|
|
300
|
+
usage: normalizeAnthropicUsage(payload.usage),
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function modelHostTrace(request) {
|
|
306
|
+
return {
|
|
307
|
+
requestId: request.requestId,
|
|
308
|
+
operationId: request.operationId,
|
|
309
|
+
requestAttempt: request.requestAttempt,
|
|
310
|
+
exchange: request.exchange,
|
|
311
|
+
bootstrap: structuredClone(request.bootstrap),
|
|
312
|
+
appended: request.appended.map((item) => item.kind !== "assistant"
|
|
313
|
+
? structuredClone(item)
|
|
314
|
+
: {
|
|
315
|
+
kind: item.kind,
|
|
316
|
+
text: item.text,
|
|
317
|
+
toolCalls: structuredClone(item.toolCalls),
|
|
318
|
+
reasoningContentAvailable: item.reasoningContent !== undefined,
|
|
319
|
+
providerStateAvailable: item.providerState !== undefined,
|
|
320
|
+
}),
|
|
321
|
+
tools: request.tools.map(({ name }) => name),
|
|
322
|
+
toolUniverse: request.toolUniverse?.map(({ name }) => name),
|
|
323
|
+
allowedTools: structuredClone(request.allowedTools),
|
|
324
|
+
toolStrategy: structuredClone(request.toolStrategy),
|
|
325
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function modelDiagnosticContext(request) {
|
|
329
|
+
return {
|
|
330
|
+
scope: request.requestId === "play_call_chain"
|
|
331
|
+
? "play_call_chain"
|
|
332
|
+
: request.operationId?.includes(":followup:") === true
|
|
333
|
+
? "play_followup"
|
|
334
|
+
: "model_host",
|
|
335
|
+
...(request.requestId === undefined
|
|
336
|
+
? {}
|
|
337
|
+
: { requestId: request.requestId }),
|
|
338
|
+
...(request.operationId === undefined
|
|
339
|
+
? {}
|
|
340
|
+
: { operationId: request.operationId }),
|
|
341
|
+
...(request.requestAttempt === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { requestAttempt: request.requestAttempt }),
|
|
344
|
+
...(request.exchange === undefined ? {} : { exchange: request.exchange }),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function providerCapture(failureLog, provider, endpoint, context, requestBody) {
|
|
348
|
+
return failureLog === undefined
|
|
349
|
+
? undefined
|
|
350
|
+
: new AiExchangeCapture({ provider, endpoint, context, requestBody });
|
|
351
|
+
}
|
|
352
|
+
function captureDelta(capture, downstream) {
|
|
353
|
+
if (capture === undefined)
|
|
354
|
+
return downstream;
|
|
355
|
+
return (delta) => {
|
|
356
|
+
if (delta.kind === "reasoning")
|
|
357
|
+
capture.observeReasoning(delta.text);
|
|
358
|
+
downstream?.(delta);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function captureSettingDelta(capture, downstream) {
|
|
362
|
+
if (capture === undefined)
|
|
363
|
+
return downstream;
|
|
364
|
+
return (delta) => {
|
|
365
|
+
if (delta.kind === "reasoning")
|
|
366
|
+
capture.observeReasoning(delta.text);
|
|
367
|
+
downstream?.(delta);
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function capturedProviderOperation(failureLog, capture, operation) {
|
|
371
|
+
const observeResponse = (response) => capture?.captureResponse(response) ?? response;
|
|
372
|
+
try {
|
|
373
|
+
const value = await operation(observeResponse);
|
|
374
|
+
if (capture === undefined)
|
|
375
|
+
return value;
|
|
376
|
+
return { ...value, diagnostics: capture.snapshot() };
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
if (capture !== undefined && failureLog !== undefined) {
|
|
380
|
+
const exchange = capture.snapshot();
|
|
381
|
+
const failure = {
|
|
382
|
+
kind: exchange.response === undefined
|
|
383
|
+
? "provider_transport"
|
|
384
|
+
: error instanceof ModelHostFailureError
|
|
385
|
+
? "provider_rejection"
|
|
386
|
+
: "provider_response_format",
|
|
387
|
+
message: error instanceof Error
|
|
388
|
+
? error.message
|
|
389
|
+
: "Provider request processing failed.",
|
|
390
|
+
error: errorDescription(error),
|
|
391
|
+
};
|
|
392
|
+
await failureLog.recordFailure({ exchange, failures: [failure] });
|
|
393
|
+
}
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function trace(kind, value) {
|
|
398
|
+
const path = process.env.NARRAEON_PROVIDER_TRACE_PATH;
|
|
399
|
+
if (path === undefined)
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
403
|
+
const size = await stat(path).then(({ size }) => size, () => 0);
|
|
404
|
+
if (size > 0)
|
|
405
|
+
await chmod(path, 0o600);
|
|
406
|
+
const line = `${JSON.stringify({ at: new Date().toISOString(), kind, value })}\n`;
|
|
407
|
+
if (Buffer.byteLength(line, "utf8") > 8 * 1024 * 1024 - size)
|
|
408
|
+
return;
|
|
409
|
+
await appendFile(path, line, { encoding: "utf8", mode: 0o600 });
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Explicit acceptance diagnostics must never change provider semantics.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
export class FileNativeSettingAuthorProvider {
|
|
416
|
+
#connection;
|
|
417
|
+
#fetch;
|
|
418
|
+
#failureLog;
|
|
419
|
+
#operationId;
|
|
420
|
+
#locale;
|
|
421
|
+
#exchange = 0;
|
|
422
|
+
constructor(connection, fetchImplementation = fetch, diagnostics) {
|
|
423
|
+
this.#connection = structuredClone(connection);
|
|
424
|
+
this.#fetch = fetchImplementation;
|
|
425
|
+
this.#failureLog = diagnostics?.failureLog;
|
|
426
|
+
this.#operationId = diagnostics?.operationId;
|
|
427
|
+
this.#locale = diagnostics?.locale ?? defaultAppLocale;
|
|
428
|
+
}
|
|
429
|
+
async next(request) {
|
|
430
|
+
this.#exchange += 1;
|
|
431
|
+
const diagnosticContext = {
|
|
432
|
+
scope: "setting_improvement",
|
|
433
|
+
requestId: "setting_improvement",
|
|
434
|
+
...(this.#operationId === undefined
|
|
435
|
+
? {}
|
|
436
|
+
: { operationId: this.#operationId }),
|
|
437
|
+
requestAttempt: 1,
|
|
438
|
+
exchange: this.#exchange,
|
|
439
|
+
};
|
|
440
|
+
const tools = request.tools.map((name) => settingToolDefinition(name, this.#locale));
|
|
441
|
+
if (this.#connection.provider === "chat_completions") {
|
|
442
|
+
const body = {
|
|
443
|
+
model: this.#connection.modelId,
|
|
444
|
+
messages: request.messages.map(chatSettingMessage),
|
|
445
|
+
...(tools.length === 0
|
|
446
|
+
? {}
|
|
447
|
+
: {
|
|
448
|
+
tools: tools.map((tool) => ({
|
|
449
|
+
type: "function",
|
|
450
|
+
function: {
|
|
451
|
+
name: tool.name,
|
|
452
|
+
description: tool.description,
|
|
453
|
+
parameters: tool.schema,
|
|
454
|
+
},
|
|
455
|
+
})),
|
|
456
|
+
}),
|
|
457
|
+
max_tokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
|
|
458
|
+
stream: true,
|
|
459
|
+
stream_options: { include_usage: true },
|
|
460
|
+
};
|
|
461
|
+
await trace("setting_chat_request", body);
|
|
462
|
+
const bodyJson = JSON.stringify(body);
|
|
463
|
+
const url = providerUrl(this.#connection);
|
|
464
|
+
const capture = providerCapture(this.#failureLog, this.#connection.provider, url, diagnosticContext, bodyJson);
|
|
465
|
+
const diagnosticDelta = captureSettingDelta(capture, request.onDelta);
|
|
466
|
+
return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
|
|
467
|
+
const response = observe(await this.#fetch(url, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: {
|
|
470
|
+
Authorization: `Bearer ${this.#connection.apiKey}`,
|
|
471
|
+
"Content-Type": "application/json",
|
|
472
|
+
},
|
|
473
|
+
body: bodyJson,
|
|
474
|
+
}));
|
|
475
|
+
if (!response.ok)
|
|
476
|
+
throw await providerError(response);
|
|
477
|
+
if (response.body === null)
|
|
478
|
+
throw new Error("The authoring response is missing a streaming body");
|
|
479
|
+
const streamed = await aggregateChatSettingStream(response.body, diagnosticDelta);
|
|
480
|
+
capture?.setReasoning(streamed.reasoningContent);
|
|
481
|
+
await trace("setting_chat_response", streamed);
|
|
482
|
+
return {
|
|
483
|
+
role: "assistant",
|
|
484
|
+
content: streamed.content,
|
|
485
|
+
...(streamed.reasoningContent === ""
|
|
486
|
+
? {}
|
|
487
|
+
: { reasoningContent: streamed.reasoningContent }),
|
|
488
|
+
toolCalls: streamed.toolCalls.map((call) => ({
|
|
489
|
+
id: call.id,
|
|
490
|
+
name: call.name,
|
|
491
|
+
arguments: asArguments(parseArguments(call.arguments)),
|
|
492
|
+
})),
|
|
493
|
+
usage: settingAuthorUsage(normalizeChatUsage(streamed.usage)),
|
|
494
|
+
};
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (this.#connection.provider === "openai_responses") {
|
|
498
|
+
const response = await openAIResponsesRequest({
|
|
499
|
+
connection: this.#connection,
|
|
500
|
+
fetchImplementation: this.#fetch,
|
|
501
|
+
input: request.messages.flatMap(responsesSettingMessage),
|
|
502
|
+
tools: tools.map((tool) => ({
|
|
503
|
+
name: tool.name,
|
|
504
|
+
description: tool.description,
|
|
505
|
+
parameters: tool.schema,
|
|
506
|
+
})),
|
|
507
|
+
maxOutputTokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
|
|
508
|
+
tracePrefix: "setting_responses",
|
|
509
|
+
...(this.#failureLog === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { failureLog: this.#failureLog }),
|
|
512
|
+
diagnosticContext,
|
|
513
|
+
...(request.onDelta === undefined ? {} : { onDelta: request.onDelta }),
|
|
514
|
+
});
|
|
515
|
+
return {
|
|
516
|
+
role: "assistant",
|
|
517
|
+
content: response.text ?? "",
|
|
518
|
+
providerState: response.providerState,
|
|
519
|
+
toolCalls: (response.toolCalls ?? []).map((call) => ({
|
|
520
|
+
...call,
|
|
521
|
+
arguments: asArguments(call.arguments),
|
|
522
|
+
})),
|
|
523
|
+
usage: settingAuthorUsage(response.usage),
|
|
524
|
+
...(response.diagnostics === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: { diagnostics: response.diagnostics }),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const system = request.messages
|
|
530
|
+
.filter(({ role }) => role === "system")
|
|
531
|
+
.map(({ content }) => content)
|
|
532
|
+
.join("\n\n");
|
|
533
|
+
const body = {
|
|
534
|
+
model: this.#connection.modelId,
|
|
535
|
+
...(system.length === 0 ? {} : { system }),
|
|
536
|
+
messages: request.messages
|
|
537
|
+
.filter(({ role }) => role !== "system")
|
|
538
|
+
.map(anthropicSettingMessage),
|
|
539
|
+
...(tools.length === 0
|
|
540
|
+
? {}
|
|
541
|
+
: {
|
|
542
|
+
tools: tools.map((tool) => ({
|
|
543
|
+
name: tool.name,
|
|
544
|
+
description: tool.description,
|
|
545
|
+
input_schema: tool.schema,
|
|
546
|
+
})),
|
|
547
|
+
}),
|
|
548
|
+
max_tokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
|
|
549
|
+
stream: true,
|
|
550
|
+
};
|
|
551
|
+
await trace("setting_anthropic_request", body);
|
|
552
|
+
const bodyJson = JSON.stringify(body);
|
|
553
|
+
const url = providerUrl(this.#connection);
|
|
554
|
+
const capture = providerCapture(this.#failureLog, this.#connection.provider, url, diagnosticContext, bodyJson);
|
|
555
|
+
const diagnosticDelta = captureSettingDelta(capture, request.onDelta);
|
|
556
|
+
return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
|
|
557
|
+
const response = observe(await this.#fetch(url, {
|
|
558
|
+
method: "POST",
|
|
559
|
+
headers: {
|
|
560
|
+
"anthropic-version": "2023-06-01",
|
|
561
|
+
"x-api-key": this.#connection.apiKey,
|
|
562
|
+
"Content-Type": "application/json",
|
|
563
|
+
},
|
|
564
|
+
body: bodyJson,
|
|
565
|
+
}));
|
|
566
|
+
if (!response.ok)
|
|
567
|
+
throw await providerError(response);
|
|
568
|
+
if (response.body === null)
|
|
569
|
+
throw new Error("The authoring response is missing a streaming body");
|
|
570
|
+
const streamed = await aggregateAnthropicSettingStream(response.body, diagnosticDelta);
|
|
571
|
+
capture?.setReasoning(streamed.reasoningContent);
|
|
572
|
+
await trace("setting_anthropic_response", streamed);
|
|
573
|
+
return {
|
|
574
|
+
role: "assistant",
|
|
575
|
+
content: streamed.content,
|
|
576
|
+
...(streamed.reasoningContent === ""
|
|
577
|
+
? {}
|
|
578
|
+
: { reasoningContent: streamed.reasoningContent }),
|
|
579
|
+
toolCalls: streamed.toolCalls.map((call) => ({
|
|
580
|
+
id: call.id,
|
|
581
|
+
name: call.name,
|
|
582
|
+
arguments: asArguments(parseArguments(call.arguments)),
|
|
583
|
+
})),
|
|
584
|
+
usage: settingAuthorUsage(normalizeAnthropicUsage(streamed.usage)),
|
|
585
|
+
};
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// The progress projection only counts tokens, so the nullable per-field
|
|
590
|
+
// provenance of ModelHostUsage collapses to two numbers here.
|
|
591
|
+
function settingAuthorUsage(usage) {
|
|
592
|
+
if (usage === undefined)
|
|
593
|
+
return { inputTokens: 0, outputTokens: 0 };
|
|
594
|
+
return {
|
|
595
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
596
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function settingToolDefinition(name, locale = defaultAppLocale) {
|
|
600
|
+
const descriptions = settingToolDescriptions[locale];
|
|
601
|
+
const schema = (properties, required) => ({
|
|
602
|
+
type: "object",
|
|
603
|
+
additionalProperties: false,
|
|
604
|
+
properties,
|
|
605
|
+
required,
|
|
606
|
+
});
|
|
607
|
+
const text = { type: "string", minLength: 1 };
|
|
608
|
+
const path = { type: "string", minLength: 1 };
|
|
609
|
+
const cursor = { type: "string", minLength: 1 };
|
|
610
|
+
const limit = { type: "integer", minimum: 1, maximum: 100 };
|
|
611
|
+
const definitions = {
|
|
612
|
+
setting_list: {
|
|
613
|
+
description: descriptions.setting_list,
|
|
614
|
+
schema: schema({
|
|
615
|
+
// World document paths accept any segment that is not "."/".."; an
|
|
616
|
+
// ASCII-only pattern here made directories the author had just
|
|
617
|
+
// created impossible to list back.
|
|
618
|
+
directory: {
|
|
619
|
+
type: "string",
|
|
620
|
+
pattern: "^world(?:/[^\\\\]+)?$",
|
|
621
|
+
},
|
|
622
|
+
limit,
|
|
623
|
+
cursor,
|
|
624
|
+
}, []),
|
|
625
|
+
},
|
|
626
|
+
setting_search: {
|
|
627
|
+
description: descriptions.setting_search,
|
|
628
|
+
schema: schema({
|
|
629
|
+
query: { type: "string", minLength: 1, maxLength: 256 },
|
|
630
|
+
within: path,
|
|
631
|
+
caseSensitive: { type: "boolean" },
|
|
632
|
+
limit,
|
|
633
|
+
cursor,
|
|
634
|
+
}, ["query"]),
|
|
635
|
+
},
|
|
636
|
+
setting_read: {
|
|
637
|
+
description: descriptions.setting_read,
|
|
638
|
+
schema: schema({
|
|
639
|
+
path,
|
|
640
|
+
maxBytes: { type: "integer", minimum: 4, maximum: 65_536 },
|
|
641
|
+
cursor,
|
|
642
|
+
}, ["path"]),
|
|
643
|
+
},
|
|
644
|
+
setting_write_file: {
|
|
645
|
+
description: descriptions.setting_write_file,
|
|
646
|
+
schema: schema({ path, contents: text }, ["path", "contents"]),
|
|
647
|
+
},
|
|
648
|
+
setting_patch: {
|
|
649
|
+
description: descriptions.setting_patch,
|
|
650
|
+
schema: schema({
|
|
651
|
+
document: text,
|
|
652
|
+
op: { type: "string", enum: ["add", "replace"] },
|
|
653
|
+
locator: {
|
|
654
|
+
type: "array",
|
|
655
|
+
minItems: 1,
|
|
656
|
+
items: { type: "string", minLength: 1 },
|
|
657
|
+
},
|
|
658
|
+
value: {},
|
|
659
|
+
}, ["document", "op", "locator", "value"]),
|
|
660
|
+
},
|
|
661
|
+
setting_move: {
|
|
662
|
+
description: descriptions.setting_move,
|
|
663
|
+
schema: schema({ from: text, to: path }, ["from", "to"]),
|
|
664
|
+
},
|
|
665
|
+
setting_preview_candidate: {
|
|
666
|
+
description: descriptions.setting_preview_candidate,
|
|
667
|
+
schema: schema({}, []),
|
|
668
|
+
},
|
|
669
|
+
setting_finish_candidate: {
|
|
670
|
+
description: descriptions.setting_finish_candidate,
|
|
671
|
+
schema: schema({}, []),
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
return { name, ...definitions[name] };
|
|
675
|
+
}
|
|
676
|
+
const settingToolDescriptions = {
|
|
677
|
+
en: {
|
|
678
|
+
setting_list: "List the world root or a world/ subdirectory from the current candidate-document snapshot. The root also lists specialized opening and control files. A pagination cursor is valid only for the same query against the candidate snapshot that produced it.",
|
|
679
|
+
setting_search: "Search literal source text in world documents from the current candidate snapshot. within may be a world/ directory, world/ document path, @short-ref, or document identity.",
|
|
680
|
+
setting_read: "Precisely read a world-document body from the current candidate snapshot by world/ path, @short-ref, or document identity. Read opening and control files through their exact dedicated paths. Write authorization is granted only after every page has been read; later candidate changes do not revoke it.",
|
|
681
|
+
setting_write_file: "Create or replace one complete candidate file. path is always a full logical path: a .yaml or .md world document under world/, control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or the root opening.md. A new world document must include a complete body with its $document technical header. When replacing an existing world document, you may omit $document and write back the body returned by setting_read; Runtime preserves the existing title, summary, and aliases. Runtime always decides id and ref: it allocates them for a new document and preserves them for an existing one, regardless of values you supply. Existing world documents and an existing opening.md must be read completely before replacement.",
|
|
682
|
+
setting_patch: 'Patch one YAML map node by document id, @short-ref, or world/ logical path and revision. op must explicitly be add or replace. locator is a stable array of map keys, for example ["relationships","player","trust"]. The existing document must already have been read completely. Rewrite Markdown documents in full with setting_write_file.',
|
|
683
|
+
setting_move: "Move a completely read world document to a new world/ logical path by revision. Use this to repair catalog-directory association while preserving document content and identity.",
|
|
684
|
+
setting_preview_candidate: "Run mechanical content-tree checks and the real Prompt Preview against the isolated candidate, returning diagnostics for path association, slots, references, and material coverage. Call this after all final changes; the candidate may finish only after it passes. A failed preview is a normal iteration: repair the diagnostics and call it again.",
|
|
685
|
+
setting_finish_candidate: "Call only after setting_preview_candidate has passed for the final changes. It may immediately follow the passing preview in the same model response. It must be the last call in that response and takes no arguments.",
|
|
686
|
+
},
|
|
687
|
+
"zh-CN": {
|
|
688
|
+
setting_list: "通过当前候选文档快照列出 world 或 world/ 子目录;根目录同时显示 opening/control 等专用文件。分页 cursor 只能用于产生它的同一候选快照和相同查询。",
|
|
689
|
+
setting_search: "通过当前候选文档快照原文字面搜索 world 文档;within 可取 world/ 目录、world/ 文档路径、@短引用或文档身份。",
|
|
690
|
+
setting_read: "通过当前候选文档快照按 world/ 路径、@短引用或文档身份精确读取世界文档正文;opening/control 使用精确路径专用读取。只有读完全部分页才获得写授权,此后不因其他修改失效。",
|
|
691
|
+
setting_write_file: "创建或整份写入一个候选文件,path 一律是完整逻辑路径:world/ 下的 .yaml 或 .md 世界文档、control/frame.yaml、control/player-views.yaml、control/blocks/*.md,或根级 opening.md。新建世界文档写完整原文并带 $document 技术头;覆盖既有世界文档可以省略 $document,直接写回 setting_read 返回的正文,Runtime 会沿用原有 title、summary 和 aliases。id 和 ref 始终由 Runtime 决定——新建时分配,覆盖时保留原值,你写什么都不会报错。既有 world 文档和既有 opening.md 必须先完整读取。",
|
|
692
|
+
setting_patch: '通过 revision 按文档 ID、@短引用或 world/ 逻辑路径 patch 一个 YAML map 节点;op 必须明确选择 add(新增)或 replace(替换),locator 是稳定 map-key 数组,例如 ["关系","对玩家","信任"]。既有文档必须已完整读取。Markdown 文档请用 setting_write_file 整份重写。',
|
|
693
|
+
setting_move: "通过 revision 把已完整读取的 world 文档移动到新的 world/ 逻辑路径;用于修复 catalog 目录关联,保留文档内容和身份。",
|
|
694
|
+
setting_preview_candidate: "对当前隔离候选运行内容树机械检查和真实 Prompt Preview,并返回路径关联、slot、引用和材料覆盖诊断;所有最终修改后调用,通过后才可结束候选。未通过属于正常迭代,修复后重新调用即可。",
|
|
695
|
+
setting_finish_candidate: "setting_preview_candidate 已对最终修改返回自检通过后调用,可以紧接在同一模型响应的自检之后;必须是当前响应的最后一个调用,且不得携带参数。",
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
function items(request) {
|
|
699
|
+
return request.appended;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* A frozen policy is present when the caller supplies the logical operation's
|
|
703
|
+
* complete tool universe. All play lifecycles share this one ModelHost seam.
|
|
704
|
+
*/
|
|
705
|
+
function hasFrozenToolPolicy(request) {
|
|
706
|
+
return frozenToolPolicy(request) !== null;
|
|
707
|
+
}
|
|
708
|
+
function frozenToolPolicy(request) {
|
|
709
|
+
if (!("toolUniverse" in request && request.toolUniverse !== undefined))
|
|
710
|
+
return null;
|
|
711
|
+
return request;
|
|
712
|
+
}
|
|
713
|
+
function providerToolDefinitions(request) {
|
|
714
|
+
const policy = frozenToolPolicy(request);
|
|
715
|
+
return policy?.toolUniverse ?? request.tools;
|
|
716
|
+
}
|
|
717
|
+
function providerAllowedTools(request) {
|
|
718
|
+
const policy = frozenToolPolicy(request);
|
|
719
|
+
if (policy !== null)
|
|
720
|
+
return [...(policy.allowedTools ?? [])];
|
|
721
|
+
return request.tools.map((tool) => tool.name);
|
|
722
|
+
}
|
|
723
|
+
function providerToolStrategy(request, provider) {
|
|
724
|
+
const policy = frozenToolPolicy(request);
|
|
725
|
+
if (policy?.toolStrategy !== undefined)
|
|
726
|
+
return policy.toolStrategy;
|
|
727
|
+
return defaultRuntimeToolDefinitionStrategy(provider);
|
|
728
|
+
}
|
|
729
|
+
function chatAppend(request) {
|
|
730
|
+
return items(request).flatMap((item) => {
|
|
731
|
+
if (item.kind === "player")
|
|
732
|
+
return { role: "user", content: item.text };
|
|
733
|
+
if (item.kind === "prompt_delta")
|
|
734
|
+
return promptDeltaProviderMessages(item.logicalMessages);
|
|
735
|
+
if (item.kind === "tool")
|
|
736
|
+
return {
|
|
737
|
+
role: "tool",
|
|
738
|
+
tool_call_id: item.toolCallId,
|
|
739
|
+
content: item.markdown,
|
|
740
|
+
};
|
|
741
|
+
if (item.providerState?.protocol === "chat_completions") {
|
|
742
|
+
const message = item.providerState.assistantMessage;
|
|
743
|
+
if (isRecord(message))
|
|
744
|
+
return [cloneProviderValue(message)];
|
|
745
|
+
}
|
|
746
|
+
const assistant = {
|
|
747
|
+
role: "assistant",
|
|
748
|
+
content: item.text,
|
|
749
|
+
...(item.reasoningContent === undefined
|
|
750
|
+
? {}
|
|
751
|
+
: { reasoning_content: item.reasoningContent }),
|
|
752
|
+
...(item.toolCalls.length === 0
|
|
753
|
+
? {}
|
|
754
|
+
: {
|
|
755
|
+
tool_calls: item.toolCalls.map((call) => ({
|
|
756
|
+
id: call.id,
|
|
757
|
+
type: "function",
|
|
758
|
+
function: {
|
|
759
|
+
name: call.name,
|
|
760
|
+
arguments: JSON.stringify(call.arguments),
|
|
761
|
+
},
|
|
762
|
+
})),
|
|
763
|
+
}),
|
|
764
|
+
};
|
|
765
|
+
return assistant;
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function anthropicAppend(request) {
|
|
769
|
+
return items(request).flatMap((item) => {
|
|
770
|
+
if (item.kind === "player")
|
|
771
|
+
return { role: "user", content: item.text };
|
|
772
|
+
if (item.kind === "prompt_delta")
|
|
773
|
+
return promptDeltaProviderMessages(item.logicalMessages);
|
|
774
|
+
if (item.kind === "tool")
|
|
775
|
+
return {
|
|
776
|
+
role: "user",
|
|
777
|
+
content: [
|
|
778
|
+
{
|
|
779
|
+
type: "tool_result",
|
|
780
|
+
tool_use_id: item.toolCallId,
|
|
781
|
+
content: item.markdown,
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
};
|
|
785
|
+
if (item.providerState?.protocol === "anthropic_messages")
|
|
786
|
+
return [
|
|
787
|
+
{
|
|
788
|
+
role: "assistant",
|
|
789
|
+
content: cloneProviderValue(item.providerState.content),
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
return {
|
|
793
|
+
role: "assistant",
|
|
794
|
+
content: [
|
|
795
|
+
...(item.text.length === 0 ? [] : [{ type: "text", text: item.text }]),
|
|
796
|
+
...item.toolCalls.map((call) => ({
|
|
797
|
+
type: "tool_use",
|
|
798
|
+
id: call.id,
|
|
799
|
+
name: call.name,
|
|
800
|
+
input: call.arguments,
|
|
801
|
+
})),
|
|
802
|
+
],
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function responsesAppend(request) {
|
|
807
|
+
return items(request).flatMap((item) => {
|
|
808
|
+
if (item.kind === "player")
|
|
809
|
+
return [{ role: "user", content: item.text }];
|
|
810
|
+
if (item.kind === "prompt_delta")
|
|
811
|
+
return promptDeltaProviderMessages(item.logicalMessages);
|
|
812
|
+
if (item.kind === "tool")
|
|
813
|
+
return [
|
|
814
|
+
{
|
|
815
|
+
type: "function_call_output",
|
|
816
|
+
call_id: item.toolCallId,
|
|
817
|
+
output: item.markdown,
|
|
818
|
+
},
|
|
819
|
+
];
|
|
820
|
+
if (item.providerState?.protocol === "openai_responses")
|
|
821
|
+
return cloneResponseOutput(item.providerState.output);
|
|
822
|
+
return fallbackResponsesAssistant(item.text, item.toolCalls);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
function promptDeltaProviderMessages(messages) {
|
|
826
|
+
return messages.map((message) => ({
|
|
827
|
+
role: "user",
|
|
828
|
+
content: renderPromptDeltaMessage(message.role, message.markdown),
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
831
|
+
function providerUrl(connection) {
|
|
832
|
+
const url = new URL(connection.baseUrl);
|
|
833
|
+
const suffix = connection.provider === "chat_completions"
|
|
834
|
+
? "/chat/completions"
|
|
835
|
+
: connection.provider === "openai_responses"
|
|
836
|
+
? "/responses"
|
|
837
|
+
: "/messages";
|
|
838
|
+
const path = url.pathname.replace(/\/+$/u, "");
|
|
839
|
+
url.pathname = path.endsWith(suffix) ? path : `${path}${suffix}`;
|
|
840
|
+
return url;
|
|
841
|
+
}
|
|
842
|
+
function parseArguments(value) {
|
|
843
|
+
try {
|
|
844
|
+
return JSON.parse(value);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return { $invalidToolArgumentsJson: value };
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function providerError(response) {
|
|
851
|
+
let text = "";
|
|
852
|
+
try {
|
|
853
|
+
text = (await response.text()).slice(0, 4096);
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// A non-2xx response is already a confirmed remote rejection. Preserve
|
|
857
|
+
// that deterministic classification even if its error body is unreadable.
|
|
858
|
+
}
|
|
859
|
+
await trace("provider_error", {
|
|
860
|
+
status: response.status,
|
|
861
|
+
statusText: response.statusText,
|
|
862
|
+
body: text,
|
|
863
|
+
});
|
|
864
|
+
return new ModelHostFailureError(`Provider request failed: ${response.status} ${text}`);
|
|
865
|
+
}
|
|
866
|
+
function chatSettingMessage(message) {
|
|
867
|
+
if (message.role === "tool")
|
|
868
|
+
return {
|
|
869
|
+
role: "tool",
|
|
870
|
+
tool_call_id: message.toolCallId,
|
|
871
|
+
content: message.content,
|
|
872
|
+
};
|
|
873
|
+
return {
|
|
874
|
+
role: message.role === "assistant"
|
|
875
|
+
? "assistant"
|
|
876
|
+
: message.role === "system"
|
|
877
|
+
? "system"
|
|
878
|
+
: "user",
|
|
879
|
+
content: message.content,
|
|
880
|
+
...(message.role !== "assistant" || message.reasoningContent === undefined
|
|
881
|
+
? {}
|
|
882
|
+
: { reasoning_content: message.reasoningContent }),
|
|
883
|
+
...(message.role !== "assistant" || (message.toolCalls?.length ?? 0) === 0
|
|
884
|
+
? {}
|
|
885
|
+
: {
|
|
886
|
+
tool_calls: message.toolCalls.map((call) => ({
|
|
887
|
+
id: call.id,
|
|
888
|
+
type: "function",
|
|
889
|
+
function: {
|
|
890
|
+
name: call.name,
|
|
891
|
+
arguments: JSON.stringify(call.arguments),
|
|
892
|
+
},
|
|
893
|
+
})),
|
|
894
|
+
}),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function anthropicSettingMessage(message) {
|
|
898
|
+
if (message.role === "tool")
|
|
899
|
+
return {
|
|
900
|
+
role: "user",
|
|
901
|
+
content: [
|
|
902
|
+
{
|
|
903
|
+
type: "tool_result",
|
|
904
|
+
tool_use_id: message.toolCallId,
|
|
905
|
+
content: message.content,
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
};
|
|
909
|
+
if (message.role === "assistant")
|
|
910
|
+
return {
|
|
911
|
+
role: "assistant",
|
|
912
|
+
content: [
|
|
913
|
+
...(message.content.length === 0
|
|
914
|
+
? []
|
|
915
|
+
: [{ type: "text", text: message.content }]),
|
|
916
|
+
...(message.toolCalls ?? []).map((call) => ({
|
|
917
|
+
type: "tool_use",
|
|
918
|
+
id: call.id,
|
|
919
|
+
name: call.name,
|
|
920
|
+
input: call.arguments,
|
|
921
|
+
})),
|
|
922
|
+
],
|
|
923
|
+
};
|
|
924
|
+
return { role: "user", content: message.content };
|
|
925
|
+
}
|
|
926
|
+
function responsesSettingMessage(message) {
|
|
927
|
+
if (message.role === "tool")
|
|
928
|
+
return [
|
|
929
|
+
{
|
|
930
|
+
type: "function_call_output",
|
|
931
|
+
call_id: message.toolCallId,
|
|
932
|
+
output: message.content,
|
|
933
|
+
},
|
|
934
|
+
];
|
|
935
|
+
if (message.role === "assistant" &&
|
|
936
|
+
message.providerState?.protocol === "openai_responses")
|
|
937
|
+
return cloneResponseOutput(message.providerState.output);
|
|
938
|
+
if (message.role === "assistant")
|
|
939
|
+
return fallbackResponsesAssistant(message.content, message.toolCalls ?? []);
|
|
940
|
+
return [{ role: message.role, content: message.content }];
|
|
941
|
+
}
|
|
942
|
+
function responsesBootstrapMessage(message) {
|
|
943
|
+
return { role: message.role, content: providerText(message.content) };
|
|
944
|
+
}
|
|
945
|
+
function providerText(content) {
|
|
946
|
+
if (typeof content === "string")
|
|
947
|
+
return content;
|
|
948
|
+
if (Array.isArray(content)) {
|
|
949
|
+
const items = content;
|
|
950
|
+
return items
|
|
951
|
+
.flatMap((item) => isRecord(item) && typeof item.text === "string" ? [item.text] : [])
|
|
952
|
+
.join("\n\n");
|
|
953
|
+
}
|
|
954
|
+
return JSON.stringify(content) ?? "";
|
|
955
|
+
}
|
|
956
|
+
function fallbackResponsesAssistant(text, toolCalls) {
|
|
957
|
+
return [
|
|
958
|
+
...(text.length === 0 ? [] : [{ role: "assistant", content: text }]),
|
|
959
|
+
...toolCalls.map((call) => ({
|
|
960
|
+
type: "function_call",
|
|
961
|
+
call_id: call.id,
|
|
962
|
+
name: call.name,
|
|
963
|
+
arguments: JSON.stringify(call.arguments),
|
|
964
|
+
})),
|
|
965
|
+
];
|
|
966
|
+
}
|
|
967
|
+
async function openAIResponsesRequest(input) {
|
|
968
|
+
const allowedTools = input.allowedTools ?? input.tools.map((tool) => tool.name);
|
|
969
|
+
const toolChoice = input.hasFrozenToolPolicy === true && allowedTools.length === 0
|
|
970
|
+
? "none"
|
|
971
|
+
: input.hasFrozenToolPolicy === true &&
|
|
972
|
+
input.toolStrategy === "native_allowed_subset"
|
|
973
|
+
? {
|
|
974
|
+
type: "allowed_tools",
|
|
975
|
+
mode: "auto",
|
|
976
|
+
tools: allowedTools.map((name) => ({ type: "function", name })),
|
|
977
|
+
}
|
|
978
|
+
: "auto";
|
|
979
|
+
const body = {
|
|
980
|
+
model: input.connection.modelId,
|
|
981
|
+
input: input.input,
|
|
982
|
+
...(input.tools.length === 0
|
|
983
|
+
? input.hasFrozenToolPolicy === true && allowedTools.length === 0
|
|
984
|
+
? { tool_choice: "none" }
|
|
985
|
+
: {}
|
|
986
|
+
: {
|
|
987
|
+
tools: input.tools.map((tool) => ({
|
|
988
|
+
type: "function",
|
|
989
|
+
name: tool.name,
|
|
990
|
+
description: tool.description,
|
|
991
|
+
parameters: tool.parameters,
|
|
992
|
+
})),
|
|
993
|
+
tool_choice: toolChoice,
|
|
994
|
+
}),
|
|
995
|
+
max_output_tokens: input.maxOutputTokens,
|
|
996
|
+
store: false,
|
|
997
|
+
include: ["reasoning.encrypted_content"],
|
|
998
|
+
stream: true,
|
|
999
|
+
};
|
|
1000
|
+
await trace(`${input.tracePrefix}_request`, body);
|
|
1001
|
+
const bodyJson = JSON.stringify(body);
|
|
1002
|
+
const url = providerUrl(input.connection);
|
|
1003
|
+
const capture = providerCapture(input.failureLog, input.connection.provider, url, input.diagnosticContext, bodyJson);
|
|
1004
|
+
const diagnosticDelta = captureDelta(capture, input.onDelta);
|
|
1005
|
+
return capturedProviderOperation(input.failureLog, capture, async (observe) => {
|
|
1006
|
+
const response = observe(await dispatchProviderRequest(input.fetchImplementation, url, {
|
|
1007
|
+
method: "POST",
|
|
1008
|
+
headers: {
|
|
1009
|
+
Accept: "text/event-stream",
|
|
1010
|
+
Authorization: `Bearer ${input.connection.apiKey}`,
|
|
1011
|
+
"Content-Type": "application/json",
|
|
1012
|
+
},
|
|
1013
|
+
body: bodyJson,
|
|
1014
|
+
}, "OpenAI Responses"));
|
|
1015
|
+
if (!response.ok)
|
|
1016
|
+
throw await providerError(response);
|
|
1017
|
+
const payload = isProviderEventStream(response)
|
|
1018
|
+
? await providerStreamResult(response, "Responses API", (body) => aggregateResponsesModelStream(body, diagnosticDelta))
|
|
1019
|
+
: await providerJson(response, "Responses API");
|
|
1020
|
+
await trace(`${input.tracePrefix}_response`, payload);
|
|
1021
|
+
if (!isRecord(payload) || !Array.isArray(payload.output))
|
|
1022
|
+
throw unknownProviderResponse("Responses API");
|
|
1023
|
+
const output = cloneResponseOutput(payload.output);
|
|
1024
|
+
if (output.some((item) => !isRecord(item) ||
|
|
1025
|
+
typeof item.type !== "string" ||
|
|
1026
|
+
(item.type === "message" && !Array.isArray(item.content)) ||
|
|
1027
|
+
(item.type === "function_call" &&
|
|
1028
|
+
(typeof item.call_id !== "string" ||
|
|
1029
|
+
typeof item.name !== "string" ||
|
|
1030
|
+
typeof item.arguments !== "string"))))
|
|
1031
|
+
throw unknownProviderResponse("Responses API");
|
|
1032
|
+
const text = output
|
|
1033
|
+
.flatMap((item) => {
|
|
1034
|
+
if (!isRecord(item) ||
|
|
1035
|
+
item.type !== "message" ||
|
|
1036
|
+
!Array.isArray(item.content))
|
|
1037
|
+
return [];
|
|
1038
|
+
return item.content.flatMap((part) => isRecord(part) &&
|
|
1039
|
+
part.type === "output_text" &&
|
|
1040
|
+
typeof part.text === "string"
|
|
1041
|
+
? [part.text]
|
|
1042
|
+
: []);
|
|
1043
|
+
})
|
|
1044
|
+
.join("");
|
|
1045
|
+
const toolCalls = output.flatMap((item) => {
|
|
1046
|
+
if (!isRecord(item) ||
|
|
1047
|
+
item.type !== "function_call" ||
|
|
1048
|
+
typeof item.call_id !== "string" ||
|
|
1049
|
+
typeof item.name !== "string" ||
|
|
1050
|
+
typeof item.arguments !== "string")
|
|
1051
|
+
return [];
|
|
1052
|
+
return [
|
|
1053
|
+
{
|
|
1054
|
+
id: item.call_id,
|
|
1055
|
+
name: item.name,
|
|
1056
|
+
arguments: parseArguments(item.arguments),
|
|
1057
|
+
},
|
|
1058
|
+
];
|
|
1059
|
+
});
|
|
1060
|
+
return {
|
|
1061
|
+
...(text.length === 0 ? {} : { text }),
|
|
1062
|
+
providerState: {
|
|
1063
|
+
protocol: "openai_responses",
|
|
1064
|
+
output,
|
|
1065
|
+
...(typeof payload.id === "string" ? { responseId: payload.id } : {}),
|
|
1066
|
+
},
|
|
1067
|
+
usage: normalizeResponsesUsage(payload.usage),
|
|
1068
|
+
toolCalls,
|
|
1069
|
+
};
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
function cloneResponseOutput(value) {
|
|
1073
|
+
if (!Array.isArray(value))
|
|
1074
|
+
throw new Error("Responses output must be an array");
|
|
1075
|
+
const source = JSON.stringify(value);
|
|
1076
|
+
return JSON.parse(source);
|
|
1077
|
+
}
|
|
1078
|
+
/** Clone provider-owned state before it enters the logical transcript. */
|
|
1079
|
+
function cloneProviderValue(value) {
|
|
1080
|
+
if (value === undefined || value === null)
|
|
1081
|
+
return value;
|
|
1082
|
+
const source = JSON.stringify(value);
|
|
1083
|
+
if (typeof source !== "string")
|
|
1084
|
+
return value;
|
|
1085
|
+
return JSON.parse(source);
|
|
1086
|
+
}
|
|
1087
|
+
function normalizeChatUsage(value) {
|
|
1088
|
+
const usage = isRecord(value) ? value : {};
|
|
1089
|
+
const promptDetails = isRecord(usage.prompt_tokens_details)
|
|
1090
|
+
? usage.prompt_tokens_details
|
|
1091
|
+
: {};
|
|
1092
|
+
const completionDetails = isRecord(usage.completion_tokens_details)
|
|
1093
|
+
? usage.completion_tokens_details
|
|
1094
|
+
: {};
|
|
1095
|
+
const inputTokens = providerNumber(usage.prompt_tokens);
|
|
1096
|
+
const cacheReadTokens = providerNumber(promptDetails.cached_tokens);
|
|
1097
|
+
const cacheWriteTokens = providerNumber(promptDetails.cache_write_tokens);
|
|
1098
|
+
return usageRecord({
|
|
1099
|
+
inputTokens,
|
|
1100
|
+
uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
|
|
1101
|
+
cacheReadTokens,
|
|
1102
|
+
cacheWriteTokens,
|
|
1103
|
+
reasoningTokens: providerNumber(completionDetails.reasoning_tokens),
|
|
1104
|
+
outputTokens: providerNumber(usage.completion_tokens),
|
|
1105
|
+
totalTokens: providerNumber(usage.total_tokens),
|
|
1106
|
+
}, ["uncachedInputTokens"]);
|
|
1107
|
+
}
|
|
1108
|
+
function normalizeAnthropicUsage(value) {
|
|
1109
|
+
const usage = isRecord(value) ? value : {};
|
|
1110
|
+
const uncachedInputTokens = providerNumber(usage.input_tokens);
|
|
1111
|
+
const cacheReadTokens = providerNumber(usage.cache_read_input_tokens);
|
|
1112
|
+
const cacheWriteTokens = providerNumber(usage.cache_creation_input_tokens);
|
|
1113
|
+
const inputTokens = sumInputTokens(uncachedInputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1114
|
+
const outputTokens = providerNumber(usage.output_tokens);
|
|
1115
|
+
return usageRecord({
|
|
1116
|
+
inputTokens,
|
|
1117
|
+
uncachedInputTokens,
|
|
1118
|
+
cacheReadTokens,
|
|
1119
|
+
cacheWriteTokens,
|
|
1120
|
+
reasoningTokens: null,
|
|
1121
|
+
outputTokens,
|
|
1122
|
+
totalTokens: inputTokens === null || outputTokens === null
|
|
1123
|
+
? null
|
|
1124
|
+
: inputTokens + outputTokens,
|
|
1125
|
+
}, ["inputTokens", "totalTokens"]);
|
|
1126
|
+
}
|
|
1127
|
+
function normalizeResponsesUsage(value) {
|
|
1128
|
+
const usage = isRecord(value) ? value : {};
|
|
1129
|
+
const inputDetails = isRecord(usage.input_tokens_details)
|
|
1130
|
+
? usage.input_tokens_details
|
|
1131
|
+
: {};
|
|
1132
|
+
const outputDetails = isRecord(usage.output_tokens_details)
|
|
1133
|
+
? usage.output_tokens_details
|
|
1134
|
+
: {};
|
|
1135
|
+
const inputTokens = providerNumber(usage.input_tokens);
|
|
1136
|
+
const cacheReadTokens = providerNumber(inputDetails.cached_tokens);
|
|
1137
|
+
const cacheWriteTokens = providerNumber(inputDetails.cache_write_tokens);
|
|
1138
|
+
return usageRecord({
|
|
1139
|
+
inputTokens,
|
|
1140
|
+
uncachedInputTokens: subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens),
|
|
1141
|
+
cacheReadTokens,
|
|
1142
|
+
cacheWriteTokens,
|
|
1143
|
+
reasoningTokens: providerNumber(outputDetails.reasoning_tokens),
|
|
1144
|
+
outputTokens: providerNumber(usage.output_tokens),
|
|
1145
|
+
totalTokens: providerNumber(usage.total_tokens),
|
|
1146
|
+
}, ["uncachedInputTokens"]);
|
|
1147
|
+
}
|
|
1148
|
+
function providerNumber(value) {
|
|
1149
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
1150
|
+
? value
|
|
1151
|
+
: null;
|
|
1152
|
+
}
|
|
1153
|
+
function usageRecord(values, derivedFields = []) {
|
|
1154
|
+
const derived = new Set(derivedFields);
|
|
1155
|
+
const provenance = (key, value) => value === null
|
|
1156
|
+
? "unavailable"
|
|
1157
|
+
: derived.has(key)
|
|
1158
|
+
? "derived_provider_fields"
|
|
1159
|
+
: "provider";
|
|
1160
|
+
return {
|
|
1161
|
+
...values,
|
|
1162
|
+
provenance: {
|
|
1163
|
+
inputTokens: provenance("inputTokens", values.inputTokens),
|
|
1164
|
+
uncachedInputTokens: provenance("uncachedInputTokens", values.uncachedInputTokens),
|
|
1165
|
+
cacheReadTokens: provenance("cacheReadTokens", values.cacheReadTokens),
|
|
1166
|
+
cacheWriteTokens: provenance("cacheWriteTokens", values.cacheWriteTokens),
|
|
1167
|
+
reasoningTokens: provenance("reasoningTokens", values.reasoningTokens),
|
|
1168
|
+
outputTokens: provenance("outputTokens", values.outputTokens),
|
|
1169
|
+
totalTokens: provenance("totalTokens", values.totalTokens),
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function subtractCachedInput(inputTokens, cacheReadTokens, cacheWriteTokens) {
|
|
1174
|
+
if (inputTokens === null ||
|
|
1175
|
+
cacheReadTokens === null ||
|
|
1176
|
+
cacheWriteTokens === null ||
|
|
1177
|
+
cacheReadTokens + cacheWriteTokens > inputTokens)
|
|
1178
|
+
return null;
|
|
1179
|
+
return inputTokens - cacheReadTokens - cacheWriteTokens;
|
|
1180
|
+
}
|
|
1181
|
+
function sumInputTokens(uncachedInputTokens, cacheReadTokens, cacheWriteTokens) {
|
|
1182
|
+
if (uncachedInputTokens === null ||
|
|
1183
|
+
cacheReadTokens === null ||
|
|
1184
|
+
cacheWriteTokens === null)
|
|
1185
|
+
return null;
|
|
1186
|
+
return uncachedInputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1187
|
+
}
|
|
1188
|
+
async function parseProviderJson(response, label) {
|
|
1189
|
+
const source = await response.text();
|
|
1190
|
+
try {
|
|
1191
|
+
return JSON.parse(source);
|
|
1192
|
+
}
|
|
1193
|
+
catch {
|
|
1194
|
+
throw new Error(`${label} response is not valid JSON`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async function providerJson(response, label) {
|
|
1198
|
+
try {
|
|
1199
|
+
return await parseProviderJson(response, label);
|
|
1200
|
+
}
|
|
1201
|
+
catch (error) {
|
|
1202
|
+
throw new ModelHostOutcomeUnknownError(`${label} was dispatched but its response could not be confirmed`, {
|
|
1203
|
+
cause: error,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function isProviderEventStream(response) {
|
|
1208
|
+
return (response.headers
|
|
1209
|
+
.get("content-type")
|
|
1210
|
+
?.toLowerCase()
|
|
1211
|
+
.includes("text/event-stream") === true);
|
|
1212
|
+
}
|
|
1213
|
+
async function providerStreamResult(response, label, read) {
|
|
1214
|
+
if (response.body === null)
|
|
1215
|
+
throw new ModelHostOutcomeUnknownError(`${label} was dispatched but its streaming response has no body`);
|
|
1216
|
+
try {
|
|
1217
|
+
return await read(response.body);
|
|
1218
|
+
}
|
|
1219
|
+
catch (error) {
|
|
1220
|
+
if (error instanceof ModelHostFailureError ||
|
|
1221
|
+
error instanceof ModelHostOutcomeUnknownError)
|
|
1222
|
+
throw error;
|
|
1223
|
+
throw new ModelHostOutcomeUnknownError(`${label} was dispatched but its streaming response could not be confirmed`, { cause: error });
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
async function dispatchProviderRequest(fetchImplementation, url, init, label) {
|
|
1227
|
+
try {
|
|
1228
|
+
return await fetchImplementation(url, init);
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
throw new ModelHostOutcomeUnknownError(`${label} request was dispatched but its outcome is unknown`, {
|
|
1232
|
+
cause: error,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function unknownProviderResponse(label) {
|
|
1237
|
+
return new ModelHostOutcomeUnknownError(`${label} was dispatched but its response shape could not be confirmed`);
|
|
1238
|
+
}
|
|
1239
|
+
function asArguments(value) {
|
|
1240
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1241
|
+
throw new Error("Authoring tool arguments must be an object");
|
|
1242
|
+
return value;
|
|
1243
|
+
}
|
|
1244
|
+
function isRecord(value) {
|
|
1245
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1246
|
+
}
|