@jaypie/llm 1.3.13 → 1.3.15
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/cjs/Llm.d.ts +6 -0
- package/dist/cjs/index.cjs +813 -344
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +112 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/observability/exchangeStore.d.ts +20 -0
- package/dist/cjs/operate/adapters/BedrockAdapter.d.ts +6 -0
- package/dist/cjs/operate/adapters/OpenAiAdapter.d.ts +5 -0
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +11 -0
- package/dist/cjs/operate/exchange/buildExchangeEnvelope.d.ts +16 -0
- package/dist/cjs/operate/exchange/emitExchange.d.ts +20 -0
- package/dist/cjs/operate/exchange/index.d.ts +2 -0
- package/dist/cjs/operate/types.d.ts +10 -1
- package/dist/cjs/providers/anthropic/types.d.ts +14 -1
- package/dist/cjs/types/LlmProvider.interface.d.ts +106 -1
- package/dist/cjs/util/cacheControl.d.ts +21 -0
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/esm/Llm.d.ts +6 -0
- package/dist/esm/index.d.ts +112 -1
- package/dist/esm/index.js +813 -344
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/exchangeStore.d.ts +20 -0
- package/dist/esm/operate/adapters/BedrockAdapter.d.ts +6 -0
- package/dist/esm/operate/adapters/OpenAiAdapter.d.ts +5 -0
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +11 -0
- package/dist/esm/operate/exchange/buildExchangeEnvelope.d.ts +16 -0
- package/dist/esm/operate/exchange/emitExchange.d.ts +20 -0
- package/dist/esm/operate/exchange/index.d.ts +2 -0
- package/dist/esm/operate/types.d.ts +10 -1
- package/dist/esm/providers/anthropic/types.d.ts +14 -1
- package/dist/esm/types/LlmProvider.interface.d.ts +106 -1
- package/dist/esm/util/cacheControl.d.ts +21 -0
- package/dist/esm/util/index.d.ts +1 -0
- package/package.json +5 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -478,234 +478,130 @@ function resolveModelChain(model) {
|
|
|
478
478
|
|
|
479
479
|
//
|
|
480
480
|
//
|
|
481
|
-
//
|
|
481
|
+
// Helpers
|
|
482
482
|
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
* Providers can extend this class to reduce boilerplate.
|
|
486
|
-
*/
|
|
487
|
-
class BaseProviderAdapter {
|
|
488
|
-
constructor() {
|
|
489
|
-
/**
|
|
490
|
-
* Whether OperateLoop may take a corrective turn when a `format` request
|
|
491
|
-
* completes with prose instead of structured output. Providers whose
|
|
492
|
-
* structured output rides a tool emulation (where compliance is a model
|
|
493
|
-
* decision) opt in; native grammar-constrained providers do not need it.
|
|
494
|
-
*/
|
|
495
|
-
this.supportsStructuredOutputRetry = false;
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Default implementation checks if error is retryable via classifyError
|
|
499
|
-
*/
|
|
500
|
-
isRetryableError(error) {
|
|
501
|
-
const classified = this.classifyError(error);
|
|
502
|
-
return classified.shouldRetry;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Default implementation checks error category via classifyError
|
|
506
|
-
*/
|
|
507
|
-
isRateLimitError(error) {
|
|
508
|
-
const classified = this.classifyError(error);
|
|
509
|
-
return classified.category === "rate_limit";
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Default implementation returns false - override for providers with native structured output
|
|
513
|
-
*/
|
|
514
|
-
hasStructuredOutput(_response) {
|
|
515
|
-
return false;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Default implementation returns undefined - override for providers with native structured output
|
|
519
|
-
*/
|
|
520
|
-
extractStructuredOutput(_response) {
|
|
483
|
+
function sumUsageByProviderModel(usage) {
|
|
484
|
+
if (!usage.length) {
|
|
521
485
|
return undefined;
|
|
522
486
|
}
|
|
487
|
+
const totals = {};
|
|
488
|
+
for (const item of usage) {
|
|
489
|
+
const key = `${item.provider ?? "unknown"}:${item.model ?? "unknown"}`;
|
|
490
|
+
if (!totals[key]) {
|
|
491
|
+
totals[key] = {
|
|
492
|
+
input: 0,
|
|
493
|
+
model: item.model,
|
|
494
|
+
output: 0,
|
|
495
|
+
provider: item.provider,
|
|
496
|
+
reasoning: 0,
|
|
497
|
+
total: 0,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
totals[key].input += item.input;
|
|
501
|
+
totals[key].output += item.output;
|
|
502
|
+
totals[key].reasoning += item.reasoning;
|
|
503
|
+
totals[key].total += item.total;
|
|
504
|
+
if (item.cacheRead !== undefined) {
|
|
505
|
+
totals[key].cacheRead = (totals[key].cacheRead ?? 0) + item.cacheRead;
|
|
506
|
+
}
|
|
507
|
+
if (item.cacheWrite !== undefined) {
|
|
508
|
+
totals[key].cacheWrite = (totals[key].cacheWrite ?? 0) + item.cacheWrite;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return totals;
|
|
523
512
|
}
|
|
524
|
-
|
|
513
|
+
function extractResponseIds(responses) {
|
|
514
|
+
const ids = responses
|
|
515
|
+
.map((response) => response && typeof response === "object"
|
|
516
|
+
? response.id
|
|
517
|
+
: undefined)
|
|
518
|
+
.filter((id) => typeof id === "string");
|
|
519
|
+
return ids.length ? ids : undefined;
|
|
520
|
+
}
|
|
521
|
+
//
|
|
522
|
+
//
|
|
523
|
+
// Main
|
|
524
|
+
//
|
|
525
525
|
/**
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
* five-point scale. Each table below maps it onto the target provider's native
|
|
530
|
-
* effort control, keeping `medium`/`high` semantically aligned across providers
|
|
531
|
-
* and using each provider's extra bottom (`minimal`) or top (`xhigh`/`max`)
|
|
532
|
-
* rung where one exists. Providers with fewer levels collapse the ends and mark
|
|
533
|
-
* the result `papered`. A single `effort` value is therefore safe to reuse
|
|
534
|
-
* across providers and fallback chains.
|
|
526
|
+
* Assemble the serializable exchange envelope for one operate() settlement.
|
|
527
|
+
* The `resolution` block is stamped by the Llm facade, which is the layer
|
|
528
|
+
* that knows fallback outcome; the loop fills provider/model best-effort.
|
|
535
529
|
*/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
530
|
+
function buildExchangeEnvelope({ duration, initialHistoryLength, input, options, response, startedAt, state, }) {
|
|
531
|
+
const historyDelta = response.history.slice(initialHistoryLength);
|
|
532
|
+
return {
|
|
533
|
+
ids: extractResponseIds(response.responses),
|
|
534
|
+
request: {
|
|
535
|
+
cache: options.cache,
|
|
536
|
+
data: options.data,
|
|
537
|
+
effort: options.effort,
|
|
538
|
+
explain: options.explain,
|
|
539
|
+
format: state.formattedFormat,
|
|
540
|
+
input,
|
|
541
|
+
instructions: options.instructions,
|
|
542
|
+
model: options.model,
|
|
543
|
+
placeholders: options.placeholders,
|
|
544
|
+
providerOptions: options.providerOptions,
|
|
545
|
+
system: options.system,
|
|
546
|
+
temperature: options.temperature,
|
|
547
|
+
tools: state.formattedTools?.map(({ name }) => name),
|
|
548
|
+
turns: options.turns,
|
|
549
|
+
user: options.user,
|
|
550
|
+
},
|
|
551
|
+
resolution: {
|
|
552
|
+
model: response.model,
|
|
553
|
+
provider: response.provider,
|
|
554
|
+
retries: state.retries,
|
|
555
|
+
},
|
|
556
|
+
response: {
|
|
557
|
+
content: response.content,
|
|
558
|
+
error: response.error,
|
|
559
|
+
historyDelta,
|
|
560
|
+
reasoning: response.reasoning,
|
|
561
|
+
status: response.status,
|
|
562
|
+
stopReason: state.lastStopReason,
|
|
563
|
+
usage: response.usage,
|
|
564
|
+
usageTotals: sumUsageByProviderModel(response.usage),
|
|
565
|
+
},
|
|
566
|
+
timing: {
|
|
567
|
+
duration,
|
|
568
|
+
startedAt,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
567
571
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
572
|
+
|
|
573
|
+
const CACHE_TTL_DEFAULT = "5m";
|
|
574
|
+
/**
|
|
575
|
+
* Normalize the caller-facing `cache` option into a concrete decision.
|
|
576
|
+
* - `undefined` / `true` → enabled at the default TTL
|
|
577
|
+
* - `false` / `0` → disabled
|
|
578
|
+
* - `"5m"` / `"1h"` → enabled at that TTL
|
|
579
|
+
*/
|
|
580
|
+
function resolveCache(cache) {
|
|
581
|
+
if (cache === false || cache === 0) {
|
|
582
|
+
return { enabled: false, ttl: CACHE_TTL_DEFAULT };
|
|
574
583
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return { papered: true, value: "high" };
|
|
584
|
+
if (cache === "5m" || cache === "1h") {
|
|
585
|
+
return { enabled: true, ttl: cache };
|
|
578
586
|
}
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
// xAI Grok `reasoning_effort` — low | medium | high. No sub-low or top rung, so
|
|
582
|
-
// `lowest` collapses onto `low` and `highest` onto `high`.
|
|
583
|
-
const XAI_EFFORT = {
|
|
584
|
-
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
585
|
-
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
586
|
-
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
587
|
-
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
588
|
-
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
589
|
-
};
|
|
590
|
-
function toXaiEffort(effort) {
|
|
591
|
-
return XAI_EFFORT[effort];
|
|
592
|
-
}
|
|
593
|
-
// Anthropic `output_config.effort` — low | medium | high | xhigh | max. No
|
|
594
|
-
// sub-low rung, so `lowest` collapses onto `low`; `highest` reaches `max`.
|
|
595
|
-
const ANTHROPIC_EFFORT = {
|
|
596
|
-
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
597
|
-
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
598
|
-
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
599
|
-
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
600
|
-
[EFFORT.HIGHEST]: { papered: false, value: "max" },
|
|
601
|
-
};
|
|
602
|
-
function toAnthropicEffort(effort) {
|
|
603
|
-
return ANTHROPIC_EFFORT[effort];
|
|
604
|
-
}
|
|
605
|
-
// Gemini 3.x `thinkingConfig.thinkingLevel` — MINIMAL | LOW | MEDIUM | HIGH. No
|
|
606
|
-
// top rung above HIGH, so `highest` collapses onto HIGH.
|
|
607
|
-
const GEMINI_THINKING_LEVEL = {
|
|
608
|
-
[EFFORT.LOWEST]: { papered: false, value: "MINIMAL" },
|
|
609
|
-
[EFFORT.LOW]: { papered: false, value: "LOW" },
|
|
610
|
-
[EFFORT.MEDIUM]: { papered: false, value: "MEDIUM" },
|
|
611
|
-
[EFFORT.HIGH]: { papered: false, value: "HIGH" },
|
|
612
|
-
[EFFORT.HIGHEST]: { papered: true, value: "HIGH" },
|
|
613
|
-
};
|
|
614
|
-
function toGeminiThinkingLevel(effort) {
|
|
615
|
-
return GEMINI_THINKING_LEVEL[effort];
|
|
616
|
-
}
|
|
617
|
-
// Gemini 2.5 `thinkingConfig.thinkingBudget` — every tier is a distinct token
|
|
618
|
-
// budget, so nothing is papered over. Floor (512) clears every 2.5 minimum;
|
|
619
|
-
// ceiling (24,576) is valid across 2.5 Pro (max 32,768) and Flash (max 24,576).
|
|
620
|
-
const GEMINI_THINKING_BUDGET = {
|
|
621
|
-
[EFFORT.LOWEST]: 512,
|
|
622
|
-
[EFFORT.LOW]: 4096,
|
|
623
|
-
[EFFORT.MEDIUM]: 8192,
|
|
624
|
-
[EFFORT.HIGH]: 16384,
|
|
625
|
-
[EFFORT.HIGHEST]: 24576,
|
|
626
|
-
};
|
|
627
|
-
function toGeminiThinkingBudget(effort) {
|
|
628
|
-
return { papered: false, value: GEMINI_THINKING_BUDGET[effort] };
|
|
629
|
-
}
|
|
630
|
-
// Fireworks `reasoning_effort` — low | medium | high. Accepted on every model
|
|
631
|
-
// (the API no-ops where unsupported). No sub-low or top rung, so `lowest`
|
|
632
|
-
// collapses onto `low` and `highest` onto `high`.
|
|
633
|
-
const FIREWORKS_EFFORT = {
|
|
634
|
-
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
635
|
-
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
636
|
-
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
637
|
-
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
638
|
-
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
639
|
-
};
|
|
640
|
-
function toFireworksEffort(effort) {
|
|
641
|
-
return FIREWORKS_EFFORT[effort];
|
|
587
|
+
// undefined or true
|
|
588
|
+
return { enabled: true, ttl: CACHE_TTL_DEFAULT };
|
|
642
589
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
590
|
+
/**
|
|
591
|
+
* Deterministic short key for providers with automatic, prefix-based caching
|
|
592
|
+
* (e.g. OpenAI `prompt_cache_key`). Derived from the stable prefix so the same
|
|
593
|
+
* system prompt + tools + model always routes to the same cache. Dependency-
|
|
594
|
+
* free FNV-1a; not cryptographic.
|
|
595
|
+
*/
|
|
596
|
+
function promptCacheKey(seed) {
|
|
597
|
+
let hash = 0x811c9dc5;
|
|
598
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
599
|
+
hash ^= seed.charCodeAt(i);
|
|
600
|
+
hash = Math.imul(hash, 0x01000193);
|
|
601
|
+
}
|
|
602
|
+
return `jaypie-${(hash >>> 0).toString(16)}`;
|
|
655
603
|
}
|
|
656
604
|
|
|
657
|
-
// Enums
|
|
658
|
-
exports.LlmMessageRole = void 0;
|
|
659
|
-
(function (LlmMessageRole) {
|
|
660
|
-
LlmMessageRole["Assistant"] = "assistant";
|
|
661
|
-
LlmMessageRole["Developer"] = "developer";
|
|
662
|
-
LlmMessageRole["System"] = "system";
|
|
663
|
-
LlmMessageRole["User"] = "user";
|
|
664
|
-
})(exports.LlmMessageRole || (exports.LlmMessageRole = {}));
|
|
665
|
-
exports.LlmMessageType = void 0;
|
|
666
|
-
(function (LlmMessageType) {
|
|
667
|
-
LlmMessageType["FunctionCall"] = "function_call";
|
|
668
|
-
LlmMessageType["FunctionCallOutput"] = "function_call_output";
|
|
669
|
-
LlmMessageType["InputFile"] = "input_file";
|
|
670
|
-
LlmMessageType["InputImage"] = "input_image";
|
|
671
|
-
LlmMessageType["InputText"] = "input_text";
|
|
672
|
-
LlmMessageType["ItemReference"] = "item_reference";
|
|
673
|
-
LlmMessageType["Message"] = "message";
|
|
674
|
-
LlmMessageType["OutputText"] = "output_text";
|
|
675
|
-
LlmMessageType["Refusal"] = "refusal";
|
|
676
|
-
})(exports.LlmMessageType || (exports.LlmMessageType = {}));
|
|
677
|
-
var LlmResponseStatus;
|
|
678
|
-
(function (LlmResponseStatus) {
|
|
679
|
-
LlmResponseStatus["Completed"] = "completed";
|
|
680
|
-
LlmResponseStatus["Incomplete"] = "incomplete";
|
|
681
|
-
LlmResponseStatus["InProgress"] = "in_progress";
|
|
682
|
-
})(LlmResponseStatus || (LlmResponseStatus = {}));
|
|
683
|
-
// Progress
|
|
684
|
-
exports.LlmProgressEventType = void 0;
|
|
685
|
-
(function (LlmProgressEventType) {
|
|
686
|
-
LlmProgressEventType["Done"] = "done";
|
|
687
|
-
LlmProgressEventType["ModelRequest"] = "model_request";
|
|
688
|
-
LlmProgressEventType["ModelResponse"] = "model_response";
|
|
689
|
-
LlmProgressEventType["Retry"] = "retry";
|
|
690
|
-
LlmProgressEventType["Start"] = "start";
|
|
691
|
-
LlmProgressEventType["ToolCall"] = "tool_call";
|
|
692
|
-
LlmProgressEventType["ToolError"] = "tool_error";
|
|
693
|
-
LlmProgressEventType["ToolResult"] = "tool_result";
|
|
694
|
-
})(exports.LlmProgressEventType || (exports.LlmProgressEventType = {}));
|
|
695
|
-
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
// Types
|
|
699
|
-
//
|
|
700
|
-
exports.LlmStreamChunkType = void 0;
|
|
701
|
-
(function (LlmStreamChunkType) {
|
|
702
|
-
LlmStreamChunkType["Done"] = "done";
|
|
703
|
-
LlmStreamChunkType["Error"] = "error";
|
|
704
|
-
LlmStreamChunkType["Text"] = "text";
|
|
705
|
-
LlmStreamChunkType["ToolCall"] = "tool_call";
|
|
706
|
-
LlmStreamChunkType["ToolResult"] = "tool_result";
|
|
707
|
-
})(exports.LlmStreamChunkType || (exports.LlmStreamChunkType = {}));
|
|
708
|
-
|
|
709
605
|
/**
|
|
710
606
|
* Type guard to check if an item is a dedicated reasoning item (OpenAI)
|
|
711
607
|
*/
|
|
@@ -924,6 +820,45 @@ function fillFormatArrays({ content, format, }) {
|
|
|
924
820
|
return fillFromSchema(schema, structuredClone(content));
|
|
925
821
|
}
|
|
926
822
|
|
|
823
|
+
// Enums
|
|
824
|
+
exports.LlmMessageRole = void 0;
|
|
825
|
+
(function (LlmMessageRole) {
|
|
826
|
+
LlmMessageRole["Assistant"] = "assistant";
|
|
827
|
+
LlmMessageRole["Developer"] = "developer";
|
|
828
|
+
LlmMessageRole["System"] = "system";
|
|
829
|
+
LlmMessageRole["User"] = "user";
|
|
830
|
+
})(exports.LlmMessageRole || (exports.LlmMessageRole = {}));
|
|
831
|
+
exports.LlmMessageType = void 0;
|
|
832
|
+
(function (LlmMessageType) {
|
|
833
|
+
LlmMessageType["FunctionCall"] = "function_call";
|
|
834
|
+
LlmMessageType["FunctionCallOutput"] = "function_call_output";
|
|
835
|
+
LlmMessageType["InputFile"] = "input_file";
|
|
836
|
+
LlmMessageType["InputImage"] = "input_image";
|
|
837
|
+
LlmMessageType["InputText"] = "input_text";
|
|
838
|
+
LlmMessageType["ItemReference"] = "item_reference";
|
|
839
|
+
LlmMessageType["Message"] = "message";
|
|
840
|
+
LlmMessageType["OutputText"] = "output_text";
|
|
841
|
+
LlmMessageType["Refusal"] = "refusal";
|
|
842
|
+
})(exports.LlmMessageType || (exports.LlmMessageType = {}));
|
|
843
|
+
var LlmResponseStatus;
|
|
844
|
+
(function (LlmResponseStatus) {
|
|
845
|
+
LlmResponseStatus["Completed"] = "completed";
|
|
846
|
+
LlmResponseStatus["Incomplete"] = "incomplete";
|
|
847
|
+
LlmResponseStatus["InProgress"] = "in_progress";
|
|
848
|
+
})(LlmResponseStatus || (LlmResponseStatus = {}));
|
|
849
|
+
// Progress
|
|
850
|
+
exports.LlmProgressEventType = void 0;
|
|
851
|
+
(function (LlmProgressEventType) {
|
|
852
|
+
LlmProgressEventType["Done"] = "done";
|
|
853
|
+
LlmProgressEventType["ModelRequest"] = "model_request";
|
|
854
|
+
LlmProgressEventType["ModelResponse"] = "model_response";
|
|
855
|
+
LlmProgressEventType["Retry"] = "retry";
|
|
856
|
+
LlmProgressEventType["Start"] = "start";
|
|
857
|
+
LlmProgressEventType["ToolCall"] = "tool_call";
|
|
858
|
+
LlmProgressEventType["ToolError"] = "tool_error";
|
|
859
|
+
LlmProgressEventType["ToolResult"] = "tool_result";
|
|
860
|
+
})(exports.LlmProgressEventType || (exports.LlmProgressEventType = {}));
|
|
861
|
+
|
|
927
862
|
/**
|
|
928
863
|
* Converts a string to a standardized LlmInputMessage
|
|
929
864
|
* @param input - String to format
|
|
@@ -1518,64 +1453,367 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
|
|
|
1518
1453
|
usageByModel[key].output += item.output;
|
|
1519
1454
|
usageByModel[key].reasoning += item.reasoning;
|
|
1520
1455
|
usageByModel[key].total += item.total;
|
|
1456
|
+
if (item.cacheRead !== undefined) {
|
|
1457
|
+
usageByModel[key].cacheRead =
|
|
1458
|
+
(usageByModel[key].cacheRead ?? 0) + item.cacheRead;
|
|
1459
|
+
}
|
|
1460
|
+
if (item.cacheWrite !== undefined) {
|
|
1461
|
+
usageByModel[key].cacheWrite =
|
|
1462
|
+
(usageByModel[key].cacheWrite ?? 0) + item.cacheWrite;
|
|
1463
|
+
}
|
|
1521
1464
|
}
|
|
1522
1465
|
llm.usage = usageByModel;
|
|
1523
1466
|
}
|
|
1524
1467
|
log$1.log.tally({ llm });
|
|
1525
1468
|
}
|
|
1526
|
-
|
|
1527
|
-
/**
|
|
1528
|
-
* Helper function to safely call a function that might throw
|
|
1529
|
-
* @param fn Function to call safely
|
|
1530
|
-
*/
|
|
1531
|
-
function callSafely(fn) {
|
|
1532
|
-
try {
|
|
1533
|
-
fn();
|
|
1534
|
-
}
|
|
1535
|
-
catch {
|
|
1536
|
-
// Silently catch any errors from the function
|
|
1537
|
-
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Helper function to safely call a function that might throw
|
|
1472
|
+
* @param fn Function to call safely
|
|
1473
|
+
*/
|
|
1474
|
+
function callSafely(fn) {
|
|
1475
|
+
try {
|
|
1476
|
+
fn();
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
// Silently catch any errors from the function
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Attempts to parse a value as a number. Returns the original input if parsing fails or results in NaN.
|
|
1484
|
+
* @param input - The value to attempt to parse as a number
|
|
1485
|
+
* @param options - Optional configuration
|
|
1486
|
+
* @param options.defaultValue - Default value to return if parsing fails or results in NaN
|
|
1487
|
+
* @param options.warnFunction - Function to call with warning message if parsing fails or results in NaN
|
|
1488
|
+
* @returns The parsed number, defaultValue (if specified and parsing fails), or the original input
|
|
1489
|
+
*/
|
|
1490
|
+
function tryParseNumber(input, options) {
|
|
1491
|
+
if (input === null || input === undefined) {
|
|
1492
|
+
return input;
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
const parsed = Number(input);
|
|
1496
|
+
if (Number.isNaN(parsed)) {
|
|
1497
|
+
if (options?.warnFunction) {
|
|
1498
|
+
const warningMessage = `Failed to parse "${String(input)}" as number`;
|
|
1499
|
+
callSafely(() => options.warnFunction(warningMessage));
|
|
1500
|
+
}
|
|
1501
|
+
return typeof options?.defaultValue === "number"
|
|
1502
|
+
? options.defaultValue
|
|
1503
|
+
: input;
|
|
1504
|
+
}
|
|
1505
|
+
return parsed;
|
|
1506
|
+
}
|
|
1507
|
+
catch (error) {
|
|
1508
|
+
if (options?.warnFunction) {
|
|
1509
|
+
let errorMessage = "";
|
|
1510
|
+
if (error instanceof Error) {
|
|
1511
|
+
errorMessage = error.message;
|
|
1512
|
+
}
|
|
1513
|
+
const warningMessage = `Error parsing "${String(input)}" as number${errorMessage ? "; " + errorMessage : ""}`;
|
|
1514
|
+
callSafely(() => options.warnFunction(warningMessage));
|
|
1515
|
+
}
|
|
1516
|
+
return typeof options?.defaultValue === "number"
|
|
1517
|
+
? options.defaultValue
|
|
1518
|
+
: input;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Truthy-except-"false"/"0" gate on LLM_EXCHANGE_ENABLED, matching the
|
|
1524
|
+
* DD_LLMOBS_ENABLED convention in observability/llmobs.ts.
|
|
1525
|
+
*/
|
|
1526
|
+
function isExchangeStoreEnabled() {
|
|
1527
|
+
const flag = process.env.LLM_EXCHANGE_ENABLED;
|
|
1528
|
+
if (!flag) {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
return flag.toLowerCase() !== "false" && flag !== "0";
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Whether the loop should assemble an exchange envelope at settlement.
|
|
1535
|
+
*/
|
|
1536
|
+
function isExchangeRequested({ onExchange, }) {
|
|
1537
|
+
return Boolean(onExchange) || isExchangeStoreEnabled();
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Deliver an exchange envelope to a callback. Errors thrown by the callback
|
|
1541
|
+
* are logged and swallowed — exchange capture must never interrupt the call.
|
|
1542
|
+
*/
|
|
1543
|
+
async function emitExchange({ envelope, onExchange, }) {
|
|
1544
|
+
if (!onExchange) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
await onExchange(envelope);
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
const log = getLogger$7();
|
|
1552
|
+
log.warn("[operate] onExchange callback threw");
|
|
1553
|
+
log.var({ error });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
//
|
|
1558
|
+
//
|
|
1559
|
+
// Constants
|
|
1560
|
+
//
|
|
1561
|
+
const MODULE$1 = {
|
|
1562
|
+
// Computed at runtime so bundlers (esbuild) do not attempt to include
|
|
1563
|
+
// @jaypie/dynamodb, which is an optional peer dependency.
|
|
1564
|
+
JAYPIE_DYNAMODB: ["@jaypie", "dynamodb"].join("/"),
|
|
1565
|
+
};
|
|
1566
|
+
//
|
|
1567
|
+
//
|
|
1568
|
+
// Helpers
|
|
1569
|
+
//
|
|
1570
|
+
// Native dynamic import that neither rollup nor tsc rewrites to require(), so
|
|
1571
|
+
// a CJS-bundled build still loads @jaypie/dynamodb's ESM entry and shares the
|
|
1572
|
+
// host's initialized module instance. A require()-based resolution would load
|
|
1573
|
+
// the CJS build, whose module-level client state is separate from the ESM
|
|
1574
|
+
// instance an ESM host initializes (dual-package hazard, issue #429).
|
|
1575
|
+
const dynamicImport = new Function("s", "return import(s)");
|
|
1576
|
+
let resolved$1 = false;
|
|
1577
|
+
let cachedSdk$2 = null;
|
|
1578
|
+
/**
|
|
1579
|
+
* Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
|
|
1580
|
+
* throws) when the peer is absent. Cached after the first attempt.
|
|
1581
|
+
*/
|
|
1582
|
+
async function resolveExchangeStore() {
|
|
1583
|
+
if (resolved$1) {
|
|
1584
|
+
return cachedSdk$2;
|
|
1585
|
+
}
|
|
1586
|
+
resolved$1 = true;
|
|
1587
|
+
try {
|
|
1588
|
+
const dynamodb = await dynamicImport(MODULE$1.JAYPIE_DYNAMODB);
|
|
1589
|
+
const sdk = (dynamodb?.default ?? dynamodb);
|
|
1590
|
+
if (sdk && typeof sdk.storeExchange === "function") {
|
|
1591
|
+
cachedSdk$2 = sdk;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
catch {
|
|
1595
|
+
cachedSdk$2 = null;
|
|
1596
|
+
}
|
|
1597
|
+
return cachedSdk$2;
|
|
1598
|
+
}
|
|
1599
|
+
//
|
|
1600
|
+
//
|
|
1601
|
+
// Main
|
|
1602
|
+
//
|
|
1603
|
+
/**
|
|
1604
|
+
* Persist an exchange envelope via @jaypie/dynamodb when
|
|
1605
|
+
* LLM_EXCHANGE_ENABLED is set. Silent no-op when the flag is unset or the
|
|
1606
|
+
* peer is absent; persister failures are logged and never thrown.
|
|
1607
|
+
*/
|
|
1608
|
+
async function persistExchange(envelope) {
|
|
1609
|
+
if (!isExchangeStoreEnabled()) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const sdk = (await resolveExchangeStore());
|
|
1613
|
+
if (!sdk) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
try {
|
|
1617
|
+
await sdk.storeExchange(envelope);
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
const log = getLogger$7();
|
|
1621
|
+
log.warn("[operate] Exchange persistence failed");
|
|
1622
|
+
log.var({ error });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
//
|
|
1627
|
+
//
|
|
1628
|
+
// Abstract Base Adapter
|
|
1629
|
+
//
|
|
1630
|
+
/**
|
|
1631
|
+
* BaseProviderAdapter provides default implementations for common adapter methods.
|
|
1632
|
+
* Providers can extend this class to reduce boilerplate.
|
|
1633
|
+
*/
|
|
1634
|
+
class BaseProviderAdapter {
|
|
1635
|
+
constructor() {
|
|
1636
|
+
/**
|
|
1637
|
+
* Whether OperateLoop may take a corrective turn when a `format` request
|
|
1638
|
+
* completes with prose instead of structured output. Providers whose
|
|
1639
|
+
* structured output rides a tool emulation (where compliance is a model
|
|
1640
|
+
* decision) opt in; native grammar-constrained providers do not need it.
|
|
1641
|
+
*/
|
|
1642
|
+
this.supportsStructuredOutputRetry = false;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Default implementation checks if error is retryable via classifyError
|
|
1646
|
+
*/
|
|
1647
|
+
isRetryableError(error) {
|
|
1648
|
+
const classified = this.classifyError(error);
|
|
1649
|
+
return classified.shouldRetry;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Default implementation checks error category via classifyError
|
|
1653
|
+
*/
|
|
1654
|
+
isRateLimitError(error) {
|
|
1655
|
+
const classified = this.classifyError(error);
|
|
1656
|
+
return classified.category === "rate_limit";
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Default implementation returns false - override for providers with native structured output
|
|
1660
|
+
*/
|
|
1661
|
+
hasStructuredOutput(_response) {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Default implementation returns undefined - override for providers with native structured output
|
|
1666
|
+
*/
|
|
1667
|
+
extractStructuredOutput(_response) {
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Per-provider translation of the provider-neutral {@link LlmEffort} scale.
|
|
1674
|
+
*
|
|
1675
|
+
* The neutral enum (lowest → low → medium → high → highest) is a relative
|
|
1676
|
+
* five-point scale. Each table below maps it onto the target provider's native
|
|
1677
|
+
* effort control, keeping `medium`/`high` semantically aligned across providers
|
|
1678
|
+
* and using each provider's extra bottom (`minimal`) or top (`xhigh`/`max`)
|
|
1679
|
+
* rung where one exists. Providers with fewer levels collapse the ends and mark
|
|
1680
|
+
* the result `papered`. A single `effort` value is therefore safe to reuse
|
|
1681
|
+
* across providers and fallback chains.
|
|
1682
|
+
*/
|
|
1683
|
+
/** Consistent debug message for a papered-over effort level. */
|
|
1684
|
+
function paperedEffortMessage({ model, provider, requested, value, }) {
|
|
1685
|
+
return `[llm] effort '${requested}' has no distinct tier on ${provider} model '${model}'; using '${value}'`;
|
|
1686
|
+
}
|
|
1687
|
+
// OpenAI Responses API `reasoning.effort`.
|
|
1688
|
+
// Full ladder: minimal | low | medium | high | xhigh (plus `none`). Availability
|
|
1689
|
+
// is not uniform across the gpt-5 line:
|
|
1690
|
+
// - `xhigh` was introduced at gpt-5.2 and has been continuous since, so it is
|
|
1691
|
+
// safe for our gpt-5.4 default and everything newer.
|
|
1692
|
+
// - `minimal` shipped on gpt-5/5.1, was dropped at gpt-5.2, and returned on
|
|
1693
|
+
// the current line; because that history is non-monotonic we only trust it
|
|
1694
|
+
// from gpt-5.4 (our default floor) onward.
|
|
1695
|
+
// Outside those windows (older gpt-5, o-series) the extreme rung is clamped and
|
|
1696
|
+
// the mapping reports `papered: true`.
|
|
1697
|
+
const OPENAI_EFFORT = {
|
|
1698
|
+
[EFFORT.LOWEST]: "minimal",
|
|
1699
|
+
[EFFORT.LOW]: "low",
|
|
1700
|
+
[EFFORT.MEDIUM]: "medium",
|
|
1701
|
+
[EFFORT.HIGH]: "high",
|
|
1702
|
+
[EFFORT.HIGHEST]: "xhigh",
|
|
1703
|
+
};
|
|
1704
|
+
function openAiGptVersion(model) {
|
|
1705
|
+
const match = model.match(/^gpt-(\d+)(?:\.(\d+))?/);
|
|
1706
|
+
if (!match)
|
|
1707
|
+
return null;
|
|
1708
|
+
return { major: Number(match[1]), minor: match[2] ? Number(match[2]) : 0 };
|
|
1709
|
+
}
|
|
1710
|
+
function atLeast(version, major, minor) {
|
|
1711
|
+
if (!version)
|
|
1712
|
+
return false;
|
|
1713
|
+
return (version.major > major || (version.major === major && version.minor >= minor));
|
|
1538
1714
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
* @returns The parsed number, defaultValue (if specified and parsing fails), or the original input
|
|
1546
|
-
*/
|
|
1547
|
-
function tryParseNumber(input, options) {
|
|
1548
|
-
if (input === null || input === undefined) {
|
|
1549
|
-
return input;
|
|
1550
|
-
}
|
|
1551
|
-
try {
|
|
1552
|
-
const parsed = Number(input);
|
|
1553
|
-
if (Number.isNaN(parsed)) {
|
|
1554
|
-
if (options?.warnFunction) {
|
|
1555
|
-
const warningMessage = `Failed to parse "${String(input)}" as number`;
|
|
1556
|
-
callSafely(() => options.warnFunction(warningMessage));
|
|
1557
|
-
}
|
|
1558
|
-
return typeof options?.defaultValue === "number"
|
|
1559
|
-
? options.defaultValue
|
|
1560
|
-
: input;
|
|
1561
|
-
}
|
|
1562
|
-
return parsed;
|
|
1715
|
+
function toOpenAiEffort(effort, { model }) {
|
|
1716
|
+
const native = OPENAI_EFFORT[effort];
|
|
1717
|
+
const version = openAiGptVersion(model);
|
|
1718
|
+
// `minimal` only from gpt-5.4 (non-monotonic history; absent on o-series)
|
|
1719
|
+
if (native === "minimal" && !atLeast(version, 5, 4)) {
|
|
1720
|
+
return { papered: true, value: "low" };
|
|
1563
1721
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if (error instanceof Error) {
|
|
1568
|
-
errorMessage = error.message;
|
|
1569
|
-
}
|
|
1570
|
-
const warningMessage = `Error parsing "${String(input)}" as number${errorMessage ? "; " + errorMessage : ""}`;
|
|
1571
|
-
callSafely(() => options.warnFunction(warningMessage));
|
|
1572
|
-
}
|
|
1573
|
-
return typeof options?.defaultValue === "number"
|
|
1574
|
-
? options.defaultValue
|
|
1575
|
-
: input;
|
|
1722
|
+
// `xhigh` from gpt-5.2 onward (absent on older gpt-5 and o-series)
|
|
1723
|
+
if (native === "xhigh" && !atLeast(version, 5, 2)) {
|
|
1724
|
+
return { papered: true, value: "high" };
|
|
1576
1725
|
}
|
|
1726
|
+
return { papered: false, value: native };
|
|
1727
|
+
}
|
|
1728
|
+
// xAI Grok `reasoning_effort` — low | medium | high. No sub-low or top rung, so
|
|
1729
|
+
// `lowest` collapses onto `low` and `highest` onto `high`.
|
|
1730
|
+
const XAI_EFFORT = {
|
|
1731
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1732
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1733
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1734
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1735
|
+
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
1736
|
+
};
|
|
1737
|
+
function toXaiEffort(effort) {
|
|
1738
|
+
return XAI_EFFORT[effort];
|
|
1739
|
+
}
|
|
1740
|
+
// Anthropic `output_config.effort` — low | medium | high | xhigh | max. No
|
|
1741
|
+
// sub-low rung, so `lowest` collapses onto `low`; `highest` reaches `max`.
|
|
1742
|
+
const ANTHROPIC_EFFORT = {
|
|
1743
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1744
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1745
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1746
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1747
|
+
[EFFORT.HIGHEST]: { papered: false, value: "max" },
|
|
1748
|
+
};
|
|
1749
|
+
function toAnthropicEffort(effort) {
|
|
1750
|
+
return ANTHROPIC_EFFORT[effort];
|
|
1751
|
+
}
|
|
1752
|
+
// Gemini 3.x `thinkingConfig.thinkingLevel` — MINIMAL | LOW | MEDIUM | HIGH. No
|
|
1753
|
+
// top rung above HIGH, so `highest` collapses onto HIGH.
|
|
1754
|
+
const GEMINI_THINKING_LEVEL = {
|
|
1755
|
+
[EFFORT.LOWEST]: { papered: false, value: "MINIMAL" },
|
|
1756
|
+
[EFFORT.LOW]: { papered: false, value: "LOW" },
|
|
1757
|
+
[EFFORT.MEDIUM]: { papered: false, value: "MEDIUM" },
|
|
1758
|
+
[EFFORT.HIGH]: { papered: false, value: "HIGH" },
|
|
1759
|
+
[EFFORT.HIGHEST]: { papered: true, value: "HIGH" },
|
|
1760
|
+
};
|
|
1761
|
+
function toGeminiThinkingLevel(effort) {
|
|
1762
|
+
return GEMINI_THINKING_LEVEL[effort];
|
|
1763
|
+
}
|
|
1764
|
+
// Gemini 2.5 `thinkingConfig.thinkingBudget` — every tier is a distinct token
|
|
1765
|
+
// budget, so nothing is papered over. Floor (512) clears every 2.5 minimum;
|
|
1766
|
+
// ceiling (24,576) is valid across 2.5 Pro (max 32,768) and Flash (max 24,576).
|
|
1767
|
+
const GEMINI_THINKING_BUDGET = {
|
|
1768
|
+
[EFFORT.LOWEST]: 512,
|
|
1769
|
+
[EFFORT.LOW]: 4096,
|
|
1770
|
+
[EFFORT.MEDIUM]: 8192,
|
|
1771
|
+
[EFFORT.HIGH]: 16384,
|
|
1772
|
+
[EFFORT.HIGHEST]: 24576,
|
|
1773
|
+
};
|
|
1774
|
+
function toGeminiThinkingBudget(effort) {
|
|
1775
|
+
return { papered: false, value: GEMINI_THINKING_BUDGET[effort] };
|
|
1776
|
+
}
|
|
1777
|
+
// Fireworks `reasoning_effort` — low | medium | high. Accepted on every model
|
|
1778
|
+
// (the API no-ops where unsupported). No sub-low or top rung, so `lowest`
|
|
1779
|
+
// collapses onto `low` and `highest` onto `high`.
|
|
1780
|
+
const FIREWORKS_EFFORT = {
|
|
1781
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1782
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1783
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1784
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1785
|
+
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
1786
|
+
};
|
|
1787
|
+
function toFireworksEffort(effort) {
|
|
1788
|
+
return FIREWORKS_EFFORT[effort];
|
|
1789
|
+
}
|
|
1790
|
+
// OpenRouter `reasoning.effort` — accepts the full minimal..xhigh ladder and
|
|
1791
|
+
// maps to the routed provider's nearest supported level itself, so nothing is
|
|
1792
|
+
// papered here.
|
|
1793
|
+
const OPENROUTER_EFFORT = {
|
|
1794
|
+
[EFFORT.LOWEST]: "minimal",
|
|
1795
|
+
[EFFORT.LOW]: "low",
|
|
1796
|
+
[EFFORT.MEDIUM]: "medium",
|
|
1797
|
+
[EFFORT.HIGH]: "high",
|
|
1798
|
+
[EFFORT.HIGHEST]: "xhigh",
|
|
1799
|
+
};
|
|
1800
|
+
function toOpenRouterEffort(effort) {
|
|
1801
|
+
return { papered: false, value: OPENROUTER_EFFORT[effort] };
|
|
1577
1802
|
}
|
|
1578
1803
|
|
|
1804
|
+
//
|
|
1805
|
+
//
|
|
1806
|
+
// Types
|
|
1807
|
+
//
|
|
1808
|
+
exports.LlmStreamChunkType = void 0;
|
|
1809
|
+
(function (LlmStreamChunkType) {
|
|
1810
|
+
LlmStreamChunkType["Done"] = "done";
|
|
1811
|
+
LlmStreamChunkType["Error"] = "error";
|
|
1812
|
+
LlmStreamChunkType["Text"] = "text";
|
|
1813
|
+
LlmStreamChunkType["ToolCall"] = "tool_call";
|
|
1814
|
+
LlmStreamChunkType["ToolResult"] = "tool_result";
|
|
1815
|
+
})(exports.LlmStreamChunkType || (exports.LlmStreamChunkType = {}));
|
|
1816
|
+
|
|
1579
1817
|
//
|
|
1580
1818
|
//
|
|
1581
1819
|
// Error Classification Types
|
|
@@ -2159,8 +2397,19 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
2159
2397
|
PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
|
|
2160
2398
|
stream: false,
|
|
2161
2399
|
};
|
|
2400
|
+
const cache = resolveCache(request.cache);
|
|
2401
|
+
const cacheControl = cache.enabled
|
|
2402
|
+
? {
|
|
2403
|
+
type: "ephemeral",
|
|
2404
|
+
...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
|
|
2405
|
+
}
|
|
2406
|
+
: undefined;
|
|
2162
2407
|
if (request.system) {
|
|
2163
|
-
|
|
2408
|
+
// A cache breakpoint on the system block caches tools+system together
|
|
2409
|
+
// (render order is tools -> system -> messages).
|
|
2410
|
+
anthropicRequest.system = cacheControl
|
|
2411
|
+
? [{ type: "text", text: request.system, cache_control: cacheControl }]
|
|
2412
|
+
: request.system;
|
|
2164
2413
|
}
|
|
2165
2414
|
const useFallbackStructuredOutput = Boolean(request.format) &&
|
|
2166
2415
|
!this.supportsStructuredOutput(anthropicRequest.model);
|
|
@@ -2177,7 +2426,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
2177
2426
|
});
|
|
2178
2427
|
}
|
|
2179
2428
|
if (allTools.length > 0) {
|
|
2180
|
-
anthropicRequest.tools = allTools.map((tool) => ({
|
|
2429
|
+
anthropicRequest.tools = allTools.map((tool, index) => ({
|
|
2181
2430
|
name: tool.name,
|
|
2182
2431
|
description: tool.description,
|
|
2183
2432
|
input_schema: {
|
|
@@ -2185,6 +2434,11 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
2185
2434
|
type: "object",
|
|
2186
2435
|
},
|
|
2187
2436
|
type: "custom",
|
|
2437
|
+
// Breakpoint on the last tool caches the tool list (which renders
|
|
2438
|
+
// before system) when there is no system prompt to anchor it.
|
|
2439
|
+
...(cacheControl && index === allTools.length - 1
|
|
2440
|
+
? { cache_control: cacheControl }
|
|
2441
|
+
: {}),
|
|
2188
2442
|
}));
|
|
2189
2443
|
anthropicRequest.tool_choice = useFallbackStructuredOutput
|
|
2190
2444
|
? { type: "any" }
|
|
@@ -2487,6 +2741,12 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
2487
2741
|
output: usage.output_tokens,
|
|
2488
2742
|
reasoning: usage.thinking_tokens || 0,
|
|
2489
2743
|
total: usage.input_tokens + usage.output_tokens,
|
|
2744
|
+
...(usage.cache_read_input_tokens !== undefined
|
|
2745
|
+
? { cacheRead: usage.cache_read_input_tokens }
|
|
2746
|
+
: {}),
|
|
2747
|
+
...(usage.cache_creation_input_tokens !== undefined
|
|
2748
|
+
? { cacheWrite: usage.cache_creation_input_tokens }
|
|
2749
|
+
: {}),
|
|
2490
2750
|
provider: this.name,
|
|
2491
2751
|
model,
|
|
2492
2752
|
};
|
|
@@ -2739,6 +2999,14 @@ function isTemperatureDeprecationError$3(error) {
|
|
|
2739
2999
|
const msg = error?.message ?? "";
|
|
2740
3000
|
return /temperature.*deprecated|deprecated.*temperature/i.test(msg);
|
|
2741
3001
|
}
|
|
3002
|
+
function isCachePointUnsupportedError(error) {
|
|
3003
|
+
const name = error?.constructor?.name ?? "";
|
|
3004
|
+
const msg = error?.message ?? "";
|
|
3005
|
+
// A model that cannot cache rejects the cachePoint block with a
|
|
3006
|
+
// ValidationException naming caching / cachePoint.
|
|
3007
|
+
return (/ValidationException|invalid|not support/i.test(name + " " + msg) &&
|
|
3008
|
+
/cachePoint|cache_point|prompt caching|caching/i.test(msg));
|
|
3009
|
+
}
|
|
2742
3010
|
function extractJson(text) {
|
|
2743
3011
|
// Try direct parse first
|
|
2744
3012
|
try {
|
|
@@ -2774,6 +3042,13 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2774
3042
|
this.defaultModel = PROVIDER.BEDROCK.DEFAULT;
|
|
2775
3043
|
this._modelsFallbackToStructuredOutputTool = new Set();
|
|
2776
3044
|
this._modelsWithoutTemperature = new Set();
|
|
3045
|
+
this._modelsWithoutCachePoint = new Set();
|
|
3046
|
+
}
|
|
3047
|
+
rememberModelRejectsCachePoint(model) {
|
|
3048
|
+
this._modelsWithoutCachePoint.add(model);
|
|
3049
|
+
}
|
|
3050
|
+
supportsCachePoint(model) {
|
|
3051
|
+
return !this._modelsWithoutCachePoint.has(model);
|
|
2777
3052
|
}
|
|
2778
3053
|
rememberModelRejectsOutputConfig(model) {
|
|
2779
3054
|
this._modelsFallbackToStructuredOutputTool.add(model);
|
|
@@ -2910,11 +3185,51 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2910
3185
|
};
|
|
2911
3186
|
}
|
|
2912
3187
|
}
|
|
3188
|
+
// Prompt caching via Converse cachePoint blocks (5-min default TTL; the
|
|
3189
|
+
// `ttl` option does not apply). Gated per model — cachePoint 400s on
|
|
3190
|
+
// unsupported models, which executeRequest catches and denylists.
|
|
3191
|
+
if (resolveCache(request.cache).enabled && this.supportsCachePoint(model)) {
|
|
3192
|
+
const cachePoint = { cachePoint: { type: "default" } };
|
|
3193
|
+
if (bedrockRequest.system && bedrockRequest.system.length > 0) {
|
|
3194
|
+
bedrockRequest.system = [
|
|
3195
|
+
...bedrockRequest.system,
|
|
3196
|
+
cachePoint,
|
|
3197
|
+
];
|
|
3198
|
+
}
|
|
3199
|
+
if (bedrockRequest.toolConfig?.tools?.length) {
|
|
3200
|
+
bedrockRequest.toolConfig = {
|
|
3201
|
+
...bedrockRequest.toolConfig,
|
|
3202
|
+
tools: [
|
|
3203
|
+
...bedrockRequest.toolConfig.tools,
|
|
3204
|
+
cachePoint,
|
|
3205
|
+
],
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
2913
3209
|
if (request.providerOptions) {
|
|
2914
3210
|
Object.assign(bedrockRequest, request.providerOptions);
|
|
2915
3211
|
}
|
|
2916
3212
|
return bedrockRequest;
|
|
2917
3213
|
}
|
|
3214
|
+
/** Remove any cachePoint blocks so the request can be retried unsupported. */
|
|
3215
|
+
stripCachePoints(request) {
|
|
3216
|
+
const stripped = { ...request };
|
|
3217
|
+
if (stripped.system) {
|
|
3218
|
+
stripped.system = stripped.system.filter((block) => !("cachePoint" in block));
|
|
3219
|
+
}
|
|
3220
|
+
if (stripped.toolConfig?.tools) {
|
|
3221
|
+
stripped.toolConfig = {
|
|
3222
|
+
...stripped.toolConfig,
|
|
3223
|
+
tools: stripped.toolConfig.tools.filter((tool) => !("cachePoint" in tool)),
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
return stripped;
|
|
3227
|
+
}
|
|
3228
|
+
requestHasCachePoints(request) {
|
|
3229
|
+
const inSystem = (request.system ?? []).some((block) => "cachePoint" in block);
|
|
3230
|
+
const inTools = (request.toolConfig?.tools ?? []).some((tool) => "cachePoint" in tool);
|
|
3231
|
+
return inSystem || inTools;
|
|
3232
|
+
}
|
|
2918
3233
|
formatTools(toolkit) {
|
|
2919
3234
|
return toolkit.tools.map((tool) => ({
|
|
2920
3235
|
name: tool.name,
|
|
@@ -2969,6 +3284,15 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2969
3284
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(bedrockRequest);
|
|
2970
3285
|
return (await bedrockClient.send(new ConverseCommand(fallbackRequest), signal ? { abortSignal: signal } : undefined));
|
|
2971
3286
|
}
|
|
3287
|
+
if (this.requestHasCachePoints(bedrockRequest) &&
|
|
3288
|
+
isCachePointUnsupportedError(error)) {
|
|
3289
|
+
this.rememberModelRejectsCachePoint(bedrockRequest.modelId || this.defaultModel);
|
|
3290
|
+
const retryRequest = this.stripCachePoints(bedrockRequest);
|
|
3291
|
+
const response = (await bedrockClient.send(new ConverseCommand(retryRequest), signal ? { abortSignal: signal } : undefined));
|
|
3292
|
+
if (wantsStructuredOutput)
|
|
3293
|
+
response.__jaypieStructuredOutput = true;
|
|
3294
|
+
return response;
|
|
3295
|
+
}
|
|
2972
3296
|
throw error;
|
|
2973
3297
|
}
|
|
2974
3298
|
}
|
|
@@ -3112,6 +3436,12 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
3112
3436
|
reasoning: 0,
|
|
3113
3437
|
total: usage?.totalTokens ??
|
|
3114
3438
|
(usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0),
|
|
3439
|
+
...(usage?.cacheReadInputTokens !== undefined
|
|
3440
|
+
? { cacheRead: usage.cacheReadInputTokens }
|
|
3441
|
+
: {}),
|
|
3442
|
+
...(usage?.cacheWriteInputTokens !== undefined
|
|
3443
|
+
? { cacheWrite: usage.cacheWriteInputTokens }
|
|
3444
|
+
: {}),
|
|
3115
3445
|
provider: this.name,
|
|
3116
3446
|
model,
|
|
3117
3447
|
};
|
|
@@ -5283,6 +5613,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
5283
5613
|
supportsReasoningEffort(model) {
|
|
5284
5614
|
return isReasoningModel(model);
|
|
5285
5615
|
}
|
|
5616
|
+
/**
|
|
5617
|
+
* Whether to emit `prompt_cache_key` (OpenAI Responses API). Overridable by
|
|
5618
|
+
* OpenAI-compatible subclasses whose backend rejects the field.
|
|
5619
|
+
*/
|
|
5620
|
+
supportsPromptCacheKey() {
|
|
5621
|
+
return true;
|
|
5622
|
+
}
|
|
5286
5623
|
/** Translate a normalized effort to this provider's `reasoning.effort` value. */
|
|
5287
5624
|
mapReasoningEffort(effort, model) {
|
|
5288
5625
|
return toOpenAiEffort(effort, { model });
|
|
@@ -5322,6 +5659,18 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
5322
5659
|
summary: "auto",
|
|
5323
5660
|
};
|
|
5324
5661
|
}
|
|
5662
|
+
// OpenAI prompt caching is automatic (prefix-based). Setting a stable
|
|
5663
|
+
// prompt_cache_key routes repeat traffic to the same cache across
|
|
5664
|
+
// instances. Keyed on the stable prefix only (system + instructions +
|
|
5665
|
+
// tools + model), never the volatile user turn.
|
|
5666
|
+
if (this.supportsPromptCacheKey() && resolveCache(request.cache).enabled) {
|
|
5667
|
+
openaiRequest.prompt_cache_key = promptCacheKey(JSON.stringify([
|
|
5668
|
+
model,
|
|
5669
|
+
request.system ?? "",
|
|
5670
|
+
request.instructions ?? "",
|
|
5671
|
+
request.tools ?? [],
|
|
5672
|
+
]));
|
|
5673
|
+
}
|
|
5325
5674
|
if (request.providerOptions) {
|
|
5326
5675
|
Object.assign(openaiRequest, request.providerOptions);
|
|
5327
5676
|
}
|
|
@@ -5580,11 +5929,13 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
5580
5929
|
model,
|
|
5581
5930
|
};
|
|
5582
5931
|
}
|
|
5932
|
+
const cachedTokens = openaiResponse.usage.input_tokens_details?.cached_tokens;
|
|
5583
5933
|
return {
|
|
5584
5934
|
input: openaiResponse.usage.input_tokens || 0,
|
|
5585
5935
|
output: openaiResponse.usage.output_tokens || 0,
|
|
5586
5936
|
reasoning: openaiResponse.usage.output_tokens_details?.reasoning_tokens || 0,
|
|
5587
5937
|
total: openaiResponse.usage.total_tokens || 0,
|
|
5938
|
+
...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
|
|
5588
5939
|
provider: this.name,
|
|
5589
5940
|
model,
|
|
5590
5941
|
};
|
|
@@ -5976,7 +6327,13 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
5976
6327
|
//
|
|
5977
6328
|
buildRequest(request) {
|
|
5978
6329
|
// Convert messages to OpenRouter format (OpenAI-compatible)
|
|
5979
|
-
const
|
|
6330
|
+
const cache = resolveCache(request.cache);
|
|
6331
|
+
const messages = this.convertMessagesToOpenRouter(request.messages, request.system, cache.enabled
|
|
6332
|
+
? {
|
|
6333
|
+
type: "ephemeral",
|
|
6334
|
+
...(cache.ttl === "1h" ? { ttl: "1h" } : {}),
|
|
6335
|
+
}
|
|
6336
|
+
: undefined);
|
|
5980
6337
|
// Append instructions to last message if provided
|
|
5981
6338
|
if (request.instructions && messages.length > 0) {
|
|
5982
6339
|
const lastMsg = messages[messages.length - 1];
|
|
@@ -6340,11 +6697,15 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
6340
6697
|
}
|
|
6341
6698
|
// SDK returns camelCase, but support snake_case as fallback
|
|
6342
6699
|
const usage = openRouterResponse.usage;
|
|
6700
|
+
const cachedTokens = usage.promptTokensDetails?.cachedTokens ??
|
|
6701
|
+
usage.promptTokensDetails?.cached_tokens ??
|
|
6702
|
+
usage.prompt_tokens_details?.cached_tokens;
|
|
6343
6703
|
return {
|
|
6344
6704
|
input: usage.promptTokens || usage.prompt_tokens || 0,
|
|
6345
6705
|
output: usage.completionTokens || usage.completion_tokens || 0,
|
|
6346
6706
|
reasoning: usage.completionTokensDetails?.reasoningTokens || 0,
|
|
6347
6707
|
total: usage.totalTokens || usage.total_tokens || 0,
|
|
6708
|
+
...(cachedTokens !== undefined ? { cacheRead: cachedTokens } : {}),
|
|
6348
6709
|
provider: this.name,
|
|
6349
6710
|
model,
|
|
6350
6711
|
};
|
|
@@ -6545,13 +6906,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
6545
6906
|
const choice = response.choices[0];
|
|
6546
6907
|
return choice?.message?.content ?? undefined;
|
|
6547
6908
|
}
|
|
6548
|
-
convertMessagesToOpenRouter(messages, system) {
|
|
6909
|
+
convertMessagesToOpenRouter(messages, system, cacheControl) {
|
|
6549
6910
|
const openRouterMessages = [];
|
|
6550
|
-
// Add system message if provided
|
|
6911
|
+
// Add system message if provided. A cache_control breakpoint on the system
|
|
6912
|
+
// content is forwarded by OpenRouter to Anthropic/Gemini backends (and
|
|
6913
|
+
// ignored by others), caching the stable system prefix.
|
|
6551
6914
|
if (system) {
|
|
6552
6915
|
openRouterMessages.push({
|
|
6553
6916
|
role: "system",
|
|
6554
|
-
content:
|
|
6917
|
+
content: cacheControl
|
|
6918
|
+
? [{ type: "text", text: system, cache_control: cacheControl }]
|
|
6919
|
+
: system,
|
|
6555
6920
|
});
|
|
6556
6921
|
}
|
|
6557
6922
|
for (const msg of messages) {
|
|
@@ -8077,10 +8442,14 @@ class OperateLoop {
|
|
|
8077
8442
|
log.trace("[operate] Starting operate loop");
|
|
8078
8443
|
log.trace.var({ "operate.input": input });
|
|
8079
8444
|
log.trace.var({ "operate.options": options });
|
|
8445
|
+
const startedAt = new Date().toISOString();
|
|
8446
|
+
const startMs = Date.now();
|
|
8080
8447
|
// Initialize state
|
|
8081
8448
|
const state = await this.initializeState(input, options);
|
|
8082
8449
|
const context = this.createContext(options);
|
|
8083
8450
|
const modelName = options.model ?? this.adapter.defaultModel;
|
|
8451
|
+
const exchangeRequested = isExchangeRequested(options);
|
|
8452
|
+
const initialHistoryLength = state.responseBuilder.getHistory().length;
|
|
8084
8453
|
await emitProgress({
|
|
8085
8454
|
event: {
|
|
8086
8455
|
maxTurns: state.maxTurns,
|
|
@@ -8092,59 +8461,102 @@ class OperateLoop {
|
|
|
8092
8461
|
});
|
|
8093
8462
|
// Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
|
|
8094
8463
|
// Child llm/tool spans nest under it via the SDK's active-span context.
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
state.currentTurn
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8464
|
+
try {
|
|
8465
|
+
return await withLlmObsSpan({
|
|
8466
|
+
kind: state.toolkit ? "agent" : "llm",
|
|
8467
|
+
modelName,
|
|
8468
|
+
modelProvider: this.adapter.name,
|
|
8469
|
+
name: "jaypie.llm.operate",
|
|
8470
|
+
}, async () => {
|
|
8471
|
+
// Build initial request
|
|
8472
|
+
let request = this.buildInitialRequest(state, options);
|
|
8473
|
+
// Multi-turn loop
|
|
8474
|
+
while (state.currentTurn < state.maxTurns) {
|
|
8475
|
+
state.currentTurn++;
|
|
8476
|
+
// Execute one turn with retry logic
|
|
8477
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
8478
|
+
if (!shouldContinue) {
|
|
8479
|
+
break;
|
|
8480
|
+
}
|
|
8481
|
+
// Rebuild request with updated history for next turn
|
|
8482
|
+
request = {
|
|
8483
|
+
effort: options.effort,
|
|
8484
|
+
format: state.formattedFormat,
|
|
8485
|
+
instructions: options.instructions,
|
|
8486
|
+
messages: state.currentInput,
|
|
8487
|
+
model: modelName,
|
|
8488
|
+
providerOptions: options.providerOptions,
|
|
8489
|
+
structuredOutputRetry: state.structuredOutputRetry,
|
|
8490
|
+
system: options.system,
|
|
8491
|
+
temperature: options.temperature,
|
|
8492
|
+
tools: state.formattedTools,
|
|
8493
|
+
user: options.user,
|
|
8494
|
+
};
|
|
8110
8495
|
}
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
});
|
|
8132
|
-
tallyOperate({
|
|
8133
|
-
toolCallNames: state.toolCallNames,
|
|
8134
|
-
turns: state.currentTurn,
|
|
8135
|
-
usage: response.usage,
|
|
8136
|
-
});
|
|
8137
|
-
await emitProgress({
|
|
8138
|
-
event: {
|
|
8139
|
-
content: response.content,
|
|
8140
|
-
turn: state.currentTurn,
|
|
8141
|
-
type: exports.LlmProgressEventType.Done,
|
|
8496
|
+
const response = state.responseBuilder.build();
|
|
8497
|
+
if (exchangeRequested) {
|
|
8498
|
+
response.exchange = buildExchangeEnvelope({
|
|
8499
|
+
duration: Date.now() - startMs,
|
|
8500
|
+
initialHistoryLength,
|
|
8501
|
+
input,
|
|
8502
|
+
options,
|
|
8503
|
+
response,
|
|
8504
|
+
startedAt,
|
|
8505
|
+
state,
|
|
8506
|
+
});
|
|
8507
|
+
}
|
|
8508
|
+
annotateLlmObs({
|
|
8509
|
+
inputData: input,
|
|
8510
|
+
metrics: usageToLlmObsMetrics(response.usage),
|
|
8511
|
+
outputData: response.content,
|
|
8512
|
+
});
|
|
8513
|
+
tallyOperate({
|
|
8514
|
+
toolCallNames: state.toolCallNames,
|
|
8515
|
+
turns: state.currentTurn,
|
|
8142
8516
|
usage: response.usage,
|
|
8143
|
-
}
|
|
8144
|
-
|
|
8517
|
+
});
|
|
8518
|
+
await emitProgress({
|
|
8519
|
+
event: {
|
|
8520
|
+
content: response.content,
|
|
8521
|
+
turn: state.currentTurn,
|
|
8522
|
+
type: exports.LlmProgressEventType.Done,
|
|
8523
|
+
usage: response.usage,
|
|
8524
|
+
},
|
|
8525
|
+
onProgress: options.onProgress,
|
|
8526
|
+
});
|
|
8527
|
+
return response;
|
|
8145
8528
|
});
|
|
8146
|
-
|
|
8147
|
-
|
|
8529
|
+
}
|
|
8530
|
+
catch (error) {
|
|
8531
|
+
// A hard failure (retry budget exhausted, unrecoverable) still settles
|
|
8532
|
+
// the exchange: attach the envelope to the thrown error so the facade
|
|
8533
|
+
// can emit it before rethrowing to the caller.
|
|
8534
|
+
if (exchangeRequested) {
|
|
8535
|
+
const response = state.responseBuilder.build();
|
|
8536
|
+
if (!response.error) {
|
|
8537
|
+
const thrown = error;
|
|
8538
|
+
response.error = {
|
|
8539
|
+
detail: thrown.detail ?? thrown.message,
|
|
8540
|
+
status: thrown.status ?? 500,
|
|
8541
|
+
title: thrown.title ?? thrown.name ?? "Error",
|
|
8542
|
+
};
|
|
8543
|
+
}
|
|
8544
|
+
if (response.status === LlmResponseStatus.InProgress) {
|
|
8545
|
+
response.status = LlmResponseStatus.Incomplete;
|
|
8546
|
+
}
|
|
8547
|
+
error.exchange =
|
|
8548
|
+
buildExchangeEnvelope({
|
|
8549
|
+
duration: Date.now() - startMs,
|
|
8550
|
+
initialHistoryLength,
|
|
8551
|
+
input,
|
|
8552
|
+
options,
|
|
8553
|
+
response,
|
|
8554
|
+
startedAt,
|
|
8555
|
+
state,
|
|
8556
|
+
});
|
|
8557
|
+
}
|
|
8558
|
+
throw error;
|
|
8559
|
+
}
|
|
8148
8560
|
}
|
|
8149
8561
|
//
|
|
8150
8562
|
// Private Methods
|
|
@@ -8203,6 +8615,7 @@ class OperateLoop {
|
|
|
8203
8615
|
formattedTools,
|
|
8204
8616
|
maxTurns,
|
|
8205
8617
|
responseBuilder,
|
|
8618
|
+
retries: 0,
|
|
8206
8619
|
toolCallNames: [],
|
|
8207
8620
|
toolkit,
|
|
8208
8621
|
};
|
|
@@ -8215,6 +8628,7 @@ class OperateLoop {
|
|
|
8215
8628
|
}
|
|
8216
8629
|
buildInitialRequest(state, options) {
|
|
8217
8630
|
return {
|
|
8631
|
+
cache: options.cache,
|
|
8218
8632
|
effort: options.effort,
|
|
8219
8633
|
format: state.formattedFormat,
|
|
8220
8634
|
instructions: options.instructions,
|
|
@@ -8264,26 +8678,26 @@ class OperateLoop {
|
|
|
8264
8678
|
options,
|
|
8265
8679
|
providerRequest,
|
|
8266
8680
|
});
|
|
8267
|
-
//
|
|
8681
|
+
// Count retries for the exchange envelope and emit retry progress
|
|
8682
|
+
// alongside the caller's onRetryableModelError hook
|
|
8268
8683
|
const hooks = context.hooks;
|
|
8269
|
-
const hooksWithProgress =
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
: hooks;
|
|
8684
|
+
const hooksWithProgress = {
|
|
8685
|
+
...hooks,
|
|
8686
|
+
onRetryableModelError: async (retryContext) => {
|
|
8687
|
+
state.retries++;
|
|
8688
|
+
await emitProgress({
|
|
8689
|
+
event: {
|
|
8690
|
+
error: retryContext.error instanceof Error
|
|
8691
|
+
? retryContext.error.message
|
|
8692
|
+
: String(retryContext.error),
|
|
8693
|
+
turn: state.currentTurn,
|
|
8694
|
+
type: exports.LlmProgressEventType.Retry,
|
|
8695
|
+
},
|
|
8696
|
+
onProgress: options.onProgress,
|
|
8697
|
+
});
|
|
8698
|
+
return hooks?.onRetryableModelError?.(retryContext);
|
|
8699
|
+
},
|
|
8700
|
+
};
|
|
8287
8701
|
// Execute with retry inside a child llm span (no-op when llmobs disabled).
|
|
8288
8702
|
// RetryExecutor handles error hooks and throws appropriate errors.
|
|
8289
8703
|
const { parsed, response, toolCalls } = await withLlmObsSpan({
|
|
@@ -8342,6 +8756,10 @@ class OperateLoop {
|
|
|
8342
8756
|
if (parsed.usage) {
|
|
8343
8757
|
state.responseBuilder.addUsage(parsed.usage);
|
|
8344
8758
|
}
|
|
8759
|
+
// Track stop reason for the exchange envelope
|
|
8760
|
+
if (parsed.stopReason) {
|
|
8761
|
+
state.lastStopReason = parsed.stopReason;
|
|
8762
|
+
}
|
|
8345
8763
|
// Add raw response
|
|
8346
8764
|
state.responseBuilder.addResponse(parsed.raw);
|
|
8347
8765
|
// Execute afterEachModelResponse hook
|
|
@@ -8844,6 +9262,7 @@ class StreamLoop {
|
|
|
8844
9262
|
}
|
|
8845
9263
|
buildInitialRequest(state, options) {
|
|
8846
9264
|
return {
|
|
9265
|
+
cache: options.cache,
|
|
8847
9266
|
effort: options.effort,
|
|
8848
9267
|
format: state.formattedFormat,
|
|
8849
9268
|
instructions: options.instructions,
|
|
@@ -10844,12 +11263,17 @@ class Llm {
|
|
|
10844
11263
|
attempts++;
|
|
10845
11264
|
try {
|
|
10846
11265
|
const response = await this._llm.operate(input, optionsWithoutFallback);
|
|
10847
|
-
|
|
11266
|
+
const settled = {
|
|
10848
11267
|
...response,
|
|
10849
11268
|
fallbackAttempts: attempts,
|
|
10850
11269
|
fallbackUsed: false,
|
|
10851
11270
|
provider: response.provider || this._provider,
|
|
10852
11271
|
};
|
|
11272
|
+
await this.settleExchange({
|
|
11273
|
+
onExchange: resolvedOptions.onExchange,
|
|
11274
|
+
response: settled,
|
|
11275
|
+
});
|
|
11276
|
+
return settled;
|
|
10853
11277
|
}
|
|
10854
11278
|
catch (error) {
|
|
10855
11279
|
lastError = error;
|
|
@@ -10858,18 +11282,28 @@ class Llm {
|
|
|
10858
11282
|
fallbacksRemaining: fallbackChain.length,
|
|
10859
11283
|
});
|
|
10860
11284
|
}
|
|
10861
|
-
// Try fallback providers
|
|
11285
|
+
// Try fallback providers. The fallback instance's underlying provider is
|
|
11286
|
+
// called directly so the nested facade does not settle the exchange —
|
|
11287
|
+
// exactly one settlement per operate() happens here.
|
|
10862
11288
|
for (const fallbackConfig of fallbackChain) {
|
|
10863
11289
|
attempts++;
|
|
10864
11290
|
try {
|
|
10865
11291
|
const fallbackInstance = this.createFallbackInstance(fallbackConfig);
|
|
10866
|
-
|
|
10867
|
-
|
|
11292
|
+
if (!fallbackInstance._llm.operate) {
|
|
11293
|
+
throw new errors.NotImplementedError(`Provider ${fallbackConfig.provider} does not support operate method`);
|
|
11294
|
+
}
|
|
11295
|
+
const response = await fallbackInstance._llm.operate(input, optionsWithoutFallback);
|
|
11296
|
+
const settled = {
|
|
10868
11297
|
...response,
|
|
10869
11298
|
fallbackAttempts: attempts,
|
|
10870
11299
|
fallbackUsed: true,
|
|
10871
11300
|
provider: response.provider || fallbackConfig.provider,
|
|
10872
11301
|
};
|
|
11302
|
+
await this.settleExchange({
|
|
11303
|
+
onExchange: resolvedOptions.onExchange,
|
|
11304
|
+
response: settled,
|
|
11305
|
+
});
|
|
11306
|
+
return settled;
|
|
10873
11307
|
}
|
|
10874
11308
|
catch (error) {
|
|
10875
11309
|
lastError = error;
|
|
@@ -10879,9 +11313,44 @@ class Llm {
|
|
|
10879
11313
|
});
|
|
10880
11314
|
}
|
|
10881
11315
|
}
|
|
10882
|
-
// All providers failed
|
|
11316
|
+
// All providers failed: settle the exchange from the envelope the loop
|
|
11317
|
+
// attached to the last error, then throw
|
|
11318
|
+
const failureEnvelope = lastError
|
|
11319
|
+
?.exchange;
|
|
11320
|
+
if (failureEnvelope) {
|
|
11321
|
+
failureEnvelope.resolution = {
|
|
11322
|
+
...failureEnvelope.resolution,
|
|
11323
|
+
fallbackAttempts: attempts,
|
|
11324
|
+
fallbackUsed: attempts > 1,
|
|
11325
|
+
};
|
|
11326
|
+
await emitExchange({
|
|
11327
|
+
envelope: failureEnvelope,
|
|
11328
|
+
onExchange: resolvedOptions.onExchange,
|
|
11329
|
+
});
|
|
11330
|
+
await persistExchange(failureEnvelope);
|
|
11331
|
+
}
|
|
10883
11332
|
throw lastError;
|
|
10884
11333
|
}
|
|
11334
|
+
/**
|
|
11335
|
+
* Stamp fallback resolution onto the envelope the operate loop attached to
|
|
11336
|
+
* the response and deliver it to the caller's onExchange. Fires once per
|
|
11337
|
+
* operate() settlement; callback errors are logged and never thrown.
|
|
11338
|
+
*/
|
|
11339
|
+
async settleExchange({ onExchange, response, }) {
|
|
11340
|
+
const envelope = response.exchange;
|
|
11341
|
+
if (!envelope) {
|
|
11342
|
+
return;
|
|
11343
|
+
}
|
|
11344
|
+
envelope.resolution = {
|
|
11345
|
+
...envelope.resolution,
|
|
11346
|
+
fallbackAttempts: response.fallbackAttempts,
|
|
11347
|
+
fallbackUsed: response.fallbackUsed,
|
|
11348
|
+
model: response.model,
|
|
11349
|
+
provider: response.provider,
|
|
11350
|
+
};
|
|
11351
|
+
await emitExchange({ envelope, onExchange });
|
|
11352
|
+
await persistExchange(envelope);
|
|
11353
|
+
}
|
|
10885
11354
|
async *stream(input, options = {}) {
|
|
10886
11355
|
if (!this._llm.stream) {
|
|
10887
11356
|
throw new errors.NotImplementedError(`Provider ${this._provider} does not support stream method`);
|