narraeon 0.7.10 → 0.8.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/dist/node/protocol/modelConnections.js +15 -0
- package/dist/node/protocol/modelUsage.js +4 -0
- package/dist/node/protocol/sillyTavernContent.js +1 -0
- package/dist/node/protocol/v1.js +75 -0
- package/dist/node/runtime/V1Runtime.js +71 -1
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +15 -3
- package/dist/node/runtime/authoring/DamagedGoogleContinuation.js +68 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +47 -22
- package/dist/node/runtime/content/ContentWorkspace.js +3 -0
- package/dist/node/runtime/content/SillyTavernContent.js +796 -0
- package/dist/node/runtime/model/AiFailureLog.js +8 -1
- package/dist/node/runtime/model/FileNativeModelAdapters.js +610 -18
- package/dist/node/runtime/model/GoogleHopJournal.js +90 -0
- package/dist/node/runtime/model/GoogleInteractionsStream.js +242 -0
- package/dist/node/runtime/model/GoogleRequestPreview.js +184 -0
- package/dist/node/runtime/model/ModelConnectionStore.js +4 -1
- package/dist/node/runtime/model/ProviderExchangeState.js +44 -1
- package/dist/node/runtime/model/ProviderStream.js +4 -2
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +4 -4
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +150 -78
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +150 -10
- package/dist/node/runtime/play/OrderedPlayPrompts.js +139 -178
- package/dist/node/runtime/play/PlayAdvanceLifecycle.js +33 -9
- package/dist/node/runtime/play/PlayCallChain.js +76 -4
- package/dist/node/runtime/play/PlayFollowupRequests.js +23 -3
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +2 -1
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +112 -8
- package/dist/node/runtime/setting/SettingImprovementSession.js +48 -7
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +84 -8
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +125 -17
- package/dist/node/shared/application-preset-files.js +29 -0
- package/dist/node/shared/default-preset-host.js +50 -38
- package/dist/node/shared/first-party-action-choices.js +7 -11
- package/dist/node/shared/first-party-generic-panels.js +7 -11
- package/dist/node/shared/first-party-player-view.js +7 -11
- package/dist/node/shared/ordered-author-prompts.js +6 -4
- package/dist/node/shared/ordered-play-prompts.js +4 -2
- package/dist/node/shared/prompt-arrangement.js +205 -0
- package/dist/node/shared/sillytavern-preset.js +454 -0
- package/dist/web/assets/{index-DH2BVsYd.css → index-BKA1D0Rz.css} +1 -1
- package/dist/web/assets/index-C_Qta8UL.js +1432 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B2kbaOGe.js +0 -1408
|
@@ -3,8 +3,10 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, } from "../../protocol/modelConnections.js";
|
|
6
|
-
import { assertModelOutputComplete, ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
6
|
+
import { assertModelOutputComplete, ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, modelHostFailureRequiresFreshContext, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
|
|
7
7
|
import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
|
|
8
|
+
import { validGoogleInteractionsContinuation, } from "./ProviderExchangeState.js";
|
|
9
|
+
import { aggregateGoogleInteractions, projectGoogleInteractions, } from "./GoogleInteractionsStream.js";
|
|
8
10
|
import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
|
|
9
11
|
import { defaultRuntimeToolDefinitionStrategy, portableRuntimeToolInputSchema, } from "../prompt/FileNativeToolRegistry.js";
|
|
10
12
|
import { AiExchangeCapture, errorDescription, } from "./AiFailureLog.js";
|
|
@@ -70,6 +72,13 @@ export class FileNativeModelHost {
|
|
|
70
72
|
responseStorage: provider === "openai_responses" ? "disabled" : "provider_default",
|
|
71
73
|
encryptedReasoningContinuation: provider === "openai_responses" ? "included" : "not_requested",
|
|
72
74
|
anthropicVersion: provider === "anthropic_messages" ? "2023-06-01" : null,
|
|
75
|
+
...(provider === "google_interactions"
|
|
76
|
+
? {
|
|
77
|
+
interactionsEndpoint: providerUrl(this.#connection).pathname,
|
|
78
|
+
apiRevision: null,
|
|
79
|
+
store: false,
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
73
82
|
dialect: this.#connection.dialect,
|
|
74
83
|
reasoningEffort: this.#connection.reasoningEffort,
|
|
75
84
|
reasoningSummary: this.#connection.reasoningSummary,
|
|
@@ -118,6 +127,8 @@ export class FileNativeModelHost {
|
|
|
118
127
|
return await this.#chat(request, observer?.onDelta, signal);
|
|
119
128
|
if (this.#connection.provider === "openai_responses")
|
|
120
129
|
return await this.#responses(request, observer?.onDelta, signal);
|
|
130
|
+
if (this.#connection.provider === "google_interactions")
|
|
131
|
+
return await this.#google(request, observer?.onDelta, signal, observer?.retainLateExtensionResponse === true, observer?.onGoogleHop, observer?.diagnosticAttempt);
|
|
121
132
|
return await this.#anthropic(request, observer?.onDelta, signal);
|
|
122
133
|
}
|
|
123
134
|
catch (error) {
|
|
@@ -129,24 +140,39 @@ export class FileNativeModelHost {
|
|
|
129
140
|
}
|
|
130
141
|
/** Uses the production encoders and deliberately omits credentials. */
|
|
131
142
|
previewRequest(request) {
|
|
132
|
-
return this.#requestHost(request).#previewRequest(request);
|
|
143
|
+
return this.#requestHost(request).#previewRequest(request, this.#connection.provider === "google_interactions");
|
|
144
|
+
}
|
|
145
|
+
previewAuthoringRequest(request) {
|
|
146
|
+
return this.#requestHost(request).#previewRequest(request, true);
|
|
133
147
|
}
|
|
134
|
-
#previewRequest(request) {
|
|
148
|
+
#previewRequest(request, redactContinuation = false) {
|
|
135
149
|
assertExchangeCompatibility(this.#connection, request);
|
|
136
150
|
const provider = this.#connection.provider;
|
|
137
151
|
const body = provider === "chat_completions"
|
|
138
152
|
? chatRequestBody(this.#connection, request)
|
|
139
153
|
: provider === "openai_responses"
|
|
140
154
|
? responsesRequestBody(responsesRequestInput(this.#connection, request))
|
|
141
|
-
:
|
|
155
|
+
: provider === "google_interactions"
|
|
156
|
+
? googleRequestBody(this.#connection, request, redactContinuation)
|
|
157
|
+
: anthropicRequestBody(this.#connection, request);
|
|
142
158
|
return {
|
|
143
159
|
provider,
|
|
144
160
|
method: "POST",
|
|
145
|
-
endpointPath:
|
|
161
|
+
endpointPath: provider === "google_interactions"
|
|
162
|
+
? googleDiagnosticPath(this.#connection)
|
|
163
|
+
: providerUrl(this.#connection).pathname,
|
|
146
164
|
headerNames: provider === "anthropic_messages"
|
|
147
165
|
? ["Accept", "anthropic-version", "x-api-key", "Content-Type"]
|
|
148
|
-
:
|
|
166
|
+
: provider === "google_interactions"
|
|
167
|
+
? ["Accept", "x-goog-api-key", "Content-Type"]
|
|
168
|
+
: ["Accept", "Authorization", "Content-Type"],
|
|
149
169
|
body: cloneProviderValue(body),
|
|
170
|
+
...(provider === "google_interactions" && redactContinuation
|
|
171
|
+
? { redactedProviderContinuation: true }
|
|
172
|
+
: {}),
|
|
173
|
+
...(provider === "google_interactions"
|
|
174
|
+
? { systemInstructionSources: googleSystemSources(request) }
|
|
175
|
+
: {}),
|
|
150
176
|
};
|
|
151
177
|
}
|
|
152
178
|
async #responses(request, onDelta, signal) {
|
|
@@ -162,6 +188,240 @@ export class FileNativeModelHost {
|
|
|
162
188
|
...(signal === undefined ? {} : { signal }),
|
|
163
189
|
});
|
|
164
190
|
}
|
|
191
|
+
async #google(request, onDelta, signal, retainLateExtensionResponse = false, onGoogleHop, diagnosticAttempt) {
|
|
192
|
+
const controller = new AbortController();
|
|
193
|
+
const capture = providerCapture(this.#failureLog, this.#connection.provider, providerUrl(this.#connection), {
|
|
194
|
+
...modelDiagnosticContext(request),
|
|
195
|
+
...(diagnosticAttempt === undefined
|
|
196
|
+
? {}
|
|
197
|
+
: { requestAttempt: diagnosticAttempt }),
|
|
198
|
+
}, JSON.stringify(googleRequestBody(this.#connection, request, true)), googleDiagnosticEndpoint(this.#connection));
|
|
199
|
+
let abort;
|
|
200
|
+
let timer;
|
|
201
|
+
try {
|
|
202
|
+
return await Promise.race([
|
|
203
|
+
this.#googleWithinDeadline(request, (delta) => {
|
|
204
|
+
if (!controller.signal.aborted)
|
|
205
|
+
onDelta?.(delta);
|
|
206
|
+
}, controller.signal, retainLateExtensionResponse, onGoogleHop, diagnosticAttempt, capture),
|
|
207
|
+
new Promise((_, reject) => {
|
|
208
|
+
timer = setTimeout(() => {
|
|
209
|
+
controller.abort();
|
|
210
|
+
if (capture !== undefined && this.#failureLog !== undefined) {
|
|
211
|
+
void this.#failureLog
|
|
212
|
+
.recordFailure({
|
|
213
|
+
exchange: capture.snapshot(),
|
|
214
|
+
failures: [
|
|
215
|
+
{
|
|
216
|
+
kind: "provider_transport",
|
|
217
|
+
message: "Google Interactions exceeded its total time limit",
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
})
|
|
221
|
+
.catch(() => {
|
|
222
|
+
// Failure logging is best effort and cannot delay the timeout.
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
reject(new ModelHostOutcomeUnknownError("Google Interactions exceeded its total time limit"));
|
|
226
|
+
}, 60 * 60 * 1000);
|
|
227
|
+
}),
|
|
228
|
+
new Promise((_, reject) => {
|
|
229
|
+
abort = () => {
|
|
230
|
+
controller.abort();
|
|
231
|
+
if (!retainLateExtensionResponse)
|
|
232
|
+
reject(cancelledModelRequest());
|
|
233
|
+
};
|
|
234
|
+
if (signal?.aborted === true)
|
|
235
|
+
abort();
|
|
236
|
+
else
|
|
237
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
238
|
+
}),
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
if (timer !== undefined)
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
if (abort !== undefined)
|
|
245
|
+
signal?.removeEventListener("abort", abort);
|
|
246
|
+
controller.abort();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async #googleWithinDeadline(request, onDelta, signal, retainLateExtensionResponse, onGoogleHop, diagnosticAttempt, capture) {
|
|
250
|
+
const bodyJson = JSON.stringify(googleRequestBody(this.#connection, request));
|
|
251
|
+
const frozenBody = JSON.parse(bodyJson);
|
|
252
|
+
const url = providerUrl(this.#connection);
|
|
253
|
+
const preview = this.#previewRequest(request, true);
|
|
254
|
+
const evidence = [];
|
|
255
|
+
const report = (hop) => {
|
|
256
|
+
capture?.setGoogleHops(evidence);
|
|
257
|
+
const snapshot = structuredClone(hop);
|
|
258
|
+
try {
|
|
259
|
+
void Promise.resolve(onGoogleHop?.(snapshot)).catch(() => {
|
|
260
|
+
// Optional diagnostics cannot affect the model request.
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Synchronous observers are optional diagnostics too.
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
signal.addEventListener("abort", () => {
|
|
268
|
+
const hop = evidence.at(-1);
|
|
269
|
+
if (hop?.status === "attempted") {
|
|
270
|
+
hop.status = "interrupted";
|
|
271
|
+
void report(hop);
|
|
272
|
+
}
|
|
273
|
+
}, { once: true });
|
|
274
|
+
return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
|
|
275
|
+
const steps = [];
|
|
276
|
+
const usages = [];
|
|
277
|
+
const seenTokens = new Set();
|
|
278
|
+
let token;
|
|
279
|
+
let payload;
|
|
280
|
+
do {
|
|
281
|
+
if (signal.aborted)
|
|
282
|
+
throw cancelledModelRequest();
|
|
283
|
+
const hop = {
|
|
284
|
+
exchange: request.exchange ?? 1,
|
|
285
|
+
attempt: diagnosticAttempt ?? request.requestAttempt ?? 1,
|
|
286
|
+
hop: evidence.length + 1,
|
|
287
|
+
tokenSupplied: token !== undefined,
|
|
288
|
+
status: "attempted",
|
|
289
|
+
...(token === undefined ? { request: preview } : {}),
|
|
290
|
+
};
|
|
291
|
+
evidence.push(hop);
|
|
292
|
+
try {
|
|
293
|
+
const dispatched = dispatchProviderRequest(this.#fetch, url, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: {
|
|
296
|
+
Accept: "text/event-stream",
|
|
297
|
+
"x-goog-api-key": this.#connection.apiKey,
|
|
298
|
+
"Content-Type": "application/json",
|
|
299
|
+
},
|
|
300
|
+
redirect: "error",
|
|
301
|
+
body: token === undefined
|
|
302
|
+
? bodyJson
|
|
303
|
+
: JSON.stringify({
|
|
304
|
+
...frozenBody,
|
|
305
|
+
continuation_token: token,
|
|
306
|
+
}),
|
|
307
|
+
...(signal === undefined ? {} : { signal }),
|
|
308
|
+
}, "Google Interactions");
|
|
309
|
+
void dispatched.catch(() => {
|
|
310
|
+
// The same rejection is classified after the diagnostic write.
|
|
311
|
+
});
|
|
312
|
+
void report(hop);
|
|
313
|
+
const response = await dispatched;
|
|
314
|
+
hop.httpStatus = response.status;
|
|
315
|
+
observe(new Response(null, { status: response.status }));
|
|
316
|
+
if (signal.aborted && !retainLateExtensionResponse)
|
|
317
|
+
throw cancelledModelRequest();
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
hop.status = "rejected";
|
|
320
|
+
void report(hop);
|
|
321
|
+
if (token !== undefined) {
|
|
322
|
+
discardGoogleErrorBody(response);
|
|
323
|
+
throw new ModelHostOutcomeUnknownError("A later Google Interactions continuation request failed after earlier progress");
|
|
324
|
+
}
|
|
325
|
+
throw await googleInitialRejection(response);
|
|
326
|
+
}
|
|
327
|
+
if (!isProviderEventStream(response)) {
|
|
328
|
+
discardGoogleErrorBody(response);
|
|
329
|
+
throw unknownProviderResponse("Google Interactions");
|
|
330
|
+
}
|
|
331
|
+
payload = await providerStreamResult(response, "Google Interactions", (body) => aggregateGoogleInteractions(body, onDelta, (raw) => {
|
|
332
|
+
const usage = safeGoogleUsage(raw);
|
|
333
|
+
if (usage !== null) {
|
|
334
|
+
hop.usage = usage;
|
|
335
|
+
void report(hop);
|
|
336
|
+
}
|
|
337
|
+
}), signal);
|
|
338
|
+
const safeUsage = safeGoogleUsage(payload.usage);
|
|
339
|
+
if (safeUsage !== null)
|
|
340
|
+
hop.usage = safeUsage;
|
|
341
|
+
if ((token !== undefined ||
|
|
342
|
+
payload.status === "requires_continuation") &&
|
|
343
|
+
!payload.completeLifecycle)
|
|
344
|
+
throw new ModelHostOutcomeUnknownError("Google Interactions continuation stream lifecycle is incomplete");
|
|
345
|
+
steps.push(...payload.steps);
|
|
346
|
+
usages.push(safeUsage);
|
|
347
|
+
hop.status =
|
|
348
|
+
payload.status === "completed" ||
|
|
349
|
+
payload.status === "requires_action" ||
|
|
350
|
+
payload.status === "requires_continuation" ||
|
|
351
|
+
payload.status === "failed" ||
|
|
352
|
+
payload.status === "cancelled" ||
|
|
353
|
+
payload.status === "incomplete"
|
|
354
|
+
? payload.status
|
|
355
|
+
: "interrupted";
|
|
356
|
+
void report(hop);
|
|
357
|
+
if (payload.status === "requires_continuation") {
|
|
358
|
+
if (signal.aborted)
|
|
359
|
+
throw cancelledModelRequest();
|
|
360
|
+
if (typeof payload.continuationToken !== "string" ||
|
|
361
|
+
!payload.continuationToken ||
|
|
362
|
+
seenTokens.has(payload.continuationToken) ||
|
|
363
|
+
usages.length >= 16)
|
|
364
|
+
throw new ModelHostOutcomeUnknownError("Google Interactions continuation cannot proceed safely");
|
|
365
|
+
token = payload.continuationToken;
|
|
366
|
+
seenTokens.add(token);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (hop.status === "attempted") {
|
|
371
|
+
hop.status = "interrupted";
|
|
372
|
+
void report(hop);
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
} while (payload.status === "requires_continuation");
|
|
377
|
+
payload.steps = steps;
|
|
378
|
+
if (signalWasAborted(signal) && !retainLateExtensionResponse)
|
|
379
|
+
throw cancelledModelRequest();
|
|
380
|
+
const projection = projectGoogleInteractions(payload);
|
|
381
|
+
const total = (field) => {
|
|
382
|
+
let sum = 0;
|
|
383
|
+
for (const raw of usages) {
|
|
384
|
+
const value = isRecord(raw) ? providerNumber(raw[field]) : null;
|
|
385
|
+
if (value === null)
|
|
386
|
+
return null;
|
|
387
|
+
sum += value;
|
|
388
|
+
}
|
|
389
|
+
return Number.isSafeInteger(sum) ? sum : null;
|
|
390
|
+
};
|
|
391
|
+
return {
|
|
392
|
+
...projection,
|
|
393
|
+
usage: {
|
|
394
|
+
...usageRecord({
|
|
395
|
+
inputTokens: total("total_input_tokens"),
|
|
396
|
+
uncachedInputTokens: null,
|
|
397
|
+
cacheReadTokens: total("total_cached_tokens"),
|
|
398
|
+
cacheWriteTokens: null,
|
|
399
|
+
reasoningTokens: total("total_thought_tokens"),
|
|
400
|
+
outputTokens: total("total_output_tokens"),
|
|
401
|
+
totalTokens: total("total_tokens"),
|
|
402
|
+
}, usages.length > 1
|
|
403
|
+
? [
|
|
404
|
+
"inputTokens",
|
|
405
|
+
"cacheReadTokens",
|
|
406
|
+
"reasoningTokens",
|
|
407
|
+
"outputTokens",
|
|
408
|
+
"totalTokens",
|
|
409
|
+
]
|
|
410
|
+
: []),
|
|
411
|
+
accounting: "google_interactions",
|
|
412
|
+
...(usages.length > 1
|
|
413
|
+
? {
|
|
414
|
+
providerRaw: {
|
|
415
|
+
hops: usages.map((usage) => isRecord(usage) ? structuredClone(usage) : null),
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
: isRecord(usages[0])
|
|
419
|
+
? { providerRaw: structuredClone(usages[0]) }
|
|
420
|
+
: {}),
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}, true);
|
|
424
|
+
}
|
|
165
425
|
async #chat(request, onDelta, signal) {
|
|
166
426
|
const body = chatRequestBody(this.#connection, request);
|
|
167
427
|
const bodyJson = JSON.stringify(body);
|
|
@@ -398,10 +658,16 @@ function modelDiagnosticContext(request) {
|
|
|
398
658
|
...(request.exchange === undefined ? {} : { exchange: request.exchange }),
|
|
399
659
|
};
|
|
400
660
|
}
|
|
401
|
-
function providerCapture(failureLog, provider, endpoint, context, requestBody) {
|
|
661
|
+
function providerCapture(failureLog, provider, endpoint, context, requestBody, endpointLabel) {
|
|
402
662
|
return failureLog === undefined
|
|
403
663
|
? undefined
|
|
404
|
-
: new AiExchangeCapture({
|
|
664
|
+
: new AiExchangeCapture({
|
|
665
|
+
provider,
|
|
666
|
+
endpoint,
|
|
667
|
+
context,
|
|
668
|
+
requestBody,
|
|
669
|
+
...(endpointLabel === undefined ? {} : { endpointLabel }),
|
|
670
|
+
});
|
|
405
671
|
}
|
|
406
672
|
function captureDelta(capture, downstream) {
|
|
407
673
|
if (capture === undefined)
|
|
@@ -412,7 +678,7 @@ function captureDelta(capture, downstream) {
|
|
|
412
678
|
downstream?.(delta);
|
|
413
679
|
};
|
|
414
680
|
}
|
|
415
|
-
async function capturedProviderOperation(failureLog, capture, operation) {
|
|
681
|
+
async function capturedProviderOperation(failureLog, capture, operation, sanitizeGoogleErrors = false) {
|
|
416
682
|
const observeResponse = (response) => capture?.captureResponse(response) ?? response;
|
|
417
683
|
try {
|
|
418
684
|
const value = await operation(observeResponse);
|
|
@@ -423,26 +689,33 @@ async function capturedProviderOperation(failureLog, capture, operation) {
|
|
|
423
689
|
catch (error) {
|
|
424
690
|
if (error instanceof ModelHostCancelledError)
|
|
425
691
|
throw error;
|
|
692
|
+
const reportedError = sanitizeGoogleErrors
|
|
693
|
+
? error instanceof ModelHostFailureError
|
|
694
|
+
? modelHostFailureRequiresFreshContext(error)
|
|
695
|
+
? new ModelHostFailureError("Google Interactions context limit exceeded; start a fresh context", { details: { code: "context_length_exceeded" } })
|
|
696
|
+
: new ModelHostFailureError("Google Interactions request was rejected")
|
|
697
|
+
: new ModelHostOutcomeUnknownError("Google Interactions request outcome could not be confirmed")
|
|
698
|
+
: error;
|
|
426
699
|
if (capture !== undefined && failureLog !== undefined) {
|
|
427
700
|
const exchange = capture.snapshot();
|
|
428
701
|
const failure = {
|
|
429
702
|
kind: exchange.response === undefined
|
|
430
703
|
? "provider_transport"
|
|
431
|
-
:
|
|
704
|
+
: reportedError instanceof ModelHostFailureError
|
|
432
705
|
? "provider_rejection"
|
|
433
706
|
: "provider_response_format",
|
|
434
|
-
message:
|
|
435
|
-
?
|
|
707
|
+
message: reportedError instanceof Error
|
|
708
|
+
? reportedError.message
|
|
436
709
|
: "Provider request processing failed.",
|
|
437
|
-
...(
|
|
438
|
-
|
|
439
|
-
? { details:
|
|
710
|
+
...(reportedError instanceof ModelHostFailureError &&
|
|
711
|
+
reportedError.details !== undefined
|
|
712
|
+
? { details: reportedError.details }
|
|
440
713
|
: {}),
|
|
441
|
-
error: errorDescription(
|
|
714
|
+
error: errorDescription(reportedError),
|
|
442
715
|
};
|
|
443
716
|
await failureLog.recordFailure({ exchange, failures: [failure] });
|
|
444
717
|
}
|
|
445
|
-
throw
|
|
718
|
+
throw reportedError;
|
|
446
719
|
}
|
|
447
720
|
}
|
|
448
721
|
async function trace(kind, value) {
|
|
@@ -463,6 +736,185 @@ async function trace(kind, value) {
|
|
|
463
736
|
// Explicit acceptance diagnostics must never change provider semantics.
|
|
464
737
|
}
|
|
465
738
|
}
|
|
739
|
+
function googleSystemSources(request) {
|
|
740
|
+
const logical = request.bootstrap.logicalMessages.filter((message) => message.role !== "player_input" &&
|
|
741
|
+
(request.bootstrap.provider.preload === undefined ||
|
|
742
|
+
message.role !== "world_context"));
|
|
743
|
+
const ordered = logical.some((message) => message.messageRole !== undefined);
|
|
744
|
+
const firstConversation = logical.findIndex((message) => (message.messageRole ?? "system") !== "system");
|
|
745
|
+
const leading = ordered
|
|
746
|
+
? logical.slice(0, firstConversation < 0 ? logical.length : firstConversation)
|
|
747
|
+
: logical.filter((message) => message.role === "runtime_system" ||
|
|
748
|
+
message.role === "author_instruction");
|
|
749
|
+
const sources = leading.map((message) => ({
|
|
750
|
+
...(message.promptId === undefined ? {} : { promptId: message.promptId }),
|
|
751
|
+
sources: message.blocks.map(({ source }) => source),
|
|
752
|
+
}));
|
|
753
|
+
const current = request.worldRevisionBoundaries?.entries.findLast(({ epochId }) => epochId === request.worldRevisionBoundaries?.currentEpochId);
|
|
754
|
+
if (current !== undefined)
|
|
755
|
+
sources.push({
|
|
756
|
+
sources: [
|
|
757
|
+
`runtime:world-revision-epoch:${current.epochId}:system-boundary`,
|
|
758
|
+
],
|
|
759
|
+
});
|
|
760
|
+
return sources;
|
|
761
|
+
}
|
|
762
|
+
function googleLateSystemSource(request) {
|
|
763
|
+
let conversation = false;
|
|
764
|
+
for (const message of request.bootstrap.logicalMessages) {
|
|
765
|
+
if (message.role === "player_input" ||
|
|
766
|
+
(request.bootstrap.provider.preload !== undefined &&
|
|
767
|
+
message.role === "world_context"))
|
|
768
|
+
continue;
|
|
769
|
+
if ((message.messageRole ?? "system") !== "system")
|
|
770
|
+
conversation = true;
|
|
771
|
+
else if (conversation)
|
|
772
|
+
return [message.promptId, ...message.blocks.map(({ source }) => source)]
|
|
773
|
+
.filter((item) => item !== undefined)
|
|
774
|
+
.join(", ");
|
|
775
|
+
}
|
|
776
|
+
return "";
|
|
777
|
+
}
|
|
778
|
+
function googleRequestBody(connection, request, redactContinuation = false) {
|
|
779
|
+
const messages = request.bootstrap.provider.messages;
|
|
780
|
+
const system = [];
|
|
781
|
+
const input = [];
|
|
782
|
+
let conversationStarted = false;
|
|
783
|
+
const text = (value) => [{ type: "text", text: value }];
|
|
784
|
+
const contentText = (value) => {
|
|
785
|
+
if (typeof value === "string")
|
|
786
|
+
return value;
|
|
787
|
+
if (Array.isArray(value) &&
|
|
788
|
+
value.every((part) => isRecord(part) &&
|
|
789
|
+
part.type === "text" &&
|
|
790
|
+
typeof part.text === "string"))
|
|
791
|
+
return value.map((part) => part.text).join("\n\n");
|
|
792
|
+
throw new Error("Google Interactions cannot encode non-text bootstrap content");
|
|
793
|
+
};
|
|
794
|
+
const preload = request.bootstrap.provider.preload;
|
|
795
|
+
const appendPreload = () => {
|
|
796
|
+
if (preload !== undefined)
|
|
797
|
+
input.push({
|
|
798
|
+
type: "user_input",
|
|
799
|
+
content: text("Runtime preload: initial world materials, not a player action or model response."),
|
|
800
|
+
}, {
|
|
801
|
+
type: "function_call",
|
|
802
|
+
id: preload.callId,
|
|
803
|
+
name: "runtime_load_context",
|
|
804
|
+
arguments: {},
|
|
805
|
+
}, {
|
|
806
|
+
type: "function_result",
|
|
807
|
+
call_id: preload.callId,
|
|
808
|
+
name: "runtime_load_context",
|
|
809
|
+
result: text(preload.markdown),
|
|
810
|
+
});
|
|
811
|
+
};
|
|
812
|
+
for (const [index, message] of messages.entries()) {
|
|
813
|
+
if (index === bootstrapPrefixEnd(request))
|
|
814
|
+
appendPreload();
|
|
815
|
+
if (message.role === "system") {
|
|
816
|
+
if (conversationStarted)
|
|
817
|
+
throw new Error(`Google Interactions cannot encode System message ${index + 1} after User/Assistant${googleLateSystemSource(request) ? `: ${googleLateSystemSource(request)}` : ""}`);
|
|
818
|
+
system.push(contentText(message.content));
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
conversationStarted = true;
|
|
822
|
+
input.push({
|
|
823
|
+
type: message.role === "user" ? "user_input" : "model_output",
|
|
824
|
+
content: text(contentText(message.content)),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (bootstrapPrefixEnd(request) === messages.length)
|
|
829
|
+
appendPreload();
|
|
830
|
+
const calls = new Map();
|
|
831
|
+
const boundaries = request.worldRevisionBoundaries;
|
|
832
|
+
const currentBoundary = boundaries?.entries.findLast(({ epochId }) => epochId === boundaries.currentEpochId);
|
|
833
|
+
if (currentBoundary !== undefined)
|
|
834
|
+
system.push(`# Runtime world-revision epoch boundary\n\nThe current epoch is ${currentBoundary.epochId}, based on ${currentBoundary.baseHead}. All prior search cursors and read authorizations are invalid. Re-read before writing, except for materials legitimately preloaded in the current revision.`);
|
|
835
|
+
for (const [itemIndex, item] of request.appended.entries()) {
|
|
836
|
+
if (item.kind === "assistant") {
|
|
837
|
+
if (item.providerState?.protocol !== "google_interactions" ||
|
|
838
|
+
!validGoogleInteractionsContinuation(item.providerState.steps))
|
|
839
|
+
throw continuationError("Google Interactions");
|
|
840
|
+
for (const step of item.providerState.steps) {
|
|
841
|
+
if (isRecord(step) &&
|
|
842
|
+
step.type === "function_call" &&
|
|
843
|
+
typeof step.id === "string" &&
|
|
844
|
+
typeof step.name === "string")
|
|
845
|
+
calls.set(step.id, step.name);
|
|
846
|
+
}
|
|
847
|
+
input.push(...(redactContinuation
|
|
848
|
+
? item.providerState.steps.map(() => ({
|
|
849
|
+
type: "runtime_redacted_provider_step",
|
|
850
|
+
}))
|
|
851
|
+
: cloneProviderValue(item.providerState.steps)));
|
|
852
|
+
}
|
|
853
|
+
else if (item.kind === "tool") {
|
|
854
|
+
const name = calls.get(item.toolCallId);
|
|
855
|
+
if (name === undefined)
|
|
856
|
+
throw continuationError("Google Interactions tool result");
|
|
857
|
+
input.push({
|
|
858
|
+
type: "function_result",
|
|
859
|
+
name,
|
|
860
|
+
call_id: item.toolCallId,
|
|
861
|
+
result: text(item.markdown),
|
|
862
|
+
...(item.isError === true ? { is_error: true } : {}),
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
else if (item.kind === "prompt_delta") {
|
|
866
|
+
const boundary = boundaries?.entries.find((entry) => entry.itemIndex === itemIndex);
|
|
867
|
+
if (boundary !== undefined) {
|
|
868
|
+
const message = item.logicalMessages[0];
|
|
869
|
+
if (item.logicalMessages.length !== 1 ||
|
|
870
|
+
message?.role !== "runtime_system" ||
|
|
871
|
+
message.markdown !== boundary.markdown ||
|
|
872
|
+
message.blocks.length !== 1 ||
|
|
873
|
+
message.blocks[0]?.source !==
|
|
874
|
+
`runtime:world-revision-epoch:${boundary.epochId}` ||
|
|
875
|
+
message.blocks[0]?.markdown !== boundary.markdown)
|
|
876
|
+
throw new Error("Invalid Runtime world-revision epoch boundary");
|
|
877
|
+
input.push({
|
|
878
|
+
type: "user_input",
|
|
879
|
+
content: text(`# Runtime notice: world-revision epoch transition\n\n${boundary.markdown}`),
|
|
880
|
+
});
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
for (const message of item.logicalMessages)
|
|
884
|
+
input.push({
|
|
885
|
+
type: "user_input",
|
|
886
|
+
content: text(renderPromptDeltaMessage(message.role, message.markdown)),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
else
|
|
890
|
+
input.push({ type: "user_input", content: text(item.text) });
|
|
891
|
+
}
|
|
892
|
+
const tools = providerToolDefinitions(request);
|
|
893
|
+
return {
|
|
894
|
+
model: connection.modelId,
|
|
895
|
+
store: false,
|
|
896
|
+
stream: true,
|
|
897
|
+
...(system.length === 0 ? {} : { system_instruction: system.join("\n\n") }),
|
|
898
|
+
input,
|
|
899
|
+
...(tools.length === 0
|
|
900
|
+
? {}
|
|
901
|
+
: {
|
|
902
|
+
tools: tools.map((tool) => ({
|
|
903
|
+
type: "function",
|
|
904
|
+
name: tool.name,
|
|
905
|
+
description: tool.description,
|
|
906
|
+
parameters: tool.inputSchema,
|
|
907
|
+
})),
|
|
908
|
+
}),
|
|
909
|
+
generation_config: {
|
|
910
|
+
max_output_tokens: request.maxOutputTokens,
|
|
911
|
+
...(hasFrozenToolPolicy(request) &&
|
|
912
|
+
providerAllowedTools(request).length === 0
|
|
913
|
+
? { tool_choice: "none" }
|
|
914
|
+
: {}),
|
|
915
|
+
},
|
|
916
|
+
};
|
|
917
|
+
}
|
|
466
918
|
function chatRequestBody(connection, request) {
|
|
467
919
|
const tools = providerToolDefinitions(request);
|
|
468
920
|
const allowedTools = providerAllowedTools(request);
|
|
@@ -1018,11 +1470,133 @@ function providerUrl(connection) {
|
|
|
1018
1470
|
? "/chat/completions"
|
|
1019
1471
|
: connection.provider === "openai_responses"
|
|
1020
1472
|
? "/responses"
|
|
1021
|
-
: "
|
|
1473
|
+
: connection.provider === "google_interactions"
|
|
1474
|
+
? "/interactions"
|
|
1475
|
+
: "/messages";
|
|
1022
1476
|
const path = url.pathname.replace(/\/+$/u, "");
|
|
1023
1477
|
url.pathname = path.endsWith(suffix) ? path : `${path}${suffix}`;
|
|
1024
1478
|
return url;
|
|
1025
1479
|
}
|
|
1480
|
+
function googleDiagnosticPath(connection) {
|
|
1481
|
+
const url = providerUrl(connection);
|
|
1482
|
+
return containsGoogleCredential(url.pathname, connection.apiKey)
|
|
1483
|
+
? ""
|
|
1484
|
+
: url.pathname;
|
|
1485
|
+
}
|
|
1486
|
+
function googleDiagnosticEndpoint(connection) {
|
|
1487
|
+
const url = providerUrl(connection);
|
|
1488
|
+
const endpoint = `${url.protocol}//${url.host}${googleDiagnosticPath(connection)}`;
|
|
1489
|
+
return containsGoogleCredential(endpoint, connection.apiKey) ? "" : endpoint;
|
|
1490
|
+
}
|
|
1491
|
+
function containsGoogleCredential(value, key) {
|
|
1492
|
+
if (key === "")
|
|
1493
|
+
return false;
|
|
1494
|
+
const needle = key.toLowerCase();
|
|
1495
|
+
let current = value;
|
|
1496
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
1497
|
+
if (current.toLowerCase().includes(needle) ||
|
|
1498
|
+
current.toLowerCase().includes(encodeURIComponent(key).toLowerCase()))
|
|
1499
|
+
return true;
|
|
1500
|
+
try {
|
|
1501
|
+
const decoded = decodeURIComponent(current);
|
|
1502
|
+
if (decoded === current)
|
|
1503
|
+
break;
|
|
1504
|
+
current = decoded;
|
|
1505
|
+
}
|
|
1506
|
+
catch {
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
/** Classify a first-hop rejection in memory; never retain the provider's echo. */
|
|
1513
|
+
async function googleInitialRejection(response) {
|
|
1514
|
+
if (![400, 401, 403, 404, 405, 413, 415, 422, 429].includes(response.status)) {
|
|
1515
|
+
discardGoogleErrorBody(response);
|
|
1516
|
+
return new ModelHostOutcomeUnknownError("Google Interactions request outcome could not be confirmed");
|
|
1517
|
+
}
|
|
1518
|
+
let body = "";
|
|
1519
|
+
if (response.body !== null) {
|
|
1520
|
+
const reader = response.body.getReader();
|
|
1521
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1522
|
+
let complete = false;
|
|
1523
|
+
let bytes = 0;
|
|
1524
|
+
try {
|
|
1525
|
+
for (;;) {
|
|
1526
|
+
const chunk = await reader.read();
|
|
1527
|
+
if (chunk.done) {
|
|
1528
|
+
complete = true;
|
|
1529
|
+
body += decoder.decode();
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
bytes += chunk.value.byteLength;
|
|
1533
|
+
if (bytes > 64 * 1024)
|
|
1534
|
+
break;
|
|
1535
|
+
body += decoder.decode(chunk.value, { stream: true });
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
catch {
|
|
1539
|
+
return new ModelHostOutcomeUnknownError("Google Interactions request outcome could not be confirmed");
|
|
1540
|
+
}
|
|
1541
|
+
finally {
|
|
1542
|
+
if (!complete)
|
|
1543
|
+
void reader.cancel().catch(() => undefined);
|
|
1544
|
+
reader.releaseLock();
|
|
1545
|
+
}
|
|
1546
|
+
if (!complete)
|
|
1547
|
+
return new ModelHostOutcomeUnknownError("Google Interactions request outcome could not be confirmed");
|
|
1548
|
+
}
|
|
1549
|
+
const contentType = response.headers
|
|
1550
|
+
.get("content-type")
|
|
1551
|
+
?.split(";", 1)[0]
|
|
1552
|
+
?.trim()
|
|
1553
|
+
.toLowerCase();
|
|
1554
|
+
const evidence = googleRejectionEvidence(body, contentType === "application/json" ||
|
|
1555
|
+
contentType?.endsWith("+json") === true);
|
|
1556
|
+
if (evidence === null)
|
|
1557
|
+
return new ModelHostOutcomeUnknownError("Google Interactions request outcome could not be confirmed");
|
|
1558
|
+
return modelHostFailureRequiresFreshContext(new ModelHostFailureError(evidence))
|
|
1559
|
+
? new ModelHostFailureError("Google Interactions context limit exceeded; start a fresh context", { details: { code: "context_length_exceeded" } })
|
|
1560
|
+
: new ModelHostFailureError("Google Interactions request was rejected");
|
|
1561
|
+
}
|
|
1562
|
+
function discardGoogleErrorBody(response) {
|
|
1563
|
+
try {
|
|
1564
|
+
void response.body?.cancel().catch(() => undefined);
|
|
1565
|
+
}
|
|
1566
|
+
catch {
|
|
1567
|
+
// Discarding an optional error body must not alter the request outcome.
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function googleRejectionEvidence(body, requireJson) {
|
|
1571
|
+
if (body.trim() === "")
|
|
1572
|
+
return null;
|
|
1573
|
+
try {
|
|
1574
|
+
const payload = JSON.parse(body);
|
|
1575
|
+
if (typeof payload === "string")
|
|
1576
|
+
return payload.trim() === "" ? null : payload;
|
|
1577
|
+
if (!isRecord(payload))
|
|
1578
|
+
return null;
|
|
1579
|
+
const error = isRecord(payload.error) ? payload.error : {};
|
|
1580
|
+
const evidence = [
|
|
1581
|
+
error.message,
|
|
1582
|
+
error.code,
|
|
1583
|
+
error.reason,
|
|
1584
|
+
payload.message,
|
|
1585
|
+
payload.code,
|
|
1586
|
+
payload.error,
|
|
1587
|
+
]
|
|
1588
|
+
.filter((value) => typeof value === "string")
|
|
1589
|
+
.join("\n");
|
|
1590
|
+
return evidence.trim() === "" ? null : evidence;
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
// An unstructured error may itself be the Provider's rejection message.
|
|
1594
|
+
const trimmed = body.trimStart();
|
|
1595
|
+
return requireJson || trimmed.startsWith("{") || trimmed.startsWith("[")
|
|
1596
|
+
? null
|
|
1597
|
+
: body;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1026
1600
|
function parseArguments(value) {
|
|
1027
1601
|
try {
|
|
1028
1602
|
return JSON.parse(value);
|
|
@@ -1337,6 +1911,24 @@ function normalizeResponsesUsage(value) {
|
|
|
1337
1911
|
totalTokens: providerNumber(usage.total_tokens),
|
|
1338
1912
|
}, ["uncachedInputTokens"]);
|
|
1339
1913
|
}
|
|
1914
|
+
function safeGoogleUsage(value) {
|
|
1915
|
+
if (!isRecord(value))
|
|
1916
|
+
return null;
|
|
1917
|
+
const result = {};
|
|
1918
|
+
for (const field of [
|
|
1919
|
+
"total_input_tokens",
|
|
1920
|
+
"total_cached_tokens",
|
|
1921
|
+
"total_thought_tokens",
|
|
1922
|
+
"total_output_tokens",
|
|
1923
|
+
"total_tool_use_tokens",
|
|
1924
|
+
"total_tokens",
|
|
1925
|
+
]) {
|
|
1926
|
+
const count = value[field];
|
|
1927
|
+
if (typeof count === "number" && Number.isSafeInteger(count) && count >= 0)
|
|
1928
|
+
result[field] = count;
|
|
1929
|
+
}
|
|
1930
|
+
return result;
|
|
1931
|
+
}
|
|
1340
1932
|
function providerNumber(value) {
|
|
1341
1933
|
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
1342
1934
|
? value
|