@agentionai/agents 1.7.0-beta.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/agents/Agent.d.ts +9 -2
- package/dist/agents/Agent.js +4 -0
- package/dist/agents/AgentConfig.d.ts +76 -2
- package/dist/agents/anthropic/ClaudeAgent.js +2 -2
- package/dist/agents/errors/AgentError.d.ts +34 -2
- package/dist/agents/errors/AgentError.js +37 -1
- package/dist/agents/google/GeminiAgent.js +7 -2
- package/dist/agents/mistral/MistralAgent.js +5 -2
- package/dist/agents/openai/OpenAiAgent.js +8 -2
- package/dist/agents/openai-compatible/OpenAICompatibleAgent.js +2 -2
- package/dist/agents/openrouter/OpenRouterAgent.d.ts +234 -0
- package/dist/agents/openrouter/OpenRouterAgent.js +754 -0
- package/dist/agents/openrouter/types.d.ts +164 -0
- package/dist/agents/openrouter/types.js +15 -0
- package/dist/history/transformers.d.ts +80 -0
- package/dist/history/transformers.js +156 -1
- package/dist/history/types.d.ts +22 -2
- package/dist/history/types.js +13 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/openrouter.d.ts +6 -0
- package/dist/openrouter.js +24 -0
- package/dist/viz/types.d.ts +1 -1
- package/package.json +10 -1
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OpenRouterAgent = void 0;
|
|
37
|
+
exports.defaultHeadersHook = defaultHeadersHook;
|
|
38
|
+
exports.parseRetryAfter = parseRetryAfter;
|
|
39
|
+
exports.parseResetAt = parseResetAt;
|
|
40
|
+
const BaseAgent_1 = require("../BaseAgent");
|
|
41
|
+
const AgentEvent_1 = require("../AgentEvent");
|
|
42
|
+
const cancellation_1 = require("../cancellation");
|
|
43
|
+
const AgentError_1 = require("../errors/AgentError");
|
|
44
|
+
const transformers_1 = require("../../history/transformers");
|
|
45
|
+
const VizReporter_1 = require("../../viz/VizReporter");
|
|
46
|
+
const VizConfig_1 = require("../../viz/VizConfig");
|
|
47
|
+
/**
|
|
48
|
+
* Retry policy the agent applies when the config does not name one.
|
|
49
|
+
*
|
|
50
|
+
* `@openrouter/sdk` already knows how to honour `Retry-After` and
|
|
51
|
+
* `retry-after-ms` (`lib/retries.js`), but `chat.send()` defaults to
|
|
52
|
+
* `retryCodes: ["5XX"]`, so a 429 never reaches that code — and its default
|
|
53
|
+
* `maxElapsedTime` is 3_600_000, an hour-long retry loop. Both are replaced
|
|
54
|
+
* here; see {@link OpenRouterSpecificConfig.retry}.
|
|
55
|
+
*/
|
|
56
|
+
const DEFAULT_RETRY = {
|
|
57
|
+
strategy: "backoff",
|
|
58
|
+
backoff: {
|
|
59
|
+
initialInterval: 500,
|
|
60
|
+
maxInterval: 30000,
|
|
61
|
+
exponent: 1.5,
|
|
62
|
+
// Two minutes: long enough to sit out a couple of `Retry-After` waits on a
|
|
63
|
+
// per-minute limit, short enough that a daily quota (whose reset is hours
|
|
64
|
+
// away) fails fast instead of blocking the run. Use `models` fallbacks for
|
|
65
|
+
// that case, not a longer wait.
|
|
66
|
+
maxElapsedTime: 120000,
|
|
67
|
+
},
|
|
68
|
+
retryConnectionErrors: true,
|
|
69
|
+
};
|
|
70
|
+
/** Status codes retried by default. `429` is the one the SDK omits. */
|
|
71
|
+
const DEFAULT_RETRY_CODES = ["408", "409", "429", "5XX"];
|
|
72
|
+
/**
|
|
73
|
+
* Build a `beforeRequest` hook that adds custom headers to every request.
|
|
74
|
+
*
|
|
75
|
+
* `@openrouter/sdk` has no `defaultHeaders` option like the Anthropic and
|
|
76
|
+
* OpenAI clients, so headers are injected at the HTTP layer instead. They
|
|
77
|
+
* overwrite headers the SDK already set, so that `defaultHeaders` means the
|
|
78
|
+
* same thing on every provider — see `CommonAgentConfig.defaultHeaders`.
|
|
79
|
+
*
|
|
80
|
+
* `httpReferer` / `appTitle` are a separate OpenRouter attribution path
|
|
81
|
+
* (`HTTP-Referer` / `X-Title`). They are not a substitute for tracing or
|
|
82
|
+
* gateway headers.
|
|
83
|
+
*/
|
|
84
|
+
function defaultHeadersHook(headers) {
|
|
85
|
+
return (request) => {
|
|
86
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
87
|
+
request.headers.set(name, value);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Agent backed by [OpenRouter](https://openrouter.ai) via the official
|
|
93
|
+
* `@openrouter/sdk`, giving one API key access to models from every provider it
|
|
94
|
+
* fronts.
|
|
95
|
+
*
|
|
96
|
+
* Beyond what an OpenAI-compatible endpoint offers, this agent exposes
|
|
97
|
+
* OpenRouter's routing controls — `models` fallbacks, `provider` preferences —
|
|
98
|
+
* reports the credit cost of each run on {@link lastGeneration}, and round-trips
|
|
99
|
+
* `reasoning_details` so multi-turn tool calls work on reasoning models whose
|
|
100
|
+
* thinking blocks are signed.
|
|
101
|
+
*
|
|
102
|
+
* @requires @openrouter/sdk - Install as a peer dependency:
|
|
103
|
+
* ```bash
|
|
104
|
+
* npm install @openrouter/sdk
|
|
105
|
+
* ```
|
|
106
|
+
* The SDK is ESM-only, so it is loaded through a dynamic import. On CommonJS
|
|
107
|
+
* that needs Node 20.19+ or 22.12+, where `require()` of an ES module works.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const agent = new OpenRouterAgent({
|
|
112
|
+
* id: "router",
|
|
113
|
+
* name: "Router",
|
|
114
|
+
* description: "Answers questions",
|
|
115
|
+
* apiKey: process.env.OPENROUTER_API_KEY!,
|
|
116
|
+
* model: "anthropic/claude-opus-4-20250514",
|
|
117
|
+
* models: ["openai/gpt-5.6"], // used if the primary is rate limited
|
|
118
|
+
* provider: { sort: "throughput" },
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* const answer = await agent.execute("Explain recursion");
|
|
122
|
+
* console.log(agent.lastGeneration?.cost, "credits");
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
class OpenRouterAgent extends BaseAgent_1.BaseAgent {
|
|
126
|
+
constructor(config, history) {
|
|
127
|
+
super({ ...config, vendor: "openrouter" }, history);
|
|
128
|
+
this.currentToolCallCount = 0;
|
|
129
|
+
// Flat config wins over the nested form, matching the other agents.
|
|
130
|
+
const nested = config.vendorConfig?.openrouter ?? {};
|
|
131
|
+
this.config = {
|
|
132
|
+
...config,
|
|
133
|
+
vendor: "openrouter",
|
|
134
|
+
model: config.model || "openrouter/auto",
|
|
135
|
+
models: config.models ?? nested.models,
|
|
136
|
+
provider: config.provider ?? nested.provider,
|
|
137
|
+
retry: config.retry ?? nested.retry,
|
|
138
|
+
retryCodes: config.retryCodes ?? nested.retryCodes,
|
|
139
|
+
reasoning: config.reasoning ?? nested.reasoning,
|
|
140
|
+
plugins: config.plugins ?? nested.plugins,
|
|
141
|
+
sessionId: config.sessionId ?? nested.sessionId,
|
|
142
|
+
user: config.user ?? nested.user,
|
|
143
|
+
serviceTier: config.serviceTier ?? nested.serviceTier,
|
|
144
|
+
httpReferer: config.httpReferer ?? nested.httpReferer,
|
|
145
|
+
appTitle: config.appTitle ?? nested.appTitle,
|
|
146
|
+
disableParallelToolUse: config.disableParallelToolUse ?? nested.disableParallelToolUse,
|
|
147
|
+
};
|
|
148
|
+
this.model = this.config.model;
|
|
149
|
+
this.addSystemMessage(this.getSystemMessage());
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Load `@openrouter/sdk` and construct the client, once per agent.
|
|
153
|
+
*
|
|
154
|
+
* The specifier goes through a variable so TypeScript does not resolve it at
|
|
155
|
+
* build time, which keeps the dependency genuinely optional — the same
|
|
156
|
+
* approach `MCPClient` uses. The promise is memoized including its rejection,
|
|
157
|
+
* so a missing package reports the install hint on every call rather than
|
|
158
|
+
* retrying the import.
|
|
159
|
+
*/
|
|
160
|
+
getClient() {
|
|
161
|
+
if (!this.clientPromise) {
|
|
162
|
+
this.clientPromise = this.createClient();
|
|
163
|
+
}
|
|
164
|
+
return this.clientPromise;
|
|
165
|
+
}
|
|
166
|
+
async createClient() {
|
|
167
|
+
const pkg = "@openrouter/sdk";
|
|
168
|
+
let OpenRouter;
|
|
169
|
+
let HTTPClient;
|
|
170
|
+
try {
|
|
171
|
+
({ OpenRouter, HTTPClient } = (await Promise.resolve(`${pkg}`).then(s => __importStar(require(s)))));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new AgentError_1.ExecutionError(`OpenRouterAgent requires the '@openrouter/sdk' package. Install it with: npm install @openrouter/sdk` +
|
|
175
|
+
`\nUnderlying error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
176
|
+
}
|
|
177
|
+
let httpClient;
|
|
178
|
+
if (this.config.defaultHeaders) {
|
|
179
|
+
httpClient = new HTTPClient();
|
|
180
|
+
httpClient.addHook("beforeRequest", defaultHeadersHook(this.config.defaultHeaders));
|
|
181
|
+
}
|
|
182
|
+
return new OpenRouter({
|
|
183
|
+
apiKey: this.config.apiKey,
|
|
184
|
+
...(this.config.baseURL ? { serverURL: this.config.baseURL } : {}),
|
|
185
|
+
...(this.config.httpReferer ? { httpReferer: this.config.httpReferer } : {}),
|
|
186
|
+
...(this.config.appTitle ? { appTitle: this.config.appTitle } : {}),
|
|
187
|
+
...(this.config.timeout ? { timeoutMs: this.config.timeout } : {}),
|
|
188
|
+
...(this.config.debug ? { debugLogger: console } : {}),
|
|
189
|
+
...(httpClient ? { httpClient } : {}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
getToolDefinitions() {
|
|
193
|
+
return Array.from(this.tools.values()).map((tool) => {
|
|
194
|
+
const prompt = tool.getPrompt();
|
|
195
|
+
return {
|
|
196
|
+
type: "function",
|
|
197
|
+
function: {
|
|
198
|
+
name: prompt.name,
|
|
199
|
+
description: prompt.description,
|
|
200
|
+
parameters: prompt.input_schema,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async process(_input) {
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* List the models OpenRouter offers, following pagination to the end.
|
|
210
|
+
*
|
|
211
|
+
* Fills `contextLength`, `maxOutputTokens` and `capabilities` from
|
|
212
|
+
* OpenRouter's own metadata: `supported_parameters` says whether a model takes
|
|
213
|
+
* `tools` and `reasoning`, and `architecture.input_modalities` whether it
|
|
214
|
+
* accepts images. Per-token pricing is on `raw.pricing`.
|
|
215
|
+
*/
|
|
216
|
+
async listModels() {
|
|
217
|
+
try {
|
|
218
|
+
const client = await this.getClient();
|
|
219
|
+
const result = await client.models.list();
|
|
220
|
+
const models = [];
|
|
221
|
+
for await (const page of result) {
|
|
222
|
+
// `models.list()` yields GetModelsResponse: `{ result: { data, links, totalCount } }`.
|
|
223
|
+
const entries = page?.result?.data ?? [];
|
|
224
|
+
for (const card of entries) {
|
|
225
|
+
const params = card.supportedParameters ?? [];
|
|
226
|
+
const modalities = card.architecture?.inputModalities ?? [];
|
|
227
|
+
models.push({
|
|
228
|
+
id: card.id,
|
|
229
|
+
displayName: card.name,
|
|
230
|
+
created: card.created ? new Date(card.created * 1000) : undefined,
|
|
231
|
+
contextLength: card.contextLength ?? card.topProvider?.contextLength ?? undefined,
|
|
232
|
+
maxOutputTokens: card.topProvider?.maxCompletionTokens ?? undefined,
|
|
233
|
+
capabilities: {
|
|
234
|
+
chat: true,
|
|
235
|
+
tools: params.includes("tools"),
|
|
236
|
+
vision: modalities.includes("image"),
|
|
237
|
+
thinking: params.includes("reasoning") || params.includes("include_reasoning"),
|
|
238
|
+
},
|
|
239
|
+
raw: card,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return models;
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
throw new AgentError_1.ExecutionError(`Failed to list OpenRouter models: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async execute(input, options) {
|
|
250
|
+
this.beginRun(input);
|
|
251
|
+
try {
|
|
252
|
+
const response = await this.callProvider(options);
|
|
253
|
+
this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, response);
|
|
254
|
+
return await this.handleResponse(response, options);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
throw this.failRun(error, options);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
this.history.endExecution();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Stream a response as an async generator of {@link StreamChunk} objects.
|
|
265
|
+
*
|
|
266
|
+
* Tool calls are executed transparently — the generator keeps streaming after
|
|
267
|
+
* each tool-call round trip.
|
|
268
|
+
*/
|
|
269
|
+
async *executeStream(input, options) {
|
|
270
|
+
this.beginRun(input);
|
|
271
|
+
try {
|
|
272
|
+
yield* this.streamTurn(options);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw this.failRun(error, options);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
this.history.endExecution();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Shared setup for `execute()` and `executeStream()`. */
|
|
282
|
+
beginRun(input) {
|
|
283
|
+
this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
|
|
284
|
+
this.resetTokenUsage();
|
|
285
|
+
this.lastGeneration = undefined;
|
|
286
|
+
this.currentToolCallCount = 0;
|
|
287
|
+
if (VizConfig_1.vizConfig.isEnabled()) {
|
|
288
|
+
this.vizEventId = VizReporter_1.vizReporter.agentStart(this.id, this.name, this.config.model, this.vendor, typeof input === "string" ? input : JSON.stringify(input));
|
|
289
|
+
}
|
|
290
|
+
if (this.history.transient) {
|
|
291
|
+
this.history.clear();
|
|
292
|
+
this.addSystemMessage(this.getSystemMessage());
|
|
293
|
+
}
|
|
294
|
+
if (typeof input === "string") {
|
|
295
|
+
this.addTextToHistory("user", input);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.addMessageToHistory("user", input);
|
|
299
|
+
}
|
|
300
|
+
this.history.setSessionAnchor();
|
|
301
|
+
this.history.beginExecution();
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Map whatever a run threw onto this library's error types, emit it, and close
|
|
305
|
+
* any open visualization event. Returns the error for the caller to throw.
|
|
306
|
+
*/
|
|
307
|
+
failRun(error, options) {
|
|
308
|
+
// The abort branch comes first and keys off the signal rather than the
|
|
309
|
+
// error's name, so a cancellation still surfaces as an AbortError even when
|
|
310
|
+
// an inner catch already wrapped it.
|
|
311
|
+
if ((0, cancellation_1.isAbortError)(error, options?.signal)) {
|
|
312
|
+
const abortError = this.abortError(error, options?.signal);
|
|
313
|
+
this.closeViz("AbortError", abortError.message, false);
|
|
314
|
+
return abortError;
|
|
315
|
+
}
|
|
316
|
+
const mapped = this.mapProviderError(error);
|
|
317
|
+
this.emit(AgentEvent_1.AgentEvent.ERROR, mapped);
|
|
318
|
+
this.closeViz(mapped.name, mapped.message, mapped instanceof AgentError_1.ApiError && mapped.statusCode === 429);
|
|
319
|
+
return mapped;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Turn an `@openrouter/sdk` error into an {@link AgentError}.
|
|
323
|
+
*
|
|
324
|
+
* The SDK throws one class per status code, all extending `OpenRouterError`
|
|
325
|
+
* with `statusCode`, `headers` and `body`. Rather than importing those classes
|
|
326
|
+
* — which would make the optional peer dependency mandatory — this reads the
|
|
327
|
+
* shape structurally.
|
|
328
|
+
*/
|
|
329
|
+
mapProviderError(error) {
|
|
330
|
+
if (error instanceof AgentError_1.AgentError)
|
|
331
|
+
return error;
|
|
332
|
+
const err = error;
|
|
333
|
+
if (typeof err?.statusCode === "number") {
|
|
334
|
+
const message = unwrapOpenRouterMessage(parseOpenRouterErrorBody(err.body), err.message ?? "Unknown error");
|
|
335
|
+
if (err.statusCode === 429) {
|
|
336
|
+
return this.rateLimitError(err, message);
|
|
337
|
+
}
|
|
338
|
+
return new AgentError_1.ApiError(`OpenRouter API error: ${message}`, err.statusCode, error);
|
|
339
|
+
}
|
|
340
|
+
return new AgentError_1.ExecutionError(`OpenRouter error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Build a {@link RateLimitError} from a 429, lifting OpenRouter's rate-limit
|
|
344
|
+
* headers onto it. They are only present on OpenRouter's own platform limits —
|
|
345
|
+
* a 429 passed through from an upstream provider carries neither, which is why
|
|
346
|
+
* every field is optional.
|
|
347
|
+
*/
|
|
348
|
+
rateLimitError(err, message) {
|
|
349
|
+
const headers = err.headers;
|
|
350
|
+
const num = (name) => {
|
|
351
|
+
const raw = headers?.get(name);
|
|
352
|
+
if (!raw)
|
|
353
|
+
return undefined;
|
|
354
|
+
const parsed = Number(raw);
|
|
355
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
356
|
+
};
|
|
357
|
+
// `Retry-After` is defined by RFC 9110 as either delay-seconds or an
|
|
358
|
+
// HTTP-date. OpenRouter sends the numeric form, but the date form is
|
|
359
|
+
// legal and costs one branch to accept.
|
|
360
|
+
const retryAfterRaw = headers?.get("retry-after");
|
|
361
|
+
const retryAfterMs = parseRetryAfter(retryAfterRaw);
|
|
362
|
+
return new AgentError_1.RateLimitError(`OpenRouter rate limit: ${message}`, retryAfterMs, num("x-ratelimit-limit"), num("x-ratelimit-remaining"), parseResetAt(num("x-ratelimit-reset")), err);
|
|
363
|
+
}
|
|
364
|
+
closeViz(name, message, throttled) {
|
|
365
|
+
if (!this.vizEventId)
|
|
366
|
+
return;
|
|
367
|
+
VizReporter_1.vizReporter.agentError(this.vizEventId, name, message, throttled);
|
|
368
|
+
this.vizEventId = undefined;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Wrap a `ChatRequest` in the envelope `@openrouter/sdk` `chat.send()` expects.
|
|
372
|
+
* Passing the body bare fails Speakeasy validation (`Input validation failed`).
|
|
373
|
+
*/
|
|
374
|
+
sendRequest(stream) {
|
|
375
|
+
return {
|
|
376
|
+
chatRequest: this.buildRequest(stream),
|
|
377
|
+
...(this.config.httpReferer ? { httpReferer: this.config.httpReferer } : {}),
|
|
378
|
+
...(this.config.appTitle ? { appTitle: this.config.appTitle } : {}),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** The `ChatRequest` body, identical for the streaming and buffered paths. */
|
|
382
|
+
buildRequest(stream) {
|
|
383
|
+
const messages = transformers_1.openRouterTransformer.toProvider(this.history.getEntries());
|
|
384
|
+
const tools = this.tools.size > 0 ? this.getToolDefinitions() : undefined;
|
|
385
|
+
return {
|
|
386
|
+
model: this.config.model,
|
|
387
|
+
messages,
|
|
388
|
+
stream,
|
|
389
|
+
// Ask for usage in the stream. Without this OpenRouter often omits the
|
|
390
|
+
// `usage` chunk, leaving `lastTokenUsage` / `lastGeneration.cost` empty
|
|
391
|
+
// on `executeStream()` — same option the OpenAI-compatible agent sends.
|
|
392
|
+
...(stream ? { stream_options: { include_usage: true } } : {}),
|
|
393
|
+
...(tools ? { tools } : {}),
|
|
394
|
+
...(this.config.models?.length ? { models: this.config.models } : {}),
|
|
395
|
+
...(this.config.provider ? { provider: this.config.provider } : {}),
|
|
396
|
+
...(this.config.reasoning ? { reasoning: this.config.reasoning } : {}),
|
|
397
|
+
...(this.config.plugins?.length ? { plugins: this.config.plugins } : {}),
|
|
398
|
+
...(this.config.sessionId ? { sessionId: this.config.sessionId } : {}),
|
|
399
|
+
...(this.config.user ? { user: this.config.user } : {}),
|
|
400
|
+
...(this.config.serviceTier ? { serviceTier: this.config.serviceTier } : {}),
|
|
401
|
+
...(this.config.disableParallelToolUse !== undefined
|
|
402
|
+
? { parallelToolCalls: !this.config.disableParallelToolUse }
|
|
403
|
+
: {}),
|
|
404
|
+
maxTokens: this.config.maxTokens,
|
|
405
|
+
temperature: this.config.temperature,
|
|
406
|
+
topP: this.config.topP,
|
|
407
|
+
topK: this.config.topK,
|
|
408
|
+
stop: this.config.stopSequences,
|
|
409
|
+
seed: this.config.seed,
|
|
410
|
+
presencePenalty: this.config.presencePenalty,
|
|
411
|
+
frequencyPenalty: this.config.frequencyPenalty,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Per-request options: the cancellation signal plus the retry policy.
|
|
416
|
+
*
|
|
417
|
+
* `retryCodes` has to be passed on every call — the SDK reads it only from the
|
|
418
|
+
* call options, never from the client's, so setting it once at construction
|
|
419
|
+
* would silently do nothing.
|
|
420
|
+
*/
|
|
421
|
+
requestOptions(options) {
|
|
422
|
+
return {
|
|
423
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
424
|
+
retries: this.config.retry ?? DEFAULT_RETRY,
|
|
425
|
+
retryCodes: this.config.retryCodes ?? DEFAULT_RETRY_CODES,
|
|
426
|
+
// Ask OpenRouter to include `openrouter_metadata` (attempt count, etc.)
|
|
427
|
+
// in the response. It only does so when the header is present, and default
|
|
428
|
+
// is off — without it `openrouterMetadata` never appears.
|
|
429
|
+
headers: { "X-OpenRouter-Metadata": "1" },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
async callProvider(options) {
|
|
433
|
+
const client = await this.getClient();
|
|
434
|
+
this.startTurnTimer();
|
|
435
|
+
return client.chat.send(this.sendRequest(false), this.requestOptions(options));
|
|
436
|
+
}
|
|
437
|
+
async handleResponse(response, options) {
|
|
438
|
+
const usage = this.accumulateUsage(this.parseUsage(response));
|
|
439
|
+
this.recordGeneration(response);
|
|
440
|
+
const choice = response?.choices?.[0];
|
|
441
|
+
// OpenRouter can report a provider failure inside a 200 body rather than as
|
|
442
|
+
// an HTTP error, with whatever text was generated before it failed. Without
|
|
443
|
+
// this the run would look like a successful short answer.
|
|
444
|
+
if (!choice) {
|
|
445
|
+
throw new AgentError_1.ExecutionError(`OpenRouter returned no choices: ${response?.error?.message ?? "empty response"}`);
|
|
446
|
+
}
|
|
447
|
+
if (choice.finishReason === "error") {
|
|
448
|
+
throw new AgentError_1.ApiError(`OpenRouter provider error mid-generation: ${response?.error?.message ?? "no message"}`, response?.error?.code, response);
|
|
449
|
+
}
|
|
450
|
+
const message = choice.message ?? {};
|
|
451
|
+
if (choice.finishReason === "length") {
|
|
452
|
+
const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
|
|
453
|
+
this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
const toolCalls = message.toolCalls ?? [];
|
|
457
|
+
if (toolCalls.length === 0) {
|
|
458
|
+
const textContent = message.content || "";
|
|
459
|
+
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(message));
|
|
460
|
+
this.emit(AgentEvent_1.AgentEvent.DONE, message, usage);
|
|
461
|
+
this.completeViz(textContent);
|
|
462
|
+
return textContent;
|
|
463
|
+
}
|
|
464
|
+
// Stop before the assistant turn is written: bailing out here avoids both
|
|
465
|
+
// running the tools' side effects and leaving a tool call in history with no
|
|
466
|
+
// tool message to answer it.
|
|
467
|
+
(0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
|
|
468
|
+
this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
|
|
469
|
+
this.currentToolCallCount += toolCalls.length;
|
|
470
|
+
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(message));
|
|
471
|
+
const toolResults = await this.handleToolCalls(toolCalls, options);
|
|
472
|
+
for (const result of toolResults) {
|
|
473
|
+
this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
|
|
474
|
+
}
|
|
475
|
+
const newResponse = await this.callProvider(options);
|
|
476
|
+
this.emit(AgentEvent_1.AgentEvent.AFTER_EXECUTE, newResponse);
|
|
477
|
+
return this.handleResponse(newResponse, options);
|
|
478
|
+
}
|
|
479
|
+
async *streamTurn(options) {
|
|
480
|
+
const client = await this.getClient();
|
|
481
|
+
this.startTurnTimer();
|
|
482
|
+
const stream = await client.chat.send(this.sendRequest(true), this.requestOptions(options));
|
|
483
|
+
let textContent = "";
|
|
484
|
+
let reasoningContent = "";
|
|
485
|
+
let reasoningDetails = [];
|
|
486
|
+
const toolCallAcc = new Map();
|
|
487
|
+
let finishReason = null;
|
|
488
|
+
let streamUsage;
|
|
489
|
+
let streamError;
|
|
490
|
+
for await (const chunk of stream) {
|
|
491
|
+
// Once the first token is out the 200 and its headers are committed, so a
|
|
492
|
+
// provider failure after that point arrives as an SSE payload instead of
|
|
493
|
+
// an HTTP status. Recorded and thrown after the loop, so the tokens
|
|
494
|
+
// already spent still get reported.
|
|
495
|
+
if (chunk?.error)
|
|
496
|
+
streamError = chunk.error;
|
|
497
|
+
// Usage rides on whichever chunk OpenRouter chooses — often the last
|
|
498
|
+
// content chunk rather than a trailing choice-less one. It is a running
|
|
499
|
+
// total for the turn, not a delta, so keeping the most recent covers both
|
|
500
|
+
// layouts without double-counting.
|
|
501
|
+
if (chunk?.usage)
|
|
502
|
+
streamUsage = chunk.usage;
|
|
503
|
+
if (chunk?.id || chunk?.model)
|
|
504
|
+
this.recordGeneration(chunk);
|
|
505
|
+
const choice = chunk?.choices?.[0];
|
|
506
|
+
if (!choice)
|
|
507
|
+
continue;
|
|
508
|
+
finishReason = choice.finishReason ?? finishReason;
|
|
509
|
+
const delta = choice.delta ?? {};
|
|
510
|
+
if (delta.content) {
|
|
511
|
+
this.markFirstToken();
|
|
512
|
+
textContent += delta.content;
|
|
513
|
+
this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
|
|
514
|
+
yield { type: "text", content: delta.content };
|
|
515
|
+
}
|
|
516
|
+
if (delta.reasoning) {
|
|
517
|
+
this.markFirstToken();
|
|
518
|
+
// Accumulated as well as yielded: the assistant turn has to carry its
|
|
519
|
+
// reasoning back on the next request.
|
|
520
|
+
reasoningContent += delta.reasoning;
|
|
521
|
+
this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, delta.reasoning);
|
|
522
|
+
yield { type: "reasoning", content: delta.reasoning };
|
|
523
|
+
}
|
|
524
|
+
if (delta.reasoningDetails?.length) {
|
|
525
|
+
reasoningDetails = reasoningDetails.concat(delta.reasoningDetails);
|
|
526
|
+
}
|
|
527
|
+
if (delta.toolCalls) {
|
|
528
|
+
for (const tc of delta.toolCalls) {
|
|
529
|
+
const index = tc.index ?? 0;
|
|
530
|
+
if (!toolCallAcc.has(index)) {
|
|
531
|
+
toolCallAcc.set(index, { id: "", name: "", arguments: "" });
|
|
532
|
+
}
|
|
533
|
+
const acc = toolCallAcc.get(index);
|
|
534
|
+
if (tc.id)
|
|
535
|
+
acc.id = tc.id;
|
|
536
|
+
if (tc.function?.name)
|
|
537
|
+
acc.name += tc.function.name;
|
|
538
|
+
if (tc.function?.arguments)
|
|
539
|
+
acc.arguments += tc.function.arguments;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Before any throw below, so a turn that failed part way still reports what
|
|
544
|
+
// it spent.
|
|
545
|
+
if (streamUsage)
|
|
546
|
+
this.accumulateUsage(this.parseUsageObject(streamUsage));
|
|
547
|
+
// The SDK's stream iterator stops yielding on abort rather than throwing, so
|
|
548
|
+
// without this an interrupted stream would look like a short but complete
|
|
549
|
+
// turn — writing partial text to history and emitting DONE.
|
|
550
|
+
(0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
|
|
551
|
+
if (streamError) {
|
|
552
|
+
throw new AgentError_1.ApiError(`OpenRouter stream error: ${unwrapOpenRouterMessage(streamError, streamError.message ?? "no message")}`, streamError.code, streamError);
|
|
553
|
+
}
|
|
554
|
+
if (finishReason === "length") {
|
|
555
|
+
const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
|
|
556
|
+
this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
const assistantMessage = {
|
|
560
|
+
role: "assistant",
|
|
561
|
+
content: textContent || null,
|
|
562
|
+
reasoning: reasoningContent || null,
|
|
563
|
+
reasoningDetails,
|
|
564
|
+
};
|
|
565
|
+
if (finishReason === "tool_calls" && toolCallAcc.size > 0) {
|
|
566
|
+
// As in handleResponse(): bail out before the assistant turn is written,
|
|
567
|
+
// so a cancelled run leaves no unanswered tool call in history.
|
|
568
|
+
(0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
|
|
569
|
+
const toolCalls = Array.from(toolCallAcc.entries())
|
|
570
|
+
.sort(([a], [b]) => a - b)
|
|
571
|
+
.map(([, tc]) => ({
|
|
572
|
+
id: tc.id,
|
|
573
|
+
type: "function",
|
|
574
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
575
|
+
}));
|
|
576
|
+
this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
|
|
577
|
+
this.currentToolCallCount += toolCalls.length;
|
|
578
|
+
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage({
|
|
579
|
+
...assistantMessage,
|
|
580
|
+
toolCalls,
|
|
581
|
+
}));
|
|
582
|
+
const toolResults = await this.handleToolCalls(toolCalls, options);
|
|
583
|
+
for (const result of toolResults) {
|
|
584
|
+
this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
|
|
585
|
+
}
|
|
586
|
+
yield* this.streamTurn(options);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(assistantMessage));
|
|
590
|
+
this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
|
|
591
|
+
this.completeViz(textContent);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
async handleToolCalls(toolCalls, options) {
|
|
595
|
+
return Promise.all(toolCalls.map(async (toolCall) => {
|
|
596
|
+
const toolName = toolCall.function?.name ?? "";
|
|
597
|
+
const tool = this.tools.get(toolName);
|
|
598
|
+
const toolCallId = toolCall.id;
|
|
599
|
+
if (!toolCall.function || !tool) {
|
|
600
|
+
const errorMessage = `Tool '${toolName}' not found`;
|
|
601
|
+
this.emit(AgentEvent_1.AgentEvent.TOOL_ERROR, new AgentError_1.ToolExecutionError(errorMessage, toolName, toolCall.function?.arguments));
|
|
602
|
+
return { toolCallId, content: errorMessage };
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const args = JSON.parse(toolCall.function.arguments || "{}");
|
|
606
|
+
const result = await tool.execute(this.getId(), this.getName(), args, toolCallId, this.config.model, this.vendor, { signal: options?.signal });
|
|
607
|
+
return { toolCallId, content: JSON.stringify(result) };
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
const errorMessage = `Error executing tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
611
|
+
if (this.debug) {
|
|
612
|
+
console.error(errorMessage);
|
|
613
|
+
}
|
|
614
|
+
this.emit(AgentEvent_1.AgentEvent.TOOL_ERROR, new AgentError_1.ToolExecutionError(errorMessage, toolName, toolCall.function.arguments));
|
|
615
|
+
return { toolCallId, content: errorMessage };
|
|
616
|
+
}
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Fold one API call's cost and routing facts into {@link lastGeneration}.
|
|
621
|
+
* Cost is summed — a tool loop bills once per hop — while the id and model
|
|
622
|
+
* describe the most recent call.
|
|
623
|
+
*/
|
|
624
|
+
recordGeneration(response) {
|
|
625
|
+
const cost = response?.usage?.cost;
|
|
626
|
+
const previous = this.lastGeneration;
|
|
627
|
+
this.lastGeneration = {
|
|
628
|
+
id: response?.id ?? previous?.id,
|
|
629
|
+
model: response?.model ?? previous?.model,
|
|
630
|
+
cost: typeof cost === "number"
|
|
631
|
+
? (previous?.cost ?? 0) + cost
|
|
632
|
+
: previous?.cost,
|
|
633
|
+
isByok: response?.usage?.isByok ?? previous?.isByok,
|
|
634
|
+
attempts: response?.openrouterMetadata?.attempt ?? previous?.attempts,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
parseUsage(response) {
|
|
638
|
+
return this.parseUsageObject(response?.usage);
|
|
639
|
+
}
|
|
640
|
+
parseUsageObject(usage) {
|
|
641
|
+
return {
|
|
642
|
+
input_tokens: usage?.promptTokens ?? 0,
|
|
643
|
+
output_tokens: usage?.completionTokens ?? 0,
|
|
644
|
+
total_tokens: usage?.totalTokens ?? 0,
|
|
645
|
+
reasoning_tokens: usage?.completionTokensDetails?.reasoningTokens ?? undefined,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
completeViz(textContent) {
|
|
649
|
+
if (!this.vizEventId)
|
|
650
|
+
return;
|
|
651
|
+
VizReporter_1.vizReporter.agentComplete(this.vizEventId, {
|
|
652
|
+
input: this.lastTokenUsage?.input_tokens || 0,
|
|
653
|
+
output: this.lastTokenUsage?.output_tokens || 0,
|
|
654
|
+
total: this.lastTokenUsage?.total_tokens || 0,
|
|
655
|
+
}, "end_turn", this.currentToolCallCount > 0, this.currentToolCallCount, textContent);
|
|
656
|
+
this.vizEventId = undefined;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
exports.OpenRouterAgent = OpenRouterAgent;
|
|
660
|
+
/**
|
|
661
|
+
* Milliseconds to wait from a `Retry-After` header.
|
|
662
|
+
*
|
|
663
|
+
* RFC 9110 allows two forms — delay-seconds (`120`) and an HTTP-date
|
|
664
|
+
* (`Wed, 21 Oct 2026 07:28:00 GMT`). OpenRouter sends the first; the second is
|
|
665
|
+
* accepted because it is legal and cheap to support. A date already in the past
|
|
666
|
+
* yields `0` rather than a negative wait.
|
|
667
|
+
*
|
|
668
|
+
* @returns The delay in milliseconds, or `undefined` when the header is absent
|
|
669
|
+
* or unparseable.
|
|
670
|
+
*/
|
|
671
|
+
function parseRetryAfter(raw) {
|
|
672
|
+
if (!raw)
|
|
673
|
+
return undefined;
|
|
674
|
+
const seconds = Number(raw);
|
|
675
|
+
if (Number.isFinite(seconds)) {
|
|
676
|
+
return Math.max(0, seconds * 1000);
|
|
677
|
+
}
|
|
678
|
+
const date = Date.parse(raw);
|
|
679
|
+
if (Number.isFinite(date)) {
|
|
680
|
+
return Math.max(0, date - Date.now());
|
|
681
|
+
}
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* The instant an `X-RateLimit-Reset` header points at.
|
|
686
|
+
*
|
|
687
|
+
* OpenRouter documents that the header exists but not what is in it, and the
|
|
688
|
+
* three encodings in common use across APIs are indistinguishable by type — so
|
|
689
|
+
* they are told apart by magnitude, taking "the answer is somewhere near now" as
|
|
690
|
+
* the tiebreaker:
|
|
691
|
+
*
|
|
692
|
+
* - below `10^9` — a duration in seconds from now (a literal epoch would be
|
|
693
|
+
* before 2001, which no live API means)
|
|
694
|
+
* - below `10^11` — Unix **seconds** (`10^11` seconds is the year 5138, so
|
|
695
|
+
* anything under it is a plausible timestamp and anything over it is not)
|
|
696
|
+
* - otherwise — Unix **milliseconds**
|
|
697
|
+
*
|
|
698
|
+
* Returns `undefined` for a missing or non-finite value, so callers see "not
|
|
699
|
+
* reported" rather than a date in 1970. Prefer
|
|
700
|
+
* {@link RateLimitError.retryAfterMs} when both are present: it is unambiguous.
|
|
701
|
+
*/
|
|
702
|
+
function parseResetAt(value) {
|
|
703
|
+
if (value === undefined || !Number.isFinite(value) || value < 0) {
|
|
704
|
+
return undefined;
|
|
705
|
+
}
|
|
706
|
+
if (value < 1e9)
|
|
707
|
+
return new Date(Date.now() + value * 1000);
|
|
708
|
+
if (value < 1e11)
|
|
709
|
+
return new Date(value * 1000);
|
|
710
|
+
return new Date(value);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Parse an OpenRouter HTTP error body (`OpenRouterError.body`) down to its
|
|
714
|
+
* `error` field. Returns `undefined` for a missing or non-JSON body rather
|
|
715
|
+
* than throwing, since a malformed body is itself just something to fall back
|
|
716
|
+
* from, not a reason to lose the original error.
|
|
717
|
+
*/
|
|
718
|
+
function parseOpenRouterErrorBody(body) {
|
|
719
|
+
if (!body)
|
|
720
|
+
return undefined;
|
|
721
|
+
try {
|
|
722
|
+
return JSON.parse(body)?.error;
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Best-effort extraction of the most specific message an OpenRouter error
|
|
730
|
+
* payload carries, unwrapping `metadata.raw` when present. `raw` holds the
|
|
731
|
+
* upstream provider's exact error text (e.g. OpenAI's own `{error: {message}}`
|
|
732
|
+
* shape) — falls back to the payload's own `message`, then to `fallback`, so an
|
|
733
|
+
* unfamiliar or non-JSON `raw` still yields something rather than throwing.
|
|
734
|
+
*/
|
|
735
|
+
function unwrapOpenRouterMessage(payload, fallback) {
|
|
736
|
+
const topMessage = payload?.message ?? fallback;
|
|
737
|
+
const raw = payload?.metadata?.raw;
|
|
738
|
+
if (typeof raw !== "string" || !raw)
|
|
739
|
+
return topMessage;
|
|
740
|
+
try {
|
|
741
|
+
const parsedRaw = JSON.parse(raw);
|
|
742
|
+
const upstreamMessage = parsedRaw?.error?.message ?? parsedRaw?.message;
|
|
743
|
+
if (typeof upstreamMessage === "string" && upstreamMessage)
|
|
744
|
+
return upstreamMessage;
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// Not JSON — some upstreams return plain text. Use it directly if short
|
|
748
|
+
// enough to be a message rather than a stack trace or HTML error page.
|
|
749
|
+
if (raw.length < 500)
|
|
750
|
+
return raw;
|
|
751
|
+
}
|
|
752
|
+
return topMessage;
|
|
753
|
+
}
|
|
754
|
+
//# sourceMappingURL=OpenRouterAgent.js.map
|