@nexface/agent 0.1.1-alpha.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/THIRD_PARTY_LICENSES.txt +245 -0
- package/dist/agent.d.ts +19 -0
- package/dist/agent.js +1509 -0
- package/dist/agent.js.map +1 -0
- package/dist/browser-prompt.d.ts +5 -0
- package/dist/browser-prompt.generated.d.ts +1 -0
- package/dist/browser-prompt.generated.js +50 -0
- package/dist/browser-prompt.generated.js.map +1 -0
- package/dist/browser-prompt.js +9 -0
- package/dist/browser-prompt.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +55 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/model/binding.d.ts +22 -0
- package/dist/model/binding.js +31 -0
- package/dist/model/binding.js.map +1 -0
- package/dist/model/error.d.ts +6 -0
- package/dist/model/error.js +10 -0
- package/dist/model/error.js.map +1 -0
- package/dist/model/internal-adapter.d.ts +8 -0
- package/dist/model/internal-adapter.js +13 -0
- package/dist/model/internal-adapter.js.map +1 -0
- package/dist/model/types.d.ts +9 -0
- package/dist/model/types.js +2 -0
- package/dist/model/types.js.map +1 -0
- package/dist/models/885.js +630 -0
- package/dist/models/956.js +5 -0
- package/dist/models/_chunks/35-e1813138.js +8304 -0
- package/dist/models/_chunks/879-7e580bb2.js +1189 -0
- package/dist/models/_chunks/958-bedced75.js +453 -0
- package/dist/models/_chunks/anthropic-messages~1-a4b25b48.js +8057 -0
- package/dist/models/_chunks/deferred-tools-90f3c977.js +37 -0
- package/dist/models/_chunks/error-body-8bee35c2.js +134 -0
- package/dist/models/_chunks/google-generative-ai~1-78bb2822.js +22104 -0
- package/dist/models/_chunks/openai-completions~1-7677fb83.js +1285 -0
- package/dist/models/_chunks/openai-responses~1-cc0a71cd.js +958 -0
- package/dist/models/anthropic-messages.d.ts +4 -0
- package/dist/models/anthropic-messages.js +32 -0
- package/dist/models/google-generative-ai.d.ts +4 -0
- package/dist/models/google-generative-ai.js +32 -0
- package/dist/models/openai-completions.d.ts +5 -0
- package/dist/models/openai-completions.js +16 -0
- package/dist/models/openai-responses.d.ts +5 -0
- package/dist/models/openai-responses.js +16 -0
- package/dist/models/rslib-runtime.js +59 -0
- package/dist/models/types.d.ts +26 -0
- package/dist/tool-bridge.d.ts +36 -0
- package/dist/tool-bridge.js +299 -0
- package/dist/tool-bridge.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
function mergeHeaders(base, override) {
|
|
9
|
+
if (!base && !override)
|
|
10
|
+
return undefined;
|
|
11
|
+
const merged = { ...base };
|
|
12
|
+
for (const [name, value] of Object.entries(override ?? {})) {
|
|
13
|
+
const lowerName = name.toLowerCase();
|
|
14
|
+
for (const existingName of Object.keys(merged)) {
|
|
15
|
+
if (existingName.toLowerCase() === lowerName)
|
|
16
|
+
delete merged[existingName];
|
|
17
|
+
}
|
|
18
|
+
merged[name] = value;
|
|
19
|
+
}
|
|
20
|
+
return merged;
|
|
21
|
+
}
|
|
22
|
+
class ModelsImpl {
|
|
23
|
+
providers = new Map();
|
|
24
|
+
credentials;
|
|
25
|
+
modelsStore;
|
|
26
|
+
authContext;
|
|
27
|
+
refreshGenerations = new Map();
|
|
28
|
+
refreshControllers = new Map();
|
|
29
|
+
publicationChains = new Map();
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.credentials = options?.credentials ?? new InMemoryCredentialStore();
|
|
32
|
+
this.modelsStore = options?.modelsStore ?? new InMemoryModelsStore();
|
|
33
|
+
this.authContext = options?.authContext ?? defaultAuthContext();
|
|
34
|
+
}
|
|
35
|
+
setProvider(provider) {
|
|
36
|
+
this.supersedeProviderRefresh(provider.id);
|
|
37
|
+
this.providers.set(provider.id, provider);
|
|
38
|
+
}
|
|
39
|
+
deleteProvider(id) {
|
|
40
|
+
this.supersedeProviderRefresh(id);
|
|
41
|
+
this.providers.delete(id);
|
|
42
|
+
}
|
|
43
|
+
clearProviders() {
|
|
44
|
+
for (const id of new Set([...this.providers.keys(), ...this.refreshControllers.keys()])) {
|
|
45
|
+
this.supersedeProviderRefresh(id);
|
|
46
|
+
}
|
|
47
|
+
this.providers.clear();
|
|
48
|
+
}
|
|
49
|
+
getProviders() {
|
|
50
|
+
return Array.from(this.providers.values());
|
|
51
|
+
}
|
|
52
|
+
getProvider(id) {
|
|
53
|
+
return this.providers.get(id);
|
|
54
|
+
}
|
|
55
|
+
getModels(provider) {
|
|
56
|
+
if (provider !== undefined) {
|
|
57
|
+
const entry = this.providers.get(provider);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return [];
|
|
60
|
+
try {
|
|
61
|
+
return entry.getModels();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const models = [];
|
|
68
|
+
for (const entry of this.providers.values()) {
|
|
69
|
+
try {
|
|
70
|
+
models.push(...entry.getModels());
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Best-effort: ill-behaved providers yield no models.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return models;
|
|
77
|
+
}
|
|
78
|
+
getModel(provider, id) {
|
|
79
|
+
return this.getModels(provider).find((model) => model.id === id);
|
|
80
|
+
}
|
|
81
|
+
supersedeProviderRefresh(providerId) {
|
|
82
|
+
const generation = (this.refreshGenerations.get(providerId) ?? 0) + 1;
|
|
83
|
+
this.refreshGenerations.set(providerId, generation);
|
|
84
|
+
const previous = this.refreshControllers.get(providerId);
|
|
85
|
+
if (previous) {
|
|
86
|
+
this.refreshControllers.delete(providerId);
|
|
87
|
+
previous.abort();
|
|
88
|
+
}
|
|
89
|
+
return generation;
|
|
90
|
+
}
|
|
91
|
+
beginProviderRefresh(providerId) {
|
|
92
|
+
const generation = this.supersedeProviderRefresh(providerId);
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
this.refreshControllers.set(providerId, controller);
|
|
95
|
+
return { generation, controller };
|
|
96
|
+
}
|
|
97
|
+
publishProviderModels(providerId, generation, signal, publication) {
|
|
98
|
+
const previous = this.publicationChains.get(providerId) ?? Promise.resolve();
|
|
99
|
+
const queued = (async () => {
|
|
100
|
+
await previous.catch(() => { });
|
|
101
|
+
if (signal.aborted || this.refreshGenerations.get(providerId) !== generation)
|
|
102
|
+
return false;
|
|
103
|
+
if (publication.persist === null) {
|
|
104
|
+
await this.modelsStore.delete(providerId, { signal });
|
|
105
|
+
}
|
|
106
|
+
else if (publication.persist !== undefined) {
|
|
107
|
+
await this.modelsStore.write(providerId, structuredClone(publication.persist), { signal });
|
|
108
|
+
}
|
|
109
|
+
if (signal.aborted || this.refreshGenerations.get(providerId) !== generation)
|
|
110
|
+
return false;
|
|
111
|
+
publication.update?.();
|
|
112
|
+
return true;
|
|
113
|
+
})();
|
|
114
|
+
const tail = queued.catch(() => { });
|
|
115
|
+
this.publicationChains.set(providerId, tail);
|
|
116
|
+
void tail.then(() => {
|
|
117
|
+
if (this.publicationChains.get(providerId) === tail)
|
|
118
|
+
this.publicationChains.delete(providerId);
|
|
119
|
+
});
|
|
120
|
+
return raceWithAbortSignal(queued, signal);
|
|
121
|
+
}
|
|
122
|
+
async runProviderRefreshPhase(provider, credential, allowNetwork, force, generation, signal) {
|
|
123
|
+
const stored = await this.modelsStore.read(provider.id, { signal });
|
|
124
|
+
await provider.refreshModels({
|
|
125
|
+
credential,
|
|
126
|
+
stored: stored ? structuredClone(stored) : undefined,
|
|
127
|
+
publish: (publication) => this.publishProviderModels(provider.id, generation, signal, publication),
|
|
128
|
+
allowNetwork,
|
|
129
|
+
force: allowNetwork ? force : undefined,
|
|
130
|
+
signal,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async refresh(options = {}) {
|
|
134
|
+
const allowNetwork = options.allowNetwork ?? true;
|
|
135
|
+
const callerSignal = operationSignal(options.signal);
|
|
136
|
+
const errors = new Map();
|
|
137
|
+
if (callerSignal.aborted)
|
|
138
|
+
return { aborted: true, errors };
|
|
139
|
+
const selected = options.providers ? new Set(options.providers) : undefined;
|
|
140
|
+
const refreshable = Array.from(this.providers.values()).filter((provider) => provider.refreshModels !== undefined && (!selected || selected.has(provider.id)));
|
|
141
|
+
const refresh = Promise.all(refreshable.map(async (provider) => {
|
|
142
|
+
const { generation, controller } = this.beginProviderRefresh(provider.id);
|
|
143
|
+
const signal = AbortSignal.any([callerSignal, controller.signal]);
|
|
144
|
+
const operation = (async () => {
|
|
145
|
+
let storedCredential;
|
|
146
|
+
let credentialError;
|
|
147
|
+
try {
|
|
148
|
+
storedCredential = await this.readCredential(provider.id, signal);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
credentialError = error;
|
|
152
|
+
}
|
|
153
|
+
// Restore cached provider state before auth resolution or network access.
|
|
154
|
+
await this.runProviderRefreshPhase(provider, storedCredential, false, undefined, generation, signal);
|
|
155
|
+
if (credentialError !== undefined)
|
|
156
|
+
throw credentialError;
|
|
157
|
+
if (!allowNetwork || signal.aborted)
|
|
158
|
+
return;
|
|
159
|
+
const credential = await this.resolveRefreshCredential(provider, storedCredential, signal);
|
|
160
|
+
if (!credential)
|
|
161
|
+
return;
|
|
162
|
+
await this.runProviderRefreshPhase(provider, credential, true, options.force, generation, signal);
|
|
163
|
+
})();
|
|
164
|
+
try {
|
|
165
|
+
await raceWithAbortSignal(operation, signal);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (!signal.aborted) {
|
|
169
|
+
errors.set(provider.id, error instanceof Error
|
|
170
|
+
? error
|
|
171
|
+
: new ModelsError("model_source", `Model refresh failed for ${provider.id}`, { cause: error }));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (this.refreshControllers.get(provider.id) === controller) {
|
|
176
|
+
this.refreshControllers.delete(provider.id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
180
|
+
try {
|
|
181
|
+
await raceWithAbortSignal(refresh, callerSignal);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!callerSignal.aborted)
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
return { aborted: callerSignal.aborted, errors: new Map(errors) };
|
|
188
|
+
}
|
|
189
|
+
async resolveRefreshCredential(provider, stored, signal) {
|
|
190
|
+
if (stored?.type === "oauth") {
|
|
191
|
+
const oauth = provider.auth.oauth;
|
|
192
|
+
if (!oauth)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (Date.now() < stored.expires)
|
|
195
|
+
return stored;
|
|
196
|
+
if (signal.aborted)
|
|
197
|
+
return undefined;
|
|
198
|
+
const post = await this.credentials.modify(provider.id, async (current) => {
|
|
199
|
+
if (current?.type !== "oauth" || Date.now() < current.expires)
|
|
200
|
+
return undefined;
|
|
201
|
+
return oauth.refresh(current, signal);
|
|
202
|
+
}, { signal });
|
|
203
|
+
return post?.type === "oauth" ? post : undefined;
|
|
204
|
+
}
|
|
205
|
+
const apiKey = provider.auth.apiKey;
|
|
206
|
+
if (!apiKey)
|
|
207
|
+
return undefined;
|
|
208
|
+
const credential = stored?.type === "api_key" ? stored : undefined;
|
|
209
|
+
const result = await apiKey.resolve({ ctx: this.authContext, credential, signal });
|
|
210
|
+
if (!result)
|
|
211
|
+
return undefined;
|
|
212
|
+
return { type: "api_key", key: result.auth.apiKey, env: result.env };
|
|
213
|
+
}
|
|
214
|
+
async readCredential(providerId, signal) {
|
|
215
|
+
try {
|
|
216
|
+
return await this.credentials.read(providerId, { signal });
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async checkProviderAuth(provider, credential, signal) {
|
|
223
|
+
if (credential?.type === "oauth") {
|
|
224
|
+
return provider.auth.oauth ? { source: "OAuth", type: "oauth" } : undefined;
|
|
225
|
+
}
|
|
226
|
+
const apiKey = provider.auth.apiKey;
|
|
227
|
+
if (!apiKey)
|
|
228
|
+
return undefined;
|
|
229
|
+
if (apiKey.check) {
|
|
230
|
+
try {
|
|
231
|
+
return await apiKey.check({
|
|
232
|
+
ctx: this.authContext,
|
|
233
|
+
credential: credential?.type === "api_key" ? credential : undefined,
|
|
234
|
+
signal,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new ModelsError("auth", `API key auth check failed for provider ${provider.id}`, { cause: error });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const resolution = await resolveProviderAuth(provider, this.credentials, this.authContext, { signal });
|
|
242
|
+
return resolution ? { source: resolution.source, type: "api_key" } : undefined;
|
|
243
|
+
}
|
|
244
|
+
checkAuth(providerId, options) {
|
|
245
|
+
const signal = operationSignal(options?.signal);
|
|
246
|
+
const check = (async () => {
|
|
247
|
+
signal.throwIfAborted();
|
|
248
|
+
const provider = this.providers.get(providerId);
|
|
249
|
+
if (!provider)
|
|
250
|
+
return undefined;
|
|
251
|
+
return this.checkProviderAuth(provider, await this.readCredential(providerId, signal), signal);
|
|
252
|
+
})();
|
|
253
|
+
return raceWithAbortSignal(check, signal);
|
|
254
|
+
}
|
|
255
|
+
getAvailable(providerId, options) {
|
|
256
|
+
const signal = operationSignal(options?.signal);
|
|
257
|
+
const available = (async () => {
|
|
258
|
+
signal.throwIfAborted();
|
|
259
|
+
const providers = providerId
|
|
260
|
+
? [this.providers.get(providerId)].filter((entry) => entry !== undefined)
|
|
261
|
+
: this.getProviders();
|
|
262
|
+
const checks = await Promise.all(providers.map(async (provider) => {
|
|
263
|
+
const credential = await this.readCredential(provider.id, signal);
|
|
264
|
+
return { provider, credential, auth: await this.checkProviderAuth(provider, credential, signal) };
|
|
265
|
+
}));
|
|
266
|
+
return checks.flatMap(({ provider, credential, auth }) => {
|
|
267
|
+
if (!auth)
|
|
268
|
+
return [];
|
|
269
|
+
const models = provider.getModels();
|
|
270
|
+
return provider.filterModels?.(models, credential) ?? models;
|
|
271
|
+
});
|
|
272
|
+
})();
|
|
273
|
+
return raceWithAbortSignal(available, signal);
|
|
274
|
+
}
|
|
275
|
+
async getAuth(providerOrModel, overrides) {
|
|
276
|
+
const signal = operationSignal(overrides?.signal);
|
|
277
|
+
const providerId = typeof providerOrModel === "string" ? providerOrModel : providerOrModel.provider;
|
|
278
|
+
const provider = this.providers.get(providerId);
|
|
279
|
+
if (!provider)
|
|
280
|
+
return undefined;
|
|
281
|
+
const result = await resolveProviderAuth(provider, this.credentials, this.authContext, { ...overrides, signal });
|
|
282
|
+
if (!result || typeof providerOrModel === "string" || !providerOrModel.headers)
|
|
283
|
+
return result;
|
|
284
|
+
return {
|
|
285
|
+
...result,
|
|
286
|
+
auth: {
|
|
287
|
+
...result.auth,
|
|
288
|
+
headers: mergeHeaders(result.auth.headers, providerOrModel.headers),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async login(providerId, type, interaction) {
|
|
293
|
+
const signal = operationSignal(interaction.signal);
|
|
294
|
+
signal.throwIfAborted();
|
|
295
|
+
const provider = this.providers.get(providerId);
|
|
296
|
+
if (!provider)
|
|
297
|
+
throw new ModelsError("provider", `Unknown provider: ${providerId}`);
|
|
298
|
+
const method = type === "oauth" ? provider.auth.oauth : provider.auth.apiKey;
|
|
299
|
+
if (!method?.login) {
|
|
300
|
+
throw new ModelsError("auth", `${provider.name} does not support ${type} login`);
|
|
301
|
+
}
|
|
302
|
+
const loginOperation = method.login({ ...interaction, signal });
|
|
303
|
+
const credential = await raceWithAbortSignal(loginOperation, signal);
|
|
304
|
+
let mutationStarted = false;
|
|
305
|
+
let markMutationStarted;
|
|
306
|
+
const started = new Promise((resolve) => {
|
|
307
|
+
markMutationStarted = resolve;
|
|
308
|
+
});
|
|
309
|
+
const mutation = this.credentials.modify(providerId, async () => {
|
|
310
|
+
mutationStarted = true;
|
|
311
|
+
markMutationStarted?.();
|
|
312
|
+
return credential;
|
|
313
|
+
}, { signal });
|
|
314
|
+
void mutation.catch(() => { });
|
|
315
|
+
try {
|
|
316
|
+
await new Promise((resolve, reject) => {
|
|
317
|
+
const onAbort = () => {
|
|
318
|
+
if (!mutationStarted)
|
|
319
|
+
reject(signal.reason);
|
|
320
|
+
};
|
|
321
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
322
|
+
void Promise.race([started, mutation]).then(() => {
|
|
323
|
+
signal.removeEventListener("abort", onAbort);
|
|
324
|
+
resolve();
|
|
325
|
+
}, (error) => {
|
|
326
|
+
signal.removeEventListener("abort", onAbort);
|
|
327
|
+
reject(error);
|
|
328
|
+
});
|
|
329
|
+
if (signal.aborted)
|
|
330
|
+
onAbort();
|
|
331
|
+
});
|
|
332
|
+
await mutation;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
signal.throwIfAborted();
|
|
336
|
+
throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error });
|
|
337
|
+
}
|
|
338
|
+
return credential;
|
|
339
|
+
}
|
|
340
|
+
async logout(providerId, options) {
|
|
341
|
+
const signal = operationSignal(options?.signal);
|
|
342
|
+
signal.throwIfAborted();
|
|
343
|
+
try {
|
|
344
|
+
await this.credentials.delete(providerId, { signal });
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
signal.throwIfAborted();
|
|
348
|
+
throw new ModelsError("auth", `Credential store delete failed for ${providerId}`, { cause: error });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
requireProvider(model) {
|
|
352
|
+
const provider = this.providers.get(model.provider);
|
|
353
|
+
if (!provider) {
|
|
354
|
+
throw new ModelsError("provider", `Unknown provider: ${model.provider}`);
|
|
355
|
+
}
|
|
356
|
+
return provider;
|
|
357
|
+
}
|
|
358
|
+
async applyAuth(model, options) {
|
|
359
|
+
this.requireProvider(model);
|
|
360
|
+
const resolution = await this.getAuth(model, {
|
|
361
|
+
apiKey: options?.apiKey,
|
|
362
|
+
env: options?.env,
|
|
363
|
+
signal: options?.signal,
|
|
364
|
+
});
|
|
365
|
+
if (!resolution) {
|
|
366
|
+
throw new ModelsError("auth", `Provider is not configured: ${model.provider}`);
|
|
367
|
+
}
|
|
368
|
+
const auth = resolution.auth;
|
|
369
|
+
// Explicit request options win per-field; the Models-only transform runs last.
|
|
370
|
+
const apiKey = options?.apiKey ?? auth.apiKey;
|
|
371
|
+
let headers = mergeHeaders(auth.headers, options?.headers);
|
|
372
|
+
if (options?.transformHeaders)
|
|
373
|
+
headers = await options.transformHeaders(headers ?? {});
|
|
374
|
+
const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined;
|
|
375
|
+
const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model;
|
|
376
|
+
const { transformHeaders: _transformHeaders, ...providerOptions } = options ?? {};
|
|
377
|
+
const requestOptions = { ...providerOptions, apiKey, headers, env };
|
|
378
|
+
return { requestModel, requestOptions };
|
|
379
|
+
}
|
|
380
|
+
stream(model, context, options) {
|
|
381
|
+
return lazyStream(model, async () => {
|
|
382
|
+
const provider = this.requireProvider(model);
|
|
383
|
+
const { requestModel, requestOptions } = await this.applyAuth(model, options);
|
|
384
|
+
return provider.stream(requestModel, context, requestOptions);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async complete(model, context, options) {
|
|
388
|
+
return this.stream(model, context, options).result();
|
|
389
|
+
}
|
|
390
|
+
streamSimple(model, context, options) {
|
|
391
|
+
return lazyStream(model, async () => {
|
|
392
|
+
const provider = this.requireProvider(model);
|
|
393
|
+
const { requestModel, requestOptions } = await this.applyAuth(model, options);
|
|
394
|
+
return provider.streamSimple(requestModel, context, requestOptions);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async completeSimple(model, context, options) {
|
|
398
|
+
return this.streamSimple(model, context, options).result();
|
|
399
|
+
}
|
|
400
|
+
async fetchDeferred(model, handle, options) {
|
|
401
|
+
return lazyStream(model, async () => {
|
|
402
|
+
const provider = this.requireProvider(model);
|
|
403
|
+
if (!provider.fetchDeferred) {
|
|
404
|
+
throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
|
|
405
|
+
}
|
|
406
|
+
const { requestModel, requestOptions } = await this.applyAuth(model, options);
|
|
407
|
+
return provider.fetchDeferred(requestModel, handle, requestOptions);
|
|
408
|
+
}).result();
|
|
409
|
+
}
|
|
410
|
+
async cancelDeferred(model, handle, options) {
|
|
411
|
+
const provider = this.requireProvider(model);
|
|
412
|
+
if (!provider.cancelDeferred) {
|
|
413
|
+
throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
|
|
414
|
+
}
|
|
415
|
+
const { requestModel, requestOptions } = await this.applyAuth(model, options);
|
|
416
|
+
await provider.cancelDeferred(requestModel, handle, requestOptions);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function createModels(options) {
|
|
420
|
+
return new ModelsImpl(options);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Builds a provider from parts. Built-in provider factories and models.json
|
|
424
|
+
* custom providers both go through this. A single `api` streams all models;
|
|
425
|
+
* an `api` map dispatches on `model.api`, and a model whose api has no entry
|
|
426
|
+
* produces a stream error.
|
|
427
|
+
*/
|
|
428
|
+
function createProvider(input) {
|
|
429
|
+
const baselineModels = input.models;
|
|
430
|
+
let dynamicModels = [];
|
|
431
|
+
const fetchModels = input.fetchModels;
|
|
432
|
+
const currentModels = () => {
|
|
433
|
+
const merged = [...baselineModels];
|
|
434
|
+
for (const model of dynamicModels) {
|
|
435
|
+
const index = merged.findIndex((entry) => entry.id === model.id);
|
|
436
|
+
if (index >= 0)
|
|
437
|
+
merged[index] = model;
|
|
438
|
+
else
|
|
439
|
+
merged.push(model);
|
|
440
|
+
}
|
|
441
|
+
return merged;
|
|
442
|
+
};
|
|
443
|
+
const single = typeof input.api.stream === "function" ? input.api : undefined;
|
|
444
|
+
const byApi = single ? undefined : input.api;
|
|
445
|
+
const apiFor = (model) => single ?? byApi?.[model.api];
|
|
446
|
+
const dispatch = (model, run) => {
|
|
447
|
+
const streams = apiFor(model);
|
|
448
|
+
if (!streams) {
|
|
449
|
+
return lazyStream(model, async () => {
|
|
450
|
+
throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model.api}"`);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return run(streams);
|
|
454
|
+
};
|
|
455
|
+
const provider = {
|
|
456
|
+
id: input.id,
|
|
457
|
+
name: input.name ?? input.id,
|
|
458
|
+
baseUrl: input.baseUrl,
|
|
459
|
+
headers: input.headers,
|
|
460
|
+
auth: input.auth,
|
|
461
|
+
getModels: currentModels,
|
|
462
|
+
refreshModels: fetchModels
|
|
463
|
+
? async (context) => {
|
|
464
|
+
if (context.stored) {
|
|
465
|
+
const restored = context.stored.models
|
|
466
|
+
.filter((model) => model.provider === input.id)
|
|
467
|
+
.map((model) => model);
|
|
468
|
+
if (!(await context.publish({
|
|
469
|
+
update: () => {
|
|
470
|
+
dynamicModels = restored;
|
|
471
|
+
},
|
|
472
|
+
}))) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!context.allowNetwork || context.signal.aborted)
|
|
477
|
+
return;
|
|
478
|
+
const refreshed = await fetchModels(context);
|
|
479
|
+
if (context.signal.aborted)
|
|
480
|
+
return;
|
|
481
|
+
await context.publish({
|
|
482
|
+
persist: { models: refreshed, checkedAt: Date.now() },
|
|
483
|
+
update: () => {
|
|
484
|
+
dynamicModels = refreshed;
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
: undefined,
|
|
489
|
+
filterModels: input.filterModels,
|
|
490
|
+
stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)),
|
|
491
|
+
streamSimple: (model, context, options) => dispatch(model, (streams) => streams.streamSimple(model, context, options)),
|
|
492
|
+
};
|
|
493
|
+
const streams = single ? [single] : Object.values(byApi ?? {}).filter((entry) => entry !== undefined);
|
|
494
|
+
if (streams.some((entry) => entry.fetchDeferred !== undefined)) {
|
|
495
|
+
provider.fetchDeferred = (model, handle, options) => lazyStream(model, async () => {
|
|
496
|
+
const implementation = apiFor(model);
|
|
497
|
+
if (!implementation?.fetchDeferred) {
|
|
498
|
+
throw new ModelsError("provider", `Provider ${input.id} does not support deferred responses for "${model.api}"`);
|
|
499
|
+
}
|
|
500
|
+
return implementation.fetchDeferred(model, handle, options);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (streams.some((entry) => entry.cancelDeferred !== undefined)) {
|
|
504
|
+
provider.cancelDeferred = async (model, handle, options) => {
|
|
505
|
+
const implementation = apiFor(model);
|
|
506
|
+
if (!implementation?.cancelDeferred) {
|
|
507
|
+
throw new ModelsError("provider", `Provider ${input.id} cannot cancel deferred responses for "${model.api}"`);
|
|
508
|
+
}
|
|
509
|
+
await implementation.cancelDeferred(model, handle, options);
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return provider;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Runtime-checked narrowing for dynamically looked-up models:
|
|
516
|
+
*
|
|
517
|
+
* ```ts
|
|
518
|
+
* const model = models.getModel("anthropic", "claude-opus-4-7");
|
|
519
|
+
* if (model && hasApi(model, "anthropic-messages")) {
|
|
520
|
+
* // model: Model<"anthropic-messages">, stream options fully typed
|
|
521
|
+
* }
|
|
522
|
+
* ```
|
|
523
|
+
*/
|
|
524
|
+
function hasApi(model, api) {
|
|
525
|
+
return model.api === api;
|
|
526
|
+
}
|
|
527
|
+
function calculateCost(model, usage) {
|
|
528
|
+
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
529
|
+
let rates = model.cost;
|
|
530
|
+
let matchedThreshold = -1;
|
|
531
|
+
for (const tier of model.cost.tiers ?? []) {
|
|
532
|
+
if (inputTokens > tier.inputTokensAbove && tier.inputTokensAbove > matchedThreshold) {
|
|
533
|
+
rates = tier;
|
|
534
|
+
matchedThreshold = tier.inputTokensAbove;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Anthropic charges 2x base input for 1h cache writes.
|
|
538
|
+
const longWrite = usage.cacheWrite1h ?? 0;
|
|
539
|
+
const shortWrite = usage.cacheWrite - longWrite;
|
|
540
|
+
usage.cost.input = (rates.input / 1000000) * usage.input;
|
|
541
|
+
usage.cost.output = (rates.output / 1000000) * usage.output;
|
|
542
|
+
usage.cost.cacheRead = (rates.cacheRead / 1000000) * usage.cacheRead;
|
|
543
|
+
usage.cost.cacheWrite = (rates.cacheWrite * shortWrite + rates.input * 2 * longWrite) / 1000000;
|
|
544
|
+
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
545
|
+
return usage.cost;
|
|
546
|
+
}
|
|
547
|
+
const EXTENDED_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
548
|
+
function getSupportedThinkingLevels(model) {
|
|
549
|
+
if (!model.reasoning)
|
|
550
|
+
return ["off"];
|
|
551
|
+
return EXTENDED_THINKING_LEVELS.filter((level) => {
|
|
552
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
553
|
+
if (mapped === null)
|
|
554
|
+
return false;
|
|
555
|
+
if (level === "xhigh" || level === "max")
|
|
556
|
+
return mapped !== undefined;
|
|
557
|
+
return true;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function clampThinkingLevel(model, level) {
|
|
561
|
+
const availableLevels = getSupportedThinkingLevels(model);
|
|
562
|
+
if (availableLevels.includes(level))
|
|
563
|
+
return level;
|
|
564
|
+
const requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level);
|
|
565
|
+
if (requestedIndex === -1)
|
|
566
|
+
return availableLevels[0] ?? "off";
|
|
567
|
+
for (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {
|
|
568
|
+
const candidate = EXTENDED_THINKING_LEVELS[i];
|
|
569
|
+
if (availableLevels.includes(candidate))
|
|
570
|
+
return candidate;
|
|
571
|
+
}
|
|
572
|
+
for (let i = requestedIndex - 1; i >= 0; i--) {
|
|
573
|
+
const candidate = EXTENDED_THINKING_LEVELS[i];
|
|
574
|
+
if (availableLevels.includes(candidate))
|
|
575
|
+
return candidate;
|
|
576
|
+
}
|
|
577
|
+
return availableLevels[0] ?? "off";
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Check if two models are equal by comparing both their id and provider.
|
|
581
|
+
* Returns false if either model is null or undefined.
|
|
582
|
+
*/
|
|
583
|
+
function modelsAreEqual(a, b) {
|
|
584
|
+
if (!a || !b)
|
|
585
|
+
return false;
|
|
586
|
+
return a.id === b.id && a.provider === b.provider;
|
|
587
|
+
}
|
|
588
|
+
//# sourceMappingURL=models.js.map
|
|
589
|
+
function headersToRecord(headers) {
|
|
590
|
+
const result = {};
|
|
591
|
+
for (const [key, value] of headers.entries()) {
|
|
592
|
+
result[key] = value;
|
|
593
|
+
}
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
function providerHeadersToRecord(headers) {
|
|
597
|
+
if (!headers)
|
|
598
|
+
return undefined;
|
|
599
|
+
const result = {};
|
|
600
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
601
|
+
if (value !== null)
|
|
602
|
+
result[key] = value;
|
|
603
|
+
}
|
|
604
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
605
|
+
}
|
|
606
|
+
//# sourceMappingURL=headers.js.map
|
|
607
|
+
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
|
|
608
|
+
function isProviderError(error) {
|
|
609
|
+
if (!(error instanceof Error) || !("status" in error) || !("headers" in error))
|
|
610
|
+
return false;
|
|
611
|
+
return ((error.status === undefined || typeof error.status === "number") &&
|
|
612
|
+
(error.headers === undefined || error.headers instanceof Headers));
|
|
613
|
+
}
|
|
614
|
+
/** Mirrors the pinned OpenAI/Anthropic SDK retry policy; review when either SDK is upgraded. */
|
|
615
|
+
function isRetryableProviderError(error) {
|
|
616
|
+
const shouldRetry = error.headers?.get("x-should-retry");
|
|
617
|
+
if (shouldRetry === "true")
|
|
618
|
+
return true;
|
|
619
|
+
if (shouldRetry === "false")
|
|
620
|
+
return false;
|
|
621
|
+
if (error.status === undefined)
|
|
622
|
+
return true;
|
|
623
|
+
return (error.status === 408 ||
|
|
624
|
+
error.status === 409 ||
|
|
625
|
+
error.status === 429 ||
|
|
626
|
+
(typeof error.status === "number" && error.status >= 500));
|
|
627
|
+
}
|
|
628
|
+
function validateServerRetryDelayMs(delayMs, maxRetryDelayMs, providerErrorMessage) {
|
|
629
|
+
const maxDelayMs = maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
630
|
+
if (maxDelayMs > 0 && delayMs > maxDelayMs) {
|
|
631
|
+
throw new Error(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxDelayMs / 1000)}s). ${providerErrorMessage}`);
|
|
632
|
+
}
|
|
633
|
+
return delayMs;
|
|
634
|
+
}
|
|
635
|
+
function getRetryDelayMs(error, retryIndex, maxRetryDelayMs) {
|
|
636
|
+
const retryAfterMs = error.headers?.get("retry-after-ms");
|
|
637
|
+
if (retryAfterMs) {
|
|
638
|
+
const value = Number.parseFloat(retryAfterMs);
|
|
639
|
+
if (!Number.isNaN(value))
|
|
640
|
+
return validateServerRetryDelayMs(value, maxRetryDelayMs, error.message);
|
|
641
|
+
}
|
|
642
|
+
const retryAfter = error.headers?.get("retry-after");
|
|
643
|
+
if (retryAfter) {
|
|
644
|
+
const seconds = Number.parseFloat(retryAfter);
|
|
645
|
+
const delayMs = Number.isNaN(seconds) ? Date.parse(retryAfter) - Date.now() : seconds * 1000;
|
|
646
|
+
return validateServerRetryDelayMs(delayMs, maxRetryDelayMs, error.message);
|
|
647
|
+
}
|
|
648
|
+
const exponentialDelay = Math.min(0.5 * 2 ** retryIndex, 8) * 1000;
|
|
649
|
+
return exponentialDelay * (1 - Math.random() * 0.25);
|
|
650
|
+
}
|
|
651
|
+
function createAbortError() {
|
|
652
|
+
const error = new Error("Request aborted");
|
|
653
|
+
error.name = "AbortError";
|
|
654
|
+
return error;
|
|
655
|
+
}
|
|
656
|
+
function abortableSleep(ms, signal) {
|
|
657
|
+
return new Promise((resolve, reject) => {
|
|
658
|
+
if (signal?.aborted) {
|
|
659
|
+
reject(createAbortError());
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const onAbort = () => {
|
|
663
|
+
clearTimeout(timeout);
|
|
664
|
+
reject(createAbortError());
|
|
665
|
+
};
|
|
666
|
+
const timeout = setTimeout(() => {
|
|
667
|
+
signal?.removeEventListener("abort", onAbort);
|
|
668
|
+
resolve();
|
|
669
|
+
}, Math.max(0, ms));
|
|
670
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Reproduce the retry behavior used by the OpenAI and Anthropic SDKs while making
|
|
675
|
+
* their backoff sleep interruptible. Their built-in retry timers ignore the
|
|
676
|
+
* request AbortSignal, so callers must invoke the SDK with `maxRetries: 0` and
|
|
677
|
+
* wrap the request with this helper. Provider-requested delays above
|
|
678
|
+
* `maxRetryDelayMs` fail immediately (60 seconds by default); set it to zero to
|
|
679
|
+
* disable the limit.
|
|
680
|
+
*/
|
|
681
|
+
async function retryProviderRequest(request, options = {}) {
|
|
682
|
+
const maxRetries = options.maxRetries ?? 0;
|
|
683
|
+
let retriesRemaining = maxRetries;
|
|
684
|
+
for (;;) {
|
|
685
|
+
try {
|
|
686
|
+
// Each retry is a fresh SDK request, so X-Stainless-Retry-Count remains zero.
|
|
687
|
+
return await request();
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
if (options.signal?.aborted)
|
|
691
|
+
throw createAbortError();
|
|
692
|
+
if (retriesRemaining <= 0 || !isProviderError(error) || !isRetryableProviderError(error))
|
|
693
|
+
throw error;
|
|
694
|
+
const retryIndex = maxRetries - retriesRemaining;
|
|
695
|
+
retriesRemaining--;
|
|
696
|
+
await abortableSleep(getRetryDelayMs(error, retryIndex, options.maxRetryDelayMs), options.signal);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
//# sourceMappingURL=provider-retry.js.map
|
|
701
|
+
/**
|
|
702
|
+
* Removes unpaired Unicode surrogate characters from a string.
|
|
703
|
+
*
|
|
704
|
+
* Unpaired surrogates (high surrogates 0xD800-0xDBFF without matching low surrogates 0xDC00-0xDFFF,
|
|
705
|
+
* or vice versa) cause JSON serialization errors in many API providers.
|
|
706
|
+
*
|
|
707
|
+
* Valid emoji and other characters outside the Basic Multilingual Plane use properly paired
|
|
708
|
+
* surrogates and will NOT be affected by this function.
|
|
709
|
+
*
|
|
710
|
+
* @param text - The text to sanitize
|
|
711
|
+
* @returns The sanitized text with unpaired surrogates removed
|
|
712
|
+
*
|
|
713
|
+
* @example
|
|
714
|
+
* // Valid emoji (properly paired surrogates) are preserved
|
|
715
|
+
* sanitizeSurrogates("Hello 🙈 World") // => "Hello 🙈 World"
|
|
716
|
+
*
|
|
717
|
+
* // Unpaired high surrogate is removed
|
|
718
|
+
* const unpaired = String.fromCharCode(0xD83D); // high surrogate without low
|
|
719
|
+
* sanitizeSurrogates(`Text ${unpaired} here`) // => "Text here"
|
|
720
|
+
*/
|
|
721
|
+
function sanitizeSurrogates(text) {
|
|
722
|
+
// Replace unpaired high surrogates (0xD800-0xDBFF not followed by low surrogate)
|
|
723
|
+
// Replace unpaired low surrogates (0xDC00-0xDFFF not preceded by high surrogate)
|
|
724
|
+
return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
|
|
725
|
+
}
|
|
726
|
+
//# sourceMappingURL=sanitize-unicode.js.map
|
|
727
|
+
function getGrammarToolInput(toolName, arguments_, inputProperty) {
|
|
728
|
+
const input = arguments_[inputProperty];
|
|
729
|
+
if (typeof input !== "string") {
|
|
730
|
+
throw new Error(`Grammar tool call "${toolName}" requires argument "${inputProperty}" to be a string.`);
|
|
731
|
+
}
|
|
732
|
+
return input;
|
|
733
|
+
}
|
|
734
|
+
function appendGrammarToolInputJsonDelta(buffer, inputProperty, nextInput, close) {
|
|
735
|
+
if (buffer.closed) {
|
|
736
|
+
if (close && nextInput === buffer.input)
|
|
737
|
+
return undefined;
|
|
738
|
+
throw new Error(`grammar tool input for property "${inputProperty}" changed after it was closed`);
|
|
739
|
+
}
|
|
740
|
+
if (!nextInput.startsWith(buffer.input)) {
|
|
741
|
+
throw new Error(`grammar tool input for property "${inputProperty}" changed non-monotonically`);
|
|
742
|
+
}
|
|
743
|
+
const inputDelta = nextInput.slice(buffer.input.length);
|
|
744
|
+
if (!close && inputDelta.length === 0)
|
|
745
|
+
return undefined;
|
|
746
|
+
let delta = "";
|
|
747
|
+
if (!buffer.started) {
|
|
748
|
+
delta += `{${JSON.stringify(inputProperty)}:"`;
|
|
749
|
+
buffer.started = true;
|
|
750
|
+
}
|
|
751
|
+
delta += JSON.stringify(inputDelta).slice(1, -1);
|
|
752
|
+
buffer.input = nextInput;
|
|
753
|
+
if (close) {
|
|
754
|
+
delta += '"}';
|
|
755
|
+
buffer.closed = true;
|
|
756
|
+
}
|
|
757
|
+
return delta;
|
|
758
|
+
}
|
|
759
|
+
function inferGrammarInputProperty(tool) {
|
|
760
|
+
const schema = tool.parameters;
|
|
761
|
+
if (schema.type !== "object") {
|
|
762
|
+
throw new Error("grammar constrained sampling requires an object parameter schema");
|
|
763
|
+
}
|
|
764
|
+
if (!Array.isArray(schema.required) || schema.required.length !== 1 || typeof schema.required[0] !== "string") {
|
|
765
|
+
throw new Error("grammar constrained sampling requires exactly one required string property");
|
|
766
|
+
}
|
|
767
|
+
const inputProperty = schema.required[0];
|
|
768
|
+
if (!schema.properties?.[inputProperty]) {
|
|
769
|
+
throw new Error(`grammar constrained sampling requires a properties entry for ${inputProperty}`);
|
|
770
|
+
}
|
|
771
|
+
if (schema.properties[inputProperty]?.type !== "string") {
|
|
772
|
+
throw new Error(`grammar constrained sampling property ${inputProperty} must have type string`);
|
|
773
|
+
}
|
|
774
|
+
return inputProperty;
|
|
775
|
+
}
|
|
776
|
+
function resolveJsonSchemaStrictSampling(tool, supportsStrictMode) {
|
|
777
|
+
const config = tool.constrainedSampling;
|
|
778
|
+
if (!config || config.type !== "json_schema") {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
if (supportsStrictMode) {
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
if (config.strict === "require") {
|
|
785
|
+
throw new Error(`Tool "${tool.name}" requires JSON-schema constrained sampling, but strict tools are unsupported.`);
|
|
786
|
+
}
|
|
787
|
+
return undefined;
|
|
788
|
+
}
|
|
789
|
+
function resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools) {
|
|
790
|
+
const config = tool.constrainedSampling;
|
|
791
|
+
if (!config || config.type !== "grammar") {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
if (!supportsOpenAIGrammarTools) {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
const larkDefinition = config.variants.openai_lark;
|
|
798
|
+
const regexDefinition = config.variants.openai_regex;
|
|
799
|
+
const hasLarkDefinition = typeof larkDefinition === "string" && larkDefinition.trim().length > 0;
|
|
800
|
+
const hasRegexDefinition = typeof regexDefinition === "string" && regexDefinition.trim().length > 0;
|
|
801
|
+
if (!hasLarkDefinition && !hasRegexDefinition) {
|
|
802
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: no supported grammar variant was provided.`);
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
return {
|
|
806
|
+
format: hasLarkDefinition ? "lark" : "regex",
|
|
807
|
+
definition: hasLarkDefinition ? larkDefinition : regexDefinition,
|
|
808
|
+
inputProperty: inferGrammarInputProperty(tool),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
813
|
+
throw new Error(`Tool "${tool.name}" cannot use grammar constrained sampling: ${message}.`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function createGrammarToolInputProperties(tools, supportsOpenAIGrammarTools) {
|
|
817
|
+
const properties = new Map();
|
|
818
|
+
for (const tool of tools ?? []) {
|
|
819
|
+
const grammar = resolveGrammarConstrainedSampling(tool, supportsOpenAIGrammarTools);
|
|
820
|
+
if (grammar) {
|
|
821
|
+
properties.set(tool.name, grammar.inputProperty);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return properties;
|
|
825
|
+
}
|
|
826
|
+
//# sourceMappingURL=constrained-sampling.js.map
|
|
827
|
+
const CHARS_PER_TOKEN = 4;
|
|
828
|
+
const ESTIMATED_IMAGE_CHARS = 4800;
|
|
829
|
+
function calculateContextTokens(usage) {
|
|
830
|
+
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
831
|
+
}
|
|
832
|
+
function safeJsonStringify(value) {
|
|
833
|
+
try {
|
|
834
|
+
return JSON.stringify(value) ?? "undefined";
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return "[unserializable]";
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function estimateTextAndImageContentChars(content) {
|
|
841
|
+
if (typeof content === "string")
|
|
842
|
+
return content.length;
|
|
843
|
+
let chars = 0;
|
|
844
|
+
for (const block of content)
|
|
845
|
+
chars += block.type === "text" ? block.text.length : ESTIMATED_IMAGE_CHARS;
|
|
846
|
+
return chars;
|
|
847
|
+
}
|
|
848
|
+
function estimateTextTokens(text) {
|
|
849
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
850
|
+
}
|
|
851
|
+
function estimateTextAndImageContentTokens(content) {
|
|
852
|
+
return Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);
|
|
853
|
+
}
|
|
854
|
+
function estimateMessageTokens(message) {
|
|
855
|
+
let chars = 0;
|
|
856
|
+
if (message.role === "user")
|
|
857
|
+
return estimateTextAndImageContentTokens(message.content);
|
|
858
|
+
if (message.role === "toolResult")
|
|
859
|
+
return estimateTextAndImageContentTokens(message.content);
|
|
860
|
+
for (const block of message.content) {
|
|
861
|
+
if (block.type === "text") {
|
|
862
|
+
chars += block.text.length;
|
|
863
|
+
}
|
|
864
|
+
else if (block.type === "thinking") {
|
|
865
|
+
chars += block.thinking.length;
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
chars += block.name.length + safeJsonStringify(block.arguments).length;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
872
|
+
}
|
|
873
|
+
function getLastAssistantUsageInfo(messages) {
|
|
874
|
+
let latestPrefixTimestamp = Number.NEGATIVE_INFINITY;
|
|
875
|
+
let usageInfo;
|
|
876
|
+
for (let i = 0; i < messages.length; i++) {
|
|
877
|
+
const message = messages[i];
|
|
878
|
+
if (message.role === "assistant") {
|
|
879
|
+
const assistant = message;
|
|
880
|
+
// A newer prefix message was inserted after this response (for example, a
|
|
881
|
+
// compaction summary), so its usage cannot describe the current prefix.
|
|
882
|
+
const usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;
|
|
883
|
+
if (usageAppliesToPrefix &&
|
|
884
|
+
assistant.stopReason !== "aborted" &&
|
|
885
|
+
assistant.stopReason !== "error" &&
|
|
886
|
+
calculateContextTokens(assistant.usage) > 0) {
|
|
887
|
+
usageInfo = { usage: assistant.usage, index: i };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
latestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);
|
|
891
|
+
}
|
|
892
|
+
return usageInfo;
|
|
893
|
+
}
|
|
894
|
+
function estimateMessages(messages) {
|
|
895
|
+
const usageInfo = getLastAssistantUsageInfo(messages);
|
|
896
|
+
if (usageInfo) {
|
|
897
|
+
const usageTokens = calculateContextTokens(usageInfo.usage);
|
|
898
|
+
let trailingTokens = 0;
|
|
899
|
+
for (let i = usageInfo.index + 1; i < messages.length; i++) {
|
|
900
|
+
trailingTokens += estimateMessageTokens(messages[i]);
|
|
901
|
+
}
|
|
902
|
+
return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };
|
|
903
|
+
}
|
|
904
|
+
let tokens = 0;
|
|
905
|
+
for (const message of messages)
|
|
906
|
+
tokens += estimateMessageTokens(message);
|
|
907
|
+
return { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };
|
|
908
|
+
}
|
|
909
|
+
function estimateToolsTokens(tools) {
|
|
910
|
+
if (!tools || tools.length === 0)
|
|
911
|
+
return 0;
|
|
912
|
+
return estimateTextTokens(safeJsonStringify(tools));
|
|
913
|
+
}
|
|
914
|
+
function isMessageArray(value) {
|
|
915
|
+
return Array.isArray(value);
|
|
916
|
+
}
|
|
917
|
+
function estimateContextTokens(context) {
|
|
918
|
+
if (isMessageArray(context))
|
|
919
|
+
return estimateMessages(context);
|
|
920
|
+
const estimate = estimateMessages(context.messages);
|
|
921
|
+
if (estimate.lastUsageIndex !== null) {
|
|
922
|
+
const addedNames = new Set(context.messages
|
|
923
|
+
.slice(estimate.lastUsageIndex + 1)
|
|
924
|
+
.filter((message) => message.role === "toolResult")
|
|
925
|
+
.flatMap((message) => message.addedToolNames ?? []));
|
|
926
|
+
const addedToolTokens = estimateToolsTokens(context.tools?.filter((tool) => addedNames.has(tool.name)));
|
|
927
|
+
return {
|
|
928
|
+
tokens: estimate.tokens + addedToolTokens,
|
|
929
|
+
usageTokens: estimate.usageTokens,
|
|
930
|
+
trailingTokens: estimate.trailingTokens + addedToolTokens,
|
|
931
|
+
lastUsageIndex: estimate.lastUsageIndex,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const prefixTokens = (context.systemPrompt ? estimateTextTokens(context.systemPrompt) : 0) + estimateToolsTokens(context.tools);
|
|
935
|
+
return {
|
|
936
|
+
tokens: estimate.tokens + prefixTokens,
|
|
937
|
+
usageTokens: estimate.usageTokens,
|
|
938
|
+
trailingTokens: estimate.trailingTokens + prefixTokens,
|
|
939
|
+
lastUsageIndex: estimate.lastUsageIndex,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
//# sourceMappingURL=estimate.js.map
|
|
943
|
+
|
|
944
|
+
const CONTEXT_SAFETY_TOKENS = 4096;
|
|
945
|
+
const MIN_MAX_TOKENS = 1;
|
|
946
|
+
function clampMaxTokensToContext(model, context, maxTokens) {
|
|
947
|
+
if (model.contextWindow <= 0)
|
|
948
|
+
return Math.max(MIN_MAX_TOKENS, maxTokens);
|
|
949
|
+
const available = model.contextWindow - estimateContextTokens(context).tokens - CONTEXT_SAFETY_TOKENS;
|
|
950
|
+
return Math.min(maxTokens, Math.max(MIN_MAX_TOKENS, available));
|
|
951
|
+
}
|
|
952
|
+
function buildBaseOptions(model, context, options, apiKey) {
|
|
953
|
+
const samplingParams = model.samplingParams || options?.samplingParams
|
|
954
|
+
? { ...model.samplingParams, ...options?.samplingParams }
|
|
955
|
+
: undefined;
|
|
956
|
+
return {
|
|
957
|
+
temperature: options?.temperature,
|
|
958
|
+
samplingParams,
|
|
959
|
+
maxTokens: clampMaxTokensToContext(model, context, options?.maxTokens ?? model.maxTokens),
|
|
960
|
+
signal: options?.signal,
|
|
961
|
+
telemetryContext: options?.telemetryContext,
|
|
962
|
+
apiKey: apiKey || options?.apiKey,
|
|
963
|
+
fetch: options?.fetch,
|
|
964
|
+
transport: options?.transport,
|
|
965
|
+
cacheRetention: options?.cacheRetention,
|
|
966
|
+
sessionId: options?.sessionId,
|
|
967
|
+
headers: options?.headers,
|
|
968
|
+
onPayload: options?.onPayload,
|
|
969
|
+
onResponse: options?.onResponse,
|
|
970
|
+
timeoutMs: options?.timeoutMs,
|
|
971
|
+
websocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,
|
|
972
|
+
maxRetries: options?.maxRetries,
|
|
973
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
974
|
+
metadata: options?.metadata,
|
|
975
|
+
env: options?.env,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/** Tokens always left for the answer when a thinking budget shares the response ceiling. */
|
|
979
|
+
const MIN_ANSWER_TOKENS = 1024;
|
|
980
|
+
function clampReasoning(effort) {
|
|
981
|
+
return effort === "xhigh" || effort === "max" ? "high" : effort;
|
|
982
|
+
}
|
|
983
|
+
function adjustMaxTokensForThinking(
|
|
984
|
+
// Undefined means no explicit caller cap. Use the model cap and fit thinking inside it.
|
|
985
|
+
baseMaxTokens, modelMaxTokens, reasoningLevel, customBudgets) {
|
|
986
|
+
const defaultBudgets = {
|
|
987
|
+
minimal: 1024,
|
|
988
|
+
low: 2048,
|
|
989
|
+
medium: 8192,
|
|
990
|
+
high: 16384,
|
|
991
|
+
};
|
|
992
|
+
const budgets = { ...defaultBudgets, ...customBudgets };
|
|
993
|
+
const level = clampReasoning(reasoningLevel);
|
|
994
|
+
let thinkingBudget = budgets[level];
|
|
995
|
+
const maxTokens = baseMaxTokens === undefined ? modelMaxTokens : Math.min(baseMaxTokens + thinkingBudget, modelMaxTokens);
|
|
996
|
+
if (maxTokens <= thinkingBudget) {
|
|
997
|
+
thinkingBudget = Math.max(0, maxTokens - MIN_ANSWER_TOKENS);
|
|
998
|
+
}
|
|
999
|
+
return { maxTokens, thinkingBudget };
|
|
1000
|
+
}
|
|
1001
|
+
//# sourceMappingURL=simple-options.js.map
|
|
1002
|
+
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
1003
|
+
const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
1004
|
+
function replaceImagesWithPlaceholder(content, placeholder) {
|
|
1005
|
+
const result = [];
|
|
1006
|
+
let previousWasPlaceholder = false;
|
|
1007
|
+
for (const block of content) {
|
|
1008
|
+
if (block.type === "image") {
|
|
1009
|
+
if (!previousWasPlaceholder) {
|
|
1010
|
+
result.push({ type: "text", text: placeholder });
|
|
1011
|
+
}
|
|
1012
|
+
previousWasPlaceholder = true;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
result.push(block);
|
|
1016
|
+
previousWasPlaceholder = block.text === placeholder;
|
|
1017
|
+
}
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
1020
|
+
function downgradeUnsupportedImages(messages, model) {
|
|
1021
|
+
if (model.input.includes("image")) {
|
|
1022
|
+
return messages;
|
|
1023
|
+
}
|
|
1024
|
+
return messages.map((msg) => {
|
|
1025
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
1026
|
+
return {
|
|
1027
|
+
...msg,
|
|
1028
|
+
content: replaceImagesWithPlaceholder(msg.content, NON_VISION_USER_IMAGE_PLACEHOLDER),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (msg.role === "toolResult") {
|
|
1032
|
+
return {
|
|
1033
|
+
...msg,
|
|
1034
|
+
content: replaceImagesWithPlaceholder(msg.content, NON_VISION_TOOL_IMAGE_PLACEHOLDER),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
return msg;
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Normalize tool call ID for cross-provider compatibility.
|
|
1042
|
+
* OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
|
|
1043
|
+
* Anthropic APIs require IDs matching ^[a-zA-Z0-9_-]+$ (max 64 chars).
|
|
1044
|
+
*/
|
|
1045
|
+
function transformMessages(messages, model, normalizeToolCallId) {
|
|
1046
|
+
// Build a map of original tool call IDs to normalized IDs
|
|
1047
|
+
const toolCallIdMap = new Map();
|
|
1048
|
+
// Normalize null/undefined content from untyped callers (custom tools, hand-built
|
|
1049
|
+
// histories, old session files) so downstream code can rely on the type contract.
|
|
1050
|
+
const normalizedMessages = messages.map((msg) => (msg.content == null ? { ...msg, content: [] } : msg));
|
|
1051
|
+
const imageAwareMessages = downgradeUnsupportedImages(normalizedMessages, model);
|
|
1052
|
+
// First pass: transform messages (unsupported image downgrade, thinking blocks, tool call ID normalization)
|
|
1053
|
+
const transformed = imageAwareMessages.map((msg) => {
|
|
1054
|
+
// User messages pass through unchanged
|
|
1055
|
+
if (msg.role === "user") {
|
|
1056
|
+
return msg;
|
|
1057
|
+
}
|
|
1058
|
+
// Handle toolResult messages - normalize toolCallId if we have a mapping
|
|
1059
|
+
if (msg.role === "toolResult") {
|
|
1060
|
+
const normalizedId = toolCallIdMap.get(msg.toolCallId);
|
|
1061
|
+
if (normalizedId && normalizedId !== msg.toolCallId) {
|
|
1062
|
+
return { ...msg, toolCallId: normalizedId };
|
|
1063
|
+
}
|
|
1064
|
+
return msg;
|
|
1065
|
+
}
|
|
1066
|
+
// Assistant messages need transformation check
|
|
1067
|
+
if (msg.role === "assistant") {
|
|
1068
|
+
const assistantMsg = msg;
|
|
1069
|
+
const isSameModel = assistantMsg.provider === model.provider &&
|
|
1070
|
+
assistantMsg.api === model.api &&
|
|
1071
|
+
assistantMsg.model === model.id;
|
|
1072
|
+
const transformedContent = assistantMsg.content.flatMap((block) => {
|
|
1073
|
+
if (block.type === "thinking") {
|
|
1074
|
+
// Redacted thinking is opaque encrypted content, only valid for the same model.
|
|
1075
|
+
// Drop it for cross-model to avoid API errors.
|
|
1076
|
+
if (block.redacted) {
|
|
1077
|
+
return isSameModel ? block : [];
|
|
1078
|
+
}
|
|
1079
|
+
// For same model: keep thinking blocks with signatures (needed for replay)
|
|
1080
|
+
// even if the thinking text is empty (OpenAI encrypted reasoning)
|
|
1081
|
+
if (isSameModel && block.thinkingSignature)
|
|
1082
|
+
return block;
|
|
1083
|
+
// Skip empty thinking blocks, convert others to plain text
|
|
1084
|
+
if (!block.thinking || block.thinking.trim() === "")
|
|
1085
|
+
return [];
|
|
1086
|
+
if (isSameModel)
|
|
1087
|
+
return block;
|
|
1088
|
+
return {
|
|
1089
|
+
type: "text",
|
|
1090
|
+
text: block.thinking,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (block.type === "text") {
|
|
1094
|
+
if (isSameModel)
|
|
1095
|
+
return block;
|
|
1096
|
+
return {
|
|
1097
|
+
type: "text",
|
|
1098
|
+
text: block.text,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
if (block.type === "toolCall") {
|
|
1102
|
+
const toolCall = block;
|
|
1103
|
+
let normalizedToolCall = toolCall;
|
|
1104
|
+
if (!isSameModel && toolCall.thoughtSignature) {
|
|
1105
|
+
normalizedToolCall = { ...toolCall };
|
|
1106
|
+
delete normalizedToolCall.thoughtSignature;
|
|
1107
|
+
}
|
|
1108
|
+
if (!isSameModel && normalizeToolCallId) {
|
|
1109
|
+
const normalizedId = normalizeToolCallId(toolCall.id, model, assistantMsg);
|
|
1110
|
+
if (normalizedId !== toolCall.id) {
|
|
1111
|
+
toolCallIdMap.set(toolCall.id, normalizedId);
|
|
1112
|
+
normalizedToolCall = { ...normalizedToolCall, id: normalizedId };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return normalizedToolCall;
|
|
1116
|
+
}
|
|
1117
|
+
return block;
|
|
1118
|
+
});
|
|
1119
|
+
return {
|
|
1120
|
+
...assistantMsg,
|
|
1121
|
+
content: transformedContent,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
return msg;
|
|
1125
|
+
});
|
|
1126
|
+
// Second pass: insert synthetic empty tool results for orphaned tool calls
|
|
1127
|
+
// This preserves thinking signatures and satisfies API requirements
|
|
1128
|
+
const result = [];
|
|
1129
|
+
let pendingToolCalls = [];
|
|
1130
|
+
let existingToolResultIds = new Set();
|
|
1131
|
+
const insertSyntheticToolResults = () => {
|
|
1132
|
+
if (pendingToolCalls.length > 0) {
|
|
1133
|
+
for (const tc of pendingToolCalls) {
|
|
1134
|
+
if (!existingToolResultIds.has(tc.id)) {
|
|
1135
|
+
result.push({
|
|
1136
|
+
role: "toolResult",
|
|
1137
|
+
toolCallId: tc.id,
|
|
1138
|
+
toolName: tc.name,
|
|
1139
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
1140
|
+
isError: true,
|
|
1141
|
+
timestamp: Date.now(),
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
pendingToolCalls = [];
|
|
1146
|
+
existingToolResultIds = new Set();
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
for (let i = 0; i < transformed.length; i++) {
|
|
1150
|
+
const msg = transformed[i];
|
|
1151
|
+
if (msg.role === "assistant") {
|
|
1152
|
+
// If we have pending orphaned tool calls from a previous assistant, insert synthetic results now
|
|
1153
|
+
insertSyntheticToolResults();
|
|
1154
|
+
// Skip errored/aborted assistant messages entirely.
|
|
1155
|
+
// These are incomplete turns that shouldn't be replayed:
|
|
1156
|
+
// - May have partial content (reasoning without message, incomplete tool calls)
|
|
1157
|
+
// - Replaying them can cause API errors (e.g., OpenAI "reasoning without following item")
|
|
1158
|
+
// - The model should retry from the last valid state
|
|
1159
|
+
const assistantMsg = msg;
|
|
1160
|
+
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
// Track tool calls from this assistant message
|
|
1164
|
+
const toolCalls = assistantMsg.content.filter((b) => b.type === "toolCall");
|
|
1165
|
+
if (toolCalls.length > 0) {
|
|
1166
|
+
pendingToolCalls = toolCalls;
|
|
1167
|
+
existingToolResultIds = new Set();
|
|
1168
|
+
}
|
|
1169
|
+
result.push(msg);
|
|
1170
|
+
}
|
|
1171
|
+
else if (msg.role === "toolResult") {
|
|
1172
|
+
existingToolResultIds.add(msg.toolCallId);
|
|
1173
|
+
result.push(msg);
|
|
1174
|
+
}
|
|
1175
|
+
else if (msg.role === "user") {
|
|
1176
|
+
// User message interrupts tool flow - insert synthetic results for orphaned calls
|
|
1177
|
+
insertSyntheticToolResults();
|
|
1178
|
+
result.push(msg);
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
result.push(msg);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// If the conversation ends with unresolved tool calls, synthesize results now.
|
|
1185
|
+
insertSyntheticToolResults();
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
//# sourceMappingURL=transform-messages.js.map
|
|
1189
|
+
export { adjustMaxTokensForThinking, appendGrammarToolInputJsonDelta, buildBaseOptions, calculateCost, clampMaxTokensToContext, clampReasoning, clampThinkingLevel, createGrammarToolInputProperties, getGrammarToolInput, headersToRecord, providerHeadersToRecord, resolveGrammarConstrainedSampling, resolveJsonSchemaStrictSampling, retryProviderRequest, sanitizeSurrogates, transformMessages };
|