@agentionai/agents 1.10.3 → 1.11.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.
@@ -228,6 +228,29 @@ export interface OpenRouterSpecificConfig {
228
228
  * Also groups the requests in OpenRouter's observability views.
229
229
  */
230
230
  sessionId?: string;
231
+ /**
232
+ * Mark the system prompt and the last tool definition with an Anthropic
233
+ * `cache_control: { type: "ephemeral" }` breakpoint, so the stable prefix of
234
+ * a request — system prompt plus tool schemas — is billed once and read
235
+ * from cache on later requests in the same conversation instead of paid in
236
+ * full every time.
237
+ *
238
+ * Worth it for an agent whose system prompt and tools are reused across
239
+ * several calls in a session. Not worth it for a one-shot or few-call
240
+ * agent: a cache write costs more than a plain input token, and there are
241
+ * too few reads after it to recoup that. Pair with {@link sessionId} — a
242
+ * cache breakpoint with no sticky routing key can land on a different
243
+ * upstream instance on the very next request, with nothing to hit.
244
+ *
245
+ * Written in Anthropic's `cache_control` shape, but not Anthropic-only:
246
+ * OpenRouter translates it for other providers it fronts (e.g. into
247
+ * OpenAI's `prompt_cache_breakpoint`), rather than passing it through
248
+ * as-is or dropping it. Some of those providers (OpenAI, DeepSeek,
249
+ * Gemini 2.5) already cache automatically with no marker needed, so the
250
+ * translated breakpoint is redundant there rather than load-bearing.
251
+ * `ttl` on `cache_control` does not survive translation to OpenAI.
252
+ */
253
+ promptCaching?: boolean;
231
254
  /** Stable per-end-user identifier used for abuse isolation. Never forwarded raw. */
232
255
  user?: string;
233
256
  /** Processing tier; `"fast"` is an accepted alias for `"priority"`. */
@@ -41,6 +41,12 @@ export type TokenUsage = {
41
41
  * for instance, folds thinking tokens into `output_tokens`.
42
42
  */
43
43
  reasoning_tokens?: number;
44
+ /**
45
+ * USD billed for this usage, straight from the provider's own accounting —
46
+ * not derived from a local price table. Undefined where the provider
47
+ * doesn't report it (most do not; OpenRouter does, per response).
48
+ */
49
+ cost_usd?: number;
44
50
  /**
45
51
  * Milliseconds from sending the request to the first token of the response —
46
52
  * prompt upload plus prompt processing.
@@ -197,6 +197,7 @@ class BaseAgent extends events_1.default {
197
197
  output_tokens: previous.output_tokens + timed.output_tokens,
198
198
  total_tokens: previous.total_tokens + timed.total_tokens,
199
199
  reasoning_tokens: sumOptional(previous.reasoning_tokens, timed.reasoning_tokens),
200
+ cost_usd: sumOptional(previous.cost_usd, timed.cost_usd),
200
201
  timeToFirstTokenMs: sumOptional(previous.timeToFirstTokenMs, timed.timeToFirstTokenMs),
201
202
  generationMs: sumOptional(previous.generationMs, timed.generationMs),
202
203
  totalMs: sumOptional(previous.totalMs, timed.totalMs),
@@ -392,9 +392,24 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
392
392
  }
393
393
  /** The `ChatRequest` body, identical for the streaming and buffered paths. */
394
394
  buildRequest(stream) {
395
- const messages = transformers_1.openRouterTransformer.toProvider(this.history.getEntries());
395
+ const cachePrompt = this.config.promptCaching === true;
396
+ const messages = transformers_1.openRouterTransformer.toProvider(this.history.getEntries(), {
397
+ cacheSystemPrompt: cachePrompt,
398
+ });
396
399
  const allTools = this.getAllToolDefinitions();
397
- const tools = allTools.length > 0 ? allTools : undefined;
400
+ // The breakpoint goes on the *last* tool: Anthropic (and OpenRouter's
401
+ // translation of this marker for other providers, see
402
+ // OpenRouterSpecificConfig.promptCaching) caches everything up through a
403
+ // marked block, so one marker at the end of the array covers the whole
404
+ // tool list in one cached segment. Marking every tool would just spend
405
+ // more cache-write budget for the same coverage.
406
+ const cacheableTools = cachePrompt && allTools.length > 0
407
+ ? [
408
+ ...allTools.slice(0, -1),
409
+ { ...allTools[allTools.length - 1], cacheControl: { type: "ephemeral" } },
410
+ ]
411
+ : allTools;
412
+ const tools = cacheableTools.length > 0 ? cacheableTools : undefined;
398
413
  return {
399
414
  model: this.config.model,
400
415
  messages,
@@ -656,6 +671,7 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
656
671
  output_tokens: usage?.completionTokens ?? 0,
657
672
  total_tokens: usage?.totalTokens ?? 0,
658
673
  reasoning_tokens: usage?.completionTokensDetails?.reasoningTokens ?? undefined,
674
+ cost_usd: typeof usage?.cost === "number" ? usage.cost : undefined,
659
675
  };
660
676
  }
661
677
  completeViz(textContent) {
@@ -163,24 +163,20 @@ export declare const chatCompletionsTransformer: {
163
163
  */
164
164
  toolResultEntry(tool_call_id: string, output: string): HistoryEntry;
165
165
  };
166
- /**
167
- * Convert normalized entries to/from the message format `@openrouter/sdk`
168
- * accepts.
169
- *
170
- * The wire format is OpenAI Chat Completions, but the SDK's TypeScript surface
171
- * is camelCase (`toolCalls`, `toolCallId`, `reasoningDetails`) and it
172
- * zod-serializes to snake_case on the way out — so this cannot reuse
173
- * {@link chatCompletionsTransformer}, whose output is already snake_case.
174
- *
175
- * Beyond the casing it also carries `reasoning_details` through the round trip,
176
- * which the OpenAI-compatible path has no equivalent for.
177
- */
178
166
  export declare const openRouterTransformer: {
179
167
  /**
180
168
  * Convert normalized entries to OpenRouter message format.
181
169
  * Tool results become role:"tool" messages; tool calls ride on the assistant message.
170
+ *
171
+ * `cacheSystemPrompt` marks the system message *and* the latest eligible
172
+ * message with a cache breakpoint each — see
173
+ * {@link markLatestCacheBreakpoint} for why it's both ends, not just the
174
+ * front, and {@link OpenRouterSpecificConfig.promptCaching} for why this is
175
+ * opt-in rather than automatic.
182
176
  */
183
- toProvider(entries: HistoryEntry[]): OpenRouterMessage[];
177
+ toProvider(entries: HistoryEntry[], options?: {
178
+ cacheSystemPrompt?: boolean;
179
+ }): OpenRouterMessage[];
184
180
  /**
185
181
  * Convert an OpenRouter assistant message to a normalized HistoryEntry.
186
182
  */
@@ -201,6 +197,7 @@ type OpenRouterToolCallParam = {
201
197
  type OpenRouterContentPart = {
202
198
  type: "text";
203
199
  text: string;
200
+ cacheControl?: OpenRouterCacheControl;
204
201
  } | {
205
202
  type: "image_url";
206
203
  imageUrl: {
@@ -208,15 +205,23 @@ type OpenRouterContentPart = {
208
205
  detail?: "auto" | "low" | "high";
209
206
  };
210
207
  };
208
+ /**
209
+ * A cache breakpoint on one content block. `cacheControl` (camelCase, per
210
+ * this file's header) rather than the wire's `cache_control` — the SDK
211
+ * zod-serializes the rename on the way out.
212
+ */
213
+ export type OpenRouterCacheControl = {
214
+ type: "ephemeral";
215
+ };
211
216
  export type OpenRouterMessage = {
212
217
  role: "system";
213
- content: string;
218
+ content: string | OpenRouterContentPart[];
214
219
  } | {
215
220
  role: "user";
216
221
  content: string | OpenRouterContentPart[];
217
222
  } | {
218
223
  role: "assistant";
219
- content: string | null;
224
+ content: string | null | OpenRouterContentPart[];
220
225
  toolCalls?: OpenRouterToolCallParam[];
221
226
  /** Plain reasoning text replayed from a previous turn. */
222
227
  reasoning?: string;
@@ -228,7 +233,7 @@ export type OpenRouterMessage = {
228
233
  } | {
229
234
  role: "tool";
230
235
  toolCallId: string;
231
- content: string;
236
+ content: string | OpenRouterContentPart[];
232
237
  };
233
238
  type OpenRouterResponseMessage = {
234
239
  role: string;
@@ -782,12 +782,49 @@ exports.chatCompletionsTransformer = {
782
782
  * Beyond the casing it also carries `reasoning_details` through the round trip,
783
783
  * which the OpenAI-compatible path has no equivalent for.
784
784
  */
785
+ /**
786
+ * Marks the *last* message carrying plain string content with an Anthropic
787
+ * cache breakpoint, mutating it in place.
788
+ *
789
+ * The system-prompt breakpoint alone only caches the fixed part of a request
790
+ * — an agentic loop's growing tool-call history is not fixed, and without a
791
+ * second breakpoint it is resent as fresh, full-price input on every turn.
792
+ * Marking the tail instead: turn N's breakpoint lands on its newest message,
793
+ * so turn N+1's *identical, longer prefix up to that point* is a cache hit,
794
+ * and only the new content past it needs to be freshly priced (and gets its
795
+ * own breakpoint in turn). Anthropic allows up to 4 breakpoints total; this
796
+ * is the second, after the one on the system message — see
797
+ * {@link OpenRouterSpecificConfig.promptCaching}.
798
+ *
799
+ * Skips a message whose content isn't a non-empty string — `null` (an
800
+ * assistant turn that's purely a tool call, nothing to attach a text block
801
+ * to) or an already-structured array (images). Landing one message later
802
+ * than ideal in that case is a smaller loss than the alternative of writing
803
+ * this for every content shape up front.
804
+ */
805
+ function markLatestCacheBreakpoint(messages) {
806
+ for (let i = messages.length - 1; i >= 0; i--) {
807
+ const message = messages[i];
808
+ if (typeof message.content === "string" && message.content.length > 0) {
809
+ message.content = [
810
+ { type: "text", text: message.content, cacheControl: { type: "ephemeral" } },
811
+ ];
812
+ return;
813
+ }
814
+ }
815
+ }
785
816
  exports.openRouterTransformer = {
786
817
  /**
787
818
  * Convert normalized entries to OpenRouter message format.
788
819
  * Tool results become role:"tool" messages; tool calls ride on the assistant message.
820
+ *
821
+ * `cacheSystemPrompt` marks the system message *and* the latest eligible
822
+ * message with a cache breakpoint each — see
823
+ * {@link markLatestCacheBreakpoint} for why it's both ends, not just the
824
+ * front, and {@link OpenRouterSpecificConfig.promptCaching} for why this is
825
+ * opt-in rather than automatic.
789
826
  */
790
- toProvider(entries) {
827
+ toProvider(entries, options) {
791
828
  const messages = [];
792
829
  for (const entry of entries) {
793
830
  const textBlocks = entry.content.filter(types_1.isTextContent);
@@ -798,9 +835,12 @@ exports.openRouterTransformer = {
798
835
  const imageBase64Blocks = entry.content.filter(types_1.isImageBase64Content);
799
836
  const hasImages = imageUrlBlocks.length > 0 || imageBase64Blocks.length > 0;
800
837
  if (entry.role === "system") {
838
+ const text = textBlocks.map((c) => c.text).join("\n");
801
839
  messages.push({
802
840
  role: "system",
803
- content: textBlocks.map((c) => c.text).join("\n"),
841
+ content: options?.cacheSystemPrompt
842
+ ? [{ type: "text", text, cacheControl: { type: "ephemeral" } }]
843
+ : text,
804
844
  });
805
845
  continue;
806
846
  }
@@ -879,6 +919,8 @@ exports.openRouterTransformer = {
879
919
  });
880
920
  }
881
921
  }
922
+ if (options?.cacheSystemPrompt)
923
+ markLatestCacheBreakpoint(messages);
882
924
  return messages;
883
925
  },
884
926
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentionai/agents",
3
3
  "author": "Laurent Zuijdwijk",
4
- "version": "1.10.3",
4
+ "version": "1.11.0",
5
5
  "description": "Agent Library",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",