@full-self-browsing/lattice 1.3.0 → 1.5.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/agent-run-C6miAzwI.d.ts +45 -0
- package/dist/agent-run-C6miAzwI.d.ts.map +1 -0
- package/dist/agent-run-CgPVFl0Z.js +47 -0
- package/dist/agent-run-CgPVFl0Z.js.map +1 -0
- package/dist/agents.d.ts +5 -0
- package/dist/agents.js +6 -0
- package/dist/artifact-Bg6mJGnm.d.ts +125 -0
- package/dist/artifact-Bg6mJGnm.d.ts.map +1 -0
- package/dist/artifact-DOfpeXLb.js +140 -0
- package/dist/artifact-DOfpeXLb.js.map +1 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +2 -0
- package/dist/audit.d.ts +3 -0
- package/dist/audit.js +4 -0
- package/dist/catalog-CAfYwB_-.js +91 -0
- package/dist/catalog-CAfYwB_-.js.map +1 -0
- package/dist/context-pack-Bz3GXmjv.js +99 -0
- package/dist/context-pack-Bz3GXmjv.js.map +1 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.js +2 -0
- package/dist/contract-S3oJGlc9.d.ts +74 -0
- package/dist/contract-S3oJGlc9.d.ts.map +1 -0
- package/dist/core.d.ts +48 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +95 -0
- package/dist/core.js.map +1 -0
- package/dist/errors-eEuEIx6X.js +407 -0
- package/dist/errors-eEuEIx6X.js.map +1 -0
- package/dist/eval.d.ts +2 -0
- package/dist/eval.js +2 -0
- package/dist/fingerprint-DodDbQKN.js +34 -0
- package/dist/fingerprint-DodDbQKN.js.map +1 -0
- package/dist/index-DpnHGHVL.d.ts +53 -0
- package/dist/index-DpnHGHVL.d.ts.map +1 -0
- package/dist/index.d.ts +78 -3234
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +365 -8434
- package/dist/index.js.map +1 -1
- package/dist/infer-DLqp5QIM.d.ts +96 -0
- package/dist/infer-DLqp5QIM.d.ts.map +1 -0
- package/dist/lineage-DBgoPWAZ.js +137 -0
- package/dist/lineage-DBgoPWAZ.js.map +1 -0
- package/dist/local-CXOGPJ1f.js +139 -0
- package/dist/local-CXOGPJ1f.js.map +1 -0
- package/dist/local-Dy--7peL.d.ts +10 -0
- package/dist/local-Dy--7peL.d.ts.map +1 -0
- package/dist/memory-CkQEW6m5.js +62 -0
- package/dist/memory-CkQEW6m5.js.map +1 -0
- package/dist/memory-DRig5EHV.d.ts +10 -0
- package/dist/memory-DRig5EHV.d.ts.map +1 -0
- package/dist/negotiate-ClD88hkc.js +10967 -0
- package/dist/negotiate-ClD88hkc.js.map +1 -0
- package/dist/otel-BgM4e55_.d.ts +421 -0
- package/dist/otel-BgM4e55_.d.ts.map +1 -0
- package/dist/permission-context-CUKMo79F.js +134 -0
- package/dist/permission-context-CUKMo79F.js.map +1 -0
- package/dist/plan-DFm8Llep.js +125 -0
- package/dist/plan-DFm8Llep.js.map +1 -0
- package/dist/preflight-DNHWuJ46.d.ts +64 -0
- package/dist/preflight-DNHWuJ46.d.ts.map +1 -0
- package/dist/provider-C2IfKsvz.d.ts +1178 -0
- package/dist/provider-C2IfKsvz.d.ts.map +1 -0
- package/dist/providers.d.ts +4 -0
- package/dist/providers.js +4 -0
- package/dist/rate-limit-group-nDsBJqSu.d.ts +235 -0
- package/dist/rate-limit-group-nDsBJqSu.d.ts.map +1 -0
- package/dist/receipt-FYouoPHv.js +205 -0
- package/dist/receipt-FYouoPHv.js.map +1 -0
- package/dist/replay-CtIhpLek.js +964 -0
- package/dist/replay-CtIhpLek.js.map +1 -0
- package/dist/result-DLEx2WvU.d.ts +38 -0
- package/dist/result-DLEx2WvU.d.ts.map +1 -0
- package/dist/router-DU4Z3pTd.js +314 -0
- package/dist/router-DU4Z3pTd.js.map +1 -0
- package/dist/router-Yo1-aDOv.d.ts +42 -0
- package/dist/router-Yo1-aDOv.d.ts.map +1 -0
- package/dist/routing.d.ts +6 -0
- package/dist/routing.js +4 -0
- package/dist/{run-crew-DDznbc3G.js → run-crew-B2fQLmgB.js} +16 -23
- package/dist/run-crew-B2fQLmgB.js.map +1 -0
- package/dist/run-crew-Bnve5dyI.d.ts +721 -0
- package/dist/run-crew-Bnve5dyI.d.ts.map +1 -0
- package/dist/{runtime-BTi8lr_O.js → runtime-Dxiet5YS.js} +100 -640
- package/dist/runtime-Dxiet5YS.js.map +1 -0
- package/dist/scaffolds-DKQrCRqh.d.ts +535 -0
- package/dist/scaffolds-DKQrCRqh.d.ts.map +1 -0
- package/dist/scaffolds-ekPIlBeU.js +3139 -0
- package/dist/scaffolds-ekPIlBeU.js.map +1 -0
- package/dist/schema-CNfa_VEy.d.ts +15 -0
- package/dist/schema-CNfa_VEy.d.ts.map +1 -0
- package/dist/storage-DJKmsaEI.d.ts +26 -0
- package/dist/storage-DJKmsaEI.d.ts.map +1 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +4 -0
- package/dist/tool-call-validation-BFoXkwbf.js +107 -0
- package/dist/tool-call-validation-BFoXkwbf.js.map +1 -0
- package/dist/tools-C4wHgGKQ.js +49 -0
- package/dist/tools-C4wHgGKQ.js.map +1 -0
- package/dist/tools.d.ts +46 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +106 -0
- package/dist/tools.js.map +1 -0
- package/dist/validate-c7EL5uuH.js +224 -0
- package/dist/validate-c7EL5uuH.js.map +1 -0
- package/package.json +105 -6
- package/dist/run-crew-DDznbc3G.js.map +0 -1
- package/dist/runtime-BTi8lr_O.js.map +0 -1
|
@@ -0,0 +1,3139 @@
|
|
|
1
|
+
import { r as defaultCapabilityForProvider } from "./catalog-CAfYwB_-.js";
|
|
2
|
+
import { c as getCapabilityProfile, i as synthesizeNegotiatedCapabilitiesFromRegistry, l as stripOpenRouterVariant, n as mapProfileToNegotiatedCapabilities, o as getRecommendedSanitizers, t as NegotiationAuthError } from "./negotiate-ClD88hkc.js";
|
|
3
|
+
import { i as standardSchemaToJsonSchema, o as parseToolUseEnvelope } from "./validate-c7EL5uuH.js";
|
|
4
|
+
import { n as validateToolCallRequests } from "./tool-call-validation-BFoXkwbf.js";
|
|
5
|
+
import canonicalize from "canonicalize";
|
|
6
|
+
//#region src/providers/multimodal.ts
|
|
7
|
+
function packagedPlanForArtifact(request, artifactId) {
|
|
8
|
+
return request.providerPackaging?.artifacts.find((item) => item.artifactId === artifactId) ?? request.plan?.providerPackaging?.artifacts.find((item) => item.artifactId === artifactId);
|
|
9
|
+
}
|
|
10
|
+
function metadataString(artifact, keys) {
|
|
11
|
+
const metadata = artifact.metadata;
|
|
12
|
+
if (metadata === void 0) return;
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
const value = metadata[key];
|
|
15
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function artifactHttpUrl(artifact) {
|
|
19
|
+
if (isHttpUrl(artifact.value)) return artifact.value;
|
|
20
|
+
const url = metadataString(artifact, ["url"]);
|
|
21
|
+
return isHttpUrl(url) ? url : void 0;
|
|
22
|
+
}
|
|
23
|
+
function anthropicFileId(artifact) {
|
|
24
|
+
return metadataString(artifact, [
|
|
25
|
+
"anthropicFileId",
|
|
26
|
+
"providerFileId",
|
|
27
|
+
"fileId"
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
function geminiFileUri(artifact) {
|
|
31
|
+
return metadataString(artifact, [
|
|
32
|
+
"geminiFileUri",
|
|
33
|
+
"providerFileUri",
|
|
34
|
+
"fileUri"
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
function mediaTypeForArtifact(artifact, fallback) {
|
|
38
|
+
if (artifact.mediaType !== void 0) return artifact.mediaType;
|
|
39
|
+
if (typeof artifact.value === "string") {
|
|
40
|
+
const dataUrl = parseDataUrl(artifact.value);
|
|
41
|
+
if (dataUrl?.mediaType !== void 0) return dataUrl.mediaType;
|
|
42
|
+
}
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
async function artifactBase64Data(artifact) {
|
|
46
|
+
const metadataData = metadataString(artifact, ["base64Data"]);
|
|
47
|
+
if (metadataData !== void 0) return metadataData;
|
|
48
|
+
const value = artifact.value;
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
const dataUrl = parseDataUrl(value);
|
|
51
|
+
if (dataUrl !== void 0) return dataUrl.data;
|
|
52
|
+
if (artifact.metadata?.encoding === "base64") return value;
|
|
53
|
+
}
|
|
54
|
+
if (isBlobLike(value)) return bufferToBase64(await value.arrayBuffer());
|
|
55
|
+
if (value instanceof ArrayBuffer) return bufferToBase64(value);
|
|
56
|
+
if (ArrayBuffer.isView(value)) return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
|
|
57
|
+
}
|
|
58
|
+
function parseDataUrl(value) {
|
|
59
|
+
const match = /^data:([^;,]+)?;base64,(.*)$/su.exec(value);
|
|
60
|
+
if (match === null) return;
|
|
61
|
+
const mediaType = match[1];
|
|
62
|
+
const data = match[2] ?? "";
|
|
63
|
+
return {
|
|
64
|
+
...mediaType !== void 0 && mediaType.length > 0 ? { mediaType } : {},
|
|
65
|
+
data
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function bufferToBase64(value) {
|
|
69
|
+
return Buffer.from(value).toString("base64");
|
|
70
|
+
}
|
|
71
|
+
function isHttpUrl(value) {
|
|
72
|
+
if (typeof value !== "string") return false;
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(value);
|
|
75
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isBlobLike(value) {
|
|
81
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/providers/no-public-url.ts
|
|
85
|
+
/**
|
|
86
|
+
* Thrown when a run with `policy.noPublicUrl: true` is about to dispatch a
|
|
87
|
+
* request whose serialized body still contains a public http(s) URL derived
|
|
88
|
+
* from `request.artifacts` (value or string metadata entry).
|
|
89
|
+
*
|
|
90
|
+
* This is the single shared egress error class for all three adapter families
|
|
91
|
+
* (OpenAI-compatible, Anthropic, Gemini). Callers may `instanceof`-check it.
|
|
92
|
+
*/
|
|
93
|
+
var NoPublicUrlEgressError = class extends Error {
|
|
94
|
+
constructor(providerId, artifactId, offendingUrl) {
|
|
95
|
+
super(`noPublicUrl policy violated: provider '${providerId}' artifact '${artifactId}' would leak public URL '${offendingUrl}'`);
|
|
96
|
+
this.providerId = providerId;
|
|
97
|
+
this.artifactId = artifactId;
|
|
98
|
+
this.offendingUrl = offendingUrl;
|
|
99
|
+
this.name = "NoPublicUrlEgressError";
|
|
100
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Shared egress assertion called immediately before every run-request `fetch`.
|
|
105
|
+
*
|
|
106
|
+
* When `policy.noPublicUrl` is not `true` this function is a zero-cost no-op.
|
|
107
|
+
*
|
|
108
|
+
* When the policy IS active it builds a forbidden-URL set from ARTIFACT-DERIVED
|
|
109
|
+
* sources only:
|
|
110
|
+
* - `artifact.value` (if it is a string and `isHttpUrl(value)` is true)
|
|
111
|
+
* - Every `string` value inside `artifact.metadata` (if `isHttpUrl(v)` is true)
|
|
112
|
+
*
|
|
113
|
+
* URLs in `policy.gateway.metadata` are NOT artifact-derived and are therefore
|
|
114
|
+
* NOT in scope. They are naturally excluded because they never appear in
|
|
115
|
+
* `request.artifacts`.
|
|
116
|
+
*
|
|
117
|
+
* A URL is considered "leaked" only if the string is present in `serializedBody`.
|
|
118
|
+
* If packaging already replaced the URL with a `data:` URL, `serializedBody`
|
|
119
|
+
* will not contain the original http(s) URL and this function will not throw.
|
|
120
|
+
* `data:` URLs are never forbidden because `isHttpUrl` rejects the `data:` scheme.
|
|
121
|
+
*
|
|
122
|
+
* @throws {NoPublicUrlEgressError} if any forbidden URL appears in `serializedBody`
|
|
123
|
+
*/
|
|
124
|
+
function assertNoPublicUrlEgress(request, providerId, serializedBody) {
|
|
125
|
+
if (request.policy?.noPublicUrl !== true) return;
|
|
126
|
+
const forbidden = [];
|
|
127
|
+
for (const artifact of request.artifacts) {
|
|
128
|
+
const artifactId = artifact.id ?? "";
|
|
129
|
+
if (typeof artifact.value === "string" && isHttpUrl(artifact.value)) forbidden.push({
|
|
130
|
+
url: artifact.value,
|
|
131
|
+
id: artifactId
|
|
132
|
+
});
|
|
133
|
+
const metadata = artifact.metadata ?? {};
|
|
134
|
+
for (const v of Object.values(metadata)) if (typeof v === "string" && isHttpUrl(v)) forbidden.push({
|
|
135
|
+
url: v,
|
|
136
|
+
id: artifactId
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
for (const entry of forbidden) if (serializedBody.includes(entry.url)) throw new NoPublicUrlEgressError(providerId, entry.id, entry.url);
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/tracing/tracing.ts
|
|
143
|
+
function createRunEvent(kind, input) {
|
|
144
|
+
return {
|
|
145
|
+
kind,
|
|
146
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
147
|
+
...input
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/sanitizers/sanitizers.ts
|
|
152
|
+
async function applyOutputSanitizers(rawOutputs, sanitizeOutput, context) {
|
|
153
|
+
if (sanitizeOutput === void 0) return rawOutputs;
|
|
154
|
+
const sanitizers = Array.isArray(sanitizeOutput) ? sanitizeOutput : [sanitizeOutput];
|
|
155
|
+
const sanitizedEntries = await Promise.all(Object.entries(rawOutputs).map(async ([outputName, value]) => {
|
|
156
|
+
if (typeof value !== "string") return [outputName, value];
|
|
157
|
+
let sanitized = value;
|
|
158
|
+
const sanitizerContext = {
|
|
159
|
+
...context,
|
|
160
|
+
outputName
|
|
161
|
+
};
|
|
162
|
+
for (const sanitizer of sanitizers) sanitized = await sanitizer(sanitized, sanitizerContext);
|
|
163
|
+
return [outputName, sanitized];
|
|
164
|
+
}));
|
|
165
|
+
return Object.fromEntries(sanitizedEntries);
|
|
166
|
+
}
|
|
167
|
+
function stripReasoningTags() {
|
|
168
|
+
return (text) => {
|
|
169
|
+
let next = text;
|
|
170
|
+
next = next.replace(/^\s*(?:reasoning|analysis|scratchpad)\s*:\s*(?:.|\n)*?(?:\n\s*(?:final|answer)\s*:\s*)/iu, "");
|
|
171
|
+
next = stripDelimitedBlock(next, "think");
|
|
172
|
+
next = stripDelimitedBlock(next, "reasoning");
|
|
173
|
+
next = stripDelimitedBlock(next, "scratchpad");
|
|
174
|
+
return next === text ? text : next.trim();
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function stripChatTemplateArtifacts() {
|
|
178
|
+
return (text) => {
|
|
179
|
+
let next = text;
|
|
180
|
+
next = next.replace(/<\|im_start\|>\s*(?:system|user|assistant)?\s*/giu, "");
|
|
181
|
+
next = next.replace(/\s*<\|im_end\|>/giu, "");
|
|
182
|
+
next = next.replace(/\[\/?INST\]/giu, "");
|
|
183
|
+
next = next.replace(/<<SYS>>|<<\/SYS>>/giu, "");
|
|
184
|
+
next = next.replace(/^\s*(?:system|user|assistant)\s*:\s*/iu, "");
|
|
185
|
+
return next === text ? text : next.trim();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function unwrapInternalEnvelope(schemaOrPath) {
|
|
189
|
+
const options = parseEnvelopeOptions(schemaOrPath);
|
|
190
|
+
return async (text) => {
|
|
191
|
+
const parsed = parseJsonObject$3(text);
|
|
192
|
+
if (parsed === void 0) return text;
|
|
193
|
+
if (options.schema !== void 0) {
|
|
194
|
+
if (!(await validateSchema(options.schema, parsed)).ok) return text;
|
|
195
|
+
}
|
|
196
|
+
const value = options.kind === "path" ? getPathValue(parsed, options.path) : findOnlyStringField(parsed);
|
|
197
|
+
return typeof value === "string" ? value : text;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function stripDelimitedBlock(text, tag) {
|
|
201
|
+
const pattern = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, "giu");
|
|
202
|
+
return text.replace(pattern, "");
|
|
203
|
+
}
|
|
204
|
+
function parseEnvelopeOptions(schemaOrPath) {
|
|
205
|
+
if (typeof schemaOrPath === "string") return {
|
|
206
|
+
kind: "path",
|
|
207
|
+
path: splitPath(schemaOrPath)
|
|
208
|
+
};
|
|
209
|
+
if (isStandardSchema(schemaOrPath)) return {
|
|
210
|
+
kind: "schema",
|
|
211
|
+
schema: schemaOrPath
|
|
212
|
+
};
|
|
213
|
+
const path = schemaOrPath.path ?? schemaOrPath.field;
|
|
214
|
+
if (path !== void 0) return {
|
|
215
|
+
kind: "path",
|
|
216
|
+
path: splitPath(path),
|
|
217
|
+
...schemaOrPath.schema !== void 0 ? { schema: schemaOrPath.schema } : {}
|
|
218
|
+
};
|
|
219
|
+
if (schemaOrPath.schema !== void 0) return {
|
|
220
|
+
kind: "schema",
|
|
221
|
+
schema: schemaOrPath.schema
|
|
222
|
+
};
|
|
223
|
+
return {
|
|
224
|
+
kind: "path",
|
|
225
|
+
path: []
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function splitPath(path) {
|
|
229
|
+
return path.split(".").map((part) => part.trim()).filter(Boolean);
|
|
230
|
+
}
|
|
231
|
+
function parseJsonObject$3(text) {
|
|
232
|
+
let parsed;
|
|
233
|
+
try {
|
|
234
|
+
parsed = JSON.parse(text);
|
|
235
|
+
} catch {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
|
|
239
|
+
return parsed;
|
|
240
|
+
}
|
|
241
|
+
function getPathValue(value, path) {
|
|
242
|
+
if (path.length === 0) return void 0;
|
|
243
|
+
let current = value;
|
|
244
|
+
for (const segment of path) {
|
|
245
|
+
if (typeof current !== "object" || current === null || Array.isArray(current)) return;
|
|
246
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) return;
|
|
247
|
+
current = current[segment];
|
|
248
|
+
}
|
|
249
|
+
return current;
|
|
250
|
+
}
|
|
251
|
+
function findOnlyStringField(value) {
|
|
252
|
+
const stringValues = Object.values(value).filter((field) => typeof field === "string");
|
|
253
|
+
return stringValues.length === 1 ? stringValues[0] : void 0;
|
|
254
|
+
}
|
|
255
|
+
async function validateSchema(schema, value) {
|
|
256
|
+
const result = schema["~standard"].validate(value);
|
|
257
|
+
return "issues" in (result instanceof Promise ? await result : result) ? { ok: false } : { ok: true };
|
|
258
|
+
}
|
|
259
|
+
function isStandardSchema(value) {
|
|
260
|
+
return typeof value === "object" && value !== null && "~standard" in value && typeof value["~standard"]?.validate === "function";
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/providers/sse.ts
|
|
264
|
+
async function* readSseEvents(response) {
|
|
265
|
+
if (response.body === null) {
|
|
266
|
+
yield* parseFrames(await response.text(), true).events;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const reader = response.body.getReader();
|
|
270
|
+
const decoder = new TextDecoder();
|
|
271
|
+
let buffer = "";
|
|
272
|
+
try {
|
|
273
|
+
while (true) {
|
|
274
|
+
const { done, value } = await reader.read();
|
|
275
|
+
if (done) break;
|
|
276
|
+
buffer += decoder.decode(value, { stream: true });
|
|
277
|
+
const parsed = parseFrames(buffer, false);
|
|
278
|
+
buffer = parsed.remaining;
|
|
279
|
+
yield* parsed.events;
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
reader.releaseLock();
|
|
283
|
+
}
|
|
284
|
+
buffer += decoder.decode();
|
|
285
|
+
yield* parseFrames(buffer, true).events;
|
|
286
|
+
}
|
|
287
|
+
function parseFrames(input, flush) {
|
|
288
|
+
const events = [];
|
|
289
|
+
let remaining = input;
|
|
290
|
+
while (true) {
|
|
291
|
+
const match = /\r?\n\r?\n/u.exec(remaining);
|
|
292
|
+
if (match?.index === void 0) break;
|
|
293
|
+
const frame = remaining.slice(0, match.index);
|
|
294
|
+
remaining = remaining.slice(match.index + match[0].length);
|
|
295
|
+
const event = parseFrame(frame);
|
|
296
|
+
if (event !== void 0) events.push(event);
|
|
297
|
+
}
|
|
298
|
+
if (flush && remaining.trim().length > 0) {
|
|
299
|
+
const event = parseFrame(remaining);
|
|
300
|
+
if (event !== void 0) events.push(event);
|
|
301
|
+
remaining = "";
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
events,
|
|
305
|
+
remaining
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function parseFrame(frame) {
|
|
309
|
+
const data = [];
|
|
310
|
+
let event;
|
|
311
|
+
for (const line of frame.split(/\r?\n/u)) {
|
|
312
|
+
if (line.length === 0 || line.startsWith(":")) continue;
|
|
313
|
+
const separator = line.indexOf(":");
|
|
314
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
315
|
+
const rawValue = separator === -1 ? "" : line.slice(separator + 1);
|
|
316
|
+
const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
|
|
317
|
+
if (field === "event") event = value;
|
|
318
|
+
else if (field === "data") data.push(value);
|
|
319
|
+
}
|
|
320
|
+
if (data.length === 0) return;
|
|
321
|
+
return {
|
|
322
|
+
...event !== void 0 ? { event } : {},
|
|
323
|
+
data: data.join("\n")
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/providers/adapters.ts
|
|
328
|
+
function isRecord$3(value) {
|
|
329
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
330
|
+
}
|
|
331
|
+
function isGatewayMetadataValue(value) {
|
|
332
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) return true;
|
|
333
|
+
if (Array.isArray(value)) return value.every(isGatewayMetadataValue);
|
|
334
|
+
if (isRecord$3(value)) return Object.values(value).every(isGatewayMetadataValue);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
function isSecretMetadataKey(key) {
|
|
338
|
+
return /api[-_]?key|authorization|headers?|secret|token|password/iu.test(key);
|
|
339
|
+
}
|
|
340
|
+
function isSecretMetadataValue(value) {
|
|
341
|
+
if (typeof value === "string") return /^sk-[\w-]+/u.test(value);
|
|
342
|
+
if (Array.isArray(value)) return value.some(isSecretMetadataValue);
|
|
343
|
+
if (typeof value === "object" && value !== null) return Object.entries(value).some(([key, nested]) => isSecretMetadataKey(key) || isSecretMetadataValue(nested));
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
function sanitizeGatewayMetadata(metadata) {
|
|
347
|
+
if (metadata === void 0) return;
|
|
348
|
+
const sanitized = Object.fromEntries(Object.entries(metadata).filter(([key, value]) => !isSecretMetadataKey(key) && !isSecretMetadataValue(value)));
|
|
349
|
+
return Object.keys(sanitized).length > 0 ? sanitized : void 0;
|
|
350
|
+
}
|
|
351
|
+
function normalizeGatewayPolicy(value) {
|
|
352
|
+
if (!isRecord$3(value)) return {};
|
|
353
|
+
const routeTags = Array.isArray(value.routeTags) ? value.routeTags.filter((tag) => typeof tag === "string") : void 0;
|
|
354
|
+
const providerPreferences = Array.isArray(value.providerPreferences) ? value.providerPreferences.filter((provider) => typeof provider === "string") : void 0;
|
|
355
|
+
const gatewayMetadata = {};
|
|
356
|
+
if (isRecord$3(value.metadata)) {
|
|
357
|
+
for (const [key, metadataValue] of Object.entries(value.metadata)) if (isGatewayMetadataValue(metadataValue)) gatewayMetadata[key] = metadataValue;
|
|
358
|
+
}
|
|
359
|
+
const metadata = Object.keys(gatewayMetadata).length > 0 ? sanitizeGatewayMetadata(gatewayMetadata) : void 0;
|
|
360
|
+
const allowFallbacks = typeof value.allowFallbacks === "boolean" ? value.allowFallbacks : void 0;
|
|
361
|
+
return {
|
|
362
|
+
...routeTags !== void 0 ? { routeTags } : {},
|
|
363
|
+
...providerPreferences !== void 0 ? { providerPreferences } : {},
|
|
364
|
+
...metadata !== void 0 ? { metadata } : {},
|
|
365
|
+
...allowFallbacks !== void 0 ? { allowFallbacks } : {}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function readGatewayPolicy(policy) {
|
|
369
|
+
if (!isRecord$3(policy) || !isRecord$3(policy.gateway)) return;
|
|
370
|
+
return normalizeGatewayPolicy(policy.gateway);
|
|
371
|
+
}
|
|
372
|
+
function mergeGatewayPolicy(providerGateway, requestGateway) {
|
|
373
|
+
if (providerGateway === void 0 && requestGateway === void 0) return;
|
|
374
|
+
const providerMetadata = sanitizeGatewayMetadata(providerGateway?.metadata);
|
|
375
|
+
const requestMetadata = sanitizeGatewayMetadata(requestGateway?.metadata);
|
|
376
|
+
const metadata = {
|
|
377
|
+
...providerMetadata ?? {},
|
|
378
|
+
...requestMetadata ?? {}
|
|
379
|
+
};
|
|
380
|
+
return {
|
|
381
|
+
routeTags: [...providerGateway?.routeTags ?? [], ...requestGateway?.routeTags ?? []],
|
|
382
|
+
providerPreferences: [...providerGateway?.providerPreferences ?? [], ...requestGateway?.providerPreferences ?? []],
|
|
383
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {},
|
|
384
|
+
...requestGateway?.allowFallbacks !== void 0 ? { allowFallbacks: requestGateway.allowFallbacks } : providerGateway?.allowFallbacks !== void 0 ? { allowFallbacks: providerGateway.allowFallbacks } : {}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function gatewayPolicyToMetadata(policy) {
|
|
388
|
+
if (policy === void 0) return;
|
|
389
|
+
const metadata = { ...sanitizeGatewayMetadata(policy.metadata) ?? {} };
|
|
390
|
+
const latticeGateway = {};
|
|
391
|
+
if (policy.routeTags !== void 0 && policy.routeTags.length > 0) latticeGateway.route_tags = [...policy.routeTags];
|
|
392
|
+
if (policy.providerPreferences !== void 0 && policy.providerPreferences.length > 0) latticeGateway.provider_preferences = [...policy.providerPreferences];
|
|
393
|
+
if (policy.allowFallbacks !== void 0) latticeGateway.allow_fallbacks = policy.allowFallbacks;
|
|
394
|
+
if (Object.keys(latticeGateway).length > 0) metadata.lattice_gateway = latticeGateway;
|
|
395
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
396
|
+
}
|
|
397
|
+
function sanitizedGatewayPolicyForPlan(policy) {
|
|
398
|
+
if (policy === void 0) return;
|
|
399
|
+
const metadata = sanitizeGatewayMetadata(policy.metadata);
|
|
400
|
+
return {
|
|
401
|
+
...policy.routeTags !== void 0 && policy.routeTags.length > 0 ? { routeTags: [...policy.routeTags] } : {},
|
|
402
|
+
...policy.providerPreferences !== void 0 && policy.providerPreferences.length > 0 ? { providerPreferences: [...policy.providerPreferences] } : {},
|
|
403
|
+
...metadata !== void 0 ? { metadata } : {},
|
|
404
|
+
...policy.allowFallbacks !== void 0 ? { allowFallbacks: policy.allowFallbacks } : {}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function observedModelFromResponse(body) {
|
|
408
|
+
if (!isRecord$3(body)) return;
|
|
409
|
+
const model = body.model;
|
|
410
|
+
return typeof model === "string" ? model : void 0;
|
|
411
|
+
}
|
|
412
|
+
function createOpenAICompatibleRequestBody(input) {
|
|
413
|
+
const nativeTools = openAIToolDefinitions(input.request.nativeTools);
|
|
414
|
+
const nativeToolChoice = openAIToolChoice(input.request.nativeToolChoice);
|
|
415
|
+
const responseFormat = openAIResponseFormat(input.request.nativeStructuredOutput);
|
|
416
|
+
return {
|
|
417
|
+
model: input.model,
|
|
418
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {},
|
|
419
|
+
messages: [{
|
|
420
|
+
role: "user",
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: "text",
|
|
424
|
+
text: input.request.task
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
type: "text",
|
|
428
|
+
text: JSON.stringify({ contextPack: input.request.contextPack === void 0 ? void 0 : {
|
|
429
|
+
id: input.request.contextPack.id,
|
|
430
|
+
tokenBudget: input.request.contextPack.tokenBudget,
|
|
431
|
+
estimatedTokens: input.request.contextPack.estimatedTokens,
|
|
432
|
+
included: input.request.contextPack.included,
|
|
433
|
+
summarized: input.request.contextPack.summarized,
|
|
434
|
+
archived: input.request.contextPack.archived,
|
|
435
|
+
omitted: input.request.contextPack.omitted,
|
|
436
|
+
warnings: input.request.contextPack.warnings
|
|
437
|
+
} })
|
|
438
|
+
},
|
|
439
|
+
...input.request.artifacts.map((inputArtifact) => {
|
|
440
|
+
const resolvedTransport = input.request.providerPackaging?.artifacts.find((item) => item.artifactId === inputArtifact.id)?.transport ?? input.request.plan?.providerPackaging?.artifacts.find((item) => item.artifactId === inputArtifact.id)?.transport;
|
|
441
|
+
return {
|
|
442
|
+
type: "text",
|
|
443
|
+
text: JSON.stringify({
|
|
444
|
+
artifactId: inputArtifact.id,
|
|
445
|
+
kind: inputArtifact.kind,
|
|
446
|
+
mediaType: inputArtifact.mediaType,
|
|
447
|
+
privacy: inputArtifact.privacy,
|
|
448
|
+
transport: resolvedTransport,
|
|
449
|
+
value: typeof inputArtifact.value === "string" && inputArtifact.kind !== "url" && !(isHttpUrl(inputArtifact.value) && resolvedTransport !== "url") ? inputArtifact.value : void 0,
|
|
450
|
+
url: inputArtifact.kind === "url" && typeof inputArtifact.value === "string" && resolvedTransport === "url" ? inputArtifact.value : void 0
|
|
451
|
+
})
|
|
452
|
+
};
|
|
453
|
+
})
|
|
454
|
+
]
|
|
455
|
+
}],
|
|
456
|
+
...input.stream === true ? {
|
|
457
|
+
stream: true,
|
|
458
|
+
stream_options: { include_usage: true }
|
|
459
|
+
} : {},
|
|
460
|
+
...nativeTools.length > 0 ? { tools: nativeTools } : {},
|
|
461
|
+
...nativeToolChoice !== void 0 ? { tool_choice: nativeToolChoice } : {},
|
|
462
|
+
...responseFormat !== void 0 ? { response_format: responseFormat } : {}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function openAIToolDefinitions(tools) {
|
|
466
|
+
return (tools ?? []).map((tool) => ({
|
|
467
|
+
type: "function",
|
|
468
|
+
function: {
|
|
469
|
+
name: tool.name,
|
|
470
|
+
...tool.description !== void 0 ? { description: tool.description } : {},
|
|
471
|
+
parameters: standardSchemaToJsonSchema(tool.inputSchema)
|
|
472
|
+
}
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
function openAIToolChoice(choice) {
|
|
476
|
+
if (choice === void 0) return;
|
|
477
|
+
if (choice === "auto" || choice === "none" || choice === "required") return choice;
|
|
478
|
+
return {
|
|
479
|
+
type: "function",
|
|
480
|
+
function: { name: choice.name }
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function openAIResponseFormat(request) {
|
|
484
|
+
if (request === void 0) return;
|
|
485
|
+
return {
|
|
486
|
+
type: "json_schema",
|
|
487
|
+
json_schema: {
|
|
488
|
+
name: request.name ?? request.output,
|
|
489
|
+
schema: standardSchemaToJsonSchema(request.schema),
|
|
490
|
+
strict: request.strict ?? true
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Phase 34 — D-04 / QUIRK-02 — OpenAI-compatible provider factory.
|
|
496
|
+
*
|
|
497
|
+
* This factory is the prototypical "intentional no remote /models endpoint"
|
|
498
|
+
* adapter per D-04. The consumer points this adapter at any OpenAI-shaped
|
|
499
|
+
* endpoint (vLLM, TGI, Ollama, custom), and the factory returns conservative
|
|
500
|
+
* defaults for the quirks block because the server could be anything.
|
|
501
|
+
*
|
|
502
|
+
* The `negotiateCapabilities` method performs NO fetch; it returns
|
|
503
|
+
* synthesizeNegotiatedCapabilitiesFromRegistry with source: "registry"
|
|
504
|
+
* (the intentional-no-endpoint signal, as distinct from "registry-fallback"
|
|
505
|
+
* which signals a transient failure). Plan 34-05 (LM Studio) reuses this
|
|
506
|
+
* same pattern.
|
|
507
|
+
*
|
|
508
|
+
* D-04 citation: "consumer adapters without a /models endpoint skip the
|
|
509
|
+
* fetch layer entirely and delegate to synthesizeNegotiatedCapabilitiesFromRegistry."
|
|
510
|
+
*/
|
|
511
|
+
function createOpenAICompatibleProvider(options) {
|
|
512
|
+
const id = options.id ?? "openai-compatible";
|
|
513
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
514
|
+
const baseUrl = options.baseUrl.replace(/\/$/u, "");
|
|
515
|
+
const negotiate = async (modelId) => {
|
|
516
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry(id, modelId, "registry");
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
id,
|
|
520
|
+
kind: "provider-adapter",
|
|
521
|
+
quirks: {
|
|
522
|
+
supportsToolChoice: false,
|
|
523
|
+
parallelToolCalls: false,
|
|
524
|
+
structuredOutputs: false,
|
|
525
|
+
responseFormatHonored: false,
|
|
526
|
+
streamingDiverges: true
|
|
527
|
+
},
|
|
528
|
+
negotiateCapabilities: negotiate,
|
|
529
|
+
capabilities: [{
|
|
530
|
+
...defaultCapabilityForProvider(id),
|
|
531
|
+
modelId: options.model,
|
|
532
|
+
fileTransport: [
|
|
533
|
+
"inline",
|
|
534
|
+
"json",
|
|
535
|
+
"url",
|
|
536
|
+
"base64",
|
|
537
|
+
"extracted-text",
|
|
538
|
+
"transcript"
|
|
539
|
+
],
|
|
540
|
+
streaming: true
|
|
541
|
+
}],
|
|
542
|
+
async execute(request) {
|
|
543
|
+
const mergedGatewayPolicy = mergeGatewayPolicy(options.gateway, readGatewayPolicy(request.policy));
|
|
544
|
+
const metadata = gatewayPolicyToMetadata(mergedGatewayPolicy);
|
|
545
|
+
const bodyStr = JSON.stringify(createOpenAICompatibleRequestBody({
|
|
546
|
+
model: options.model,
|
|
547
|
+
request,
|
|
548
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
549
|
+
}));
|
|
550
|
+
assertNoPublicUrlEgress(request, id, bodyStr);
|
|
551
|
+
const init = {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: {
|
|
554
|
+
"content-type": "application/json",
|
|
555
|
+
...options.apiKey !== void 0 ? { authorization: `Bearer ${options.apiKey}` } : {}
|
|
556
|
+
},
|
|
557
|
+
body: bodyStr,
|
|
558
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
559
|
+
};
|
|
560
|
+
const response = await fetchImpl(`${baseUrl}/chat/completions`, init);
|
|
561
|
+
if (!response.ok) throw new Error(`OpenAI-compatible provider failed with ${response.status}.`);
|
|
562
|
+
const body = await response.json();
|
|
563
|
+
const observedModel = observedModelFromResponse(body);
|
|
564
|
+
const choice = firstOpenAIChoice(body);
|
|
565
|
+
const message = openAIMessageFromChoice(choice);
|
|
566
|
+
const text = openAIMessageText(message);
|
|
567
|
+
const structuredOutput = openAIStructuredOutputValue(request.nativeStructuredOutput, message, text);
|
|
568
|
+
const sanitizedOutputs = await applyOutputSanitizers(rawOutputsForRequest$2({
|
|
569
|
+
outputs: request.outputs,
|
|
570
|
+
text,
|
|
571
|
+
structuredOutputRequest: request.nativeStructuredOutput,
|
|
572
|
+
structuredOutput
|
|
573
|
+
}), options.sanitizeOutput, {
|
|
574
|
+
providerId: id,
|
|
575
|
+
modelId: options.model
|
|
576
|
+
});
|
|
577
|
+
const parsedToolCalls = parseToolUseEnvelope(text);
|
|
578
|
+
const promptToolCalls = parsedToolCalls === null ? void 0 : await validateToolCallRequests(parsedToolCalls, options.validateToolCalls);
|
|
579
|
+
const nativeToolRequests = openAIToolUseRequestsFromMessage(message);
|
|
580
|
+
const nativeToolCalls = nativeToolRequests.length === 0 ? void 0 : await validateToolCallRequests(nativeToolRequests, options.validateToolCalls);
|
|
581
|
+
const hasToolCallResult = promptToolCalls !== void 0 || nativeToolCalls !== void 0;
|
|
582
|
+
const toolCalls = [...promptToolCalls ?? [], ...nativeToolCalls ?? []];
|
|
583
|
+
const usage = normalizeUsage(body.usage);
|
|
584
|
+
const normalizedUsage = normalizeUsageToRunUsage(body.usage, options.pricing);
|
|
585
|
+
const sanitizedGatewayPolicy = sanitizedGatewayPolicyForPlan(mergedGatewayPolicy);
|
|
586
|
+
const gateway = id === "litellm" || mergedGatewayPolicy !== void 0 ? {
|
|
587
|
+
used: true,
|
|
588
|
+
requestedModel: options.model,
|
|
589
|
+
...observedModel !== void 0 ? { observedModel } : {},
|
|
590
|
+
...sanitizedGatewayPolicy !== void 0 ? { policy: sanitizedGatewayPolicy } : {}
|
|
591
|
+
} : void 0;
|
|
592
|
+
const finish = openAIFinishMetadata(choice, toolCalls);
|
|
593
|
+
return {
|
|
594
|
+
rawOutputs: sanitizedOutputs,
|
|
595
|
+
...usage !== void 0 ? { usage } : {},
|
|
596
|
+
normalizedUsage,
|
|
597
|
+
...hasToolCallResult ? { toolCalls } : {},
|
|
598
|
+
...gateway !== void 0 ? { gateway } : {},
|
|
599
|
+
...finish !== void 0 ? { finish } : {},
|
|
600
|
+
rawResponse: body
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
executeStream(request) {
|
|
604
|
+
return streamOpenAICompatibleResponse({
|
|
605
|
+
id,
|
|
606
|
+
model: options.model,
|
|
607
|
+
baseUrl,
|
|
608
|
+
fetchImpl,
|
|
609
|
+
request,
|
|
610
|
+
...options.apiKey !== void 0 ? { apiKey: options.apiKey } : {},
|
|
611
|
+
...options.gateway !== void 0 ? { providerGateway: options.gateway } : {},
|
|
612
|
+
...options.pricing !== void 0 ? { pricing: options.pricing } : {},
|
|
613
|
+
...options.sanitizeOutput !== void 0 ? { sanitizeOutput: options.sanitizeOutput } : {},
|
|
614
|
+
...options.validateToolCalls !== void 0 ? { validateToolCalls: options.validateToolCalls } : {}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
async function* streamOpenAICompatibleResponse(input) {
|
|
620
|
+
const mergedGatewayPolicy = mergeGatewayPolicy(input.providerGateway, readGatewayPolicy(input.request.policy));
|
|
621
|
+
const metadata = gatewayPolicyToMetadata(mergedGatewayPolicy);
|
|
622
|
+
const streamBodyStr = JSON.stringify(createOpenAICompatibleRequestBody({
|
|
623
|
+
model: input.model,
|
|
624
|
+
request: input.request,
|
|
625
|
+
...metadata !== void 0 ? { metadata } : {},
|
|
626
|
+
stream: true
|
|
627
|
+
}));
|
|
628
|
+
assertNoPublicUrlEgress(input.request, input.id, streamBodyStr);
|
|
629
|
+
const response = await input.fetchImpl(`${input.baseUrl}/chat/completions`, {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: {
|
|
632
|
+
"content-type": "application/json",
|
|
633
|
+
...input.apiKey !== void 0 ? { authorization: `Bearer ${input.apiKey}` } : {}
|
|
634
|
+
},
|
|
635
|
+
body: streamBodyStr,
|
|
636
|
+
...input.request.signal !== void 0 ? { signal: input.request.signal } : {}
|
|
637
|
+
});
|
|
638
|
+
if (!response.ok) throw new Error(`OpenAI-compatible provider failed with ${response.status}.`);
|
|
639
|
+
const textParts = [];
|
|
640
|
+
const rawChunks = [];
|
|
641
|
+
const nativeToolCalls = /* @__PURE__ */ new Map();
|
|
642
|
+
let usagePayload;
|
|
643
|
+
let observedModel;
|
|
644
|
+
let finishReason;
|
|
645
|
+
for await (const event of readSseEvents(response)) {
|
|
646
|
+
const data = event.data.trim();
|
|
647
|
+
if (data.length === 0) continue;
|
|
648
|
+
if (data === "[DONE]") break;
|
|
649
|
+
const chunk = parseJsonObject$2(data);
|
|
650
|
+
rawChunks.push(chunk);
|
|
651
|
+
const chunkObservedModel = observedModelFromResponse(chunk);
|
|
652
|
+
if (chunkObservedModel !== void 0) observedModel = chunkObservedModel;
|
|
653
|
+
if (isRecord$3(chunk) && chunk.usage !== void 0) usagePayload = chunk.usage;
|
|
654
|
+
for (const choice of streamChoices(chunk)) {
|
|
655
|
+
const choiceFinishReason = stringField(choice, "finish_reason");
|
|
656
|
+
if (choiceFinishReason !== void 0) finishReason = choiceFinishReason;
|
|
657
|
+
const delta = isRecord$3(choice.delta) ? choice.delta : {};
|
|
658
|
+
const content = typeof delta.content === "string" ? delta.content : void 0;
|
|
659
|
+
if (content !== void 0 && content.length > 0) {
|
|
660
|
+
textParts.push(content);
|
|
661
|
+
for (const output of input.request.outputs) yield {
|
|
662
|
+
kind: "text-delta",
|
|
663
|
+
output,
|
|
664
|
+
text: content
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
accumulateOpenAIToolCalls(nativeToolCalls, delta.tool_calls);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const text = textParts.join("");
|
|
671
|
+
const structuredOutput = input.request.nativeStructuredOutput === void 0 ? void 0 : parseJsonValue$1(text);
|
|
672
|
+
const sanitizedOutputs = await applyOutputSanitizers(rawOutputsForRequest$2({
|
|
673
|
+
outputs: input.request.outputs,
|
|
674
|
+
text,
|
|
675
|
+
structuredOutputRequest: input.request.nativeStructuredOutput,
|
|
676
|
+
structuredOutput
|
|
677
|
+
}), input.sanitizeOutput, {
|
|
678
|
+
providerId: input.id,
|
|
679
|
+
modelId: input.model
|
|
680
|
+
});
|
|
681
|
+
const parsedToolCalls = parseToolUseEnvelope(text);
|
|
682
|
+
const promptToolCalls = parsedToolCalls === null ? void 0 : await validateToolCallRequests(parsedToolCalls, input.validateToolCalls);
|
|
683
|
+
const nativeToolRequests = openAIToolUseRequests(nativeToolCalls);
|
|
684
|
+
const nativeValidatedToolCalls = nativeToolRequests.length === 0 ? void 0 : await validateToolCallRequests(nativeToolRequests, input.validateToolCalls);
|
|
685
|
+
const toolCalls = [...promptToolCalls ?? [], ...nativeValidatedToolCalls ?? []];
|
|
686
|
+
const usage = normalizeUsage(usagePayload);
|
|
687
|
+
const normalizedUsage = normalizeUsageToRunUsage(usagePayload, input.pricing);
|
|
688
|
+
const sanitizedGatewayPolicy = sanitizedGatewayPolicyForPlan(mergedGatewayPolicy);
|
|
689
|
+
const gateway = input.id === "litellm" || input.id === "openrouter" || mergedGatewayPolicy !== void 0 ? {
|
|
690
|
+
used: true,
|
|
691
|
+
requestedModel: input.model,
|
|
692
|
+
...observedModel !== void 0 ? { observedModel } : {},
|
|
693
|
+
...sanitizedGatewayPolicy !== void 0 ? { policy: sanitizedGatewayPolicy } : {}
|
|
694
|
+
} : void 0;
|
|
695
|
+
const finish = finishMetadata$2({
|
|
696
|
+
reason: finishReason,
|
|
697
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id)
|
|
698
|
+
});
|
|
699
|
+
yield {
|
|
700
|
+
kind: "complete",
|
|
701
|
+
rawOutputs: sanitizedOutputs,
|
|
702
|
+
...usage !== void 0 ? { usage } : {},
|
|
703
|
+
normalizedUsage,
|
|
704
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
705
|
+
...gateway !== void 0 ? { gateway } : {},
|
|
706
|
+
...finish !== void 0 ? { finish } : {},
|
|
707
|
+
rawResponse: {
|
|
708
|
+
kind: "openai-compatible-stream",
|
|
709
|
+
chunks: rawChunks
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function firstOpenAIChoice(body) {
|
|
714
|
+
if (!isRecord$3(body) || !Array.isArray(body.choices)) return;
|
|
715
|
+
return body.choices.find(isRecord$3);
|
|
716
|
+
}
|
|
717
|
+
function openAIMessageFromChoice(choice) {
|
|
718
|
+
return choice !== void 0 && isRecord$3(choice.message) ? choice.message : void 0;
|
|
719
|
+
}
|
|
720
|
+
function openAIMessageText(message) {
|
|
721
|
+
if (message === void 0) return "";
|
|
722
|
+
if (typeof message.content === "string") return message.content;
|
|
723
|
+
if (Array.isArray(message.content)) return message.content.flatMap((part) => isRecord$3(part) && typeof part.text === "string" ? [part.text] : []).join("");
|
|
724
|
+
return "";
|
|
725
|
+
}
|
|
726
|
+
function openAIToolUseRequestsFromMessage(message) {
|
|
727
|
+
if (message === void 0 || !Array.isArray(message.tool_calls)) return [];
|
|
728
|
+
const calls = /* @__PURE__ */ new Map();
|
|
729
|
+
for (const [index, item] of message.tool_calls.entries()) {
|
|
730
|
+
if (!isRecord$3(item)) continue;
|
|
731
|
+
const current = { arguments: "" };
|
|
732
|
+
if (typeof item.id === "string") current.id = item.id;
|
|
733
|
+
if (isRecord$3(item.function)) {
|
|
734
|
+
if (typeof item.function.name === "string") current.name = item.function.name;
|
|
735
|
+
if (typeof item.function.arguments === "string") current.arguments = item.function.arguments;
|
|
736
|
+
}
|
|
737
|
+
calls.set(index, current);
|
|
738
|
+
}
|
|
739
|
+
return openAIToolUseRequests(calls);
|
|
740
|
+
}
|
|
741
|
+
function openAIStructuredOutputValue(request, message, text) {
|
|
742
|
+
if (request === void 0) return;
|
|
743
|
+
if (message !== void 0 && "parsed" in message) return message.parsed;
|
|
744
|
+
return parseJsonValue$1(text);
|
|
745
|
+
}
|
|
746
|
+
function rawOutputsForRequest$2(input) {
|
|
747
|
+
const rawOutputs = Object.fromEntries(input.outputs.map((name) => [name, input.text]));
|
|
748
|
+
if (input.structuredOutputRequest !== void 0 && input.structuredOutput !== void 0) rawOutputs[input.structuredOutputRequest.output] = input.structuredOutput;
|
|
749
|
+
return rawOutputs;
|
|
750
|
+
}
|
|
751
|
+
function parseJsonValue$1(text) {
|
|
752
|
+
const trimmed = text.trim();
|
|
753
|
+
if (trimmed.length === 0) return;
|
|
754
|
+
try {
|
|
755
|
+
return JSON.parse(trimmed);
|
|
756
|
+
} catch {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function openAIFinishMetadata(choice, toolCalls) {
|
|
761
|
+
return finishMetadata$2({
|
|
762
|
+
reason: stringField(choice, "finish_reason"),
|
|
763
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id)
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function finishMetadata$2(input) {
|
|
767
|
+
const toolCallIds = input.toolCallIds.filter((id) => id.length > 0);
|
|
768
|
+
if (input.reason === void 0 && toolCallIds.length === 0) return;
|
|
769
|
+
return {
|
|
770
|
+
...input.reason !== void 0 ? { reason: input.reason } : {},
|
|
771
|
+
...toolCallIds.length > 0 ? { toolCallIds } : {}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function stringField(record, key) {
|
|
775
|
+
const value = record?.[key];
|
|
776
|
+
return typeof value === "string" ? value : void 0;
|
|
777
|
+
}
|
|
778
|
+
function parseJsonObject$2(data) {
|
|
779
|
+
try {
|
|
780
|
+
return JSON.parse(data);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
783
|
+
throw new Error(`OpenAI-compatible stream returned invalid JSON: ${message}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function streamChoices(chunk) {
|
|
787
|
+
if (!isRecord$3(chunk) || !Array.isArray(chunk.choices)) return [];
|
|
788
|
+
return chunk.choices.filter(isRecord$3);
|
|
789
|
+
}
|
|
790
|
+
function accumulateOpenAIToolCalls(calls, deltas) {
|
|
791
|
+
if (!Array.isArray(deltas)) return;
|
|
792
|
+
for (const delta of deltas) {
|
|
793
|
+
if (!isRecord$3(delta) || typeof delta.index !== "number") continue;
|
|
794
|
+
const current = calls.get(delta.index) ?? { arguments: "" };
|
|
795
|
+
if (typeof delta.id === "string") current.id = delta.id;
|
|
796
|
+
if (isRecord$3(delta.function)) {
|
|
797
|
+
if (typeof delta.function.name === "string") current.name = `${current.name ?? ""}${delta.function.name}`;
|
|
798
|
+
if (typeof delta.function.arguments === "string") current.arguments += delta.function.arguments;
|
|
799
|
+
}
|
|
800
|
+
calls.set(delta.index, current);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function openAIToolUseRequests(calls) {
|
|
804
|
+
return [...calls.entries()].sort(([left], [right]) => left - right).flatMap(([index, call]) => {
|
|
805
|
+
if (call.name === void 0) return [];
|
|
806
|
+
return [{
|
|
807
|
+
id: call.id ?? `tool-call-${index}`,
|
|
808
|
+
name: call.name,
|
|
809
|
+
args: parseToolArguments(call.arguments)
|
|
810
|
+
}];
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function parseToolArguments(value) {
|
|
814
|
+
const trimmed = value.trim();
|
|
815
|
+
if (trimmed.length === 0) return {};
|
|
816
|
+
try {
|
|
817
|
+
return JSON.parse(trimmed);
|
|
818
|
+
} catch (error) {
|
|
819
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
820
|
+
throw new Error(`OpenAI-compatible stream returned invalid tool arguments: ${message}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Phase 7 normalization: maps raw provider usage payloads (OpenAI's
|
|
825
|
+
* `prompt_tokens`/`completion_tokens`, the Responses API's
|
|
826
|
+
* `input_tokens`/`output_tokens`, or camelCase variants) to the shared
|
|
827
|
+
* `Usage` shape. When `pricing` is supplied, `costUsd` is computed from
|
|
828
|
+
* the normalized token counts. Otherwise `costUsd` is `null` so consumers
|
|
829
|
+
* can distinguish "unmeasured" from "zero".
|
|
830
|
+
*/
|
|
831
|
+
function normalizeUsageToRunUsage(rawUsage, pricing) {
|
|
832
|
+
let promptTokens = 0;
|
|
833
|
+
let completionTokens = 0;
|
|
834
|
+
if (typeof rawUsage === "object" && rawUsage !== null) {
|
|
835
|
+
const record = rawUsage;
|
|
836
|
+
promptTokens = numberField$2(record, "prompt_tokens") ?? numberField$2(record, "input_tokens") ?? numberField$2(record, "inputTokens") ?? 0;
|
|
837
|
+
completionTokens = numberField$2(record, "completion_tokens") ?? numberField$2(record, "output_tokens") ?? numberField$2(record, "outputTokens") ?? 0;
|
|
838
|
+
}
|
|
839
|
+
let costUsd = null;
|
|
840
|
+
if (pricing !== void 0 && (pricing.inputPer1kTokens !== void 0 || pricing.outputPer1kTokens !== void 0)) costUsd = (pricing.inputPer1kTokens ?? 0) * promptTokens / 1e3 + (pricing.outputPer1kTokens ?? 0) * completionTokens / 1e3;
|
|
841
|
+
return {
|
|
842
|
+
promptTokens,
|
|
843
|
+
completionTokens,
|
|
844
|
+
costUsd
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function normalizeUsage(usage) {
|
|
848
|
+
if (typeof usage !== "object" || usage === null) return;
|
|
849
|
+
const record = usage;
|
|
850
|
+
const inputTokens = numberField$2(record, "prompt_tokens") ?? numberField$2(record, "input_tokens");
|
|
851
|
+
const outputTokens = numberField$2(record, "completion_tokens") ?? numberField$2(record, "output_tokens");
|
|
852
|
+
const totalTokens = numberField$2(record, "total_tokens");
|
|
853
|
+
return {
|
|
854
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
855
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
856
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function numberField$2(record, key) {
|
|
860
|
+
const value = record[key];
|
|
861
|
+
return typeof value === "number" ? value : void 0;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Phase 34 — D-12 — Emits a "capabilities.negotiation.fallback" RunEvent if
|
|
865
|
+
* a sink is provided. The runId uses a synthetic value since negotiation
|
|
866
|
+
* happens outside a run context.
|
|
867
|
+
*
|
|
868
|
+
* T-34-03-01: errorReason is produced by stringifyErr (message only, NOT
|
|
869
|
+
* stack) to prevent apiKey leaking in error strings that include request headers.
|
|
870
|
+
*/
|
|
871
|
+
function emitFallbackEvent$1(sink, payload) {
|
|
872
|
+
if (sink === void 0) return;
|
|
873
|
+
sink(createRunEvent("capabilities.negotiation.fallback", {
|
|
874
|
+
runId: `negotiate-${payload.adapter}-${payload.modelId}`,
|
|
875
|
+
providerId: payload.adapter,
|
|
876
|
+
modelId: payload.modelId,
|
|
877
|
+
metadata: {
|
|
878
|
+
adapter: payload.adapter,
|
|
879
|
+
modelId: payload.modelId,
|
|
880
|
+
errorReason: payload.errorReason,
|
|
881
|
+
fallbackSource: payload.fallbackSource
|
|
882
|
+
}
|
|
883
|
+
}));
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Stringify an error for event metadata. Returns only the message (NOT the
|
|
887
|
+
* stack) to prevent apiKey or sensitive header values from leaking into
|
|
888
|
+
* event payloads via fetch errors that may embed the request init.
|
|
889
|
+
* T-34-03-01 mitigation.
|
|
890
|
+
*/
|
|
891
|
+
function stringifyErr$3(err) {
|
|
892
|
+
return err instanceof Error ? err.message : String(err);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Phase 34 — QUIRK-02 / NEG-01 / NEG-02 — Merge an OpenAI /v1/models
|
|
896
|
+
* sparse response with the Phase 33 registry.
|
|
897
|
+
*
|
|
898
|
+
* OpenAI's /models response is famously SPARSE per RESEARCH §Q2:
|
|
899
|
+
* `{ id, object, created, owned_by }` only. No capabilities block.
|
|
900
|
+
* The /models call confirms the model EXISTS in the user's org, but
|
|
901
|
+
* tells us nothing about its capabilities. We source supports.* from
|
|
902
|
+
* the Phase 33 registry instead.
|
|
903
|
+
*
|
|
904
|
+
* Source semantics per D-09:
|
|
905
|
+
* - "live" when the model id is found in the /models response
|
|
906
|
+
* (the id was verified to exist — useful signal for org membership)
|
|
907
|
+
* - "registry-fallback" when the model is NOT in the /models response
|
|
908
|
+
* (model not in org, or stale; emit fallback event)
|
|
909
|
+
*
|
|
910
|
+
* Anti-pattern warning (RESEARCH §Anti-patterns): DO NOT assume OpenAI
|
|
911
|
+
* /v1/models returns capability flags. It doesn't. Only id/object/created/
|
|
912
|
+
* owned_by are returned. Source supports.* ONLY from the registry.
|
|
913
|
+
*/
|
|
914
|
+
function mergeOpenAIModelsWithRegistry(modelId, body, emitFallback) {
|
|
915
|
+
const data = body?.data;
|
|
916
|
+
if ((Array.isArray(data) ? data.find((m) => typeof m === "object" && m !== null && m.id === modelId) : void 0) === void 0) {
|
|
917
|
+
emitFallback();
|
|
918
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("openai", modelId, "registry-fallback");
|
|
919
|
+
}
|
|
920
|
+
const registryProfile = getCapabilityProfile(`openai:${modelId}`);
|
|
921
|
+
if (registryProfile !== void 0) return mapProfileToNegotiatedCapabilities(registryProfile, "live");
|
|
922
|
+
return {
|
|
923
|
+
modelId,
|
|
924
|
+
contextWindow: 0,
|
|
925
|
+
supports: {
|
|
926
|
+
nativeToolCalling: false,
|
|
927
|
+
structuredOutputs: false,
|
|
928
|
+
parallelToolCalls: false,
|
|
929
|
+
extendedThinking: false,
|
|
930
|
+
streaming: true
|
|
931
|
+
},
|
|
932
|
+
knownFailureModes: [],
|
|
933
|
+
recommendedSanitizers: [],
|
|
934
|
+
source: "live"
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Phase 34 — QUIRK-02 / NEG-01 / NEG-02 — OpenAI provider factory.
|
|
939
|
+
*
|
|
940
|
+
* Extends the base OpenAI-compat factory with:
|
|
941
|
+
* 1. `quirks: OpenAIQuirks` — verified per RESEARCH §Q6 OpenAI vocabulary.
|
|
942
|
+
* 2. `negotiateCapabilities(modelId)` — queries OpenAI /v1/models GET with
|
|
943
|
+
* Authorization: Bearer header; SPARSE response; intersects with Phase 33
|
|
944
|
+
* registry for supports.* (per RESEARCH §Anti-patterns — don't assume
|
|
945
|
+
* OpenAI /v1/models returns capability flags, it doesn't).
|
|
946
|
+
*
|
|
947
|
+
* The negotiate() pattern mirrors Plan 34-02 (Anthropic thick reference):
|
|
948
|
+
* - Per-instance TTL cache (modelsCacheTtlMs, default 300_000ms)
|
|
949
|
+
* - Single-flight inflight coalescing with .finally cleanup (Pitfall 4)
|
|
950
|
+
* - Retry with [0, 200, 1000]ms backoff (modelsRetryCount, default 2)
|
|
951
|
+
* - 401/403 throws NegotiationAuthError (D-10: no retry, no fallback, no event)
|
|
952
|
+
* - 5xx/network/timeout falls back to registry with source: "registry-fallback"
|
|
953
|
+
* - emitFallbackEvent fires the "capabilities.negotiation.fallback" RunEvent
|
|
954
|
+
*
|
|
955
|
+
* SECURITY (T-34-03-07): inflight Map MUST use .finally cleanup to prevent
|
|
956
|
+
* leak on rejection. Verifiable: grep `.finally` in this file.
|
|
957
|
+
*/
|
|
958
|
+
function createOpenAIProvider(options) {
|
|
959
|
+
const id = options.id ?? "openai";
|
|
960
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
961
|
+
const baseUrl = (options.baseUrl ?? "https://api.openai.com").replace(/\/$/u, "");
|
|
962
|
+
const ttlMs = options.modelsCacheTtlMs ?? 3e5;
|
|
963
|
+
const retryCount = options.modelsRetryCount ?? 2;
|
|
964
|
+
const cache = /* @__PURE__ */ new Map();
|
|
965
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
966
|
+
async function fetchAndNegotiate(modelId) {
|
|
967
|
+
const url = `${baseUrl}/v1/models`;
|
|
968
|
+
const headers = {
|
|
969
|
+
"accept": "application/json",
|
|
970
|
+
...options.apiKey !== void 0 ? { authorization: `Bearer ${options.apiKey}` } : {}
|
|
971
|
+
};
|
|
972
|
+
const attempts = retryCount + 1;
|
|
973
|
+
const backoffMs = [
|
|
974
|
+
0,
|
|
975
|
+
200,
|
|
976
|
+
1e3
|
|
977
|
+
];
|
|
978
|
+
let lastErr;
|
|
979
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
980
|
+
if (i > 0) {
|
|
981
|
+
const delay = backoffMs[Math.min(i, backoffMs.length - 1)] ?? 1e3;
|
|
982
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
const resp = await fetchImpl(url, {
|
|
986
|
+
method: "GET",
|
|
987
|
+
headers,
|
|
988
|
+
signal: AbortSignal.timeout(3e4)
|
|
989
|
+
});
|
|
990
|
+
if (resp.status === 401 || resp.status === 403) throw new NegotiationAuthError("openai", modelId, resp.status, `OpenAI /v1/models returned ${resp.status}: check apiKey config.`);
|
|
991
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
992
|
+
return mergeOpenAIModelsWithRegistry(modelId, await resp.json(), () => {
|
|
993
|
+
emitFallbackEvent$1(options.runEventSink, {
|
|
994
|
+
adapter: "openai",
|
|
995
|
+
modelId,
|
|
996
|
+
errorReason: "model not found in /v1/models response",
|
|
997
|
+
fallbackSource: "registry-fallback"
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
if (err instanceof NegotiationAuthError) throw err;
|
|
1002
|
+
lastErr = err;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
emitFallbackEvent$1(options.runEventSink, {
|
|
1006
|
+
adapter: "openai",
|
|
1007
|
+
modelId,
|
|
1008
|
+
errorReason: stringifyErr$3(lastErr),
|
|
1009
|
+
fallbackSource: "registry-fallback"
|
|
1010
|
+
});
|
|
1011
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("openai", modelId, "registry-fallback");
|
|
1012
|
+
}
|
|
1013
|
+
async function negotiate(modelId) {
|
|
1014
|
+
const cached = cache.get(modelId);
|
|
1015
|
+
if (cached !== void 0 && cached.expiresAt > Date.now()) return cached.result;
|
|
1016
|
+
const existing = inflight.get(modelId);
|
|
1017
|
+
if (existing !== void 0) return existing;
|
|
1018
|
+
const fetchPromise = (async () => {
|
|
1019
|
+
try {
|
|
1020
|
+
const result = await fetchAndNegotiate(modelId);
|
|
1021
|
+
if (ttlMs > 0) cache.set(modelId, {
|
|
1022
|
+
result,
|
|
1023
|
+
expiresAt: Date.now() + ttlMs
|
|
1024
|
+
});
|
|
1025
|
+
return result;
|
|
1026
|
+
} finally {
|
|
1027
|
+
inflight.delete(modelId);
|
|
1028
|
+
}
|
|
1029
|
+
})();
|
|
1030
|
+
inflight.set(modelId, fetchPromise);
|
|
1031
|
+
return fetchPromise;
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
...createOpenAICompatibleProvider({
|
|
1035
|
+
...options,
|
|
1036
|
+
id,
|
|
1037
|
+
baseUrl
|
|
1038
|
+
}),
|
|
1039
|
+
quirks: {
|
|
1040
|
+
supportsToolChoice: true,
|
|
1041
|
+
parallelToolCalls: true,
|
|
1042
|
+
structuredOutputs: true,
|
|
1043
|
+
responseFormatHonored: true,
|
|
1044
|
+
streamingDiverges: false,
|
|
1045
|
+
strictModeSupported: true,
|
|
1046
|
+
structuredOutputsTier2: true
|
|
1047
|
+
},
|
|
1048
|
+
negotiateCapabilities: negotiate
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function createAISdkProvider(options) {
|
|
1052
|
+
const id = options.id ?? "ai-sdk";
|
|
1053
|
+
return {
|
|
1054
|
+
id,
|
|
1055
|
+
kind: "provider-adapter",
|
|
1056
|
+
capabilities: [{
|
|
1057
|
+
...defaultCapabilityForProvider(id),
|
|
1058
|
+
modelId: options.model,
|
|
1059
|
+
toolUse: true,
|
|
1060
|
+
streaming: true
|
|
1061
|
+
}],
|
|
1062
|
+
execute: async (request) => {
|
|
1063
|
+
const response = await options.generate({
|
|
1064
|
+
task: request.task,
|
|
1065
|
+
outputNames: request.outputs
|
|
1066
|
+
});
|
|
1067
|
+
const normalizedUsage = {
|
|
1068
|
+
promptTokens: response.usage?.inputTokens ?? 0,
|
|
1069
|
+
completionTokens: response.usage?.outputTokens ?? 0,
|
|
1070
|
+
costUsd: null
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
...response,
|
|
1074
|
+
normalizedUsage
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
//#endregion
|
|
1080
|
+
//#region src/providers/anthropic.ts
|
|
1081
|
+
const DEFAULT_BASE_URL$1 = "https://api.anthropic.com";
|
|
1082
|
+
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
1083
|
+
const DEFAULT_MAX_TOKENS = 2e3;
|
|
1084
|
+
const DEFAULT_MODELS_CACHE_TTL_MS = 3e5;
|
|
1085
|
+
const DEFAULT_MODELS_RETRY_COUNT = 2;
|
|
1086
|
+
/** D-11: Backoff schedule for transient /v1/models failures -- immediate, 200ms, 1s. */
|
|
1087
|
+
const MODELS_BACKOFF_MS = [
|
|
1088
|
+
0,
|
|
1089
|
+
200,
|
|
1090
|
+
1e3
|
|
1091
|
+
];
|
|
1092
|
+
async function createAnthropicMessagesBody(input) {
|
|
1093
|
+
const system = input.request.cacheSystemPrefix !== void 0 ? [{
|
|
1094
|
+
type: "text",
|
|
1095
|
+
text: input.request.cacheSystemPrefix,
|
|
1096
|
+
cache_control: { type: "ephemeral" }
|
|
1097
|
+
}] : "";
|
|
1098
|
+
const content = await createAnthropicUserContent(input.request);
|
|
1099
|
+
const nativeTools = anthropicToolDefinitions(input.request.nativeTools);
|
|
1100
|
+
const structuredTool = anthropicStructuredOutputTool(input.request.nativeStructuredOutput);
|
|
1101
|
+
const tools = [...nativeTools, ...structuredTool !== void 0 ? [structuredTool] : []];
|
|
1102
|
+
const toolChoice = structuredTool !== void 0 ? {
|
|
1103
|
+
type: "tool",
|
|
1104
|
+
name: structuredTool.name
|
|
1105
|
+
} : anthropicToolChoice(input.request.nativeToolChoice);
|
|
1106
|
+
return {
|
|
1107
|
+
body: {
|
|
1108
|
+
model: input.model,
|
|
1109
|
+
system,
|
|
1110
|
+
messages: [{
|
|
1111
|
+
role: "user",
|
|
1112
|
+
content: content.blocks.length === 0 ? input.request.task : [...content.blocks, {
|
|
1113
|
+
type: "text",
|
|
1114
|
+
text: input.request.task
|
|
1115
|
+
}]
|
|
1116
|
+
}],
|
|
1117
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
1118
|
+
...input.stream === true ? { stream: true } : {},
|
|
1119
|
+
...tools.length > 0 ? { tools } : {},
|
|
1120
|
+
...toolChoice !== void 0 ? { tool_choice: toolChoice } : {}
|
|
1121
|
+
},
|
|
1122
|
+
usesFilesApi: content.usesFilesApi
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function anthropicToolDefinitions(tools) {
|
|
1126
|
+
return (tools ?? []).map((tool) => ({
|
|
1127
|
+
name: tool.name,
|
|
1128
|
+
...tool.description !== void 0 ? { description: tool.description } : {},
|
|
1129
|
+
input_schema: standardSchemaToJsonSchema(tool.inputSchema)
|
|
1130
|
+
}));
|
|
1131
|
+
}
|
|
1132
|
+
function anthropicToolChoice(choice) {
|
|
1133
|
+
if (choice === void 0) return;
|
|
1134
|
+
if (choice === "required") return { type: "any" };
|
|
1135
|
+
if (choice === "auto" || choice === "none") return { type: choice };
|
|
1136
|
+
return {
|
|
1137
|
+
type: "tool",
|
|
1138
|
+
name: choice.name
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function anthropicStructuredOutputTool(request) {
|
|
1142
|
+
if (request === void 0) return;
|
|
1143
|
+
return {
|
|
1144
|
+
name: anthropicStructuredOutputToolName(request),
|
|
1145
|
+
description: `Return structured output for ${request.output}.`,
|
|
1146
|
+
input_schema: standardSchemaToJsonSchema(request.schema)
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
async function createAnthropicUserContent(request) {
|
|
1150
|
+
const blocks = [];
|
|
1151
|
+
let usesFilesApi = false;
|
|
1152
|
+
for (const inputArtifact of request.artifacts) {
|
|
1153
|
+
if (inputArtifact.kind !== "image") continue;
|
|
1154
|
+
const packaged = packagedPlanForArtifact(request, inputArtifact.id);
|
|
1155
|
+
if (packaged === void 0) continue;
|
|
1156
|
+
if (packaged.transport === "file-id") {
|
|
1157
|
+
const fileId = anthropicFileId(inputArtifact);
|
|
1158
|
+
if (fileId === void 0) continue;
|
|
1159
|
+
blocks.push({
|
|
1160
|
+
type: "image",
|
|
1161
|
+
source: {
|
|
1162
|
+
type: "file",
|
|
1163
|
+
file_id: fileId
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
usesFilesApi = true;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
if (packaged.transport === "url") {
|
|
1170
|
+
const url = artifactHttpUrl(inputArtifact);
|
|
1171
|
+
if (url === void 0) continue;
|
|
1172
|
+
blocks.push({
|
|
1173
|
+
type: "image",
|
|
1174
|
+
source: {
|
|
1175
|
+
type: "url",
|
|
1176
|
+
url
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
if (packaged.transport === "base64" || packaged.transport === "inline") {
|
|
1182
|
+
const data = await artifactBase64Data(inputArtifact);
|
|
1183
|
+
if (data === void 0) continue;
|
|
1184
|
+
blocks.push({
|
|
1185
|
+
type: "image",
|
|
1186
|
+
source: {
|
|
1187
|
+
type: "base64",
|
|
1188
|
+
media_type: mediaTypeForArtifact(inputArtifact, "image/jpeg"),
|
|
1189
|
+
data
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
blocks,
|
|
1196
|
+
usesFilesApi
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function createAnthropicProvider(options) {
|
|
1200
|
+
const id = options.id ?? "anthropic";
|
|
1201
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1202
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL$1).replace(/\/$/u, "");
|
|
1203
|
+
const anthropicVersion = options.anthropicVersion ?? DEFAULT_ANTHROPIC_VERSION;
|
|
1204
|
+
const ttlMs = options.modelsCacheTtlMs ?? DEFAULT_MODELS_CACHE_TTL_MS;
|
|
1205
|
+
const retryCount = options.modelsRetryCount ?? DEFAULT_MODELS_RETRY_COUNT;
|
|
1206
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1207
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1208
|
+
/**
|
|
1209
|
+
* D-12: Emits the `capabilities.negotiation.fallback` RunEvent via the
|
|
1210
|
+
* consumer-supplied sink. If no sink is provided, this is a no-op.
|
|
1211
|
+
*
|
|
1212
|
+
* SECURITY (T-34-02-01): errorReason is derived from `err.message` ONLY --
|
|
1213
|
+
* not `err.stack`, `err.toString()`, or any serialization that could include
|
|
1214
|
+
* request headers (which carry the apiKey). `stringifyErr` enforces this.
|
|
1215
|
+
*
|
|
1216
|
+
* JSDoc synthetic runId: negotiate() runs outside of a Lattice run context
|
|
1217
|
+
* (no ai.run() in scope). The runId `"negotiate-${id}-${modelId}"` is a
|
|
1218
|
+
* synthetic value that scopes the event to this adapter instance + modelId.
|
|
1219
|
+
* Consumers filtering on runId should treat "negotiate-" prefix as a signal
|
|
1220
|
+
* that this event originated from capability negotiation, not a user-facing run.
|
|
1221
|
+
*/
|
|
1222
|
+
function emitFallbackEvent(payload) {
|
|
1223
|
+
if (options.runEventSink === void 0) return;
|
|
1224
|
+
const event = createRunEvent("capabilities.negotiation.fallback", {
|
|
1225
|
+
runId: `negotiate-${id}-${payload.modelId}`,
|
|
1226
|
+
providerId: id,
|
|
1227
|
+
modelId: payload.modelId,
|
|
1228
|
+
metadata: {
|
|
1229
|
+
adapter: payload.adapter,
|
|
1230
|
+
modelId: payload.modelId,
|
|
1231
|
+
errorReason: payload.errorReason,
|
|
1232
|
+
fallbackSource: payload.fallbackSource
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
options.runEventSink(event);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Pure error message extractor. Returns `err.message` for Error instances,
|
|
1239
|
+
* `String(err)` for everything else. Deliberately does NOT include stack,
|
|
1240
|
+
* headers, or other fields (T-34-02-01 mitigation).
|
|
1241
|
+
*/
|
|
1242
|
+
function stringifyErr(err) {
|
|
1243
|
+
return err instanceof Error ? err.message : String(err);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Merges a live /v1/models response body with the Phase 33 static registry
|
|
1247
|
+
* profile for the given modelId. Called on HTTP 200 responses only.
|
|
1248
|
+
*
|
|
1249
|
+
* LENIENT PARSING (Pitfall 1): every field access uses optional chaining.
|
|
1250
|
+
* Missing `capabilities.thinking` or other sub-fields default to false rather
|
|
1251
|
+
* than throwing. This ensures forward-compatibility with future API shape changes.
|
|
1252
|
+
*
|
|
1253
|
+
* contextWindow policy: Anthropic's max_input_tokens is set to 0 in the fixture
|
|
1254
|
+
* for models where it is unreliable. When 0, falls through to the registry profile's
|
|
1255
|
+
* contextWindow (if present) or 0 as a final default (RESEARCH §Q1).
|
|
1256
|
+
*/
|
|
1257
|
+
function mergeAnthropicModelsWithRegistry(modelId, body) {
|
|
1258
|
+
const found = body?.data?.find?.((m) => {
|
|
1259
|
+
if (typeof m !== "object" || m === null) return false;
|
|
1260
|
+
return m["id"] === modelId;
|
|
1261
|
+
});
|
|
1262
|
+
if (found === void 0) {
|
|
1263
|
+
emitFallbackEvent({
|
|
1264
|
+
adapter: "anthropic",
|
|
1265
|
+
modelId,
|
|
1266
|
+
errorReason: "model not found in /v1/models response",
|
|
1267
|
+
fallbackSource: "registry-fallback"
|
|
1268
|
+
});
|
|
1269
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("anthropic", modelId, "registry-fallback");
|
|
1270
|
+
}
|
|
1271
|
+
const caps = found["capabilities"] ?? {};
|
|
1272
|
+
const structuredOutputsSupported = caps["structured_outputs"]?.["supported"] === true;
|
|
1273
|
+
const thinkingSupported = caps["thinking"]?.["supported"] === true;
|
|
1274
|
+
const maxInputTokensRaw = found["max_input_tokens"];
|
|
1275
|
+
const maxInputTokens = typeof maxInputTokensRaw === "number" && maxInputTokensRaw > 0 ? maxInputTokensRaw : void 0;
|
|
1276
|
+
const registryProfile = getCapabilityProfile(`anthropic:${modelId}`);
|
|
1277
|
+
const contextWindow = maxInputTokens ?? registryProfile?.contextWindow ?? 0;
|
|
1278
|
+
const knownFailureModes = registryProfile?.knownFailureModes ?? [];
|
|
1279
|
+
return {
|
|
1280
|
+
modelId,
|
|
1281
|
+
contextWindow,
|
|
1282
|
+
supports: {
|
|
1283
|
+
nativeToolCalling: true,
|
|
1284
|
+
structuredOutputs: structuredOutputsSupported,
|
|
1285
|
+
parallelToolCalls: true,
|
|
1286
|
+
extendedThinking: thinkingSupported,
|
|
1287
|
+
streaming: true
|
|
1288
|
+
},
|
|
1289
|
+
knownFailureModes,
|
|
1290
|
+
recommendedSanitizers: getRecommendedSanitizers(knownFailureModes),
|
|
1291
|
+
source: "live"
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* D-09 / D-10 / D-11: Core /v1/models fetch with retry-backoff, auth-error-throw,
|
|
1296
|
+
* and transient-fallback. Called only once per modelId (inflight coalescing prevents
|
|
1297
|
+
* concurrent duplicate fetches).
|
|
1298
|
+
*
|
|
1299
|
+
* URL shape: `${baseUrl}/v1/models?limit=1000` to page all models in one request.
|
|
1300
|
+
* Headers per RESEARCH §Q1: x-api-key, anthropic-version, accept.
|
|
1301
|
+
*/
|
|
1302
|
+
async function fetchAndNegotiate(modelId) {
|
|
1303
|
+
const url = `${baseUrl}/v1/models?limit=1000`;
|
|
1304
|
+
const headers = {
|
|
1305
|
+
"x-api-key": options.apiKey,
|
|
1306
|
+
"anthropic-version": anthropicVersion,
|
|
1307
|
+
"accept": "application/json"
|
|
1308
|
+
};
|
|
1309
|
+
const attempts = retryCount + 1;
|
|
1310
|
+
let lastErr;
|
|
1311
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
1312
|
+
const delayMs = MODELS_BACKOFF_MS[i] ?? MODELS_BACKOFF_MS[MODELS_BACKOFF_MS.length - 1];
|
|
1313
|
+
if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1314
|
+
try {
|
|
1315
|
+
const resp = await fetchImpl(url, {
|
|
1316
|
+
method: "GET",
|
|
1317
|
+
headers,
|
|
1318
|
+
signal: AbortSignal.timeout(3e4)
|
|
1319
|
+
});
|
|
1320
|
+
if (resp.status === 401 || resp.status === 403) throw new NegotiationAuthError("anthropic", modelId, resp.status, `Anthropic /v1/models returned ${resp.status}: check apiKey config.`);
|
|
1321
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
1322
|
+
return mergeAnthropicModelsWithRegistry(modelId, await resp.json());
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
if (err instanceof NegotiationAuthError) throw err;
|
|
1325
|
+
lastErr = err;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
emitFallbackEvent({
|
|
1329
|
+
adapter: "anthropic",
|
|
1330
|
+
modelId,
|
|
1331
|
+
errorReason: stringifyErr(lastErr),
|
|
1332
|
+
fallbackSource: "registry-fallback"
|
|
1333
|
+
});
|
|
1334
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("anthropic", modelId, "registry-fallback");
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* D-07: Lazy expiry cache check + D-Q7: inflight coalescing.
|
|
1338
|
+
*
|
|
1339
|
+
* Cache check: stale entries are evicted lazily on read (no background setInterval
|
|
1340
|
+
* -- library must not pin the Node event loop).
|
|
1341
|
+
*
|
|
1342
|
+
* Inflight coalescing: concurrent calls for the same modelId share one fetch
|
|
1343
|
+
* Promise. Pitfall 4 mitigation: `.finally` block ALWAYS clears the inflight
|
|
1344
|
+
* Map entry, even on rejection. This ensures that a rejected Promise doesn't
|
|
1345
|
+
* "poison" the Map -- the next caller after all concurrent calls settle will
|
|
1346
|
+
* trigger a fresh fetch attempt.
|
|
1347
|
+
*/
|
|
1348
|
+
async function negotiateCapabilities(modelId) {
|
|
1349
|
+
const cached = cache.get(modelId);
|
|
1350
|
+
if (cached !== void 0 && cached.expiresAt > Date.now()) return cached.result;
|
|
1351
|
+
const existing = inflight.get(modelId);
|
|
1352
|
+
if (existing !== void 0) return existing;
|
|
1353
|
+
const fetchPromise = (async () => {
|
|
1354
|
+
try {
|
|
1355
|
+
const result = await fetchAndNegotiate(modelId);
|
|
1356
|
+
if (ttlMs > 0) cache.set(modelId, {
|
|
1357
|
+
result,
|
|
1358
|
+
expiresAt: ttlMs === Infinity ? Infinity : Date.now() + ttlMs
|
|
1359
|
+
});
|
|
1360
|
+
return result;
|
|
1361
|
+
} finally {
|
|
1362
|
+
inflight.delete(modelId);
|
|
1363
|
+
}
|
|
1364
|
+
})();
|
|
1365
|
+
inflight.set(modelId, fetchPromise);
|
|
1366
|
+
return fetchPromise;
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
id,
|
|
1370
|
+
kind: "provider-adapter",
|
|
1371
|
+
capabilities: [{
|
|
1372
|
+
...defaultCapabilityForProvider(id),
|
|
1373
|
+
modelId: options.model,
|
|
1374
|
+
fileTransport: [
|
|
1375
|
+
"inline",
|
|
1376
|
+
"json",
|
|
1377
|
+
"url",
|
|
1378
|
+
"base64",
|
|
1379
|
+
"file-id",
|
|
1380
|
+
"extracted-text",
|
|
1381
|
+
"transcript"
|
|
1382
|
+
],
|
|
1383
|
+
streaming: true
|
|
1384
|
+
}],
|
|
1385
|
+
quirks: {
|
|
1386
|
+
supportsToolChoice: true,
|
|
1387
|
+
parallelToolCalls: true,
|
|
1388
|
+
structuredOutputs: true,
|
|
1389
|
+
responseFormatHonored: true,
|
|
1390
|
+
streamingDiverges: false,
|
|
1391
|
+
promptCachingSupported: true,
|
|
1392
|
+
extendedThinkingSupported: true,
|
|
1393
|
+
toolUseInputSchemaStrict: true
|
|
1394
|
+
},
|
|
1395
|
+
negotiateCapabilities,
|
|
1396
|
+
async execute(request) {
|
|
1397
|
+
const messagesBody = await createAnthropicMessagesBody({
|
|
1398
|
+
model: options.model,
|
|
1399
|
+
request
|
|
1400
|
+
});
|
|
1401
|
+
const bodyStr = JSON.stringify(messagesBody.body);
|
|
1402
|
+
assertNoPublicUrlEgress(request, id, bodyStr);
|
|
1403
|
+
const init = {
|
|
1404
|
+
method: "POST",
|
|
1405
|
+
headers: {
|
|
1406
|
+
"content-type": "application/json",
|
|
1407
|
+
"x-api-key": options.apiKey,
|
|
1408
|
+
"anthropic-version": anthropicVersion,
|
|
1409
|
+
...messagesBody.usesFilesApi ? { "anthropic-beta": "files-api-2025-04-14" } : {}
|
|
1410
|
+
},
|
|
1411
|
+
body: bodyStr,
|
|
1412
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
1413
|
+
};
|
|
1414
|
+
const response = await fetchImpl(`${baseUrl}/v1/messages`, init);
|
|
1415
|
+
if (!response.ok) throw new Error(`Anthropic provider failed with ${response.status}.`);
|
|
1416
|
+
const body = await response.json();
|
|
1417
|
+
const text = anthropicTextFromContent(body.content);
|
|
1418
|
+
const structuredOutput = anthropicStructuredOutputFromContent(body.content, request.nativeStructuredOutput);
|
|
1419
|
+
const sanitizedOutputs = await applyOutputSanitizers(rawOutputsForRequest$1({
|
|
1420
|
+
outputs: request.outputs,
|
|
1421
|
+
text,
|
|
1422
|
+
structuredOutputRequest: request.nativeStructuredOutput,
|
|
1423
|
+
structuredOutput
|
|
1424
|
+
}), options.sanitizeOutput, {
|
|
1425
|
+
providerId: id,
|
|
1426
|
+
modelId: options.model
|
|
1427
|
+
});
|
|
1428
|
+
const parsedToolCalls = parseToolUseEnvelope(text);
|
|
1429
|
+
const promptToolCalls = parsedToolCalls === null ? void 0 : await validateToolCallRequests(parsedToolCalls, options.validateToolCalls);
|
|
1430
|
+
const nativeToolRequests = anthropicToolUseRequestsFromContent(body.content, request.nativeStructuredOutput);
|
|
1431
|
+
const nativeToolCalls = nativeToolRequests.length === 0 ? void 0 : await validateToolCallRequests(nativeToolRequests, options.validateToolCalls);
|
|
1432
|
+
const hasToolCallResult = promptToolCalls !== void 0 || nativeToolCalls !== void 0;
|
|
1433
|
+
const toolCalls = [...promptToolCalls ?? [], ...nativeToolCalls ?? []];
|
|
1434
|
+
const usage = normalizeAnthropicUsage(body.usage);
|
|
1435
|
+
const normalizedUsage = normalizeAnthropicUsageToRunUsage(body.usage, options.pricing);
|
|
1436
|
+
const finish = finishMetadata$1({
|
|
1437
|
+
reason: typeof body.stop_reason === "string" ? body.stop_reason : void 0,
|
|
1438
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id)
|
|
1439
|
+
});
|
|
1440
|
+
return {
|
|
1441
|
+
rawOutputs: sanitizedOutputs,
|
|
1442
|
+
...usage !== void 0 ? { usage } : {},
|
|
1443
|
+
normalizedUsage,
|
|
1444
|
+
...hasToolCallResult ? { toolCalls } : {},
|
|
1445
|
+
...finish !== void 0 ? { finish } : {},
|
|
1446
|
+
rawResponse: body
|
|
1447
|
+
};
|
|
1448
|
+
},
|
|
1449
|
+
executeStream(request) {
|
|
1450
|
+
return streamAnthropicResponse({
|
|
1451
|
+
id,
|
|
1452
|
+
model: options.model,
|
|
1453
|
+
baseUrl,
|
|
1454
|
+
apiKey: options.apiKey,
|
|
1455
|
+
anthropicVersion,
|
|
1456
|
+
fetchImpl,
|
|
1457
|
+
request,
|
|
1458
|
+
...options.pricing !== void 0 ? { pricing: options.pricing } : {},
|
|
1459
|
+
...options.sanitizeOutput !== void 0 ? { sanitizeOutput: options.sanitizeOutput } : {},
|
|
1460
|
+
...options.validateToolCalls !== void 0 ? { validateToolCalls: options.validateToolCalls } : {}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
async function* streamAnthropicResponse(input) {
|
|
1466
|
+
const messagesBody = await createAnthropicMessagesBody({
|
|
1467
|
+
model: input.model,
|
|
1468
|
+
request: input.request,
|
|
1469
|
+
stream: true
|
|
1470
|
+
});
|
|
1471
|
+
const streamBodyStr = JSON.stringify(messagesBody.body);
|
|
1472
|
+
assertNoPublicUrlEgress(input.request, input.id, streamBodyStr);
|
|
1473
|
+
const response = await input.fetchImpl(`${input.baseUrl}/v1/messages`, {
|
|
1474
|
+
method: "POST",
|
|
1475
|
+
headers: {
|
|
1476
|
+
"content-type": "application/json",
|
|
1477
|
+
"x-api-key": input.apiKey,
|
|
1478
|
+
"anthropic-version": input.anthropicVersion,
|
|
1479
|
+
...messagesBody.usesFilesApi ? { "anthropic-beta": "files-api-2025-04-14" } : {}
|
|
1480
|
+
},
|
|
1481
|
+
body: streamBodyStr,
|
|
1482
|
+
...input.request.signal !== void 0 ? { signal: input.request.signal } : {}
|
|
1483
|
+
});
|
|
1484
|
+
if (!response.ok) throw new Error(`Anthropic provider failed with ${response.status}.`);
|
|
1485
|
+
const textParts = [];
|
|
1486
|
+
const rawChunks = [];
|
|
1487
|
+
const toolBlocks = /* @__PURE__ */ new Map();
|
|
1488
|
+
const nativeToolRequests = [];
|
|
1489
|
+
let usagePayload;
|
|
1490
|
+
let finishReason;
|
|
1491
|
+
for await (const event of readSseEvents(response)) {
|
|
1492
|
+
const data = event.data.trim();
|
|
1493
|
+
if (data.length === 0) continue;
|
|
1494
|
+
if (data === "[DONE]") break;
|
|
1495
|
+
const chunk = parseJsonObject$1(data, "Anthropic");
|
|
1496
|
+
rawChunks.push(event.event === void 0 ? chunk : {
|
|
1497
|
+
event: event.event,
|
|
1498
|
+
data: chunk
|
|
1499
|
+
});
|
|
1500
|
+
usagePayload = mergeAnthropicUsage(usagePayload, usageFromAnthropicChunk(chunk));
|
|
1501
|
+
finishReason = anthropicStopReason(chunk) ?? finishReason;
|
|
1502
|
+
const eventType = eventTypeFromAnthropicChunk(chunk) ?? event.event;
|
|
1503
|
+
if (eventType === "content_block_start") {
|
|
1504
|
+
startAnthropicToolBlock(toolBlocks, chunk);
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (eventType === "content_block_delta") {
|
|
1508
|
+
const text = anthropicTextDelta(chunk);
|
|
1509
|
+
if (text !== void 0 && text.length > 0) {
|
|
1510
|
+
textParts.push(text);
|
|
1511
|
+
for (const output of input.request.outputs) yield {
|
|
1512
|
+
kind: "text-delta",
|
|
1513
|
+
output,
|
|
1514
|
+
text
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
appendAnthropicToolInput(toolBlocks, chunk);
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (eventType === "content_block_stop") {
|
|
1521
|
+
const request = completeAnthropicToolBlock(toolBlocks, chunk);
|
|
1522
|
+
if (request !== void 0) nativeToolRequests.push(request);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const text = textParts.join("");
|
|
1526
|
+
const structuredToolName = anthropicStructuredOutputToolName(input.request.nativeStructuredOutput);
|
|
1527
|
+
const providerToolRequests = structuredToolName === void 0 ? nativeToolRequests : nativeToolRequests.filter((request) => request.name !== structuredToolName);
|
|
1528
|
+
const structuredOutput = structuredToolName === void 0 ? void 0 : nativeToolRequests.find((request) => request.name === structuredToolName)?.args;
|
|
1529
|
+
const sanitizedOutputs = await applyOutputSanitizers(rawOutputsForRequest$1({
|
|
1530
|
+
outputs: input.request.outputs,
|
|
1531
|
+
text,
|
|
1532
|
+
structuredOutputRequest: input.request.nativeStructuredOutput,
|
|
1533
|
+
structuredOutput
|
|
1534
|
+
}), input.sanitizeOutput, {
|
|
1535
|
+
providerId: input.id,
|
|
1536
|
+
modelId: input.model
|
|
1537
|
+
});
|
|
1538
|
+
const parsedToolCalls = parseToolUseEnvelope(text);
|
|
1539
|
+
const promptToolCalls = parsedToolCalls === null ? void 0 : await validateToolCallRequests(parsedToolCalls, input.validateToolCalls);
|
|
1540
|
+
const nativeToolCalls = providerToolRequests.length === 0 ? void 0 : await validateToolCallRequests(providerToolRequests, input.validateToolCalls);
|
|
1541
|
+
const toolCalls = [...promptToolCalls ?? [], ...nativeToolCalls ?? []];
|
|
1542
|
+
const usage = normalizeAnthropicUsage(usagePayload);
|
|
1543
|
+
const normalizedUsage = normalizeAnthropicUsageToRunUsage(usagePayload, input.pricing);
|
|
1544
|
+
const finish = finishMetadata$1({
|
|
1545
|
+
reason: finishReason,
|
|
1546
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id)
|
|
1547
|
+
});
|
|
1548
|
+
yield {
|
|
1549
|
+
kind: "complete",
|
|
1550
|
+
rawOutputs: sanitizedOutputs,
|
|
1551
|
+
...usage !== void 0 ? { usage } : {},
|
|
1552
|
+
normalizedUsage,
|
|
1553
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
1554
|
+
...finish !== void 0 ? { finish } : {},
|
|
1555
|
+
rawResponse: {
|
|
1556
|
+
kind: "anthropic-stream",
|
|
1557
|
+
chunks: rawChunks
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function parseJsonObject$1(data, providerName) {
|
|
1562
|
+
try {
|
|
1563
|
+
return JSON.parse(data);
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
1566
|
+
throw new Error(`${providerName} stream returned invalid JSON: ${message}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
function eventTypeFromAnthropicChunk(chunk) {
|
|
1570
|
+
return isRecord$2(chunk) && typeof chunk.type === "string" ? chunk.type : void 0;
|
|
1571
|
+
}
|
|
1572
|
+
function anthropicStopReason(chunk) {
|
|
1573
|
+
if (!isRecord$2(chunk)) return;
|
|
1574
|
+
if (typeof chunk.stop_reason === "string") return chunk.stop_reason;
|
|
1575
|
+
if (isRecord$2(chunk.delta) && typeof chunk.delta.stop_reason === "string") return chunk.delta.stop_reason;
|
|
1576
|
+
}
|
|
1577
|
+
function usageFromAnthropicChunk(chunk) {
|
|
1578
|
+
if (!isRecord$2(chunk)) return;
|
|
1579
|
+
if (isRecord$2(chunk.usage)) return chunk.usage;
|
|
1580
|
+
if (isRecord$2(chunk.message) && isRecord$2(chunk.message.usage)) return chunk.message.usage;
|
|
1581
|
+
}
|
|
1582
|
+
function mergeAnthropicUsage(current, next) {
|
|
1583
|
+
if (!isRecord$2(next)) return current;
|
|
1584
|
+
return {
|
|
1585
|
+
...current ?? {},
|
|
1586
|
+
...next
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
function anthropicIndex(chunk) {
|
|
1590
|
+
return isRecord$2(chunk) && typeof chunk.index === "number" ? chunk.index : void 0;
|
|
1591
|
+
}
|
|
1592
|
+
function startAnthropicToolBlock(blocks, chunk) {
|
|
1593
|
+
const index = anthropicIndex(chunk);
|
|
1594
|
+
const contentBlock = isRecord$2(chunk) && isRecord$2(chunk.content_block) ? chunk.content_block : void 0;
|
|
1595
|
+
if (index === void 0 || contentBlock === void 0 || contentBlock.type !== "tool_use" || typeof contentBlock.id !== "string" || typeof contentBlock.name !== "string") return;
|
|
1596
|
+
blocks.set(index, {
|
|
1597
|
+
id: contentBlock.id,
|
|
1598
|
+
name: contentBlock.name,
|
|
1599
|
+
jsonParts: []
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
function anthropicTextDelta(chunk) {
|
|
1603
|
+
if (!isRecord$2(chunk) || !isRecord$2(chunk.delta)) return;
|
|
1604
|
+
return chunk.delta.type === "text_delta" && typeof chunk.delta.text === "string" ? chunk.delta.text : void 0;
|
|
1605
|
+
}
|
|
1606
|
+
function appendAnthropicToolInput(blocks, chunk) {
|
|
1607
|
+
const index = anthropicIndex(chunk);
|
|
1608
|
+
if (index === void 0 || !isRecord$2(chunk) || !isRecord$2(chunk.delta)) return;
|
|
1609
|
+
if (chunk.delta.type !== "input_json_delta" || typeof chunk.delta.partial_json !== "string") return;
|
|
1610
|
+
blocks.get(index)?.jsonParts.push(chunk.delta.partial_json);
|
|
1611
|
+
}
|
|
1612
|
+
function completeAnthropicToolBlock(blocks, chunk) {
|
|
1613
|
+
const index = anthropicIndex(chunk);
|
|
1614
|
+
if (index === void 0) return;
|
|
1615
|
+
const block = blocks.get(index);
|
|
1616
|
+
if (block === void 0) return;
|
|
1617
|
+
blocks.delete(index);
|
|
1618
|
+
return {
|
|
1619
|
+
id: block.id,
|
|
1620
|
+
name: block.name,
|
|
1621
|
+
args: parseAnthropicToolInput(block)
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function parseAnthropicToolInput(block) {
|
|
1625
|
+
const value = block.jsonParts.join("").trim();
|
|
1626
|
+
if (value.length === 0) return {};
|
|
1627
|
+
try {
|
|
1628
|
+
return JSON.parse(value);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
1631
|
+
throw new Error(`Anthropic stream returned invalid tool input JSON: ${message}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
function anthropicTextFromContent(content) {
|
|
1635
|
+
if (!Array.isArray(content)) return "";
|
|
1636
|
+
return content.flatMap((block) => isRecord$2(block) && block.type === "text" && typeof block.text === "string" ? [block.text] : []).join("");
|
|
1637
|
+
}
|
|
1638
|
+
function anthropicToolUseRequestsFromContent(content, structuredOutput) {
|
|
1639
|
+
if (!Array.isArray(content)) return [];
|
|
1640
|
+
const structuredToolName = anthropicStructuredOutputToolName(structuredOutput);
|
|
1641
|
+
return content.flatMap((block, index) => {
|
|
1642
|
+
if (!isRecord$2(block) || block.type !== "tool_use" || typeof block.name !== "string" || block.name === structuredToolName) return [];
|
|
1643
|
+
return [{
|
|
1644
|
+
id: typeof block.id === "string" ? block.id : `anthropic-tool-use-${index}`,
|
|
1645
|
+
name: block.name,
|
|
1646
|
+
args: block.input ?? {}
|
|
1647
|
+
}];
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
function anthropicStructuredOutputFromContent(content, request) {
|
|
1651
|
+
const toolName = anthropicStructuredOutputToolName(request);
|
|
1652
|
+
if (toolName === void 0 || !Array.isArray(content)) return;
|
|
1653
|
+
const block = content.find((item) => isRecord$2(item) && item.type === "tool_use" && item.name === toolName && "input" in item);
|
|
1654
|
+
return isRecord$2(block) ? block.input : void 0;
|
|
1655
|
+
}
|
|
1656
|
+
function anthropicStructuredOutputToolName(request) {
|
|
1657
|
+
if (request === void 0) return;
|
|
1658
|
+
return sanitizeToolName(request.name ?? `lattice_${request.output}`);
|
|
1659
|
+
}
|
|
1660
|
+
function sanitizeToolName(name) {
|
|
1661
|
+
const sanitized = name.replace(/[^A-Za-z0-9_-]/gu, "_").slice(0, 64);
|
|
1662
|
+
return sanitized.length > 0 ? sanitized : "lattice_output";
|
|
1663
|
+
}
|
|
1664
|
+
function rawOutputsForRequest$1(input) {
|
|
1665
|
+
const rawOutputs = Object.fromEntries(input.outputs.map((name) => [name, input.text]));
|
|
1666
|
+
if (input.structuredOutputRequest !== void 0 && input.structuredOutput !== void 0) rawOutputs[input.structuredOutputRequest.output] = input.structuredOutput;
|
|
1667
|
+
return rawOutputs;
|
|
1668
|
+
}
|
|
1669
|
+
function finishMetadata$1(input) {
|
|
1670
|
+
const toolCallIds = input.toolCallIds.filter((id) => id.length > 0);
|
|
1671
|
+
if (input.reason === void 0 && toolCallIds.length === 0) return;
|
|
1672
|
+
return {
|
|
1673
|
+
...input.reason !== void 0 ? { reason: input.reason } : {},
|
|
1674
|
+
...toolCallIds.length > 0 ? { toolCallIds } : {}
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function isRecord$2(value) {
|
|
1678
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Anthropic uses `input_tokens` / `output_tokens` (not OpenAI's
|
|
1682
|
+
* `prompt_tokens` / `completion_tokens`). This helper maps to Lattice's
|
|
1683
|
+
* `Usage` shape and applies pricing when supplied (Phase 7 pattern).
|
|
1684
|
+
*/
|
|
1685
|
+
function normalizeAnthropicUsageToRunUsage(rawUsage, pricing) {
|
|
1686
|
+
let promptTokens = 0;
|
|
1687
|
+
let completionTokens = 0;
|
|
1688
|
+
if (typeof rawUsage === "object" && rawUsage !== null) {
|
|
1689
|
+
const record = rawUsage;
|
|
1690
|
+
promptTokens = numberField$1(record, "input_tokens") ?? numberField$1(record, "inputTokens") ?? 0;
|
|
1691
|
+
completionTokens = numberField$1(record, "output_tokens") ?? numberField$1(record, "outputTokens") ?? 0;
|
|
1692
|
+
}
|
|
1693
|
+
let costUsd = null;
|
|
1694
|
+
if (pricing !== void 0 && (pricing.inputPer1kTokens !== void 0 || pricing.outputPer1kTokens !== void 0)) costUsd = (pricing.inputPer1kTokens ?? 0) * promptTokens / 1e3 + (pricing.outputPer1kTokens ?? 0) * completionTokens / 1e3;
|
|
1695
|
+
return {
|
|
1696
|
+
promptTokens,
|
|
1697
|
+
completionTokens,
|
|
1698
|
+
costUsd
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function normalizeAnthropicUsage(usage) {
|
|
1702
|
+
if (typeof usage !== "object" || usage === null) return;
|
|
1703
|
+
const record = usage;
|
|
1704
|
+
const inputTokens = numberField$1(record, "input_tokens");
|
|
1705
|
+
const outputTokens = numberField$1(record, "output_tokens");
|
|
1706
|
+
const totalTokens = inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0;
|
|
1707
|
+
return {
|
|
1708
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
1709
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
1710
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function numberField$1(record, key) {
|
|
1714
|
+
const value = record[key];
|
|
1715
|
+
return typeof value === "number" ? value : void 0;
|
|
1716
|
+
}
|
|
1717
|
+
//#endregion
|
|
1718
|
+
//#region src/providers/fake.ts
|
|
1719
|
+
const DEFAULT_FAKE_USAGE = {
|
|
1720
|
+
promptTokens: 0,
|
|
1721
|
+
completionTokens: 0,
|
|
1722
|
+
costUsd: null
|
|
1723
|
+
};
|
|
1724
|
+
function createFakeProvider(options = {}) {
|
|
1725
|
+
const id = options.id ?? "fake";
|
|
1726
|
+
const modelId = options.modelId ?? `${id}:deterministic`;
|
|
1727
|
+
const defaultCapability = {
|
|
1728
|
+
...defaultCapabilityForProvider(id),
|
|
1729
|
+
modelId,
|
|
1730
|
+
inputModalities: [
|
|
1731
|
+
"text",
|
|
1732
|
+
"json",
|
|
1733
|
+
"image",
|
|
1734
|
+
"audio",
|
|
1735
|
+
"document",
|
|
1736
|
+
"file",
|
|
1737
|
+
"url",
|
|
1738
|
+
"tool"
|
|
1739
|
+
],
|
|
1740
|
+
outputModalities: ["text", "json"],
|
|
1741
|
+
toolUse: true
|
|
1742
|
+
};
|
|
1743
|
+
return {
|
|
1744
|
+
id,
|
|
1745
|
+
kind: "provider-adapter",
|
|
1746
|
+
capabilities: options.capabilities ?? [defaultCapability],
|
|
1747
|
+
async execute(request) {
|
|
1748
|
+
const baseResponse = typeof options.response === "function" ? await options.response(request) : options.response;
|
|
1749
|
+
if (baseResponse !== void 0) return baseResponse.normalizedUsage !== void 0 ? baseResponse : {
|
|
1750
|
+
...baseResponse,
|
|
1751
|
+
normalizedUsage: { ...DEFAULT_FAKE_USAGE }
|
|
1752
|
+
};
|
|
1753
|
+
return {
|
|
1754
|
+
rawOutputs: Object.fromEntries(request.outputs.map((name) => [name, defaultOutputForName(name)])),
|
|
1755
|
+
...options.artifacts !== void 0 ? { artifactRefs: options.artifacts } : {},
|
|
1756
|
+
normalizedUsage: { ...DEFAULT_FAKE_USAGE }
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function defaultOutputForName(name) {
|
|
1762
|
+
if (/action|json|data|decision/u.test(name)) return {
|
|
1763
|
+
kind: "clarify",
|
|
1764
|
+
reason: "fake provider default structured response"
|
|
1765
|
+
};
|
|
1766
|
+
if (/citations|evidence/u.test(name)) return [];
|
|
1767
|
+
if (/generated|artifacts/u.test(name)) return [];
|
|
1768
|
+
return `Fake response for ${name}.`;
|
|
1769
|
+
}
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/providers/gemini.ts
|
|
1772
|
+
const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
1773
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 2e3;
|
|
1774
|
+
const DEFAULT_TEMPERATURE = .7;
|
|
1775
|
+
const DEFAULT_TOP_P = .9;
|
|
1776
|
+
/**
|
|
1777
|
+
* 4 HARM_CATEGORY entries at BLOCK_NONE (FSB convention mirrored from
|
|
1778
|
+
* `extension/ai/universal-provider.js:255-272`). If Google restricts
|
|
1779
|
+
* BLOCK_NONE in the future, that is a re-spec concern, not a Phase 4
|
|
1780
|
+
* design defect (CONTEXT.md Specific Ideas note).
|
|
1781
|
+
*/
|
|
1782
|
+
const SAFETY_SETTINGS = [
|
|
1783
|
+
{
|
|
1784
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
1785
|
+
threshold: "BLOCK_NONE"
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
1789
|
+
threshold: "BLOCK_NONE"
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
1793
|
+
threshold: "BLOCK_NONE"
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
1797
|
+
threshold: "BLOCK_NONE"
|
|
1798
|
+
}
|
|
1799
|
+
];
|
|
1800
|
+
/**
|
|
1801
|
+
* Phase 34 — D-03 — Gemini quirks block. Values verified against
|
|
1802
|
+
* Gemini API documentation and gemini.ts:50-55 (safety settings) behavior.
|
|
1803
|
+
*
|
|
1804
|
+
* CITED: https://ai.google.dev/api/generate-content#v1beta.GenerationConfig
|
|
1805
|
+
* - responseSchemaSupported: gemini-1.5-pro+ and gemini-2.x
|
|
1806
|
+
* - safetySettingsConfigurable: verified in gemini.ts:50-55
|
|
1807
|
+
* - systemInstructionSupported: gemini-1.5+ systemInstruction field
|
|
1808
|
+
*/
|
|
1809
|
+
const GEMINI_QUIRKS = {
|
|
1810
|
+
supportsToolChoice: true,
|
|
1811
|
+
parallelToolCalls: true,
|
|
1812
|
+
structuredOutputs: true,
|
|
1813
|
+
responseFormatHonored: true,
|
|
1814
|
+
streamingDiverges: false,
|
|
1815
|
+
responseSchemaSupported: true,
|
|
1816
|
+
safetySettingsConfigurable: true,
|
|
1817
|
+
systemInstructionSupported: true
|
|
1818
|
+
};
|
|
1819
|
+
async function createGeminiGenerateContentBody(request) {
|
|
1820
|
+
const parts = await createGeminiUserParts(request);
|
|
1821
|
+
const functionDeclarations = geminiFunctionDeclarations(request.nativeTools);
|
|
1822
|
+
const toolConfig = geminiToolConfig(request.nativeToolChoice);
|
|
1823
|
+
const structuredOutputConfig = geminiStructuredOutputConfig(request.nativeStructuredOutput);
|
|
1824
|
+
return {
|
|
1825
|
+
contents: [{
|
|
1826
|
+
role: "user",
|
|
1827
|
+
parts
|
|
1828
|
+
}],
|
|
1829
|
+
generationConfig: {
|
|
1830
|
+
temperature: DEFAULT_TEMPERATURE,
|
|
1831
|
+
topP: DEFAULT_TOP_P,
|
|
1832
|
+
maxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
1833
|
+
...structuredOutputConfig !== void 0 ? structuredOutputConfig : {}
|
|
1834
|
+
},
|
|
1835
|
+
safetySettings: SAFETY_SETTINGS,
|
|
1836
|
+
...functionDeclarations.length > 0 ? { tools: [{ functionDeclarations }] } : {},
|
|
1837
|
+
...toolConfig !== void 0 ? { toolConfig } : {}
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function geminiFunctionDeclarations(tools) {
|
|
1841
|
+
return (tools ?? []).map((tool) => ({
|
|
1842
|
+
name: tool.name,
|
|
1843
|
+
...tool.description !== void 0 ? { description: tool.description } : {},
|
|
1844
|
+
parameters: standardSchemaToJsonSchema(tool.inputSchema)
|
|
1845
|
+
}));
|
|
1846
|
+
}
|
|
1847
|
+
function geminiToolConfig(choice) {
|
|
1848
|
+
if (choice === void 0) return;
|
|
1849
|
+
if (choice === "auto") return { functionCallingConfig: { mode: "AUTO" } };
|
|
1850
|
+
if (choice === "none") return { functionCallingConfig: { mode: "NONE" } };
|
|
1851
|
+
if (choice === "required") return { functionCallingConfig: { mode: "ANY" } };
|
|
1852
|
+
return { functionCallingConfig: {
|
|
1853
|
+
mode: "ANY",
|
|
1854
|
+
allowedFunctionNames: [choice.name]
|
|
1855
|
+
} };
|
|
1856
|
+
}
|
|
1857
|
+
function geminiStructuredOutputConfig(request) {
|
|
1858
|
+
if (request === void 0) return;
|
|
1859
|
+
return {
|
|
1860
|
+
responseMimeType: "application/json",
|
|
1861
|
+
responseSchema: standardSchemaToJsonSchema(request.schema)
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
async function createGeminiUserParts(request) {
|
|
1865
|
+
const parts = [{ text: request.task }];
|
|
1866
|
+
for (const inputArtifact of request.artifacts) {
|
|
1867
|
+
if (!isGeminiMediaArtifact(inputArtifact.kind)) continue;
|
|
1868
|
+
const packaged = packagedPlanForArtifact(request, inputArtifact.id);
|
|
1869
|
+
if (packaged === void 0) continue;
|
|
1870
|
+
if (packaged.transport === "file-id") {
|
|
1871
|
+
const fileUri = geminiFileUri(inputArtifact);
|
|
1872
|
+
if (fileUri === void 0) continue;
|
|
1873
|
+
parts.push({ fileData: {
|
|
1874
|
+
mimeType: mediaTypeForArtifact(inputArtifact, fallbackGeminiMimeType(inputArtifact.kind)),
|
|
1875
|
+
fileUri
|
|
1876
|
+
} });
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (packaged.transport === "url") {
|
|
1880
|
+
const fileUri = artifactHttpUrl(inputArtifact);
|
|
1881
|
+
if (fileUri === void 0) continue;
|
|
1882
|
+
parts.push({ fileData: {
|
|
1883
|
+
mimeType: mediaTypeForArtifact(inputArtifact, fallbackGeminiMimeType(inputArtifact.kind)),
|
|
1884
|
+
fileUri
|
|
1885
|
+
} });
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
if (packaged.transport === "base64" || packaged.transport === "inline") {
|
|
1889
|
+
const data = await artifactBase64Data(inputArtifact);
|
|
1890
|
+
if (data === void 0) continue;
|
|
1891
|
+
parts.push({ inlineData: {
|
|
1892
|
+
mimeType: mediaTypeForArtifact(inputArtifact, fallbackGeminiMimeType(inputArtifact.kind)),
|
|
1893
|
+
data
|
|
1894
|
+
} });
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return parts;
|
|
1898
|
+
}
|
|
1899
|
+
function isGeminiMediaArtifact(kind) {
|
|
1900
|
+
return kind === "image" || kind === "audio" || kind === "video";
|
|
1901
|
+
}
|
|
1902
|
+
function fallbackGeminiMimeType(kind) {
|
|
1903
|
+
switch (kind) {
|
|
1904
|
+
case "image": return "image/jpeg";
|
|
1905
|
+
case "audio": return "audio/mpeg";
|
|
1906
|
+
case "video": return "video/mp4";
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function geminiGenerateContentUrl(input) {
|
|
1910
|
+
const method = input.stream === true ? "streamGenerateContent" : "generateContent";
|
|
1911
|
+
const params = new URLSearchParams({ key: input.apiKey });
|
|
1912
|
+
if (input.stream === true) params.set("alt", "sse");
|
|
1913
|
+
const encodedModel = encodeURIComponent(input.model);
|
|
1914
|
+
return `${input.baseUrl}/v1beta/models/${encodedModel}:${method}?${params.toString()}`;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Phase 34 — D-03 / D-05..D-12 — Extended Gemini provider factory.
|
|
1918
|
+
*
|
|
1919
|
+
* Returns a `ProviderAdapter` narrowed to expose:
|
|
1920
|
+
* - `quirks: GeminiQuirks` — static adapter capability flags
|
|
1921
|
+
* - `negotiateCapabilities(modelId)` — live /v1beta/models fetch with medium-thick
|
|
1922
|
+
* derivation (inputTokenLimit + thinking + supportedGenerationMethods from upstream)
|
|
1923
|
+
* intersected with Phase 33 registry; TTL cache + inflight coalescing + retry +
|
|
1924
|
+
* auth-throw + transient-fallback + event.
|
|
1925
|
+
*
|
|
1926
|
+
* NOTE on auth strategy (T-34-04-01): negotiate() uses x-goog-api-key HEADER
|
|
1927
|
+
* (preferred per RESEARCH §Q3 -- avoids leaking the key in server-side logs that
|
|
1928
|
+
* capture URL query strings). The existing execute() path uses ?key= query string
|
|
1929
|
+
* and is NOT changed by Phase 34 (out-of-scope migration).
|
|
1930
|
+
*/
|
|
1931
|
+
function createGeminiProvider(options) {
|
|
1932
|
+
const id = options.id ?? "gemini";
|
|
1933
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1934
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/u, "");
|
|
1935
|
+
const ttlMs = options.modelsCacheTtlMs ?? 3e5;
|
|
1936
|
+
const retryCount = options.modelsRetryCount ?? 2;
|
|
1937
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1938
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
1939
|
+
/**
|
|
1940
|
+
* D-07 lazy expiry + Q7 inflight coalescing + Pitfall 4 .finally cleanup.
|
|
1941
|
+
* Public surface: `adapter.negotiateCapabilities(modelId)`.
|
|
1942
|
+
*/
|
|
1943
|
+
async function negotiate(modelId) {
|
|
1944
|
+
const cached = cache.get(modelId);
|
|
1945
|
+
if (cached !== void 0 && cached.expiresAt > Date.now()) return cached.result;
|
|
1946
|
+
const existing = inflight.get(modelId);
|
|
1947
|
+
if (existing !== void 0) return existing;
|
|
1948
|
+
const fetchPromise = (async () => {
|
|
1949
|
+
try {
|
|
1950
|
+
const result = await fetchAndNegotiate(modelId);
|
|
1951
|
+
if (ttlMs > 0) cache.set(modelId, {
|
|
1952
|
+
result,
|
|
1953
|
+
expiresAt: Date.now() + ttlMs
|
|
1954
|
+
});
|
|
1955
|
+
return result;
|
|
1956
|
+
} finally {
|
|
1957
|
+
inflight.delete(modelId);
|
|
1958
|
+
}
|
|
1959
|
+
})();
|
|
1960
|
+
inflight.set(modelId, fetchPromise);
|
|
1961
|
+
return fetchPromise;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Phase 34 — D-09..D-11 — Fetches /v1beta/models and merges with registry.
|
|
1965
|
+
*
|
|
1966
|
+
* URL: ${baseUrl}/v1beta/models (NOT /v1/models -- Gemini uses /v1beta/ prefix)
|
|
1967
|
+
* Auth: x-goog-api-key HEADER (preferred per RESEARCH §Q3 -- NOT ?key= query-string;
|
|
1968
|
+
* avoids leaking the key in server-side log captures of request URLs).
|
|
1969
|
+
* Retry: [0ms, 200ms, 1000ms] backoff on transient errors (D-11).
|
|
1970
|
+
* Auth error (401/403): throws NegotiationAuthError (D-10, no fallback).
|
|
1971
|
+
* Transient error (5xx/network): falls back to registry with "registry-fallback" (D-09).
|
|
1972
|
+
*/
|
|
1973
|
+
async function fetchAndNegotiate(modelId) {
|
|
1974
|
+
const url = `${baseUrl}/v1beta/models`;
|
|
1975
|
+
const headers = {
|
|
1976
|
+
"x-goog-api-key": options.apiKey,
|
|
1977
|
+
"accept": "application/json"
|
|
1978
|
+
};
|
|
1979
|
+
const attempts = retryCount + 1;
|
|
1980
|
+
const backoffSchedule = [
|
|
1981
|
+
0,
|
|
1982
|
+
200,
|
|
1983
|
+
1e3
|
|
1984
|
+
];
|
|
1985
|
+
let lastErr;
|
|
1986
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
1987
|
+
const delay = backoffSchedule[i] ?? 1e3;
|
|
1988
|
+
if (delay > 0) await new Promise((r) => setTimeout(r, delay));
|
|
1989
|
+
try {
|
|
1990
|
+
const resp = await fetchImpl(url, {
|
|
1991
|
+
method: "GET",
|
|
1992
|
+
headers,
|
|
1993
|
+
signal: AbortSignal.timeout(3e4)
|
|
1994
|
+
});
|
|
1995
|
+
if (resp.status === 401 || resp.status === 403) throw new NegotiationAuthError("gemini", modelId, resp.status, `Gemini /v1beta/models returned ${resp.status}: check apiKey config.`);
|
|
1996
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
1997
|
+
return mergeGeminiModelsWithRegistry(modelId, await resp.json());
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
if (err instanceof NegotiationAuthError) throw err;
|
|
2000
|
+
lastErr = err;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
emitFallbackEvent({
|
|
2004
|
+
adapter: "gemini",
|
|
2005
|
+
modelId,
|
|
2006
|
+
errorReason: stringifyErr$2(lastErr),
|
|
2007
|
+
fallbackSource: "registry-fallback"
|
|
2008
|
+
});
|
|
2009
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("gemini", modelId, "registry-fallback");
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* MEDIUM-THICK derivation: consumes upstream truth from Gemini /v1beta/models
|
|
2013
|
+
* where available (inputTokenLimit -> contextWindow, thinking -> extendedThinking,
|
|
2014
|
+
* supportedGenerationMethods -> streaming + nativeToolCalling) and falls back
|
|
2015
|
+
* to registry for the rest (knownFailureModes, recommendedSanitizers).
|
|
2016
|
+
*
|
|
2017
|
+
* Lenient parsing per Pitfall 1: all field accesses use optional chaining.
|
|
2018
|
+
* Missing `thinking` field does not crash -- defaults to false.
|
|
2019
|
+
*/
|
|
2020
|
+
function mergeGeminiModelsWithRegistry(modelId, body) {
|
|
2021
|
+
const models = body?.models;
|
|
2022
|
+
const found = Array.isArray(models) ? models.find((m) => {
|
|
2023
|
+
const rec = m;
|
|
2024
|
+
return rec?.name === `models/${modelId}` || rec?.baseModelId === modelId || rec?.name === modelId;
|
|
2025
|
+
}) : void 0;
|
|
2026
|
+
if (found === void 0) {
|
|
2027
|
+
emitFallbackEvent({
|
|
2028
|
+
adapter: "gemini",
|
|
2029
|
+
modelId,
|
|
2030
|
+
errorReason: "model not found in /v1beta/models response",
|
|
2031
|
+
fallbackSource: "registry-fallback"
|
|
2032
|
+
});
|
|
2033
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("gemini", modelId, "registry-fallback");
|
|
2034
|
+
}
|
|
2035
|
+
const foundRec = found;
|
|
2036
|
+
const registryProfile = getCapabilityProfile(`gemini:${modelId}`);
|
|
2037
|
+
const contextWindow = typeof foundRec.inputTokenLimit === "number" && foundRec.inputTokenLimit > 0 ? foundRec.inputTokenLimit : registryProfile?.contextWindow ?? 0;
|
|
2038
|
+
const extendedThinking = foundRec.thinking === true;
|
|
2039
|
+
const methods = Array.isArray(foundRec.supportedGenerationMethods) ? foundRec.supportedGenerationMethods.map(String) : [];
|
|
2040
|
+
const streaming = methods.includes("streamGenerateContent");
|
|
2041
|
+
const nativeToolCalling = methods.includes("generateContent") || methods.length > 0;
|
|
2042
|
+
const structuredOutputs = true;
|
|
2043
|
+
const parallelToolCalls = true;
|
|
2044
|
+
const knownFailureModes = registryProfile?.knownFailureModes ?? [];
|
|
2045
|
+
const recommendedSanitizers = getRecommendedSanitizers(knownFailureModes);
|
|
2046
|
+
return {
|
|
2047
|
+
modelId,
|
|
2048
|
+
contextWindow,
|
|
2049
|
+
supports: {
|
|
2050
|
+
nativeToolCalling,
|
|
2051
|
+
structuredOutputs,
|
|
2052
|
+
parallelToolCalls,
|
|
2053
|
+
extendedThinking,
|
|
2054
|
+
streaming
|
|
2055
|
+
},
|
|
2056
|
+
knownFailureModes,
|
|
2057
|
+
recommendedSanitizers,
|
|
2058
|
+
source: "live"
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* D-12: Emit capabilities.negotiation.fallback RunEvent via the optional sink.
|
|
2063
|
+
* SECURITY (T-34-04-02): stringifyErr extracts err.message only -- NOT err.stack
|
|
2064
|
+
* or JSON.stringify(headers), so the apiKey cannot leak into the event payload.
|
|
2065
|
+
* Synthetic runId pattern: negotiate happens outside a run; documented here.
|
|
2066
|
+
*/
|
|
2067
|
+
function emitFallbackEvent(payload) {
|
|
2068
|
+
if (options.runEventSink === void 0) return;
|
|
2069
|
+
const event = createRunEvent("capabilities.negotiation.fallback", {
|
|
2070
|
+
runId: `negotiate-gemini-${payload.modelId}`,
|
|
2071
|
+
providerId: id,
|
|
2072
|
+
modelId: payload.modelId,
|
|
2073
|
+
metadata: {
|
|
2074
|
+
adapter: payload.adapter,
|
|
2075
|
+
modelId: payload.modelId,
|
|
2076
|
+
errorReason: payload.errorReason,
|
|
2077
|
+
fallbackSource: payload.fallbackSource
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
options.runEventSink(event);
|
|
2081
|
+
}
|
|
2082
|
+
return {
|
|
2083
|
+
id,
|
|
2084
|
+
kind: "provider-adapter",
|
|
2085
|
+
capabilities: [{
|
|
2086
|
+
...defaultCapabilityForProvider(id),
|
|
2087
|
+
modelId: options.model,
|
|
2088
|
+
fileTransport: [
|
|
2089
|
+
"inline",
|
|
2090
|
+
"json",
|
|
2091
|
+
"url",
|
|
2092
|
+
"base64",
|
|
2093
|
+
"file-id",
|
|
2094
|
+
"extracted-text",
|
|
2095
|
+
"transcript"
|
|
2096
|
+
],
|
|
2097
|
+
streaming: true
|
|
2098
|
+
}],
|
|
2099
|
+
quirks: GEMINI_QUIRKS,
|
|
2100
|
+
negotiateCapabilities: negotiate,
|
|
2101
|
+
async execute(request) {
|
|
2102
|
+
const requestBody = await createGeminiGenerateContentBody(request);
|
|
2103
|
+
const bodyStr = JSON.stringify(requestBody);
|
|
2104
|
+
assertNoPublicUrlEgress(request, id, bodyStr);
|
|
2105
|
+
const init = {
|
|
2106
|
+
method: "POST",
|
|
2107
|
+
headers: { "content-type": "application/json" },
|
|
2108
|
+
body: bodyStr,
|
|
2109
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
2110
|
+
};
|
|
2111
|
+
const response = await fetchImpl(geminiGenerateContentUrl({
|
|
2112
|
+
baseUrl,
|
|
2113
|
+
model: options.model,
|
|
2114
|
+
apiKey: options.apiKey
|
|
2115
|
+
}), init);
|
|
2116
|
+
if (!response.ok) throw new Error(`Gemini provider failed with ${response.status}.`);
|
|
2117
|
+
const body = await response.json();
|
|
2118
|
+
if (!Array.isArray(body.candidates) || body.candidates.length === 0) throw new Error("Gemini provider returned no candidates.");
|
|
2119
|
+
const text = geminiTextFromBody(body);
|
|
2120
|
+
const structuredOutput = request.nativeStructuredOutput === void 0 ? void 0 : parseJsonValue(text);
|
|
2121
|
+
const sanitizedOutputs = await applyOutputSanitizers(rawOutputsForRequest({
|
|
2122
|
+
outputs: request.outputs,
|
|
2123
|
+
text,
|
|
2124
|
+
structuredOutputRequest: request.nativeStructuredOutput,
|
|
2125
|
+
structuredOutput
|
|
2126
|
+
}), options.sanitizeOutput, {
|
|
2127
|
+
providerId: id,
|
|
2128
|
+
modelId: options.model
|
|
2129
|
+
});
|
|
2130
|
+
const parsedToolCalls = parseToolUseEnvelope(text);
|
|
2131
|
+
const promptToolCalls = parsedToolCalls === null ? void 0 : await validateToolCallRequests(parsedToolCalls, options.validateToolCalls);
|
|
2132
|
+
const nativeToolRequests = geminiParts(body).flatMap(({ part, candidateIndex, partIndex }) => {
|
|
2133
|
+
const request = geminiFunctionCallRequest(part, candidateIndex, partIndex);
|
|
2134
|
+
return request === void 0 ? [] : [request];
|
|
2135
|
+
});
|
|
2136
|
+
const nativeToolCalls = nativeToolRequests.length === 0 ? void 0 : await validateToolCallRequests(nativeToolRequests, options.validateToolCalls);
|
|
2137
|
+
const hasToolCallResult = promptToolCalls !== void 0 || nativeToolCalls !== void 0;
|
|
2138
|
+
const toolCalls = [...promptToolCalls ?? [], ...nativeToolCalls ?? []];
|
|
2139
|
+
const usage = normalizeGeminiUsage(body.usageMetadata);
|
|
2140
|
+
const normalizedUsage = normalizeGeminiUsageToRunUsage(body.usageMetadata, options.pricing);
|
|
2141
|
+
const finish = finishMetadata({
|
|
2142
|
+
reason: geminiFinishReason(body),
|
|
2143
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id)
|
|
2144
|
+
});
|
|
2145
|
+
return {
|
|
2146
|
+
rawOutputs: sanitizedOutputs,
|
|
2147
|
+
...usage !== void 0 ? { usage } : {},
|
|
2148
|
+
normalizedUsage,
|
|
2149
|
+
...hasToolCallResult ? { toolCalls } : {},
|
|
2150
|
+
...finish !== void 0 ? { finish } : {},
|
|
2151
|
+
rawResponse: body
|
|
2152
|
+
};
|
|
2153
|
+
},
|
|
2154
|
+
executeStream(request) {
|
|
2155
|
+
return streamGeminiResponse({
|
|
2156
|
+
id,
|
|
2157
|
+
model: options.model,
|
|
2158
|
+
baseUrl,
|
|
2159
|
+
apiKey: options.apiKey,
|
|
2160
|
+
fetchImpl,
|
|
2161
|
+
request,
|
|
2162
|
+
...options.pricing !== void 0 ? { pricing: options.pricing } : {},
|
|
2163
|
+
...options.sanitizeOutput !== void 0 ? { sanitizeOutput: options.sanitizeOutput } : {},
|
|
2164
|
+
...options.validateToolCalls !== void 0 ? { validateToolCalls: options.validateToolCalls } : {}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
async function* streamGeminiResponse(input) {
|
|
2170
|
+
const requestBody = await createGeminiGenerateContentBody(input.request);
|
|
2171
|
+
const streamBodyStr = JSON.stringify(requestBody);
|
|
2172
|
+
assertNoPublicUrlEgress(input.request, input.id, streamBodyStr);
|
|
2173
|
+
const response = await input.fetchImpl(geminiGenerateContentUrl({
|
|
2174
|
+
baseUrl: input.baseUrl,
|
|
2175
|
+
model: input.model,
|
|
2176
|
+
apiKey: input.apiKey,
|
|
2177
|
+
stream: true
|
|
2178
|
+
}), {
|
|
2179
|
+
method: "POST",
|
|
2180
|
+
headers: { "content-type": "application/json" },
|
|
2181
|
+
body: streamBodyStr,
|
|
2182
|
+
...input.request.signal !== void 0 ? { signal: input.request.signal } : {}
|
|
2183
|
+
});
|
|
2184
|
+
if (!response.ok) throw new Error(`Gemini provider failed with ${response.status}.`);
|
|
2185
|
+
const textParts = [];
|
|
2186
|
+
const rawChunks = [];
|
|
2187
|
+
const nativeToolRequests = [];
|
|
2188
|
+
let usagePayload;
|
|
2189
|
+
let finishReason;
|
|
2190
|
+
for await (const event of readSseEvents(response)) {
|
|
2191
|
+
const data = event.data.trim();
|
|
2192
|
+
if (data.length === 0) continue;
|
|
2193
|
+
if (data === "[DONE]") break;
|
|
2194
|
+
const chunk = parseJsonObject(data, "Gemini");
|
|
2195
|
+
rawChunks.push(chunk);
|
|
2196
|
+
const usage = geminiUsageMetadata(chunk);
|
|
2197
|
+
if (usage !== void 0) usagePayload = usage;
|
|
2198
|
+
finishReason = geminiFinishReason(chunk) ?? finishReason;
|
|
2199
|
+
for (const { part, candidateIndex, partIndex } of geminiParts(chunk)) {
|
|
2200
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
2201
|
+
textParts.push(part.text);
|
|
2202
|
+
for (const output of input.request.outputs) yield {
|
|
2203
|
+
kind: "text-delta",
|
|
2204
|
+
output,
|
|
2205
|
+
text: part.text
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
const toolRequest = geminiFunctionCallRequest(part, candidateIndex, partIndex);
|
|
2209
|
+
if (toolRequest !== void 0) nativeToolRequests.push(toolRequest);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
const text = textParts.join("");
|
|
2213
|
+
const structuredOutput = input.request.nativeStructuredOutput === void 0 ? void 0 : parseJsonValue(text);
|
|
2214
|
+
const sanitizedOutputs = await applyOutputSanitizers(rawOutputsForRequest({
|
|
2215
|
+
outputs: input.request.outputs,
|
|
2216
|
+
text,
|
|
2217
|
+
structuredOutputRequest: input.request.nativeStructuredOutput,
|
|
2218
|
+
structuredOutput
|
|
2219
|
+
}), input.sanitizeOutput, {
|
|
2220
|
+
providerId: input.id,
|
|
2221
|
+
modelId: input.model
|
|
2222
|
+
});
|
|
2223
|
+
const parsedToolCalls = parseToolUseEnvelope(text);
|
|
2224
|
+
const promptToolCalls = parsedToolCalls === null ? void 0 : await validateToolCallRequests(parsedToolCalls, input.validateToolCalls);
|
|
2225
|
+
const nativeToolCalls = nativeToolRequests.length === 0 ? void 0 : await validateToolCallRequests(nativeToolRequests, input.validateToolCalls);
|
|
2226
|
+
const toolCalls = [...promptToolCalls ?? [], ...nativeToolCalls ?? []];
|
|
2227
|
+
const usage = normalizeGeminiUsage(usagePayload);
|
|
2228
|
+
const normalizedUsage = normalizeGeminiUsageToRunUsage(usagePayload, input.pricing);
|
|
2229
|
+
const finish = finishMetadata({
|
|
2230
|
+
reason: finishReason,
|
|
2231
|
+
toolCallIds: toolCalls.map((toolCall) => toolCall.id)
|
|
2232
|
+
});
|
|
2233
|
+
yield {
|
|
2234
|
+
kind: "complete",
|
|
2235
|
+
rawOutputs: sanitizedOutputs,
|
|
2236
|
+
...usage !== void 0 ? { usage } : {},
|
|
2237
|
+
normalizedUsage,
|
|
2238
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
2239
|
+
...finish !== void 0 ? { finish } : {},
|
|
2240
|
+
rawResponse: {
|
|
2241
|
+
kind: "gemini-stream",
|
|
2242
|
+
chunks: rawChunks
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
function parseJsonObject(data, providerName) {
|
|
2247
|
+
try {
|
|
2248
|
+
return JSON.parse(data);
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
2251
|
+
throw new Error(`${providerName} stream returned invalid JSON: ${message}`);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function geminiUsageMetadata(chunk) {
|
|
2255
|
+
return isRecord$1(chunk) ? chunk.usageMetadata : void 0;
|
|
2256
|
+
}
|
|
2257
|
+
function geminiFinishReason(chunk) {
|
|
2258
|
+
if (!isRecord$1(chunk) || !Array.isArray(chunk.candidates)) return;
|
|
2259
|
+
for (const candidate of chunk.candidates) if (isRecord$1(candidate) && typeof candidate.finishReason === "string") return candidate.finishReason;
|
|
2260
|
+
}
|
|
2261
|
+
function geminiTextFromBody(body) {
|
|
2262
|
+
return geminiParts(body).flatMap(({ part }) => typeof part.text === "string" ? [part.text] : []).join("");
|
|
2263
|
+
}
|
|
2264
|
+
function geminiParts(chunk) {
|
|
2265
|
+
if (!isRecord$1(chunk) || !Array.isArray(chunk.candidates)) return [];
|
|
2266
|
+
return chunk.candidates.flatMap((candidate, candidateIndex) => {
|
|
2267
|
+
if (!isRecord$1(candidate) || !isRecord$1(candidate.content)) return [];
|
|
2268
|
+
const parts = candidate.content.parts;
|
|
2269
|
+
if (!Array.isArray(parts)) return [];
|
|
2270
|
+
return parts.flatMap((part, partIndex) => isRecord$1(part) ? [{
|
|
2271
|
+
part,
|
|
2272
|
+
candidateIndex,
|
|
2273
|
+
partIndex
|
|
2274
|
+
}] : []);
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
function geminiFunctionCallRequest(part, candidateIndex, partIndex) {
|
|
2278
|
+
if (!isRecord$1(part.functionCall)) return;
|
|
2279
|
+
const name = part.functionCall.name;
|
|
2280
|
+
if (typeof name !== "string") return;
|
|
2281
|
+
return {
|
|
2282
|
+
id: `gemini-function-call-${candidateIndex}-${partIndex}`,
|
|
2283
|
+
name,
|
|
2284
|
+
args: part.functionCall.args ?? {}
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function parseJsonValue(text) {
|
|
2288
|
+
const trimmed = text.trim();
|
|
2289
|
+
if (trimmed.length === 0) return;
|
|
2290
|
+
try {
|
|
2291
|
+
return JSON.parse(trimmed);
|
|
2292
|
+
} catch {
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
function rawOutputsForRequest(input) {
|
|
2297
|
+
const rawOutputs = Object.fromEntries(input.outputs.map((name) => [name, input.text]));
|
|
2298
|
+
if (input.structuredOutputRequest !== void 0 && input.structuredOutput !== void 0) rawOutputs[input.structuredOutputRequest.output] = input.structuredOutput;
|
|
2299
|
+
return rawOutputs;
|
|
2300
|
+
}
|
|
2301
|
+
function finishMetadata(input) {
|
|
2302
|
+
const toolCallIds = input.toolCallIds.filter((id) => id.length > 0);
|
|
2303
|
+
if (input.reason === void 0 && toolCallIds.length === 0) return;
|
|
2304
|
+
return {
|
|
2305
|
+
...input.reason !== void 0 ? { reason: input.reason } : {},
|
|
2306
|
+
...toolCallIds.length > 0 ? { toolCallIds } : {}
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function isRecord$1(value) {
|
|
2310
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Gemini uses `usageMetadata.promptTokenCount` / `candidatesTokenCount` /
|
|
2314
|
+
* `totalTokenCount` (NOT OpenAI's `prompt_tokens` / `completion_tokens`).
|
|
2315
|
+
* This helper maps to Lattice's `Usage` shape and applies pricing when supplied.
|
|
2316
|
+
*/
|
|
2317
|
+
function normalizeGeminiUsageToRunUsage(rawUsage, pricing) {
|
|
2318
|
+
let promptTokens = 0;
|
|
2319
|
+
let completionTokens = 0;
|
|
2320
|
+
if (typeof rawUsage === "object" && rawUsage !== null) {
|
|
2321
|
+
const record = rawUsage;
|
|
2322
|
+
promptTokens = numberField(record, "promptTokenCount") ?? 0;
|
|
2323
|
+
completionTokens = numberField(record, "candidatesTokenCount") ?? 0;
|
|
2324
|
+
}
|
|
2325
|
+
let costUsd = null;
|
|
2326
|
+
if (pricing !== void 0 && (pricing.inputPer1kTokens !== void 0 || pricing.outputPer1kTokens !== void 0)) costUsd = (pricing.inputPer1kTokens ?? 0) * promptTokens / 1e3 + (pricing.outputPer1kTokens ?? 0) * completionTokens / 1e3;
|
|
2327
|
+
return {
|
|
2328
|
+
promptTokens,
|
|
2329
|
+
completionTokens,
|
|
2330
|
+
costUsd
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
function normalizeGeminiUsage(usage) {
|
|
2334
|
+
if (typeof usage !== "object" || usage === null) return;
|
|
2335
|
+
const record = usage;
|
|
2336
|
+
const inputTokens = numberField(record, "promptTokenCount");
|
|
2337
|
+
const outputTokens = numberField(record, "candidatesTokenCount");
|
|
2338
|
+
const totalTokens = numberField(record, "totalTokenCount");
|
|
2339
|
+
return {
|
|
2340
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
2341
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
2342
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
function numberField(record, key) {
|
|
2346
|
+
const value = record[key];
|
|
2347
|
+
return typeof value === "number" ? value : void 0;
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* T-34-04-02: Returns err.message only -- NOT err.stack (which could include
|
|
2351
|
+
* headers or the apiKey via a fetch rejection), NOT JSON.stringify(err).
|
|
2352
|
+
*/
|
|
2353
|
+
function stringifyErr$2(err) {
|
|
2354
|
+
return err instanceof Error ? err.message : String(err);
|
|
2355
|
+
}
|
|
2356
|
+
//#endregion
|
|
2357
|
+
//#region src/providers/litellm.ts
|
|
2358
|
+
const DEFAULT_LITELLM_BASE_URL = "http://localhost:4000";
|
|
2359
|
+
function createLiteLLMProvider(options) {
|
|
2360
|
+
const resolvedId = options.id ?? "litellm";
|
|
2361
|
+
const resolvedBaseUrl = options.baseUrl ?? DEFAULT_LITELLM_BASE_URL;
|
|
2362
|
+
const gateway = {
|
|
2363
|
+
allowFallbacks: false,
|
|
2364
|
+
...options.gateway
|
|
2365
|
+
};
|
|
2366
|
+
const inner = createOpenAICompatibleProvider({
|
|
2367
|
+
...options,
|
|
2368
|
+
id: resolvedId,
|
|
2369
|
+
baseUrl: resolvedBaseUrl,
|
|
2370
|
+
gateway
|
|
2371
|
+
});
|
|
2372
|
+
const negotiate = async (modelId) => {
|
|
2373
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("litellm", modelId, "registry");
|
|
2374
|
+
};
|
|
2375
|
+
return {
|
|
2376
|
+
...inner,
|
|
2377
|
+
quirks: {
|
|
2378
|
+
supportsToolChoice: false,
|
|
2379
|
+
parallelToolCalls: false,
|
|
2380
|
+
structuredOutputs: false,
|
|
2381
|
+
responseFormatHonored: false,
|
|
2382
|
+
streamingDiverges: true,
|
|
2383
|
+
gatewayMetadataSupported: true,
|
|
2384
|
+
gatewayFallbacksSupported: true,
|
|
2385
|
+
openAIErrorMapping: true
|
|
2386
|
+
},
|
|
2387
|
+
negotiateCapabilities: negotiate
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
//#endregion
|
|
2391
|
+
//#region src/providers/lm-studio.ts
|
|
2392
|
+
const DEFAULT_LM_STUDIO_BASE_URL = "http://localhost:1234/v1";
|
|
2393
|
+
/**
|
|
2394
|
+
* Phase 34 — D-04 / QUIRK-02 — LM Studio provider factory.
|
|
2395
|
+
*
|
|
2396
|
+
* LM Studio is the prototypical "intentional no remote /models endpoint"
|
|
2397
|
+
* adapter per D-04 (alongside OpenAI-compat). The factory returns conservative
|
|
2398
|
+
* defaults for the quirks block because LM Studio runs LOCAL quantized models
|
|
2399
|
+
* whose capabilities vary wildly by chat template + model file.
|
|
2400
|
+
*
|
|
2401
|
+
* The `negotiateCapabilities` method performs NO fetch; it returns
|
|
2402
|
+
* `synthesizeNegotiatedCapabilitiesFromRegistry` with source: "registry"
|
|
2403
|
+
* (the intentional-no-endpoint signal, distinct from "registry-fallback"
|
|
2404
|
+
* which signals a transient failure). Mirrors Plan 34-03 Task 2 (OpenAI-compat
|
|
2405
|
+
* registry-only pattern) verbatim.
|
|
2406
|
+
*
|
|
2407
|
+
* D-04 citation: "consumer adapters without a /models endpoint skip the
|
|
2408
|
+
* fetch layer entirely and delegate to synthesizeNegotiatedCapabilitiesFromRegistry."
|
|
2409
|
+
*
|
|
2410
|
+
* Open Question 5 (RESEARCH §): no event emitted for source: "registry" because
|
|
2411
|
+
* this is the intentional happy path for LM Studio -- emitting a "fallback" event
|
|
2412
|
+
* would produce false-positive noise for consumers monitoring the event stream.
|
|
2413
|
+
*/
|
|
2414
|
+
function createLmStudioProvider(options) {
|
|
2415
|
+
const resolvedId = options.id ?? "lm-studio";
|
|
2416
|
+
const resolvedBaseUrl = options.baseUrl ?? DEFAULT_LM_STUDIO_BASE_URL;
|
|
2417
|
+
const negotiate = async (modelId) => {
|
|
2418
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("lm-studio", modelId, "registry");
|
|
2419
|
+
};
|
|
2420
|
+
return {
|
|
2421
|
+
...createOpenAICompatibleProvider({
|
|
2422
|
+
...options,
|
|
2423
|
+
id: resolvedId,
|
|
2424
|
+
baseUrl: resolvedBaseUrl
|
|
2425
|
+
}),
|
|
2426
|
+
quirks: {
|
|
2427
|
+
supportsToolChoice: false,
|
|
2428
|
+
parallelToolCalls: false,
|
|
2429
|
+
structuredOutputs: false,
|
|
2430
|
+
responseFormatHonored: false,
|
|
2431
|
+
streamingDiverges: true,
|
|
2432
|
+
customChatTemplateRiskFlag: true,
|
|
2433
|
+
noAuthRequired: true
|
|
2434
|
+
},
|
|
2435
|
+
negotiateCapabilities: negotiate
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
//#endregion
|
|
2439
|
+
//#region src/providers/openrouter.ts
|
|
2440
|
+
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
2441
|
+
function normalizeFallbackModels(models) {
|
|
2442
|
+
if (models === void 0) return void 0;
|
|
2443
|
+
const normalized = models.map((model) => model.trim()).filter((model) => model.length > 0);
|
|
2444
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
2445
|
+
}
|
|
2446
|
+
function observedModelFromRawResponse(rawResponse) {
|
|
2447
|
+
if (typeof rawResponse !== "object" || rawResponse === null || Array.isArray(rawResponse)) return;
|
|
2448
|
+
const model = rawResponse.model;
|
|
2449
|
+
return typeof model === "string" ? model : void 0;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Phase 34 — D-03 — OpenRouter quirks block. Values verified against
|
|
2453
|
+
* OpenRouter API documentation and observed behavior.
|
|
2454
|
+
*
|
|
2455
|
+
* CITED: https://openrouter.ai/docs/provider-routing
|
|
2456
|
+
* - providerRoutingArraySupported: provider.order/only/ignore arrays for explicit routing
|
|
2457
|
+
* CITED: https://openrouter.ai/docs pricing / sort options
|
|
2458
|
+
* - floorPricingHints: max_price, sort: "throughput" | "price" hints for cost-aware routing
|
|
2459
|
+
* CITED: https://openrouter.ai/docs allow_fallbacks
|
|
2460
|
+
* - allowFallbacks: provider.allow_fallbacks boolean controls upstream-provider fallback behavior
|
|
2461
|
+
*/
|
|
2462
|
+
const OPENROUTER_QUIRKS = {
|
|
2463
|
+
supportsToolChoice: true,
|
|
2464
|
+
parallelToolCalls: true,
|
|
2465
|
+
structuredOutputs: true,
|
|
2466
|
+
responseFormatHonored: true,
|
|
2467
|
+
streamingDiverges: false,
|
|
2468
|
+
providerRoutingArraySupported: true,
|
|
2469
|
+
floorPricingHints: true,
|
|
2470
|
+
allowFallbacks: true
|
|
2471
|
+
};
|
|
2472
|
+
/**
|
|
2473
|
+
* Phase 34 — D-03 / D-05..D-12 — Extended OpenRouter provider factory.
|
|
2474
|
+
*
|
|
2475
|
+
* Returns a `ProviderAdapter` narrowed to expose:
|
|
2476
|
+
* - `quirks: OpenRouterQuirks` — static adapter capability flags (8 booleans)
|
|
2477
|
+
* - `negotiateCapabilities(modelId)` — live /api/v1/models fetch with rich /models
|
|
2478
|
+
* intersection (supported_parameters -> nativeToolCalling + structuredOutputs,
|
|
2479
|
+
* top_provider.context_length -> contextWindow) intersected with Phase 33 registry
|
|
2480
|
+
* for knownFailureModes + recommendedSanitizers.
|
|
2481
|
+
*
|
|
2482
|
+
* CRITICAL for ANCHOR CASE STUDY (session_1780792387779):
|
|
2483
|
+
* negotiate("openai/gpt-oss-120b:free") MUST resolve to:
|
|
2484
|
+
* - result.knownFailureModes.includes("internal_envelope_leak") -> TRUE
|
|
2485
|
+
* - result.recommendedSanitizers.includes("unwrapInternalEnvelope") -> TRUE
|
|
2486
|
+
* - result.source === "live" -> TRUE
|
|
2487
|
+
* This proves: live-fetch -> id suffix-strip via stripOpenRouterVariant
|
|
2488
|
+
* -> registry intersection -> getRecommendedSanitizers derivation.
|
|
2489
|
+
*
|
|
2490
|
+
* Anti-pattern (RESEARCH §Anti-pattern, lines 534-535):
|
|
2491
|
+
* The /api/v1/models endpoint is UNAUTHENTICATED (public discovery surface verified
|
|
2492
|
+
* Phase 33). Do NOT send Authorization Bearer to this endpoint -- it is NOT required
|
|
2493
|
+
* and would add unnecessary API key exposure surface in transit logs.
|
|
2494
|
+
*/
|
|
2495
|
+
function createOpenRouterProvider(options) {
|
|
2496
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_OPENROUTER_BASE_URL).replace(/\/$/u, "");
|
|
2497
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
2498
|
+
const fallbackModels = normalizeFallbackModels(options.fallbackModels);
|
|
2499
|
+
const ttlMs = options.modelsCacheTtlMs ?? 3e5;
|
|
2500
|
+
const retryCount = options.modelsRetryCount ?? 2;
|
|
2501
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2502
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
2503
|
+
/**
|
|
2504
|
+
* D-07 lazy expiry + Q7 inflight coalescing + Pitfall 4 .finally cleanup.
|
|
2505
|
+
* Public surface: `adapter.negotiateCapabilities(modelId)`.
|
|
2506
|
+
*/
|
|
2507
|
+
async function negotiate(modelId) {
|
|
2508
|
+
const cached = cache.get(modelId);
|
|
2509
|
+
if (cached !== void 0 && cached.expiresAt > Date.now()) return cached.result;
|
|
2510
|
+
const existing = inflight.get(modelId);
|
|
2511
|
+
if (existing !== void 0) return existing;
|
|
2512
|
+
const fetchPromise = (async () => {
|
|
2513
|
+
try {
|
|
2514
|
+
const result = await fetchAndNegotiate(modelId);
|
|
2515
|
+
if (ttlMs > 0) cache.set(modelId, {
|
|
2516
|
+
result,
|
|
2517
|
+
expiresAt: Date.now() + ttlMs
|
|
2518
|
+
});
|
|
2519
|
+
return result;
|
|
2520
|
+
} finally {
|
|
2521
|
+
inflight.delete(modelId);
|
|
2522
|
+
}
|
|
2523
|
+
})();
|
|
2524
|
+
inflight.set(modelId, fetchPromise);
|
|
2525
|
+
return fetchPromise;
|
|
2526
|
+
}
|
|
2527
|
+
/**
|
|
2528
|
+
* Phase 34 — D-09..D-11 — Fetches /api/v1/models and merges with registry.
|
|
2529
|
+
*
|
|
2530
|
+
* URL: ${baseUrl}/api/v1/models (NOTE: /api/v1/models -- different prefix from
|
|
2531
|
+
* OpenAI's /v1/models; OpenRouter's discovery endpoint is under /api/v1/)
|
|
2532
|
+
* Auth: NONE -- OpenRouter /api/v1/models is a public unauthenticated endpoint.
|
|
2533
|
+
* Per RESEARCH §Anti-pattern (lines 534-535): do NOT send Authorization Bearer
|
|
2534
|
+
* to this endpoint. This is a known anti-pattern; do not "fix" it.
|
|
2535
|
+
* Retry: [0ms, 200ms, 1000ms] backoff on transient errors (D-11).
|
|
2536
|
+
* Auth error (401/403): throws NegotiationAuthError (D-10, no fallback) -- defensive,
|
|
2537
|
+
* even though the endpoint is unauthenticated today, OpenRouter may add auth later.
|
|
2538
|
+
* Transient error (5xx/network): falls back to registry with "registry-fallback" (D-09).
|
|
2539
|
+
*/
|
|
2540
|
+
async function fetchAndNegotiate(modelId) {
|
|
2541
|
+
const url = `${baseUrl}/api/v1/models`;
|
|
2542
|
+
const headers = { "accept": "application/json" };
|
|
2543
|
+
const attempts = retryCount + 1;
|
|
2544
|
+
const backoffSchedule = [
|
|
2545
|
+
0,
|
|
2546
|
+
200,
|
|
2547
|
+
1e3
|
|
2548
|
+
];
|
|
2549
|
+
let lastErr;
|
|
2550
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
2551
|
+
const delay = backoffSchedule[i] ?? 1e3;
|
|
2552
|
+
if (delay > 0) await new Promise((r) => setTimeout(r, delay));
|
|
2553
|
+
try {
|
|
2554
|
+
const resp = await fetchImpl(url, {
|
|
2555
|
+
method: "GET",
|
|
2556
|
+
headers,
|
|
2557
|
+
signal: AbortSignal.timeout(3e4)
|
|
2558
|
+
});
|
|
2559
|
+
if (resp.status === 401 || resp.status === 403) throw new NegotiationAuthError("openrouter", modelId, resp.status, `OpenRouter /api/v1/models returned ${resp.status}.`);
|
|
2560
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
2561
|
+
return mergeOpenRouterModelsWithRegistry(modelId, await resp.json());
|
|
2562
|
+
} catch (err) {
|
|
2563
|
+
if (err instanceof NegotiationAuthError) throw err;
|
|
2564
|
+
lastErr = err;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
emitFallbackEvent({
|
|
2568
|
+
adapter: "openrouter",
|
|
2569
|
+
modelId,
|
|
2570
|
+
errorReason: stringifyErr$1(lastErr),
|
|
2571
|
+
fallbackSource: "registry-fallback"
|
|
2572
|
+
});
|
|
2573
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("openrouter", modelId, "registry-fallback");
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* RICH /models intersection: consumes OpenRouter's /api/v1/models structured data
|
|
2577
|
+
* to populate NegotiatedCapabilities.supports.* from upstream (THICK derivation where
|
|
2578
|
+
* available), then intersects with Phase 33 registry for knownFailureModes +
|
|
2579
|
+
* recommendedSanitizers.
|
|
2580
|
+
*
|
|
2581
|
+
* ANCHOR CASE STUDY (session_1780792387779) flow:
|
|
2582
|
+
* 1. Find "openai/gpt-oss-120b:free" (or strip suffix -> "openai/gpt-oss-120b")
|
|
2583
|
+
* 2. Build canonical key: "openrouter:openai/gpt-oss-120b" (via stripOpenRouterVariant)
|
|
2584
|
+
* 3. getCapabilityProfile("openrouter:openai/gpt-oss-120b") -> Phase 33 profile with
|
|
2585
|
+
* knownFailureModes: ["internal_envelope_leak", "system_prompt_echo", "malformed_tool_arguments"]
|
|
2586
|
+
* 4. getRecommendedSanitizers(knownFailureModes) -> ["unwrapInternalEnvelope"]
|
|
2587
|
+
* 5. result.recommendedSanitizers.includes("unwrapInternalEnvelope") -> TRUE
|
|
2588
|
+
*
|
|
2589
|
+
* Pitfall 3 / A1 precedence chain (RESEARCH §Q5):
|
|
2590
|
+
* contextWindow = top_provider.context_length ?? context_length ?? registryProfile.contextWindow
|
|
2591
|
+
*
|
|
2592
|
+
* Lenient parsing per Pitfall 1: all field accesses use optional chaining.
|
|
2593
|
+
*/
|
|
2594
|
+
function mergeOpenRouterModelsWithRegistry(modelId, body) {
|
|
2595
|
+
const rows = body?.data;
|
|
2596
|
+
const found = Array.isArray(rows) ? rows.find((m) => {
|
|
2597
|
+
const rec = m;
|
|
2598
|
+
if (typeof rec?.id !== "string") return false;
|
|
2599
|
+
const rowId = rec.id;
|
|
2600
|
+
if (rowId === modelId || rowId === stripOpenRouterVariant(modelId)) return true;
|
|
2601
|
+
const strippedModelId = stripOpenRouterVariant(modelId);
|
|
2602
|
+
return stripOpenRouterVariant(rowId) === strippedModelId;
|
|
2603
|
+
}) : void 0;
|
|
2604
|
+
const stripped = stripOpenRouterVariant(modelId);
|
|
2605
|
+
const registryProfile = getCapabilityProfile(`openrouter:${stripped}`);
|
|
2606
|
+
if (found === void 0) {
|
|
2607
|
+
emitFallbackEvent({
|
|
2608
|
+
adapter: "openrouter",
|
|
2609
|
+
modelId,
|
|
2610
|
+
errorReason: "model not found in /api/v1/models response",
|
|
2611
|
+
fallbackSource: "registry-fallback"
|
|
2612
|
+
});
|
|
2613
|
+
return {
|
|
2614
|
+
...synthesizeNegotiatedCapabilitiesFromRegistry("openrouter", stripped, "registry-fallback"),
|
|
2615
|
+
modelId
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
const foundRec = found;
|
|
2619
|
+
const topProvider = foundRec.top_provider;
|
|
2620
|
+
const contextWindow = typeof topProvider?.context_length === "number" && topProvider.context_length > 0 ? topProvider.context_length : typeof foundRec.context_length === "number" && foundRec.context_length > 0 ? foundRec.context_length : registryProfile?.contextWindow ?? 0;
|
|
2621
|
+
const supportedParams = Array.isArray(foundRec.supported_parameters) ? foundRec.supported_parameters.map(String) : [];
|
|
2622
|
+
const nativeToolCalling = supportedParams.includes("tools");
|
|
2623
|
+
const structuredOutputs = supportedParams.includes("response_format");
|
|
2624
|
+
const parallelToolCalls = supportedParams.includes("tool_choice");
|
|
2625
|
+
const extendedThinking = supportedParams.includes("reasoning") || supportedParams.includes("thinking");
|
|
2626
|
+
const streaming = true;
|
|
2627
|
+
const knownFailureModes = registryProfile?.knownFailureModes ?? [];
|
|
2628
|
+
const recommendedSanitizers = getRecommendedSanitizers(knownFailureModes);
|
|
2629
|
+
return {
|
|
2630
|
+
modelId,
|
|
2631
|
+
contextWindow,
|
|
2632
|
+
supports: {
|
|
2633
|
+
nativeToolCalling,
|
|
2634
|
+
structuredOutputs,
|
|
2635
|
+
parallelToolCalls,
|
|
2636
|
+
extendedThinking,
|
|
2637
|
+
streaming
|
|
2638
|
+
},
|
|
2639
|
+
knownFailureModes,
|
|
2640
|
+
recommendedSanitizers,
|
|
2641
|
+
source: "live"
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* D-12: Emit capabilities.negotiation.fallback RunEvent via the optional sink.
|
|
2646
|
+
* SECURITY (T-34-04-02): stringifyErr extracts err.message only -- NOT err.stack
|
|
2647
|
+
* or JSON.stringify(headers), so the apiKey cannot leak into the event payload.
|
|
2648
|
+
* Synthetic runId pattern: negotiate happens outside a run; documented here.
|
|
2649
|
+
*/
|
|
2650
|
+
function emitFallbackEvent(payload) {
|
|
2651
|
+
if (options.runEventSink === void 0) return;
|
|
2652
|
+
const event = createRunEvent("capabilities.negotiation.fallback", {
|
|
2653
|
+
runId: `negotiate-openrouter-${payload.modelId}`,
|
|
2654
|
+
providerId: options.id ?? "openrouter",
|
|
2655
|
+
modelId: payload.modelId,
|
|
2656
|
+
metadata: {
|
|
2657
|
+
adapter: payload.adapter,
|
|
2658
|
+
modelId: payload.modelId,
|
|
2659
|
+
errorReason: payload.errorReason,
|
|
2660
|
+
fallbackSource: payload.fallbackSource
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
options.runEventSink(event);
|
|
2664
|
+
}
|
|
2665
|
+
const executeFetch = fallbackModels === void 0 ? fetchImpl : (async (url, init) => {
|
|
2666
|
+
if (typeof init?.body !== "string") return fetchImpl(url, init);
|
|
2667
|
+
const body = JSON.parse(init.body);
|
|
2668
|
+
return fetchImpl(url, {
|
|
2669
|
+
...init,
|
|
2670
|
+
body: JSON.stringify({
|
|
2671
|
+
...body,
|
|
2672
|
+
models: [...fallbackModels]
|
|
2673
|
+
})
|
|
2674
|
+
});
|
|
2675
|
+
});
|
|
2676
|
+
const baseAdapter = createOpenAICompatibleProvider({
|
|
2677
|
+
...options,
|
|
2678
|
+
id: options.id ?? "openrouter",
|
|
2679
|
+
baseUrl,
|
|
2680
|
+
fetch: executeFetch
|
|
2681
|
+
});
|
|
2682
|
+
const baseExecuteStream = baseAdapter.executeStream;
|
|
2683
|
+
return {
|
|
2684
|
+
...baseAdapter,
|
|
2685
|
+
async execute(request) {
|
|
2686
|
+
const response = await baseAdapter.execute(request);
|
|
2687
|
+
const observedModel = response.gateway?.observedModel ?? observedModelFromRawResponse(response.rawResponse);
|
|
2688
|
+
return {
|
|
2689
|
+
...response,
|
|
2690
|
+
gateway: {
|
|
2691
|
+
...response.gateway ?? { used: true },
|
|
2692
|
+
used: true,
|
|
2693
|
+
requestedModel: options.model,
|
|
2694
|
+
...fallbackModels !== void 0 ? { fallbackModels } : {},
|
|
2695
|
+
...observedModel !== void 0 ? { observedModel } : {}
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
},
|
|
2699
|
+
...baseExecuteStream !== void 0 ? { executeStream: async (request) => {
|
|
2700
|
+
return withOpenRouterStreamGateway(await baseExecuteStream(request));
|
|
2701
|
+
} } : {},
|
|
2702
|
+
quirks: OPENROUTER_QUIRKS,
|
|
2703
|
+
negotiateCapabilities: negotiate
|
|
2704
|
+
};
|
|
2705
|
+
async function* withOpenRouterStreamGateway(stream) {
|
|
2706
|
+
for await (const chunk of stream) {
|
|
2707
|
+
if (chunk.kind !== "complete") {
|
|
2708
|
+
yield chunk;
|
|
2709
|
+
continue;
|
|
2710
|
+
}
|
|
2711
|
+
const observedModel = chunk.gateway?.observedModel ?? observedModelFromRawResponse(chunk.rawResponse);
|
|
2712
|
+
yield {
|
|
2713
|
+
...chunk,
|
|
2714
|
+
gateway: {
|
|
2715
|
+
...chunk.gateway ?? { used: true },
|
|
2716
|
+
used: true,
|
|
2717
|
+
requestedModel: options.model,
|
|
2718
|
+
...fallbackModels !== void 0 ? { fallbackModels } : {},
|
|
2719
|
+
...observedModel !== void 0 ? { observedModel } : {}
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* T-34-04-02: Returns err.message only -- NOT err.stack (which could include
|
|
2727
|
+
* headers or the apiKey via a fetch rejection), NOT JSON.stringify(err).
|
|
2728
|
+
*/
|
|
2729
|
+
function stringifyErr$1(err) {
|
|
2730
|
+
return err instanceof Error ? err.message : String(err);
|
|
2731
|
+
}
|
|
2732
|
+
//#endregion
|
|
2733
|
+
//#region src/providers/xai.ts
|
|
2734
|
+
const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
|
|
2735
|
+
/**
|
|
2736
|
+
* Phase 34 — D-12 — Emit a "capabilities.negotiation.fallback" RunEvent if
|
|
2737
|
+
* a sink is provided. Uses a synthetic runId (negotiation is outside a run).
|
|
2738
|
+
*
|
|
2739
|
+
* T-34-03-01: errorReason uses stringifyErr (message only, not stack) to
|
|
2740
|
+
* prevent apiKey leaking via fetch error strings that may embed request headers.
|
|
2741
|
+
*/
|
|
2742
|
+
function emitFallbackEvent(sink, payload) {
|
|
2743
|
+
if (sink === void 0) return;
|
|
2744
|
+
sink(createRunEvent("capabilities.negotiation.fallback", {
|
|
2745
|
+
runId: `negotiate-${payload.adapter}-${payload.modelId}`,
|
|
2746
|
+
providerId: payload.adapter,
|
|
2747
|
+
modelId: payload.modelId,
|
|
2748
|
+
metadata: {
|
|
2749
|
+
adapter: payload.adapter,
|
|
2750
|
+
modelId: payload.modelId,
|
|
2751
|
+
errorReason: payload.errorReason,
|
|
2752
|
+
fallbackSource: payload.fallbackSource
|
|
2753
|
+
}
|
|
2754
|
+
}));
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Stringify an error for event metadata. Returns only the message (NOT the
|
|
2758
|
+
* stack) to prevent apiKey or sensitive header values from leaking into event
|
|
2759
|
+
* payloads via fetch errors that may embed the request init.
|
|
2760
|
+
* T-34-03-01 mitigation.
|
|
2761
|
+
*/
|
|
2762
|
+
function stringifyErr(err) {
|
|
2763
|
+
return err instanceof Error ? err.message : String(err);
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Phase 34 — QUIRK-02 / NEG-01 / NEG-02 — Merge an xAI /v1/models sparse
|
|
2767
|
+
* OpenAI-shaped response with the Phase 33 registry.
|
|
2768
|
+
*
|
|
2769
|
+
* RESEARCH §A1 (INFERRED): xAI's /v1/models shape is undocumented; the
|
|
2770
|
+
* endpoint is assumed to return an OpenAI-compatible sparse list based on
|
|
2771
|
+
* the shared OpenAI-compat wire format used for chat completions.
|
|
2772
|
+
* LENIENT-PARSE is mandatory per Pitfall 1 (RESEARCH §Q4): if xAI changes
|
|
2773
|
+
* their response shape, the adapter must not crash.
|
|
2774
|
+
*
|
|
2775
|
+
* CITED: Pitfall 1 — "When integrating with less-documented endpoints like
|
|
2776
|
+
* xAI's /v1/models, lenient parsing prevents runtime crashes when the
|
|
2777
|
+
* endpoint returns an unexpected shape."
|
|
2778
|
+
*
|
|
2779
|
+
* Source semantics per D-09:
|
|
2780
|
+
* - "live" when the model id is found in the response AND body.data is an array
|
|
2781
|
+
* - "registry-fallback" when body.data is not an array (unexpected shape)
|
|
2782
|
+
* OR when the model id is not in the response
|
|
2783
|
+
*/
|
|
2784
|
+
function mergeXaiModelsWithRegistry(modelId, body, emitFallback) {
|
|
2785
|
+
const rawData = body?.data;
|
|
2786
|
+
if (!Array.isArray(rawData)) {
|
|
2787
|
+
emitFallback();
|
|
2788
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("xai", modelId, "registry-fallback");
|
|
2789
|
+
}
|
|
2790
|
+
if (rawData.find((m) => typeof m === "object" && m !== null && m.id === modelId) === void 0) {
|
|
2791
|
+
emitFallback();
|
|
2792
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("xai", modelId, "registry-fallback");
|
|
2793
|
+
}
|
|
2794
|
+
const registryProfile = getCapabilityProfile(`xai:${modelId}`);
|
|
2795
|
+
if (registryProfile !== void 0) return mapProfileToNegotiatedCapabilities(registryProfile, "live");
|
|
2796
|
+
return {
|
|
2797
|
+
modelId,
|
|
2798
|
+
contextWindow: 0,
|
|
2799
|
+
supports: {
|
|
2800
|
+
nativeToolCalling: true,
|
|
2801
|
+
structuredOutputs: true,
|
|
2802
|
+
parallelToolCalls: true,
|
|
2803
|
+
extendedThinking: false,
|
|
2804
|
+
streaming: true
|
|
2805
|
+
},
|
|
2806
|
+
knownFailureModes: [],
|
|
2807
|
+
recommendedSanitizers: [],
|
|
2808
|
+
source: "live"
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Phase 34 — QUIRK-02 / NEG-01 / NEG-02 — xAI provider factory.
|
|
2813
|
+
*
|
|
2814
|
+
* Extends the base OpenAI-compat execution wrapper with:
|
|
2815
|
+
* 1. `quirks: XaiQuirks` — verified per RESEARCH §Q6 xAI vocabulary.
|
|
2816
|
+
* 2. `negotiateCapabilities(modelId)` — queries xAI /v1/models GET with
|
|
2817
|
+
* Authorization: Bearer header; LENIENT-PARSE sparse OpenAI-shaped
|
|
2818
|
+
* response; intersects with Phase 33 registry for supports.*.
|
|
2819
|
+
*
|
|
2820
|
+
* CITED: RESEARCH §Q4 (INFERRED) — xAI /v1/models shape is undocumented;
|
|
2821
|
+
* assumed OpenAI-compatible based on the chat completions wire format.
|
|
2822
|
+
*
|
|
2823
|
+
* CITED: RESEARCH §A1 — Pitfall 1 lenient parse: if xAI publishes a
|
|
2824
|
+
* different /models shape, only the parsing logic updates; the contract
|
|
2825
|
+
* (source values, NegotiatedCapabilities shape) holds.
|
|
2826
|
+
*
|
|
2827
|
+
* The negotiate() pattern mirrors Plan 34-02 (Anthropic thick reference):
|
|
2828
|
+
* - Per-instance TTL cache (modelsCacheTtlMs, default 300_000ms)
|
|
2829
|
+
* - Single-flight inflight coalescing with .finally cleanup (Pitfall 4)
|
|
2830
|
+
* - Retry with [0, 200, 1000]ms backoff (modelsRetryCount, default 2)
|
|
2831
|
+
* - 401/403 throws NegotiationAuthError with adapter: "xai" (D-10)
|
|
2832
|
+
* - 5xx/network/timeout falls back to registry + emits fallback event
|
|
2833
|
+
*
|
|
2834
|
+
* SECURITY (T-34-03-07): inflight Map MUST use .finally cleanup to prevent
|
|
2835
|
+
* leak on rejection. Verifiable: grep `.finally` in this file.
|
|
2836
|
+
*/
|
|
2837
|
+
function createXaiProvider(options) {
|
|
2838
|
+
const resolvedBaseUrl = (options.baseUrl ?? DEFAULT_XAI_BASE_URL).replace(/\/$/u, "");
|
|
2839
|
+
const ttlMs = options.modelsCacheTtlMs ?? 3e5;
|
|
2840
|
+
const retryCount = options.modelsRetryCount ?? 2;
|
|
2841
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
2842
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2843
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
2844
|
+
async function fetchAndNegotiate(modelId) {
|
|
2845
|
+
const url = `${resolvedBaseUrl}/models`;
|
|
2846
|
+
const headers = {
|
|
2847
|
+
"accept": "application/json",
|
|
2848
|
+
...options.apiKey !== void 0 ? { authorization: `Bearer ${options.apiKey}` } : {}
|
|
2849
|
+
};
|
|
2850
|
+
const attempts = retryCount + 1;
|
|
2851
|
+
const backoffMs = [
|
|
2852
|
+
0,
|
|
2853
|
+
200,
|
|
2854
|
+
1e3
|
|
2855
|
+
];
|
|
2856
|
+
let lastErr;
|
|
2857
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
2858
|
+
if (i > 0) {
|
|
2859
|
+
const delay = backoffMs[Math.min(i, backoffMs.length - 1)] ?? 1e3;
|
|
2860
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2861
|
+
}
|
|
2862
|
+
try {
|
|
2863
|
+
const resp = await fetchImpl(url, {
|
|
2864
|
+
method: "GET",
|
|
2865
|
+
headers,
|
|
2866
|
+
signal: AbortSignal.timeout(3e4)
|
|
2867
|
+
});
|
|
2868
|
+
if (resp.status === 401 || resp.status === 403) throw new NegotiationAuthError("xai", modelId, resp.status, `xAI /v1/models returned ${resp.status}: check apiKey config.`);
|
|
2869
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
2870
|
+
return mergeXaiModelsWithRegistry(modelId, await resp.json(), () => {
|
|
2871
|
+
emitFallbackEvent(options.runEventSink, {
|
|
2872
|
+
adapter: "xai",
|
|
2873
|
+
modelId,
|
|
2874
|
+
errorReason: "model not found in /v1/models response or unexpected body shape",
|
|
2875
|
+
fallbackSource: "registry-fallback"
|
|
2876
|
+
});
|
|
2877
|
+
});
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
if (err instanceof NegotiationAuthError) throw err;
|
|
2880
|
+
lastErr = err;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
emitFallbackEvent(options.runEventSink, {
|
|
2884
|
+
adapter: "xai",
|
|
2885
|
+
modelId,
|
|
2886
|
+
errorReason: stringifyErr(lastErr),
|
|
2887
|
+
fallbackSource: "registry-fallback"
|
|
2888
|
+
});
|
|
2889
|
+
return synthesizeNegotiatedCapabilitiesFromRegistry("xai", modelId, "registry-fallback");
|
|
2890
|
+
}
|
|
2891
|
+
async function negotiate(modelId) {
|
|
2892
|
+
const cached = cache.get(modelId);
|
|
2893
|
+
if (cached !== void 0 && cached.expiresAt > Date.now()) return cached.result;
|
|
2894
|
+
const existing = inflight.get(modelId);
|
|
2895
|
+
if (existing !== void 0) return existing;
|
|
2896
|
+
const fetchPromise = (async () => {
|
|
2897
|
+
try {
|
|
2898
|
+
const result = await fetchAndNegotiate(modelId);
|
|
2899
|
+
if (ttlMs > 0) cache.set(modelId, {
|
|
2900
|
+
result,
|
|
2901
|
+
expiresAt: Date.now() + ttlMs
|
|
2902
|
+
});
|
|
2903
|
+
return result;
|
|
2904
|
+
} finally {
|
|
2905
|
+
inflight.delete(modelId);
|
|
2906
|
+
}
|
|
2907
|
+
})();
|
|
2908
|
+
inflight.set(modelId, fetchPromise);
|
|
2909
|
+
return fetchPromise;
|
|
2910
|
+
}
|
|
2911
|
+
const inner = createOpenAICompatibleProvider({
|
|
2912
|
+
...options,
|
|
2913
|
+
id: options.id ?? "xai",
|
|
2914
|
+
baseUrl: resolvedBaseUrl
|
|
2915
|
+
});
|
|
2916
|
+
const innerExecute = inner.execute;
|
|
2917
|
+
const innerExecuteStream = inner.executeStream;
|
|
2918
|
+
const wrappedExecute = innerExecute === void 0 ? void 0 : async (request) => {
|
|
2919
|
+
const response = await innerExecute(request);
|
|
2920
|
+
const reasoningTokens = reasoningTokensFromRawResponse(response.rawResponse);
|
|
2921
|
+
if (typeof reasoningTokens === "number" && response.usage !== void 0) {
|
|
2922
|
+
const inputTokens = response.usage.inputTokens ?? 0;
|
|
2923
|
+
const outputTokens = response.usage.outputTokens ?? 0;
|
|
2924
|
+
return {
|
|
2925
|
+
...response,
|
|
2926
|
+
usage: {
|
|
2927
|
+
...response.usage,
|
|
2928
|
+
totalTokens: inputTokens + outputTokens + reasoningTokens
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
return response;
|
|
2933
|
+
};
|
|
2934
|
+
const wrappedExecuteStream = innerExecuteStream === void 0 ? void 0 : async function* (request) {
|
|
2935
|
+
const stream = await innerExecuteStream(request);
|
|
2936
|
+
for await (const chunk of stream) {
|
|
2937
|
+
if (chunk.kind !== "complete") {
|
|
2938
|
+
yield chunk;
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2941
|
+
const reasoningTokens = reasoningTokensFromRawResponse(chunk.rawResponse);
|
|
2942
|
+
if (typeof reasoningTokens === "number" && chunk.usage !== void 0) {
|
|
2943
|
+
const inputTokens = chunk.usage.inputTokens ?? 0;
|
|
2944
|
+
const outputTokens = chunk.usage.outputTokens ?? 0;
|
|
2945
|
+
yield {
|
|
2946
|
+
...chunk,
|
|
2947
|
+
usage: {
|
|
2948
|
+
...chunk.usage,
|
|
2949
|
+
totalTokens: inputTokens + outputTokens + reasoningTokens
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
yield chunk;
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
return {
|
|
2958
|
+
id: inner.id,
|
|
2959
|
+
kind: inner.kind,
|
|
2960
|
+
quirks: {
|
|
2961
|
+
supportsToolChoice: true,
|
|
2962
|
+
parallelToolCalls: true,
|
|
2963
|
+
structuredOutputs: true,
|
|
2964
|
+
responseFormatHonored: true,
|
|
2965
|
+
streamingDiverges: false,
|
|
2966
|
+
reasoningTokensReported: true,
|
|
2967
|
+
logprobsSupported: false
|
|
2968
|
+
},
|
|
2969
|
+
negotiateCapabilities: negotiate,
|
|
2970
|
+
...inner.capabilities !== void 0 ? { capabilities: inner.capabilities } : {},
|
|
2971
|
+
...wrappedExecute !== void 0 ? { execute: wrappedExecute } : {},
|
|
2972
|
+
...wrappedExecuteStream !== void 0 ? { executeStream: wrappedExecuteStream } : {}
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
function reasoningTokensFromRawResponse(rawResponse) {
|
|
2976
|
+
const direct = reasoningTokensFromUsage(rawResponse?.usage);
|
|
2977
|
+
if (direct !== void 0) return direct;
|
|
2978
|
+
if (!isRecord(rawResponse) || !Array.isArray(rawResponse.chunks)) return;
|
|
2979
|
+
for (let index = rawResponse.chunks.length - 1; index >= 0; index -= 1) {
|
|
2980
|
+
const chunk = rawResponse.chunks[index];
|
|
2981
|
+
const fromChunk = reasoningTokensFromUsage(chunk?.usage);
|
|
2982
|
+
if (fromChunk !== void 0) return fromChunk;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
function reasoningTokensFromUsage(usage) {
|
|
2986
|
+
if (!isRecord(usage) || !isRecord(usage.completion_tokens_details)) return;
|
|
2987
|
+
const value = usage.completion_tokens_details.reasoning_tokens;
|
|
2988
|
+
return typeof value === "number" ? value : void 0;
|
|
2989
|
+
}
|
|
2990
|
+
function isRecord(value) {
|
|
2991
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2992
|
+
}
|
|
2993
|
+
//#endregion
|
|
2994
|
+
//#region src/providers/streaming.ts
|
|
2995
|
+
async function collectStream(stream, options = {}) {
|
|
2996
|
+
const rawOutputs = {};
|
|
2997
|
+
const textParts = /* @__PURE__ */ new Map();
|
|
2998
|
+
const toolCalls = [];
|
|
2999
|
+
let artifactRefs;
|
|
3000
|
+
let usage;
|
|
3001
|
+
let normalizedUsage;
|
|
3002
|
+
let gateway;
|
|
3003
|
+
let finish;
|
|
3004
|
+
let rawResponse;
|
|
3005
|
+
let rawResponseProvided = false;
|
|
3006
|
+
let chunkCount = 0;
|
|
3007
|
+
for await (const chunk of stream) {
|
|
3008
|
+
chunkCount += 1;
|
|
3009
|
+
switch (chunk.kind) {
|
|
3010
|
+
case "text-delta": {
|
|
3011
|
+
const output = chunk.output ?? options.defaultOutput ?? "text";
|
|
3012
|
+
const parts = textParts.get(output);
|
|
3013
|
+
if (parts === void 0) textParts.set(output, [chunk.text]);
|
|
3014
|
+
else parts.push(chunk.text);
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
case "output":
|
|
3018
|
+
rawOutputs[chunk.output] = chunk.value;
|
|
3019
|
+
break;
|
|
3020
|
+
case "usage":
|
|
3021
|
+
if (chunk.usage !== void 0) usage = chunk.usage;
|
|
3022
|
+
if (chunk.normalizedUsage !== void 0) normalizedUsage = chunk.normalizedUsage;
|
|
3023
|
+
break;
|
|
3024
|
+
case "gateway":
|
|
3025
|
+
gateway = chunk.gateway;
|
|
3026
|
+
break;
|
|
3027
|
+
case "tool-call":
|
|
3028
|
+
toolCalls.push(chunk.toolCall);
|
|
3029
|
+
break;
|
|
3030
|
+
case "complete":
|
|
3031
|
+
if (chunk.rawOutputs !== void 0) Object.assign(rawOutputs, chunk.rawOutputs);
|
|
3032
|
+
if (chunk.artifactRefs !== void 0) artifactRefs = chunk.artifactRefs;
|
|
3033
|
+
if (chunk.usage !== void 0) usage = chunk.usage;
|
|
3034
|
+
if (chunk.normalizedUsage !== void 0) normalizedUsage = chunk.normalizedUsage;
|
|
3035
|
+
if (chunk.gateway !== void 0) gateway = chunk.gateway;
|
|
3036
|
+
if (chunk.toolCalls !== void 0) toolCalls.push(...chunk.toolCalls);
|
|
3037
|
+
if (chunk.finish !== void 0) finish = chunk.finish;
|
|
3038
|
+
if ("rawResponse" in chunk) {
|
|
3039
|
+
rawResponse = chunk.rawResponse;
|
|
3040
|
+
rawResponseProvided = true;
|
|
3041
|
+
}
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
for (const [output, parts] of textParts) if (!(output in rawOutputs)) rawOutputs[output] = parts.join("");
|
|
3046
|
+
return {
|
|
3047
|
+
rawOutputs,
|
|
3048
|
+
...artifactRefs !== void 0 ? { artifactRefs } : {},
|
|
3049
|
+
...usage !== void 0 ? { usage } : {},
|
|
3050
|
+
...normalizedUsage !== void 0 ? { normalizedUsage } : {},
|
|
3051
|
+
...toolCalls.length > 0 ? { toolCalls } : {},
|
|
3052
|
+
...gateway !== void 0 ? { gateway } : {},
|
|
3053
|
+
...finish !== void 0 ? { finish } : {},
|
|
3054
|
+
rawResponse: rawResponseProvided ? rawResponse : {
|
|
3055
|
+
kind: "lattice-stream-summary",
|
|
3056
|
+
chunkCount,
|
|
3057
|
+
outputNames: Object.keys(rawOutputs)
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
//#endregion
|
|
3062
|
+
//#region src/prompts/scaffolds.ts
|
|
3063
|
+
const PROMPT_SCAFFOLD_VERSION = "lattice.prompt-scaffold/v1";
|
|
3064
|
+
const PROMPT_STRATEGIES = [
|
|
3065
|
+
"frontier",
|
|
3066
|
+
"mid_tier",
|
|
3067
|
+
"open_weight",
|
|
3068
|
+
"reasoning",
|
|
3069
|
+
"local"
|
|
3070
|
+
];
|
|
3071
|
+
const JSON_SERIALIZATION_ERRORS = {
|
|
3072
|
+
schema: "getStructuredOutputContract: schema must be JSON-serializable for deterministic prompt scaffolds.",
|
|
3073
|
+
tools: "getToolUseContract: tools must be JSON-serializable for deterministic prompt scaffolds."
|
|
3074
|
+
};
|
|
3075
|
+
const STRATEGY_INSTRUCTIONS = {
|
|
3076
|
+
frontier: {
|
|
3077
|
+
structuredOutput: ["Return only content that satisfies the contract.", "Do not include prose before or after the structured output."],
|
|
3078
|
+
toolUse: ["Use a tool only when the task requires an external action or lookup.", "Return a normal answer when the user request can be completed without a tool."]
|
|
3079
|
+
},
|
|
3080
|
+
mid_tier: {
|
|
3081
|
+
structuredOutput: ["Treat the contract as instructions, not as prose to repeat.", "Return the requested answer in the shape required by the contract."],
|
|
3082
|
+
toolUse: ["The tool definitions are available actions, not answer text.", "Call only a listed tool, and only with arguments that match its definition."]
|
|
3083
|
+
},
|
|
3084
|
+
open_weight: {
|
|
3085
|
+
structuredOutput: [
|
|
3086
|
+
"The contract below is an instruction, not text to output.",
|
|
3087
|
+
"Do not answer with the schema, envelope, or any field name unless that field belongs in the final user-visible JSON.",
|
|
3088
|
+
"Bad: {\"summary\":\"Greeted the user.\"} when the user asked for a natural-language reply.",
|
|
3089
|
+
"Good: Greeted the user."
|
|
3090
|
+
],
|
|
3091
|
+
toolUse: [
|
|
3092
|
+
"The tool list below is action metadata, not text to output.",
|
|
3093
|
+
"Do not copy the tool descriptor into the final answer.",
|
|
3094
|
+
"If no listed tool is needed, answer normally without fabricating a tool call."
|
|
3095
|
+
]
|
|
3096
|
+
},
|
|
3097
|
+
reasoning: {
|
|
3098
|
+
structuredOutput: ["Do not expose hidden reasoning, scratchpad text, or analysis in the final answer.", "Return only the final content that satisfies the contract."],
|
|
3099
|
+
toolUse: ["Keep tool selection separate from hidden reasoning.", "Do not include scratchpad text when explaining whether a tool was used."]
|
|
3100
|
+
},
|
|
3101
|
+
local: {
|
|
3102
|
+
structuredOutput: ["Do not copy the contract, chat template, or wrapper text into the answer.", "Return the requested answer directly in the required shape."],
|
|
3103
|
+
toolUse: ["Do not invent tool names or arguments.", "If the task does not require a listed tool, answer directly."]
|
|
3104
|
+
}
|
|
3105
|
+
};
|
|
3106
|
+
function canonicalPromptJson(value, payload) {
|
|
3107
|
+
const errorMessage = JSON_SERIALIZATION_ERRORS[payload];
|
|
3108
|
+
let json;
|
|
3109
|
+
try {
|
|
3110
|
+
json = canonicalize(value);
|
|
3111
|
+
if (json === void 0) throw new Error(errorMessage);
|
|
3112
|
+
JSON.parse(json);
|
|
3113
|
+
} catch {
|
|
3114
|
+
throw new Error(errorMessage);
|
|
3115
|
+
}
|
|
3116
|
+
return json;
|
|
3117
|
+
}
|
|
3118
|
+
function renderPromptScaffold(strategy, purpose, instructions, payloadHeading, payload) {
|
|
3119
|
+
return [
|
|
3120
|
+
`Lattice Prompt Scaffold: ${PROMPT_SCAFFOLD_VERSION}`,
|
|
3121
|
+
`Strategy: ${strategy}`,
|
|
3122
|
+
`Purpose: ${purpose}`,
|
|
3123
|
+
"",
|
|
3124
|
+
...instructions,
|
|
3125
|
+
"",
|
|
3126
|
+
`${payloadHeading}:`,
|
|
3127
|
+
payload
|
|
3128
|
+
].join("\n");
|
|
3129
|
+
}
|
|
3130
|
+
function getStructuredOutputContract(strategy, schema) {
|
|
3131
|
+
return renderPromptScaffold(strategy, "structured-output", STRATEGY_INSTRUCTIONS[strategy].structuredOutput, "Contract", canonicalPromptJson(schema, "schema"));
|
|
3132
|
+
}
|
|
3133
|
+
function getToolUseContract(strategy, tools) {
|
|
3134
|
+
return renderPromptScaffold(strategy, "tool-use", STRATEGY_INSTRUCTIONS[strategy].toolUse, "Tools", canonicalPromptJson(tools, "tools"));
|
|
3135
|
+
}
|
|
3136
|
+
//#endregion
|
|
3137
|
+
export { stripReasoningTags as _, collectStream as a, NoPublicUrlEgressError as b, createLmStudioProvider as c, createFakeProvider as d, createAnthropicProvider as f, stripChatTemplateArtifacts as g, createOpenAIProvider as h, getToolUseContract as i, createLiteLLMProvider as l, createOpenAICompatibleProvider as m, PROMPT_STRATEGIES as n, createXaiProvider as o, createAISdkProvider as p, getStructuredOutputContract as r, createOpenRouterProvider as s, PROMPT_SCAFFOLD_VERSION as t, createGeminiProvider as u, unwrapInternalEnvelope as v, mediaTypeForArtifact as x, createRunEvent as y };
|
|
3138
|
+
|
|
3139
|
+
//# sourceMappingURL=scaffolds-ekPIlBeU.js.map
|