@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.
@@ -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