agent-accelerator 0.4.0 → 0.4.5
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/package.json +1 -1
- package/src/agent/agent.ts +16 -1
- package/src/agent/loop.ts +74 -1
- package/src/index.ts +19 -0
- package/src/models/catalog.ts +26 -0
- package/src/providers/google.ts +56 -7
- package/src/providers/openai-compat.ts +110 -20
- package/src/providers/openai.ts +128 -13
- package/src/providers/openrouter.ts +89 -15
- package/src/providers/shared.ts +63 -5
- package/src/providers.ts +256 -5
- package/src/session/store.ts +18 -9
- package/src/types/agent.ts +17 -2
- package/src/types/model.ts +21 -0
- package/src/types/response.ts +7 -0
- package/src/utils/env.ts +21 -7
- package/src/utils/errors.ts +15 -1
- package/src/utils/headers.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sashvat Bharat",
|
package/src/agent/agent.ts
CHANGED
|
@@ -12,7 +12,8 @@ import { runAgentLoop, streamAgentLoop, type SteerEntry } from "./loop.ts";
|
|
|
12
12
|
import { createSessionId } from "../utils/session.ts";
|
|
13
13
|
import { getSubAgentTrace, subscribeToSubAgent, listSubAgentTraceIds, type SubAgentTrace } from "./delegation.ts";
|
|
14
14
|
import { saveSessionDir, saveSessionFile } from "../session/store.ts";
|
|
15
|
-
import { clearSessionRouting } from "../providers.ts";
|
|
15
|
+
import { clearSessionRouting, normalizeStructuredOutput } from "../providers.ts";
|
|
16
|
+
import type { StructuredOutputSpec } from "../types/model.ts";
|
|
16
17
|
import { clearInteractionChains } from "../providers/google.ts";
|
|
17
18
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
18
19
|
import { validateModelThinking, ensureModelCatalogFresh } from "../models/catalog.ts";
|
|
@@ -139,6 +140,10 @@ export class Agent {
|
|
|
139
140
|
readonly tools: Record<string, ToolDefinition> = {};
|
|
140
141
|
/** Normalized reasoning configuration. */
|
|
141
142
|
readonly thinkingConfig?: ThinkingConfig;
|
|
143
|
+
/** Canonical structured-output request (raw `AgentConfig.output`). */
|
|
144
|
+
readonly output?: unknown;
|
|
145
|
+
/** Normalized structured-output spec sent to providers. */
|
|
146
|
+
readonly outputSpec?: StructuredOutputSpec;
|
|
142
147
|
/** Cache retention/session settings. */
|
|
143
148
|
readonly cacheConfig?: CacheConfig;
|
|
144
149
|
/** Provider service tier. */
|
|
@@ -298,6 +303,12 @@ export class Agent {
|
|
|
298
303
|
const mpThinking = (rawModel as any)?.thinkingLevel;
|
|
299
304
|
this.thinkingConfig = normalizeThinking(config, mpThinking);
|
|
300
305
|
|
|
306
|
+
// Structured outputs: validate early so invalid schemas fail fast at construction.
|
|
307
|
+
if (config.output !== undefined && config.output !== null) {
|
|
308
|
+
this.output = config.output;
|
|
309
|
+
this.outputSpec = normalizeStructuredOutput(config.output);
|
|
310
|
+
}
|
|
311
|
+
|
|
301
312
|
// DX5: cache retention short|medium|long, undefined = no explicit
|
|
302
313
|
this.cacheConfig = {
|
|
303
314
|
sessionId: this.sessionId,
|
|
@@ -798,6 +809,7 @@ export class Agent {
|
|
|
798
809
|
: { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
|
|
799
810
|
serviceTier: this.serviceTier,
|
|
800
811
|
sessionId: runOptions?.sessionId || this.sessionId,
|
|
812
|
+
...(this.outputSpec ? { output: this.outputSpec } : {}),
|
|
801
813
|
};
|
|
802
814
|
|
|
803
815
|
const loopConfig = {
|
|
@@ -815,6 +827,7 @@ export class Agent {
|
|
|
815
827
|
onSteerInjected: () => this.persistNow(),
|
|
816
828
|
// Q-22: loop enforces these (plus local timeout races).
|
|
817
829
|
limits: { ...this.runLimits },
|
|
830
|
+
...(this.output !== undefined ? { output: this.output } : {}),
|
|
818
831
|
};
|
|
819
832
|
|
|
820
833
|
const res = await runAgentLoop(loopConfig);
|
|
@@ -865,6 +878,7 @@ export class Agent {
|
|
|
865
878
|
: { ...this.cacheConfig, sessionId: runOptions?.sessionId || this.sessionId },
|
|
866
879
|
serviceTier: this.serviceTier,
|
|
867
880
|
sessionId: runOptions?.sessionId || this.sessionId,
|
|
881
|
+
...(this.outputSpec ? { output: this.outputSpec } : {}),
|
|
868
882
|
};
|
|
869
883
|
|
|
870
884
|
const loopConfig = {
|
|
@@ -882,6 +896,7 @@ export class Agent {
|
|
|
882
896
|
onSteerInjected: () => this.persistNow(),
|
|
883
897
|
// Q-22: loop enforces these (plus local timeout races).
|
|
884
898
|
limits: { ...this.runLimits },
|
|
899
|
+
...(this.output !== undefined ? { output: this.output } : {}),
|
|
885
900
|
};
|
|
886
901
|
|
|
887
902
|
const s = streamAgentLoop(loopConfig);
|
package/src/agent/loop.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { toStandardToolDeclarations } from "../tools/tool.ts";
|
|
|
12
12
|
import { executeToolCalls } from "../tools/executor.ts";
|
|
13
13
|
import { getModelFromCatalog, ensureModelCatalogFresh, validateModelThinking } from "../models/catalog.ts";
|
|
14
14
|
import { preprocessFilePartsForBypass } from "../utils/documents.ts";
|
|
15
|
-
import { noteProviderTurn } from "../providers.ts";
|
|
15
|
+
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
16
16
|
|
|
17
17
|
/** One mid-session steer request buffered while a run is active. */
|
|
18
18
|
export interface SteerEntry {
|
|
@@ -42,6 +42,12 @@ export interface AgentLoopConfig {
|
|
|
42
42
|
steerInbox?: SteerEntry[];
|
|
43
43
|
/** Called after steer entries are injected (streaming loops also push `steer_injected` events). */
|
|
44
44
|
onSteerInjected?: (injectedPrompts: string[]) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Canonical structured-output request (raw `AgentConfig.output`).
|
|
47
|
+
* Normalized per turn for the provider wire; validated once at the end
|
|
48
|
+
* into `AgentResponse.parsed`. Agent-level only.
|
|
49
|
+
*/
|
|
50
|
+
output?: unknown;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/** Short preview for `steer_injected` / `queued` event payloads (bounded, display-only). */
|
|
@@ -105,6 +111,30 @@ function emitProgress(config: { onProgress?: () => void }): void {
|
|
|
105
111
|
try { config.onProgress?.(); } catch {}
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Structured outputs (canonical output wiring for the loop).
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: import("../types/model.ts").StructuredOutputSpec | undefined } {
|
|
119
|
+
const raw = (config as { output?: unknown }).output;
|
|
120
|
+
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)?.output;
|
|
121
|
+
if (raw !== undefined && raw !== null) {
|
|
122
|
+
// Throws ValidationError for invalid shapes — caught by run/stream catch
|
|
123
|
+
// with partial attached, same as other config validation.
|
|
124
|
+
const spec = normalizeStructuredOutput(raw);
|
|
125
|
+
return { raw, spec };
|
|
126
|
+
}
|
|
127
|
+
if (fromOptions) return { raw: fromOptions, spec: fromOptions };
|
|
128
|
+
return { raw: undefined, spec: undefined };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseFinalStructuredOutput(text: string, raw: unknown, spec: import("../types/model.ts").StructuredOutputSpec | undefined): unknown | undefined {
|
|
132
|
+
if (raw === undefined && spec === undefined) return undefined;
|
|
133
|
+
// Strict-fail when opted in: invalid JSON/schema throws ValidationError.
|
|
134
|
+
// Tool turns skip this (called only for final no-tool text).
|
|
135
|
+
return parseStructuredOutput(text, raw ?? spec);
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
/**
|
|
109
139
|
* Normalizes `maxTurns` to a safe positive integer (default Infinity).
|
|
110
140
|
* `0` means infinite (no limit), matching the `timeout: 0` convention.
|
|
@@ -493,6 +523,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
493
523
|
const fallbackMaxTurns = normalizeMaxTurns(rawMaxTurns);
|
|
494
524
|
const limits = resolveLoopLimits(config, fallbackMaxTurns);
|
|
495
525
|
const maxTurns = limits.maxTurns;
|
|
526
|
+
// Structured outputs: normalize once (throws ValidationError for bad shapes).
|
|
527
|
+
const loopOutput = resolveLoopOutput(config);
|
|
496
528
|
// Q-19: pre-run checkpoint for onFailure rollback.
|
|
497
529
|
const preRunCp = context.checkpoint();
|
|
498
530
|
const sessionIdForRollback = loopSessionId(config);
|
|
@@ -544,6 +576,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
544
576
|
cache: options?.cache,
|
|
545
577
|
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
546
578
|
signal: runOptions?.signal,
|
|
579
|
+
...(loopOutput.spec ? { output: loopOutput.spec } : {}),
|
|
547
580
|
};
|
|
548
581
|
// Q-22: per-request timeout pass-through (providers honor timeoutMs).
|
|
549
582
|
try {
|
|
@@ -720,8 +753,16 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
720
753
|
);
|
|
721
754
|
}
|
|
722
755
|
|
|
756
|
+
// Structured outputs: validate final text only (tool turns skip this).
|
|
757
|
+
// Strict-fail throws ValidationError with partial attached via catch below.
|
|
758
|
+
let parsedOutput: unknown | undefined;
|
|
759
|
+
if (loopOutput.raw !== undefined && !truncatedByMaxTurns) {
|
|
760
|
+
parsedOutput = parseFinalStructuredOutput(finalResult?.text || "", loopOutput.raw, loopOutput.spec);
|
|
761
|
+
}
|
|
762
|
+
|
|
723
763
|
return new AgentResponse({
|
|
724
764
|
text: finalResult?.text || "",
|
|
765
|
+
...(parsedOutput !== undefined ? { parsed: parsedOutput } : {}),
|
|
725
766
|
thinking: finalResult?.thinking,
|
|
726
767
|
thoughtSignature: finalResult?.thoughtSignature,
|
|
727
768
|
toolCalls: allToolCalls,
|
|
@@ -869,6 +910,17 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
869
910
|
const maxTurns = streamLimits.maxTurns;
|
|
870
911
|
|
|
871
912
|
const standardTools = toStandardToolDeclarations(tools);
|
|
913
|
+
// Structured outputs: normalize once for the whole stream run.
|
|
914
|
+
let streamOutputRaw: unknown;
|
|
915
|
+
let streamOutputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
916
|
+
try {
|
|
917
|
+
const resolved = resolveLoopOutput(config);
|
|
918
|
+
streamOutputRaw = resolved.raw;
|
|
919
|
+
streamOutputSpec = resolved.spec;
|
|
920
|
+
} catch (err) {
|
|
921
|
+
outerStream.fail(err instanceof Error ? err : new Error(String(err)));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
872
924
|
|
|
873
925
|
accumulatedUsage = {
|
|
874
926
|
inputTokens: 0,
|
|
@@ -900,6 +952,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
900
952
|
cache: options?.cache,
|
|
901
953
|
tools: standardTools.length > 0 ? standardTools : undefined,
|
|
902
954
|
signal: linked.signal,
|
|
955
|
+
...(streamOutputSpec ? { output: streamOutputSpec } : {}),
|
|
903
956
|
};
|
|
904
957
|
try {
|
|
905
958
|
if (Number.isFinite(streamLimits.requestTimeoutMs) && streamLimits.requestTimeoutMs !== Infinity) {
|
|
@@ -1137,8 +1190,28 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1137
1190
|
}
|
|
1138
1191
|
const truncatedFinish = canonicalFinishReason(lastResponse?.finishReason, truncatedByMaxTurns);
|
|
1139
1192
|
|
|
1193
|
+
let streamParsed: unknown | undefined;
|
|
1194
|
+
if (streamOutputRaw !== undefined && !truncatedByMaxTurns) {
|
|
1195
|
+
try {
|
|
1196
|
+
streamParsed = parseFinalStructuredOutput(lastResponse?.text || "", streamOutputRaw, streamOutputSpec);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1199
|
+
try { (raw as unknown as { partial?: unknown }).partial ??= {
|
|
1200
|
+
usage: accumulatedUsage,
|
|
1201
|
+
cost: accumulatedUsage.cost,
|
|
1202
|
+
turns,
|
|
1203
|
+
toolCalls: allToolCalls,
|
|
1204
|
+
toolResults: allToolResults,
|
|
1205
|
+
text: lastResponse?.text,
|
|
1206
|
+
}; } catch {}
|
|
1207
|
+
outerStream.fail(raw);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1140
1212
|
const finalAgentResponse = new AgentResponse({
|
|
1141
1213
|
text: lastResponse?.text || "",
|
|
1214
|
+
...(streamParsed !== undefined ? { parsed: streamParsed } : {}),
|
|
1142
1215
|
thinking: lastResponse?.thinking,
|
|
1143
1216
|
thoughtSignature: lastResponse?.thoughtSignature,
|
|
1144
1217
|
toolCalls: allToolCalls,
|
package/src/index.ts
CHANGED
|
@@ -210,6 +210,15 @@ export {
|
|
|
210
210
|
mapToolChoiceToOpenAI,
|
|
211
211
|
normalizeToolChoice,
|
|
212
212
|
parseStreamedToolArguments,
|
|
213
|
+
normalizeStructuredOutput,
|
|
214
|
+
extractOutputValidator,
|
|
215
|
+
parseStructuredOutput,
|
|
216
|
+
mapOutputToOpenAI,
|
|
217
|
+
mapOutputToGoogle,
|
|
218
|
+
mapOutputToOpenRouterChat,
|
|
219
|
+
mapOutputToCompatChat,
|
|
220
|
+
buildStructuredOutputInstruction,
|
|
221
|
+
augmentSystemPromptWithStructuredOutput,
|
|
213
222
|
} from "./providers.ts";
|
|
214
223
|
export type {
|
|
215
224
|
ProviderCapabilityStatus,
|
|
@@ -217,13 +226,21 @@ export type {
|
|
|
217
226
|
OpenRouterToolChoice,
|
|
218
227
|
OpenRouterChatToolChoice,
|
|
219
228
|
OpenAIToolChoice,
|
|
229
|
+
ChatCompletionsResponseFormat,
|
|
220
230
|
} from "./providers.ts";
|
|
221
231
|
|
|
222
232
|
export {
|
|
223
233
|
GOOGLE_MODELS,
|
|
224
234
|
OPENAI_MODELS,
|
|
225
235
|
OPENROUTER_MODELS,
|
|
236
|
+
modelSupportsStructuredOutput,
|
|
226
237
|
} from "./models/catalog.ts";
|
|
238
|
+
export {
|
|
239
|
+
hasAuthHeader,
|
|
240
|
+
clampToolCallId,
|
|
241
|
+
hasTruncatedToolArguments,
|
|
242
|
+
shortHashId,
|
|
243
|
+
} from "./providers/shared.ts";
|
|
227
244
|
|
|
228
245
|
export {
|
|
229
246
|
isValidThoughtSignature,
|
|
@@ -368,6 +385,7 @@ export type {
|
|
|
368
385
|
ProviderRequestOptions,
|
|
369
386
|
ProviderGenerateResult,
|
|
370
387
|
ProviderRawData,
|
|
388
|
+
StructuredOutputSpec,
|
|
371
389
|
} from "./types/model.ts";
|
|
372
390
|
|
|
373
391
|
export type {
|
|
@@ -388,6 +406,7 @@ export type {
|
|
|
388
406
|
WhenBusyMode,
|
|
389
407
|
ModelRef,
|
|
390
408
|
ThinkingOption,
|
|
409
|
+
StructuredOutputOption,
|
|
391
410
|
SamplingConfig,
|
|
392
411
|
RetryPolicyConfig,
|
|
393
412
|
AgentLogger,
|
package/src/models/catalog.ts
CHANGED
|
@@ -138,6 +138,7 @@ function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall
|
|
|
138
138
|
supportsExplicitCaching: cost.cache_read !== undefined,
|
|
139
139
|
supportsParallelToolCalls: toolCall,
|
|
140
140
|
supportsStreaming: true,
|
|
141
|
+
supportsStructuredOutput: raw.structured_output === undefined ? true : !!raw.structured_output,
|
|
141
142
|
supportsReasoningToggle: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "toggle"),
|
|
142
143
|
supportsReasoningEffort: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "effort"),
|
|
143
144
|
modalities: [...(modalities.input || []), ...(modalities.output || [])].filter((v: any, i: any, a: any) => a.indexOf(v) === i),
|
|
@@ -588,6 +589,30 @@ export {
|
|
|
588
589
|
type RefreshCatalogOptions,
|
|
589
590
|
} from "./catalog-cache.ts";
|
|
590
591
|
|
|
592
|
+
/**
|
|
593
|
+
* True when the model can enforce `response_format`/`text.format` natively.
|
|
594
|
+
* Unknown models (custom endpoints, dynamic routers) count as capable —
|
|
595
|
+
* the provider verdict stands, matching `assertModalitiesSupported`.
|
|
596
|
+
* Models with catalog `structured_output: false` return false so callers can
|
|
597
|
+
* emulate via prompt instruction + client validation instead of 400ing.
|
|
598
|
+
*
|
|
599
|
+
* @example `modelSupportsStructuredOutput("openrouter", "inclusionai/ling-3.0-flash-sante:free")`
|
|
600
|
+
*/
|
|
601
|
+
export function modelSupportsStructuredOutput(provider: string, modelId: string): boolean {
|
|
602
|
+
try {
|
|
603
|
+
const spec = getModelFromCatalog(provider, modelId);
|
|
604
|
+
if (!spec) return true;
|
|
605
|
+
if (typeof spec.capabilities?.supportsStructuredOutput === "boolean") {
|
|
606
|
+
return spec.capabilities.supportsStructuredOutput;
|
|
607
|
+
}
|
|
608
|
+
const raw = (spec as { raw?: { structured_output?: unknown } }).raw;
|
|
609
|
+
if (raw && typeof raw.structured_output === "boolean") return raw.structured_output;
|
|
610
|
+
return true;
|
|
611
|
+
} catch {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
591
616
|
/**
|
|
592
617
|
* Permissive placeholder so custom endpoints never fail preflight:
|
|
593
618
|
* allows every ThinkingLevel (none -> max + dynamic).
|
|
@@ -622,6 +647,7 @@ export function createGenericModelSpec(provider: string, modelId: string): Model
|
|
|
622
647
|
supportsLongCacheRetention: false,
|
|
623
648
|
supportsParallelToolCalls: true,
|
|
624
649
|
supportsStreaming: true,
|
|
650
|
+
supportsStructuredOutput: true,
|
|
625
651
|
modalities: ["text"],
|
|
626
652
|
},
|
|
627
653
|
pricing: {},
|
package/src/providers/google.ts
CHANGED
|
@@ -48,9 +48,11 @@ import {
|
|
|
48
48
|
incompleteStreamError,
|
|
49
49
|
combinedSignal,
|
|
50
50
|
timeoutFor,
|
|
51
|
+
hasAuthHeader,
|
|
52
|
+
clampToolCallId,
|
|
51
53
|
} from "./shared.ts";
|
|
52
54
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
53
|
-
import { getModelFromCatalog, getModelsForProvider } from "../models/catalog.ts";
|
|
55
|
+
import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutput } from "../models/catalog.ts";
|
|
54
56
|
import {
|
|
55
57
|
mapThinkingLevelToGoogle,
|
|
56
58
|
mapServiceTierToGoogle,
|
|
@@ -59,6 +61,10 @@ import {
|
|
|
59
61
|
lastProviderFor,
|
|
60
62
|
noteProviderTurn,
|
|
61
63
|
parseStreamedToolArguments,
|
|
64
|
+
mapOutputToGoogle,
|
|
65
|
+
normalizeStructuredOutput,
|
|
66
|
+
augmentSystemPromptWithStructuredOutput,
|
|
67
|
+
emitProviderWarning,
|
|
62
68
|
} from "../providers.ts";
|
|
63
69
|
|
|
64
70
|
// ---------------------------------------------------------------------------
|
|
@@ -176,7 +182,10 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
179
|
-
|
|
185
|
+
const key = getApiKey("google", options?.apiKey, options?.env);
|
|
186
|
+
if (key) return key;
|
|
187
|
+
if (hasAuthHeader(options?.headers)) return "unused";
|
|
188
|
+
return undefined;
|
|
180
189
|
}
|
|
181
190
|
|
|
182
191
|
function cleanModelId(model: string | ModelSpec): string {
|
|
@@ -517,11 +526,43 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
517
526
|
input: "",
|
|
518
527
|
...(stream ? { stream: true } : {}),
|
|
519
528
|
};
|
|
520
|
-
|
|
529
|
+
// Structured outputs: native `response_format` when supported, else
|
|
530
|
+
// emulated via system instruction (catalog `structured_output: false`
|
|
531
|
+
// would 400 on the wire shape).
|
|
532
|
+
let outputEmulated = false;
|
|
533
|
+
let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
534
|
+
try {
|
|
535
|
+
outputSpec = normalizeStructuredOutput(options?.output ?? (options as { outputSchema?: unknown })?.outputSchema);
|
|
536
|
+
} catch {
|
|
537
|
+
outputSpec = undefined;
|
|
538
|
+
}
|
|
539
|
+
let effectiveSystemPrompt = context.systemPrompt;
|
|
540
|
+
if (outputSpec && !modelSupportsStructuredOutput("google", modelId)) {
|
|
541
|
+
outputEmulated = true;
|
|
542
|
+
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec);
|
|
543
|
+
emitProviderWarning({
|
|
544
|
+
provider: "google",
|
|
545
|
+
capability: "structured output",
|
|
546
|
+
requested: `${outputSpec.name} (google/${modelId})`,
|
|
547
|
+
reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
|
|
548
|
+
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (effectiveSystemPrompt) body.system_instruction = effectiveSystemPrompt;
|
|
521
552
|
const googleTools = toGoogleTools(tools);
|
|
522
553
|
if (googleTools) body.tools = googleTools;
|
|
523
554
|
if (Object.keys(generationConfig).length > 0) body.generation_config = generationConfig;
|
|
524
555
|
if (serviceTier) body.service_tier = serviceTier;
|
|
556
|
+
// Structured outputs: Interactions `response_format{type:text,mime_type:application/json,schema}`.
|
|
557
|
+
// Skipped when emulated (instruction already in `system_instruction`).
|
|
558
|
+
if (outputSpec && !outputEmulated) {
|
|
559
|
+
try {
|
|
560
|
+
const mapped = mapOutputToGoogle(outputSpec);
|
|
561
|
+
if (mapped) (body as unknown as Record<string, unknown>)["response_format"] = mapped.responseFormat;
|
|
562
|
+
} catch {
|
|
563
|
+
// why: invalid output shapes fail fast at Agent construction/loop validation.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
525
566
|
|
|
526
567
|
const prevProvider = lastProviderFor(sessionId);
|
|
527
568
|
const switched = !!prevProvider && prevProvider !== "google";
|
|
@@ -564,8 +605,9 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
564
605
|
}
|
|
565
606
|
// Q-17: key travels as the `x-goog-api-key` header (never `?key=` in the
|
|
566
607
|
// URL, which would leak into logs). Case-insensitive merge so a custom
|
|
567
|
-
// `X-Goog-Api-Key` never duplicates the canonical header.
|
|
568
|
-
|
|
608
|
+
// `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
|
|
609
|
+
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
610
|
+
if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "x-goog-api-key", apiKey);
|
|
569
611
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
570
612
|
}
|
|
571
613
|
|
|
@@ -598,14 +640,21 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
598
640
|
modelId: string
|
|
599
641
|
): void {
|
|
600
642
|
if (status >= 200 && status < 300) return;
|
|
601
|
-
const { message, code, errorType } = readErrorPayload(bodyText);
|
|
602
|
-
const
|
|
643
|
+
const { message, code, errorType, raw } = readErrorPayload(bodyText);
|
|
644
|
+
const hint =
|
|
645
|
+
status === 401
|
|
646
|
+
? " Check GEMINI_API_KEY/GOOGLE_API_KEY (no quotes/spaces, correct provider), GOOGLE_BASE_URL, and that the key is loaded (bun --env-file=.env.local)."
|
|
647
|
+
: "";
|
|
648
|
+
const fullMessage = `${message}${hint}`;
|
|
649
|
+
const err: Record<string, unknown> & Error = new Error(fullMessage) as Record<string, unknown> & Error;
|
|
603
650
|
(err as Record<string, unknown>)["statusCode"] = status;
|
|
604
651
|
(err as Record<string, unknown>)["status"] = status;
|
|
605
652
|
(err as Record<string, unknown>)["responseBody"] = bodyText.slice(0, 500);
|
|
606
653
|
(err as Record<string, unknown>)["url"] = url.split("?")[0];
|
|
654
|
+
(err as Record<string, unknown>)["data"] = { error: { message: fullMessage } };
|
|
607
655
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
608
656
|
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
657
|
+
if (raw) (err as Record<string, unknown>)["raw"] = raw;
|
|
609
658
|
throw toConciseProviderError(err, "google", modelId);
|
|
610
659
|
}
|
|
611
660
|
|