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