agent-accelerator 0.5.9 → 0.6.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/README.md +3 -1
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +366 -161
- package/src/agent/architecture.md +13 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +8 -3
- package/src/agent/delegation.ts +82 -29
- package/src/agent/loop.ts +444 -365
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +45 -7
- package/src/agent/subagents.md +3 -0
- package/src/agent/turns.ts +3 -1
- package/src/agent/worker.ts +56 -27
- package/src/agent/workers.ts +113 -20
- package/src/architecture.md +16 -15
- package/src/index.ts +34 -10
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +147 -59
- package/src/providers/openai-compat.ts +57 -49
- package/src/providers/openai.ts +54 -37
- package/src/providers/openrouter.ts +85 -30
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +7 -6
- package/src/scheduler/scheduled-actions.md +4 -0
- package/src/scheduler/scheduler.ts +186 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +33 -7
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +38 -12
- package/src/session/store.ts +122 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +8 -7
- package/src/tools/executor.ts +50 -48
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +2 -21
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +1 -9
- package/src/types/errors.ts +26 -4
- package/src/types/model.ts +0 -4
- package/src/types/tool.ts +12 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +6 -14
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/providers/google.ts
CHANGED
|
@@ -29,7 +29,7 @@ import type {
|
|
|
29
29
|
ProviderGenerateResult,
|
|
30
30
|
ProviderRawData,
|
|
31
31
|
} from "../types/model.ts";
|
|
32
|
-
import type { ProviderContext,
|
|
32
|
+
import type { ProviderContext, ContentPart, ProviderSteps, ToolResultPart, MediaResolution } from "../types/message.ts";
|
|
33
33
|
import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
|
|
34
34
|
import type { TokenUsage } from "../types/core.ts";
|
|
35
35
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
@@ -53,7 +53,6 @@ import {
|
|
|
53
53
|
combinedSignal,
|
|
54
54
|
timeoutFor,
|
|
55
55
|
hasAuthHeader,
|
|
56
|
-
clampToolCallId,
|
|
57
56
|
openStreamResponse,
|
|
58
57
|
retryOptionsFrom,
|
|
59
58
|
headersToRecord,
|
|
@@ -85,19 +84,70 @@ import {
|
|
|
85
84
|
// ---------------------------------------------------------------------------
|
|
86
85
|
|
|
87
86
|
/** Image MIME types Google accepts inline. */
|
|
88
|
-
export const GOOGLE_IMAGE_MIME = [
|
|
87
|
+
export const GOOGLE_IMAGE_MIME = [
|
|
88
|
+
"image/png",
|
|
89
|
+
"image/jpeg",
|
|
90
|
+
"image/webp",
|
|
91
|
+
"image/heic",
|
|
92
|
+
"image/heif",
|
|
93
|
+
"image/gif",
|
|
94
|
+
"image/bmp",
|
|
95
|
+
"image/tiff",
|
|
96
|
+
] as const;
|
|
89
97
|
/** Audio MIME types Google accepts inline (audio is never fetched by URL). */
|
|
90
|
-
export const GOOGLE_AUDIO_MIME = [
|
|
98
|
+
export const GOOGLE_AUDIO_MIME = [
|
|
99
|
+
"audio/wav",
|
|
100
|
+
"audio/mp3",
|
|
101
|
+
"audio/aiff",
|
|
102
|
+
"audio/aac",
|
|
103
|
+
"audio/ogg",
|
|
104
|
+
"audio/flac",
|
|
105
|
+
"audio/mpeg",
|
|
106
|
+
"audio/m4a",
|
|
107
|
+
"audio/l16",
|
|
108
|
+
"audio/opus",
|
|
109
|
+
"audio/alaw",
|
|
110
|
+
"audio/mulaw",
|
|
111
|
+
"audio/webm",
|
|
112
|
+
] as const;
|
|
91
113
|
/** Video MIME types Google accepts inline. */
|
|
92
|
-
export const GOOGLE_VIDEO_MIME = [
|
|
114
|
+
export const GOOGLE_VIDEO_MIME = [
|
|
115
|
+
"video/mp4",
|
|
116
|
+
"video/mpeg",
|
|
117
|
+
"video/mpg",
|
|
118
|
+
"video/mov",
|
|
119
|
+
"video/avi",
|
|
120
|
+
"video/x-flv",
|
|
121
|
+
"video/webm",
|
|
122
|
+
"video/wmv",
|
|
123
|
+
"video/3gpp",
|
|
124
|
+
] as const;
|
|
93
125
|
/** Document MIME types Google accepts (the only two). */
|
|
94
126
|
export const GOOGLE_DOCUMENT_MIME = ["application/pdf", "text/csv"] as const;
|
|
95
127
|
/** MIME types Google fetches itself from a public `https://` URL. */
|
|
96
128
|
export const GOOGLE_URL_MIME = [
|
|
97
|
-
"text/html",
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
129
|
+
"text/html",
|
|
130
|
+
"text/css",
|
|
131
|
+
"text/plain",
|
|
132
|
+
"text/xml",
|
|
133
|
+
"text/csv",
|
|
134
|
+
"text/rtf",
|
|
135
|
+
"text/javascript",
|
|
136
|
+
"application/json",
|
|
137
|
+
"application/pdf",
|
|
138
|
+
"image/bmp",
|
|
139
|
+
"image/jpeg",
|
|
140
|
+
"image/png",
|
|
141
|
+
"image/webp",
|
|
142
|
+
"video/mp4",
|
|
143
|
+
"video/mpeg",
|
|
144
|
+
"video/quicktime",
|
|
145
|
+
"video/avi",
|
|
146
|
+
"video/x-flv",
|
|
147
|
+
"video/mpg",
|
|
148
|
+
"video/webm",
|
|
149
|
+
"video/wmv",
|
|
150
|
+
"video/3gpp",
|
|
101
151
|
] as const;
|
|
102
152
|
/** Common MIME spellings mapped to Google's inline names. */
|
|
103
153
|
export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
|
|
@@ -174,7 +224,13 @@ const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
|
|
|
174
224
|
// Interactions wire shapes (subset used by this adapter)
|
|
175
225
|
// ---------------------------------------------------------------------------
|
|
176
226
|
|
|
177
|
-
type MediaBlock = {
|
|
227
|
+
type MediaBlock = {
|
|
228
|
+
type: MediaBlockType;
|
|
229
|
+
mime_type?: string;
|
|
230
|
+
data?: string;
|
|
231
|
+
uri?: string;
|
|
232
|
+
resolution?: MediaResolution;
|
|
233
|
+
};
|
|
178
234
|
type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
|
|
179
235
|
|
|
180
236
|
type InteractionInputStep =
|
|
@@ -317,7 +373,10 @@ function liveChainFor(sessionId: string | undefined, options?: ProviderRequestOp
|
|
|
317
373
|
* below the previous turn's total (Rule 3: a caller dropped the chain without
|
|
318
374
|
* rehydrating history). Never throws; observability-only.
|
|
319
375
|
*/
|
|
320
|
-
function checkTurnTokens(
|
|
376
|
+
function checkTurnTokens(
|
|
377
|
+
sessionId: string | undefined,
|
|
378
|
+
usage: { inputTokens: number; outputTokens: number; cachedTokens?: number }
|
|
379
|
+
): void {
|
|
321
380
|
try {
|
|
322
381
|
if (!sessionId) return;
|
|
323
382
|
const total = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
@@ -409,9 +468,7 @@ function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string,
|
|
|
409
468
|
type: "function",
|
|
410
469
|
name: t.name,
|
|
411
470
|
description: t.description,
|
|
412
|
-
parameters: stripSchemaForGoogle(
|
|
413
|
-
(t.parameters || { type: "object", properties: {} }) as Record<string, unknown>
|
|
414
|
-
),
|
|
471
|
+
parameters: stripSchemaForGoogle((t.parameters || { type: "object", properties: {} }) as Record<string, unknown>),
|
|
415
472
|
}));
|
|
416
473
|
}
|
|
417
474
|
|
|
@@ -443,7 +500,9 @@ function uriBlock(type: MediaBlockType, value: string, mimeType?: string): Media
|
|
|
443
500
|
if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
|
|
444
501
|
const mime = explicit ?? bareMime(inferMimeType(value, ""));
|
|
445
502
|
if (value.startsWith(FILES_API_PREFIX) || value.startsWith("gs://")) return withMime(mime);
|
|
446
|
-
return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
|
|
503
|
+
return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
|
|
504
|
+
? withMime(mime)
|
|
505
|
+
: undefined;
|
|
447
506
|
}
|
|
448
507
|
|
|
449
508
|
// Inlines a media part as base64 under Google's MIME name, failing before the network on an unsupported type; in: any media value (bytes, data URL, path, http URL), out: contentPartsToBlocks.
|
|
@@ -452,7 +511,8 @@ async function inlineBlock(type: MediaBlockType, value: MediaValue, mimeType?: s
|
|
|
452
511
|
const raw = bareMime(norm.mimeType) ?? "";
|
|
453
512
|
const mime = GOOGLE_INLINE_MIME_ALIASES[raw] ?? raw;
|
|
454
513
|
if (!INLINE_MIME[type].includes(mime)) {
|
|
455
|
-
const what =
|
|
514
|
+
const what =
|
|
515
|
+
type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
|
|
456
516
|
throw new ValidationError(
|
|
457
517
|
`[Agent Accelerator] ${what} (got "${mime}"; supported: ${INLINE_MIME[type].join(", ")}). Convert the file or pass the correct mimeType.`
|
|
458
518
|
);
|
|
@@ -534,7 +594,9 @@ function functionResultStep(part: ToolResultPart): InteractionInputStep {
|
|
|
534
594
|
* behind them. Sending only the last message would drop the tool results and
|
|
535
595
|
* leave the chained interaction waiting on its function calls.
|
|
536
596
|
*/
|
|
537
|
-
async function latestTurnInput(
|
|
597
|
+
async function latestTurnInput(
|
|
598
|
+
context: ProviderContext
|
|
599
|
+
): Promise<string | InteractionContentBlock[] | InteractionInputStep[]> {
|
|
538
600
|
const messages = context.messages;
|
|
539
601
|
let lastAssistant = -1;
|
|
540
602
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -617,10 +679,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
617
679
|
const sig = shouldEchoSignature(partProvider ?? msgProvider, "google")
|
|
618
680
|
? part.thoughtSignature || m.thoughtSignature || ""
|
|
619
681
|
: "";
|
|
620
|
-
const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } =
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
682
|
+
const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } =
|
|
683
|
+
{
|
|
684
|
+
type: "thought",
|
|
685
|
+
summary: [{ type: "text", text: part.thinking }],
|
|
686
|
+
};
|
|
624
687
|
if (sig) thoughtStep.signature = sig;
|
|
625
688
|
steps.push(thoughtStep);
|
|
626
689
|
} else if (part.type === "tool_call") {
|
|
@@ -733,11 +796,7 @@ function parseInteraction(
|
|
|
733
796
|
// truncated marker); absent args mean a zero-argument call.
|
|
734
797
|
const rawArgs: unknown = step.arguments;
|
|
735
798
|
const args =
|
|
736
|
-
rawArgs && typeof rawArgs === "object"
|
|
737
|
-
? rawArgs
|
|
738
|
-
: typeof rawArgs === "string"
|
|
739
|
-
? parseArguments(rawArgs)
|
|
740
|
-
: {};
|
|
799
|
+
rawArgs && typeof rawArgs === "object" ? rawArgs : typeof rawArgs === "string" ? parseArguments(rawArgs) : {};
|
|
741
800
|
toolCalls.push({
|
|
742
801
|
id: step.id || newToolCallId(),
|
|
743
802
|
name: step.name || "unknown",
|
|
@@ -815,12 +874,11 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
815
874
|
// only signatures return and canonical `thinking` would always be empty.
|
|
816
875
|
// Enabled whenever thinking is active — including the default/dynamic
|
|
817
876
|
// case where no explicit level is sent — and off only when disabled.
|
|
818
|
-
const thinkingActive =
|
|
819
|
-
options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
|
|
877
|
+
const thinkingActive = options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
|
|
820
878
|
if (thinkingActive) generationConfig["thinking_summaries"] = "auto";
|
|
821
879
|
if (toolChoice) generationConfig["tool_choice"] = toolChoice;
|
|
822
|
-
// max_output_tokens only
|
|
823
|
-
Object.assign(generationConfig, mapGenerationParamsToGoogle(options
|
|
880
|
+
// max_output_tokens only.
|
|
881
|
+
Object.assign(generationConfig, mapGenerationParamsToGoogle(options));
|
|
824
882
|
|
|
825
883
|
const body: InteractionRequestBody = {
|
|
826
884
|
model: modelId,
|
|
@@ -834,7 +892,9 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
834
892
|
let outputEmulated = false;
|
|
835
893
|
let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
836
894
|
try {
|
|
837
|
-
outputSpec = normalizeStructuredOutput(
|
|
895
|
+
outputSpec = normalizeStructuredOutput(
|
|
896
|
+
options?.output ?? (options as { outputSchema?: unknown })?.outputSchema
|
|
897
|
+
);
|
|
838
898
|
} catch {
|
|
839
899
|
outputSpec = undefined;
|
|
840
900
|
}
|
|
@@ -846,7 +906,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
846
906
|
provider: "google",
|
|
847
907
|
capability: "structured output",
|
|
848
908
|
requested: `${outputSpec.name} (google/${modelId})`,
|
|
849
|
-
reason:
|
|
909
|
+
reason:
|
|
910
|
+
"the catalog reports structured_output: false for this model, so native response_format is unsupported.",
|
|
850
911
|
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
851
912
|
});
|
|
852
913
|
}
|
|
@@ -927,7 +988,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
927
988
|
apiKey: string,
|
|
928
989
|
signal?: AbortSignal
|
|
929
990
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
930
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
991
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
992
|
+
signal,
|
|
993
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
994
|
+
);
|
|
931
995
|
try {
|
|
932
996
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(method, body, options, apiKey, effective));
|
|
933
997
|
const text = await res.text();
|
|
@@ -944,7 +1008,6 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
944
1008
|
// Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: every Google HTTP call, out: nothing or a thrown error.
|
|
945
1009
|
private throwIfError(
|
|
946
1010
|
status: number,
|
|
947
|
-
statusText: string,
|
|
948
1011
|
url: string,
|
|
949
1012
|
bodyText: string,
|
|
950
1013
|
modelId: string,
|
|
@@ -989,9 +1052,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
989
1052
|
modelId: string
|
|
990
1053
|
): Promise<InteractionReply> {
|
|
991
1054
|
const res = await this.doFetch(url, method, body, options, apiKey, options?.signal);
|
|
992
|
-
this.throwIfError(res.status,
|
|
1055
|
+
this.throwIfError(res.status, url, res.text, modelId, res.headers);
|
|
993
1056
|
try {
|
|
994
|
-
return {
|
|
1057
|
+
return {
|
|
1058
|
+
status: res.status,
|
|
1059
|
+
statusText: res.statusText,
|
|
1060
|
+
headers: res.headers,
|
|
1061
|
+
interaction: JSON.parse(res.text) as InteractionObject,
|
|
1062
|
+
};
|
|
995
1063
|
} catch {
|
|
996
1064
|
throw toConciseProviderError(
|
|
997
1065
|
Object.assign(new Error("Invalid JSON response from Google Interactions API"), {
|
|
@@ -1006,7 +1074,12 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1006
1074
|
}
|
|
1007
1075
|
|
|
1008
1076
|
// Fire-and-forget cancel so an aborted background run stops billing; in: an aborted generate/stream, out: POST {id}/cancel.
|
|
1009
|
-
private cancelInteraction(
|
|
1077
|
+
private cancelInteraction(
|
|
1078
|
+
baseUrl: string,
|
|
1079
|
+
id: string,
|
|
1080
|
+
options: ProviderRequestOptions | undefined,
|
|
1081
|
+
apiKey: string
|
|
1082
|
+
): void {
|
|
1010
1083
|
void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
|
|
1011
1084
|
}
|
|
1012
1085
|
|
|
@@ -1060,7 +1133,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1060
1133
|
try {
|
|
1061
1134
|
for (let poll = 0; isRunning(reply.interaction.status); poll++) {
|
|
1062
1135
|
await sleep(this.pollDelaysMs[poll === 0 ? 0 : 1], options?.signal);
|
|
1063
|
-
reply = await withRetries(
|
|
1136
|
+
reply = await withRetries(
|
|
1137
|
+
() => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean),
|
|
1138
|
+
retryOptions
|
|
1139
|
+
);
|
|
1064
1140
|
}
|
|
1065
1141
|
return reply;
|
|
1066
1142
|
} catch (err) {
|
|
@@ -1071,7 +1147,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1071
1147
|
|
|
1072
1148
|
// Create (retried as a whole), wait if it runs in the background, then record the chain and map the result.
|
|
1073
1149
|
const run = async (): Promise<ProviderGenerateResult> => {
|
|
1074
|
-
let reply = await withRetries(
|
|
1150
|
+
let reply = await withRetries(
|
|
1151
|
+
() => this.fetchInteraction(url, "POST", body, options, apiKey, clean),
|
|
1152
|
+
retryOptions
|
|
1153
|
+
);
|
|
1075
1154
|
if (body.background) reply = await waitForCompletion(reply);
|
|
1076
1155
|
const { interaction } = reply;
|
|
1077
1156
|
if (interaction.id && sessionId && body.store !== false) {
|
|
@@ -1081,12 +1160,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1081
1160
|
lastTurnTotals.delete(sessionId);
|
|
1082
1161
|
}
|
|
1083
1162
|
noteProviderTurn(sessionId, "google");
|
|
1084
|
-
const parsed = parseInteraction(
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
{ request: rawRequest, response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction } }
|
|
1089
|
-
);
|
|
1163
|
+
const parsed = parseInteraction(interaction, clean, Date.now() - startTime, {
|
|
1164
|
+
request: rawRequest,
|
|
1165
|
+
response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction },
|
|
1166
|
+
});
|
|
1090
1167
|
checkTurnTokens(sessionId, parsed.usage);
|
|
1091
1168
|
return parsed;
|
|
1092
1169
|
};
|
|
@@ -1165,9 +1242,16 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1165
1242
|
// (429/5xx/network) are retried before the first byte; a live stream is never retried.
|
|
1166
1243
|
const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
|
|
1167
1244
|
openStreamResponse({
|
|
1168
|
-
fetchOnce: () =>
|
|
1169
|
-
|
|
1170
|
-
|
|
1245
|
+
fetchOnce: () =>
|
|
1246
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
1247
|
+
(options?.fetch ?? fetch)(
|
|
1248
|
+
target,
|
|
1249
|
+
this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal)
|
|
1250
|
+
)
|
|
1251
|
+
),
|
|
1252
|
+
onHttpError: (r, text) => this.throwIfError(r.status, target, text, clean, headersToRecord(r.headers)),
|
|
1253
|
+
noBodyError: () =>
|
|
1254
|
+
toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
|
|
1171
1255
|
retry: retryOptionsFrom(options),
|
|
1172
1256
|
signal: linked.signal,
|
|
1173
1257
|
label: { providerId: "google", modelId: clean },
|
|
@@ -1177,7 +1261,11 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1177
1261
|
res.headers.forEach((v, k) => {
|
|
1178
1262
|
responseHeaders[k] = v;
|
|
1179
1263
|
});
|
|
1180
|
-
const responseMeta: Record<string, unknown> & {
|
|
1264
|
+
const responseMeta: Record<string, unknown> & {
|
|
1265
|
+
status: number;
|
|
1266
|
+
statusText: string;
|
|
1267
|
+
headers: Record<string, string>;
|
|
1268
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
1181
1269
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1182
1270
|
|
|
1183
1271
|
let reader: StreamReader | undefined;
|
|
@@ -1286,7 +1374,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1286
1374
|
thinking += t;
|
|
1287
1375
|
eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
|
|
1288
1376
|
}
|
|
1289
|
-
if (!signature && typeof step["signature"] === "string" && step["signature"])
|
|
1377
|
+
if (!signature && typeof step["signature"] === "string" && step["signature"])
|
|
1378
|
+
signature = step["signature"];
|
|
1290
1379
|
} else if (step["type"] === "function_call") {
|
|
1291
1380
|
const args = step["arguments"];
|
|
1292
1381
|
const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
|
|
@@ -1343,7 +1432,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1343
1432
|
break;
|
|
1344
1433
|
}
|
|
1345
1434
|
if (chunk.done) break;
|
|
1346
|
-
for (const m of parser.feed(decoder.decode(chunk.value, { stream: true })))
|
|
1435
|
+
for (const m of parser.feed(decoder.decode(chunk.value, { stream: true })))
|
|
1436
|
+
handleMessage(m.event, m.data);
|
|
1347
1437
|
}
|
|
1348
1438
|
for (const m of parser.flush()) handleMessage(m.event, m.data);
|
|
1349
1439
|
} finally {
|
|
@@ -1383,7 +1473,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1383
1473
|
}
|
|
1384
1474
|
|
|
1385
1475
|
if (sessionId) {
|
|
1386
|
-
if (responseId && body.store !== false)
|
|
1476
|
+
if (responseId && body.store !== false)
|
|
1477
|
+
setChain(sessionId, { interactionId: responseId, model: clean, at: Date.now() });
|
|
1387
1478
|
else if (body.store === false) {
|
|
1388
1479
|
interactionChains.delete(sessionId);
|
|
1389
1480
|
lastTurnTotals.delete(sessionId);
|
|
@@ -1402,7 +1493,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1402
1493
|
const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
1403
1494
|
const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
1404
1495
|
const isSSEPayload =
|
|
1405
|
-
(typeof ctEntry?.[1] === "string" &&
|
|
1496
|
+
(typeof ctEntry?.[1] === "string" &&
|
|
1497
|
+
((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
|
|
1406
1498
|
framesSeen > 0 ||
|
|
1407
1499
|
badFrames > 0;
|
|
1408
1500
|
if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
|
|
@@ -1433,14 +1525,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1433
1525
|
} catch (err: unknown) {
|
|
1434
1526
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1435
1527
|
const isAbort =
|
|
1436
|
-
linked.signal.aborted ||
|
|
1437
|
-
eventStream.isCancelled() ||
|
|
1438
|
-
(raw as { name?: string }).name === "AbortError";
|
|
1528
|
+
linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
|
|
1439
1529
|
if (isAbort) {
|
|
1440
1530
|
cancelBackground();
|
|
1441
|
-
eventStream.fail(
|
|
1442
|
-
Object.assign(new Error("Stream aborted"), { name: "AbortError" })
|
|
1443
|
-
);
|
|
1531
|
+
eventStream.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
|
|
1444
1532
|
} else if (evictSessionId && usedChain && isNotFoundChainError(raw)) {
|
|
1445
1533
|
// Reactive self-heal, fail-clean variant (Rule 2): the `start`
|
|
1446
1534
|
// event is already delivered, so a transparent in-stream retry
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Strictness contract (differs from the old lenient passthrough on purpose):
|
|
14
14
|
* - Requests carry ONLY standard Chat Completions fields (`model`, `messages`,
|
|
15
15
|
* `tools`, `tool_choice`, `stream`, plus `max_tokens`/
|
|
16
|
-
*
|
|
16
|
+
* when explicitly set). No `reasoning`, `service_tier`,
|
|
17
17
|
* `session_id`, `prompt_cache_key`, or other extras: strict endpoints
|
|
18
18
|
* (groq, ollama, …) fail unknown body properties, so anything unsupported
|
|
19
19
|
* is warn+drop, never sent.
|
|
@@ -66,7 +66,6 @@ import {
|
|
|
66
66
|
newToolCallId,
|
|
67
67
|
redactedHeaders,
|
|
68
68
|
readErrorPayload,
|
|
69
|
-
incompleteStreamError,
|
|
70
69
|
hasUnfinishedToolCall,
|
|
71
70
|
linkStreamAbort,
|
|
72
71
|
withAbortTimer,
|
|
@@ -130,7 +129,10 @@ interface ChatRequestBody {
|
|
|
130
129
|
messages: ChatMessage[];
|
|
131
130
|
tools?: Array<Record<string, unknown>>;
|
|
132
131
|
tool_choice?: string | { type: "function"; function: { name: string } };
|
|
133
|
-
response_format?: {
|
|
132
|
+
response_format?: {
|
|
133
|
+
type: "json_schema";
|
|
134
|
+
json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
|
|
135
|
+
};
|
|
134
136
|
stream?: boolean;
|
|
135
137
|
[k: string]: unknown;
|
|
136
138
|
}
|
|
@@ -231,18 +233,20 @@ function resolveApiKey(
|
|
|
231
233
|
): string | undefined {
|
|
232
234
|
const clean = (v: unknown): string | undefined => {
|
|
233
235
|
if (typeof v !== "string") return undefined;
|
|
234
|
-
const t = v
|
|
236
|
+
const t = v
|
|
237
|
+
.trim()
|
|
238
|
+
.replace(/^["']|["']$/g, "")
|
|
239
|
+
.trim();
|
|
235
240
|
return t || undefined;
|
|
236
241
|
};
|
|
237
242
|
const envPrefix = envPrefixOf(prefix);
|
|
238
|
-
const key =
|
|
243
|
+
const key =
|
|
239
244
|
clean(options?.apiKey) ||
|
|
240
245
|
clean(configuredApiKey) ||
|
|
241
246
|
clean(options?.env?.[`${envPrefix}_API_KEY`]) ||
|
|
242
247
|
clean(options?.env?.[`${envPrefix}_BASE_API_KEY`]) ||
|
|
243
248
|
getApiKey(prefix, undefined, options?.env) ||
|
|
244
|
-
undefined
|
|
245
|
-
);
|
|
249
|
+
undefined;
|
|
246
250
|
if (key) return key;
|
|
247
251
|
if (hasAuthHeader(options?.headers)) return "unused";
|
|
248
252
|
return undefined;
|
|
@@ -308,7 +312,8 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
|
|
|
308
312
|
provider: prefix,
|
|
309
313
|
capability: "thinking level",
|
|
310
314
|
requested: `${level} (${modelRef})`,
|
|
311
|
-
reason:
|
|
315
|
+
reason:
|
|
316
|
+
"custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
|
|
312
317
|
fallback: "the server default (no reasoning payload is sent)",
|
|
313
318
|
});
|
|
314
319
|
}
|
|
@@ -325,11 +330,7 @@ function audioFormatFor(mimeType?: string): string {
|
|
|
325
330
|
* Completions subset has no shape for them, and a provider 400 would only say
|
|
326
331
|
* so confusingly. Images and audio keep flowing natively.
|
|
327
332
|
*/
|
|
328
|
-
function assertNoVideoOrFileParts(
|
|
329
|
-
context: ProviderContext,
|
|
330
|
-
prefix: string,
|
|
331
|
-
modelId: string
|
|
332
|
-
): void {
|
|
333
|
+
function assertNoVideoOrFileParts(context: ProviderContext, prefix: string, modelId: string): void {
|
|
333
334
|
let kind: "video" | "file" | undefined;
|
|
334
335
|
for (const msg of context.messages) {
|
|
335
336
|
if (!Array.isArray((msg as { content?: unknown }).content)) continue;
|
|
@@ -374,10 +375,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
|
|
|
374
375
|
const raw = (part as { audio?: unknown }).audio;
|
|
375
376
|
const mimeType = (part as { mimeType?: string }).mimeType;
|
|
376
377
|
// The OpenAI chat audio shape carries data only — always inline.
|
|
377
|
-
const norm = await normalizeMediaInput(
|
|
378
|
-
raw as string | Uint8Array | ArrayBuffer,
|
|
379
|
-
mimeType
|
|
380
|
-
);
|
|
378
|
+
const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
|
|
381
379
|
blocks.push({
|
|
382
380
|
type: "input_audio",
|
|
383
381
|
input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
|
|
@@ -428,10 +426,16 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
428
426
|
messages.push({ role: "assistant", content: "I have processed the tool results." });
|
|
429
427
|
}
|
|
430
428
|
if (typeof m.content === "string") {
|
|
431
|
-
if (m.content) {
|
|
429
|
+
if (m.content) {
|
|
430
|
+
messages.push({ role: "user", content: m.content });
|
|
431
|
+
lastRole = "user";
|
|
432
|
+
}
|
|
432
433
|
} else {
|
|
433
434
|
const blocks = await contentPartsToBlocks(m.content);
|
|
434
|
-
if (blocks.length > 0) {
|
|
435
|
+
if (blocks.length > 0) {
|
|
436
|
+
messages.push({ role: "user", content: blocks });
|
|
437
|
+
lastRole = "user";
|
|
438
|
+
}
|
|
435
439
|
}
|
|
436
440
|
} else if (m.role === "assistant") {
|
|
437
441
|
if (typeof m.content === "string") {
|
|
@@ -527,25 +531,22 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
|
|
|
527
531
|
}
|
|
528
532
|
|
|
529
533
|
// The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
|
|
530
|
-
function extractSignature(rawTc: {
|
|
531
|
-
extra_content?: { google?: { thought_signature?: string } };
|
|
532
|
-
}): string | undefined {
|
|
534
|
+
function extractSignature(rawTc: { extra_content?: { google?: { thought_signature?: string } } }): string | undefined {
|
|
533
535
|
const sig = rawTc?.extra_content?.google?.thought_signature;
|
|
534
536
|
return typeof sig === "string" && sig ? sig : undefined;
|
|
535
537
|
}
|
|
536
538
|
|
|
537
539
|
/** Tags a captured compat signature with its issuing provider (`google`). */
|
|
538
|
-
function withCompatSignature(sig: string | undefined): {
|
|
540
|
+
function withCompatSignature(sig: string | undefined): {
|
|
541
|
+
thoughtSignature?: string;
|
|
542
|
+
thoughtSignatureProvider?: string;
|
|
543
|
+
} {
|
|
539
544
|
if (!sig) return {};
|
|
540
545
|
return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
|
|
541
546
|
}
|
|
542
547
|
|
|
543
548
|
// Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
|
|
544
|
-
function throwResponseError(
|
|
545
|
-
response: ChatResponse,
|
|
546
|
-
prefix: string,
|
|
547
|
-
modelId: string
|
|
548
|
-
): void {
|
|
549
|
+
function throwResponseError(response: ChatResponse, prefix: string, modelId: string): void {
|
|
549
550
|
const message =
|
|
550
551
|
response.error && typeof response.error.message === "string" && response.error.message
|
|
551
552
|
? response.error.message
|
|
@@ -716,7 +717,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
716
717
|
provider: this.prefix,
|
|
717
718
|
capability: "service tier",
|
|
718
719
|
requested: `${options.serviceTier} (${this.prefix}/${modelId})`,
|
|
719
|
-
reason:
|
|
720
|
+
reason:
|
|
721
|
+
"custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
|
|
720
722
|
fallback: "standard routing (no service_tier payload is sent)",
|
|
721
723
|
});
|
|
722
724
|
}
|
|
@@ -745,7 +747,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
745
747
|
provider: this.prefix,
|
|
746
748
|
capability: "structured output",
|
|
747
749
|
requested: `${outputSpec.name} (${this.prefix}/${modelId})`,
|
|
748
|
-
reason:
|
|
750
|
+
reason:
|
|
751
|
+
"the catalog reports structured_output: false for this model, so native response_format is unsupported.",
|
|
749
752
|
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
750
753
|
});
|
|
751
754
|
}
|
|
@@ -758,7 +761,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
758
761
|
const compatTools = toCompatTools(tools);
|
|
759
762
|
if (compatTools) body.tools = compatTools;
|
|
760
763
|
// Standard Chat Completions generation fields, only when the caller set them.
|
|
761
|
-
Object.assign(body, mapGenerationParamsToChat(options
|
|
764
|
+
Object.assign(body, mapGenerationParamsToChat(options));
|
|
762
765
|
const toolChoice = mapToolChoiceToOpenAI(
|
|
763
766
|
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
764
767
|
);
|
|
@@ -791,7 +794,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
791
794
|
const noStreamOptions =
|
|
792
795
|
(options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
|
|
793
796
|
getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
|
|
794
|
-
|
|
797
|
+
options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1";
|
|
795
798
|
if (!noStreamOptions) {
|
|
796
799
|
(body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
|
|
797
800
|
}
|
|
@@ -807,11 +810,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
807
810
|
sessionId: string | undefined,
|
|
808
811
|
signal?: AbortSignal
|
|
809
812
|
): RequestInit {
|
|
810
|
-
const headers: Record<string, string> = buildSessionHeaders(
|
|
811
|
-
this.prefix,
|
|
812
|
-
options?.headers,
|
|
813
|
-
sessionId
|
|
814
|
-
);
|
|
813
|
+
const headers: Record<string, string> = buildSessionHeaders(this.prefix, options?.headers, sessionId);
|
|
815
814
|
// Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
|
|
816
815
|
// `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
|
|
817
816
|
// drops affinity headers that trigger CORS preflights / strict 400s.
|
|
@@ -844,7 +843,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
844
843
|
sessionId: string | undefined,
|
|
845
844
|
signal?: AbortSignal
|
|
846
845
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
847
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
846
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
847
|
+
signal,
|
|
848
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
849
|
+
);
|
|
848
850
|
try {
|
|
849
851
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
850
852
|
const text = await res.text();
|
|
@@ -939,13 +941,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
939
941
|
// Stateless turns never establish chains, but the session store still
|
|
940
942
|
// records the turn so other providers' switch detection keeps working.
|
|
941
943
|
noteProviderTurn(sessionId, this.prefix);
|
|
942
|
-
return parseResponse(
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
Date.now() - startTime,
|
|
947
|
-
{ request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
|
|
948
|
-
);
|
|
944
|
+
return parseResponse(response, this.prefix, clean, Date.now() - startTime, {
|
|
945
|
+
request: rawRequest,
|
|
946
|
+
response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
|
|
947
|
+
});
|
|
949
948
|
};
|
|
950
949
|
|
|
951
950
|
try {
|
|
@@ -1000,9 +999,13 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1000
999
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
1001
1000
|
// first byte; once the stream is live it is never retried.
|
|
1002
1001
|
const res = await openStreamResponse({
|
|
1003
|
-
fetchOnce: () =>
|
|
1002
|
+
fetchOnce: () =>
|
|
1003
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
1004
|
+
(options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
|
|
1005
|
+
),
|
|
1004
1006
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
1005
|
-
noBodyError: () =>
|
|
1007
|
+
noBodyError: () =>
|
|
1008
|
+
toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
|
|
1006
1009
|
retry: retryOptionsFrom(options),
|
|
1007
1010
|
signal: linked.signal,
|
|
1008
1011
|
label: { providerId: prefix, modelId: clean },
|
|
@@ -1011,7 +1014,11 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1011
1014
|
res.headers.forEach((v, k) => {
|
|
1012
1015
|
responseHeaders[k] = v;
|
|
1013
1016
|
});
|
|
1014
|
-
const responseMeta: Record<string, unknown> & {
|
|
1017
|
+
const responseMeta: Record<string, unknown> & {
|
|
1018
|
+
status: number;
|
|
1019
|
+
statusText: string;
|
|
1020
|
+
headers: Record<string, string>;
|
|
1021
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
1015
1022
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1016
1023
|
|
|
1017
1024
|
let text = "";
|
|
@@ -1059,7 +1066,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1059
1066
|
if (typeof msg["id"] === "string" && !responseId) {
|
|
1060
1067
|
responseId = msg["id"] as string;
|
|
1061
1068
|
}
|
|
1062
|
-
const choice =
|
|
1069
|
+
const choice =
|
|
1070
|
+
(Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
|
|
1063
1071
|
const delta = (choice["delta"] as Record<string, unknown>) ?? {};
|
|
1064
1072
|
// Text delta (content-free accounting frames carry "" — ignored).
|
|
1065
1073
|
// Refusal deltas surface as text with a `refusal` finish.
|