@agentionai/agents 1.10.3 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/AgentConfig.d.ts +23 -0
- package/dist/agents/AgentEvent.d.ts +5 -0
- package/dist/agents/AgentEvent.js +5 -0
- package/dist/agents/BaseAgent.d.ts +113 -1
- package/dist/agents/BaseAgent.js +57 -0
- package/dist/agents/anthropic/ClaudeAgent.js +157 -117
- package/dist/agents/errors/AgentError.d.ts +12 -0
- package/dist/agents/openai/OpenAiAgent.js +116 -59
- package/dist/agents/openai-compatible/OpenAICompatibleAgent.js +140 -107
- package/dist/agents/openrouter/OpenRouterAgent.js +146 -97
- package/dist/agents/reasoning-text.d.ts +44 -0
- package/dist/agents/reasoning-text.js +43 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +1 -0
- package/dist/history/transformers.d.ts +21 -16
- package/dist/history/transformers.js +44 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -3
|
@@ -294,6 +294,7 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
|
|
|
294
294
|
beginRun(input) {
|
|
295
295
|
this.emit(AgentEvent_1.AgentEvent.BEFORE_EXECUTE, input);
|
|
296
296
|
this.resetTokenUsage();
|
|
297
|
+
this.resetPartialTurn();
|
|
297
298
|
this.lastGeneration = undefined;
|
|
298
299
|
this.currentToolCallCount = 0;
|
|
299
300
|
if (VizConfig_1.vizConfig.isEnabled()) {
|
|
@@ -323,12 +324,12 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
|
|
|
323
324
|
if ((0, cancellation_1.isAbortError)(error, options?.signal)) {
|
|
324
325
|
const abortError = this.abortError(error, options?.signal);
|
|
325
326
|
this.closeViz("AbortError", abortError.message, false);
|
|
326
|
-
return abortError;
|
|
327
|
+
return this.withPartialTurn(abortError);
|
|
327
328
|
}
|
|
328
329
|
const mapped = this.mapProviderError(error);
|
|
329
330
|
this.emit(AgentEvent_1.AgentEvent.ERROR, mapped);
|
|
330
331
|
this.closeViz(mapped.name, mapped.message, mapped instanceof AgentError_1.ApiError && mapped.statusCode === 429);
|
|
331
|
-
return mapped;
|
|
332
|
+
return this.withPartialTurn(mapped);
|
|
332
333
|
}
|
|
333
334
|
/**
|
|
334
335
|
* Turn an `@openrouter/sdk` error into an {@link AgentError}.
|
|
@@ -392,9 +393,24 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
|
|
|
392
393
|
}
|
|
393
394
|
/** The `ChatRequest` body, identical for the streaming and buffered paths. */
|
|
394
395
|
buildRequest(stream) {
|
|
395
|
-
const
|
|
396
|
+
const cachePrompt = this.config.promptCaching === true;
|
|
397
|
+
const messages = transformers_1.openRouterTransformer.toProvider(this.history.getEntries(), {
|
|
398
|
+
cacheSystemPrompt: cachePrompt,
|
|
399
|
+
});
|
|
396
400
|
const allTools = this.getAllToolDefinitions();
|
|
397
|
-
|
|
401
|
+
// The breakpoint goes on the *last* tool: Anthropic (and OpenRouter's
|
|
402
|
+
// translation of this marker for other providers, see
|
|
403
|
+
// OpenRouterSpecificConfig.promptCaching) caches everything up through a
|
|
404
|
+
// marked block, so one marker at the end of the array covers the whole
|
|
405
|
+
// tool list in one cached segment. Marking every tool would just spend
|
|
406
|
+
// more cache-write budget for the same coverage.
|
|
407
|
+
const cacheableTools = cachePrompt && allTools.length > 0
|
|
408
|
+
? [
|
|
409
|
+
...allTools.slice(0, -1),
|
|
410
|
+
{ ...allTools[allTools.length - 1], cacheControl: { type: "ephemeral" } },
|
|
411
|
+
]
|
|
412
|
+
: allTools;
|
|
413
|
+
const tools = cacheableTools.length > 0 ? cacheableTools : undefined;
|
|
398
414
|
return {
|
|
399
415
|
model: this.config.model,
|
|
400
416
|
messages,
|
|
@@ -500,108 +516,140 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
|
|
|
500
516
|
let finishReason = null;
|
|
501
517
|
let streamUsage;
|
|
502
518
|
let streamError;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
519
|
+
// Set once this frame's assistant message reaches history. Until then the
|
|
520
|
+
// turn exists only in the accumulators above, and the `finally` salvages
|
|
521
|
+
// them — a reasoning trail can be minutes of generation, and the stream
|
|
522
|
+
// throwing (or the consumer walking away) would otherwise drop it.
|
|
523
|
+
let committed = false;
|
|
524
|
+
let failure;
|
|
525
|
+
try {
|
|
526
|
+
for await (const chunk of stream) {
|
|
527
|
+
// Once the first token is out the 200 and its headers are committed, so a
|
|
528
|
+
// provider failure after that point arrives as an SSE payload instead of
|
|
529
|
+
// an HTTP status. Recorded and thrown after the loop, so the tokens
|
|
530
|
+
// already spent still get reported.
|
|
531
|
+
if (chunk?.error)
|
|
532
|
+
streamError = chunk.error;
|
|
533
|
+
// Usage rides on whichever chunk OpenRouter chooses — often the last
|
|
534
|
+
// content chunk rather than a trailing choice-less one. It is a running
|
|
535
|
+
// total for the turn, not a delta, so keeping the most recent covers both
|
|
536
|
+
// layouts without double-counting.
|
|
537
|
+
if (chunk?.usage)
|
|
538
|
+
streamUsage = chunk.usage;
|
|
539
|
+
if (chunk?.id || chunk?.model)
|
|
540
|
+
this.recordGeneration(chunk);
|
|
541
|
+
const choice = chunk?.choices?.[0];
|
|
542
|
+
if (!choice)
|
|
543
|
+
continue;
|
|
544
|
+
finishReason = choice.finishReason ?? finishReason;
|
|
545
|
+
const delta = choice.delta ?? {};
|
|
546
|
+
if (delta.content) {
|
|
547
|
+
this.markFirstToken();
|
|
548
|
+
textContent += delta.content;
|
|
549
|
+
this.emit(AgentEvent_1.AgentEvent.CHUNK, delta.content);
|
|
550
|
+
yield { type: "text", content: delta.content };
|
|
551
|
+
}
|
|
552
|
+
if (delta.reasoning) {
|
|
553
|
+
this.markFirstToken();
|
|
554
|
+
// Accumulated as well as yielded: the assistant turn has to carry its
|
|
555
|
+
// reasoning back on the next request.
|
|
556
|
+
reasoningContent += delta.reasoning;
|
|
557
|
+
this.emit(AgentEvent_1.AgentEvent.REASONING_CHUNK, delta.reasoning);
|
|
558
|
+
yield { type: "reasoning", content: delta.reasoning };
|
|
559
|
+
}
|
|
560
|
+
if (delta.reasoningDetails?.length) {
|
|
561
|
+
reasoningDetails = reasoningDetails.concat(delta.reasoningDetails);
|
|
562
|
+
}
|
|
563
|
+
if (delta.toolCalls) {
|
|
564
|
+
for (const tc of delta.toolCalls) {
|
|
565
|
+
const index = tc.index ?? 0;
|
|
566
|
+
if (!toolCallAcc.has(index)) {
|
|
567
|
+
toolCallAcc.set(index, { id: "", name: "", arguments: "" });
|
|
568
|
+
}
|
|
569
|
+
const acc = toolCallAcc.get(index);
|
|
570
|
+
if (tc.id)
|
|
571
|
+
acc.id = tc.id;
|
|
572
|
+
if (tc.function?.name)
|
|
573
|
+
acc.name += tc.function.name;
|
|
574
|
+
if (tc.function?.arguments)
|
|
575
|
+
acc.arguments += tc.function.arguments;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
528
578
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
579
|
+
// Before any throw below, so a turn that failed part way still reports what
|
|
580
|
+
// it spent.
|
|
581
|
+
if (streamUsage)
|
|
582
|
+
this.accumulateUsage(this.parseUsageObject(streamUsage));
|
|
583
|
+
// The SDK's stream iterator stops yielding on abort rather than throwing, so
|
|
584
|
+
// without this an interrupted stream would look like a short but complete
|
|
585
|
+
// turn — writing partial text to history and emitting DONE.
|
|
586
|
+
(0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
|
|
587
|
+
if (streamError) {
|
|
588
|
+
throw new AgentError_1.ApiError(`OpenRouter stream error: ${unwrapOpenRouterMessage(streamError, streamError.message ?? "no message")}`, streamError.code, streamError);
|
|
536
589
|
}
|
|
537
|
-
if (
|
|
538
|
-
|
|
590
|
+
if (finishReason === "length") {
|
|
591
|
+
const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
|
|
592
|
+
this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
|
|
593
|
+
throw error;
|
|
539
594
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
595
|
+
const assistantMessage = {
|
|
596
|
+
role: "assistant",
|
|
597
|
+
content: textContent || null,
|
|
598
|
+
reasoning: reasoningContent || null,
|
|
599
|
+
reasoningDetails,
|
|
600
|
+
};
|
|
601
|
+
if (finishReason === "tool_calls" && toolCallAcc.size > 0) {
|
|
602
|
+
// As in handleResponse(): bail out before the assistant turn is written,
|
|
603
|
+
// so a cancelled run leaves no unanswered tool call in history.
|
|
604
|
+
(0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
|
|
605
|
+
const toolCalls = Array.from(toolCallAcc.entries())
|
|
606
|
+
.sort(([a], [b]) => a - b)
|
|
607
|
+
.map(([, tc]) => ({
|
|
608
|
+
id: tc.id,
|
|
609
|
+
type: "function",
|
|
610
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
611
|
+
}));
|
|
612
|
+
this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
|
|
613
|
+
this.currentToolCallCount += toolCalls.length;
|
|
614
|
+
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage({
|
|
615
|
+
...assistantMessage,
|
|
616
|
+
toolCalls,
|
|
617
|
+
}));
|
|
618
|
+
committed = true;
|
|
619
|
+
const toolResults = await this.handleToolCalls(toolCalls, options);
|
|
620
|
+
for (const result of toolResults) {
|
|
621
|
+
this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
|
|
553
622
|
}
|
|
623
|
+
yield* this.streamTurn(options);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(assistantMessage));
|
|
627
|
+
committed = true;
|
|
628
|
+
this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
|
|
629
|
+
this.completeViz(textContent);
|
|
554
630
|
}
|
|
555
631
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (streamUsage)
|
|
559
|
-
this.accumulateUsage(this.parseUsageObject(streamUsage));
|
|
560
|
-
// The SDK's stream iterator stops yielding on abort rather than throwing, so
|
|
561
|
-
// without this an interrupted stream would look like a short but complete
|
|
562
|
-
// turn — writing partial text to history and emitting DONE.
|
|
563
|
-
(0, cancellation_1.throwIfAborted)(options?.signal, `Execution of agent ${this.getName()}`);
|
|
564
|
-
if (streamError) {
|
|
565
|
-
throw new AgentError_1.ApiError(`OpenRouter stream error: ${unwrapOpenRouterMessage(streamError, streamError.message ?? "no message")}`, streamError.code, streamError);
|
|
566
|
-
}
|
|
567
|
-
if (finishReason === "length") {
|
|
568
|
-
const error = new AgentError_1.MaxTokensExceededError("Response exceeded maximum token limit", this.config.maxTokens);
|
|
569
|
-
this.emit(AgentEvent_1.AgentEvent.MAX_TOKENS_EXCEEDED, error);
|
|
632
|
+
catch (error) {
|
|
633
|
+
failure = error;
|
|
570
634
|
throw error;
|
|
571
635
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}));
|
|
589
|
-
this.emit(AgentEvent_1.AgentEvent.TOOL_USE, toolCalls);
|
|
590
|
-
this.currentToolCallCount += toolCalls.length;
|
|
591
|
-
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage({
|
|
592
|
-
...assistantMessage,
|
|
593
|
-
toolCalls,
|
|
594
|
-
}));
|
|
595
|
-
const toolResults = await this.handleToolCalls(toolCalls, options);
|
|
596
|
-
for (const result of toolResults) {
|
|
597
|
-
this.addToHistory(transformers_1.openRouterTransformer.toolResultEntry(result.toolCallId, result.content));
|
|
636
|
+
finally {
|
|
637
|
+
if (!committed) {
|
|
638
|
+
this.capturePartialTurn({
|
|
639
|
+
text: textContent,
|
|
640
|
+
reasoning: reasoningContent,
|
|
641
|
+
toolCalls: Array.from(toolCallAcc.entries())
|
|
642
|
+
.sort(([a], [b]) => a - b)
|
|
643
|
+
.map(([, tc]) => ({
|
|
644
|
+
id: tc.id,
|
|
645
|
+
name: tc.name,
|
|
646
|
+
arguments: tc.arguments,
|
|
647
|
+
})),
|
|
648
|
+
reason: this.partialTurnReason(failure, options?.signal),
|
|
649
|
+
error: failure,
|
|
650
|
+
meta: reasoningDetails.length ? { reasoningDetails } : undefined,
|
|
651
|
+
});
|
|
598
652
|
}
|
|
599
|
-
yield* this.streamTurn(options);
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
this.addToHistory(transformers_1.openRouterTransformer.fromProviderMessage(assistantMessage));
|
|
603
|
-
this.emit(AgentEvent_1.AgentEvent.DONE, { content: textContent }, this.lastTokenUsage);
|
|
604
|
-
this.completeViz(textContent);
|
|
605
653
|
}
|
|
606
654
|
}
|
|
607
655
|
async handleToolCalls(toolCalls, options) {
|
|
@@ -656,6 +704,7 @@ class OpenRouterAgent extends BaseAgent_1.BaseAgent {
|
|
|
656
704
|
output_tokens: usage?.completionTokens ?? 0,
|
|
657
705
|
total_tokens: usage?.totalTokens ?? 0,
|
|
658
706
|
reasoning_tokens: usage?.completionTokensDetails?.reasoningTokens ?? undefined,
|
|
707
|
+
cost_usd: typeof usage?.cost === "number" ? usage.cost : undefined,
|
|
659
708
|
};
|
|
660
709
|
}
|
|
661
710
|
completeViz(textContent) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only normalization for streamed reasoning text.
|
|
3
|
+
*
|
|
4
|
+
* Reasoning models routed through OpenRouter (and other OpenAI-compatible
|
|
5
|
+
* backends) sometimes stream `reasoning`/`reasoning_content` whose formatting
|
|
6
|
+
* is far noisier than the model's final answer — GLM-series models in
|
|
7
|
+
* particular emit heavily bulleted chain-of-thought with a blank line between
|
|
8
|
+
* almost every point, and some provider routes break tokens one phrase per
|
|
9
|
+
* line instead of wrapping normally. That formatting comes from the model
|
|
10
|
+
* itself (verified against live OpenRouter streams — the SDK and this
|
|
11
|
+
* library's accumulation just concatenate deltas verbatim), so it can't be
|
|
12
|
+
* fixed at the source.
|
|
13
|
+
*
|
|
14
|
+
* This is display-only: never apply it to the string that gets stored in
|
|
15
|
+
* history or replayed to the provider on the next turn (DeepSeek/GLM require
|
|
16
|
+
* that text back byte-for-byte, see {@link OpenAICompatibleAgent.streamTurn}).
|
|
17
|
+
* Apply it only where you render or log a `reasoning` chunk for a human.
|
|
18
|
+
*/
|
|
19
|
+
export interface CollapseReasoningWhitespaceOptions {
|
|
20
|
+
/** Collapse runs of 3+ newlines down to a single blank line. Default `true`. */
|
|
21
|
+
collapseBlankLines?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Merge consecutive non-blank lines into one, joined by a space — for
|
|
24
|
+
* providers that stream reasoning broken one word or phrase per line. A
|
|
25
|
+
* line starting a markdown block (list item, heading, blockquote) is never
|
|
26
|
+
* merged into the line before it, so intentional structure survives.
|
|
27
|
+
* Off by default since it can also merge genuinely short paragraphs;
|
|
28
|
+
* enable it for the specific model/provider you've seen this on.
|
|
29
|
+
*/
|
|
30
|
+
collapseLineWraps?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Collapses excess linebreaks in reasoning text for display, leaving the
|
|
34
|
+
* original string untouched for anything that needs it verbatim.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* agent.on(AgentEvent.REASONING_CHUNK, (delta) => {
|
|
39
|
+
* process.stdout.write(collapseReasoningWhitespace(delta));
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function collapseReasoningWhitespace(text: string, options?: CollapseReasoningWhitespaceOptions): string;
|
|
44
|
+
//# sourceMappingURL=reasoning-text.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collapseReasoningWhitespace = collapseReasoningWhitespace;
|
|
4
|
+
const MARKDOWN_BLOCK_START = /^\s*(?:[-*+]\s|\d+[.)]\s|#{1,6}\s|>)/;
|
|
5
|
+
/**
|
|
6
|
+
* Collapses excess linebreaks in reasoning text for display, leaving the
|
|
7
|
+
* original string untouched for anything that needs it verbatim.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* agent.on(AgentEvent.REASONING_CHUNK, (delta) => {
|
|
12
|
+
* process.stdout.write(collapseReasoningWhitespace(delta));
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
function collapseReasoningWhitespace(text, options = {}) {
|
|
17
|
+
const { collapseBlankLines = true, collapseLineWraps = false } = options;
|
|
18
|
+
let result = text;
|
|
19
|
+
if (collapseBlankLines) {
|
|
20
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
21
|
+
}
|
|
22
|
+
if (collapseLineWraps) {
|
|
23
|
+
const lines = result.split("\n");
|
|
24
|
+
const merged = [];
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const prev = merged[merged.length - 1];
|
|
27
|
+
const canMergeIntoPrev = prev !== undefined &&
|
|
28
|
+
prev.trim().length > 0 &&
|
|
29
|
+
line.trim().length > 0 &&
|
|
30
|
+
!MARKDOWN_BLOCK_START.test(line) &&
|
|
31
|
+
!MARKDOWN_BLOCK_START.test(prev);
|
|
32
|
+
if (canMergeIntoPrev) {
|
|
33
|
+
merged[merged.length - 1] = `${prev.replace(/\s+$/, "")} ${line.replace(/^\s+/, "")}`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
merged.push(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
result = merged.join("\n");
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=reasoning-text.js.map
|
package/dist/core.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./agents/AgentConfig";
|
|
|
4
4
|
export * from "./agents/AgentEvent";
|
|
5
5
|
export * from "./agents/errors/AgentError";
|
|
6
6
|
export * from "./agents/cancellation";
|
|
7
|
+
export * from "./agents/reasoning-text";
|
|
7
8
|
export * from "./history/History";
|
|
8
9
|
export * from "./history/types";
|
|
9
10
|
export * from "./graph/AgentGraph";
|
package/dist/core.js
CHANGED
|
@@ -23,6 +23,7 @@ __exportStar(require("./agents/AgentConfig"), exports);
|
|
|
23
23
|
__exportStar(require("./agents/AgentEvent"), exports);
|
|
24
24
|
__exportStar(require("./agents/errors/AgentError"), exports);
|
|
25
25
|
__exportStar(require("./agents/cancellation"), exports);
|
|
26
|
+
__exportStar(require("./agents/reasoning-text"), exports);
|
|
26
27
|
// History
|
|
27
28
|
__exportStar(require("./history/History"), exports);
|
|
28
29
|
__exportStar(require("./history/types"), exports);
|
|
@@ -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[]
|
|
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:
|
|
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/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from "./agents/AgentConfig";
|
|
|
19
19
|
export * from "./agents/AgentEvent";
|
|
20
20
|
export * from "./agents/errors/AgentError";
|
|
21
21
|
export * from "./agents/cancellation";
|
|
22
|
+
export * from "./agents/reasoning-text";
|
|
22
23
|
export * from "./history/History";
|
|
23
24
|
export * from "./history/types";
|
|
24
25
|
export { anthropicTransformer, openAiTransformer, mistralTransformer, geminiTransformer, ollamaTransformer, chatCompletionsTransformer, openRouterTransformer, } from "./history/transformers";
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,7 @@ __exportStar(require("./agents/AgentConfig"), exports);
|
|
|
46
46
|
__exportStar(require("./agents/AgentEvent"), exports);
|
|
47
47
|
__exportStar(require("./agents/errors/AgentError"), exports);
|
|
48
48
|
__exportStar(require("./agents/cancellation"), exports);
|
|
49
|
+
__exportStar(require("./agents/reasoning-text"), exports);
|
|
49
50
|
// History
|
|
50
51
|
__exportStar(require("./history/History"), exports);
|
|
51
52
|
__exportStar(require("./history/types"), exports);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentionai/agents",
|
|
3
3
|
"author": "Laurent Zuijdwijk",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.12.0",
|
|
5
5
|
"description": "Agent Library",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"@lancedb/lancedb": "^0.23.0",
|
|
119
119
|
"@mistralai/mistralai": "^1.13.0",
|
|
120
120
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
121
|
-
"@openrouter/sdk": "^1.2.
|
|
121
|
+
"@openrouter/sdk": "^1.2.106",
|
|
122
122
|
"@types/jest": "^29.5.0",
|
|
123
123
|
"@types/node": "^18.15.11",
|
|
124
124
|
"apache-arrow": "^18.1.0",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"@lancedb/lancedb": "^0.23.0",
|
|
151
151
|
"@mistralai/mistralai": "^1.13.0",
|
|
152
152
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
153
|
-
"@openrouter/sdk": "^1.2.
|
|
153
|
+
"@openrouter/sdk": "^1.2.106",
|
|
154
154
|
"apache-arrow": "^18.0.0",
|
|
155
155
|
"ollama": "^0.5.18",
|
|
156
156
|
"openai": "^6.16.0",
|