@jaypie/llm 1.3.13 → 1.3.14
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 +632 -338
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +93 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/observability/exchangeStore.d.ts +20 -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 +4 -0
- package/dist/cjs/types/LlmProvider.interface.d.ts +87 -1
- package/dist/esm/Llm.d.ts +6 -0
- package/dist/esm/index.d.ts +93 -1
- package/dist/esm/index.js +632 -338
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/exchangeStore.d.ts +20 -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 +4 -0
- package/dist/esm/types/LlmProvider.interface.d.ts +87 -1
- package/package.json +5 -1
package/dist/esm/index.js
CHANGED
|
@@ -475,233 +475,90 @@ 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
|
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// OpenAI Responses API `reasoning.effort`.
|
|
538
|
-
// Full ladder: minimal | low | medium | high | xhigh (plus `none`). Availability
|
|
539
|
-
// is not uniform across the gpt-5 line:
|
|
540
|
-
// - `xhigh` was introduced at gpt-5.2 and has been continuous since, so it is
|
|
541
|
-
// safe for our gpt-5.4 default and everything newer.
|
|
542
|
-
// - `minimal` shipped on gpt-5/5.1, was dropped at gpt-5.2, and returned on
|
|
543
|
-
// the current line; because that history is non-monotonic we only trust it
|
|
544
|
-
// from gpt-5.4 (our default floor) onward.
|
|
545
|
-
// Outside those windows (older gpt-5, o-series) the extreme rung is clamped and
|
|
546
|
-
// the mapping reports `papered: true`.
|
|
547
|
-
const OPENAI_EFFORT = {
|
|
548
|
-
[EFFORT.LOWEST]: "minimal",
|
|
549
|
-
[EFFORT.LOW]: "low",
|
|
550
|
-
[EFFORT.MEDIUM]: "medium",
|
|
551
|
-
[EFFORT.HIGH]: "high",
|
|
552
|
-
[EFFORT.HIGHEST]: "xhigh",
|
|
553
|
-
};
|
|
554
|
-
function openAiGptVersion(model) {
|
|
555
|
-
const match = model.match(/^gpt-(\d+)(?:\.(\d+))?/);
|
|
556
|
-
if (!match)
|
|
557
|
-
return null;
|
|
558
|
-
return { major: Number(match[1]), minor: match[2] ? Number(match[2]) : 0 };
|
|
559
|
-
}
|
|
560
|
-
function atLeast(version, major, minor) {
|
|
561
|
-
if (!version)
|
|
562
|
-
return false;
|
|
563
|
-
return (version.major > major || (version.major === major && version.minor >= minor));
|
|
564
|
-
}
|
|
565
|
-
function toOpenAiEffort(effort, { model }) {
|
|
566
|
-
const native = OPENAI_EFFORT[effort];
|
|
567
|
-
const version = openAiGptVersion(model);
|
|
568
|
-
// `minimal` only from gpt-5.4 (non-monotonic history; absent on o-series)
|
|
569
|
-
if (native === "minimal" && !atLeast(version, 5, 4)) {
|
|
570
|
-
return { papered: true, value: "low" };
|
|
571
|
-
}
|
|
572
|
-
// `xhigh` from gpt-5.2 onward (absent on older gpt-5 and o-series)
|
|
573
|
-
if (native === "xhigh" && !atLeast(version, 5, 2)) {
|
|
574
|
-
return { papered: true, value: "high" };
|
|
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;
|
|
575
501
|
}
|
|
576
|
-
return
|
|
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];
|
|
502
|
+
return totals;
|
|
601
503
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
[EFFORT.HIGHEST]: { papered: true, value: "HIGH" },
|
|
610
|
-
};
|
|
611
|
-
function toGeminiThinkingLevel(effort) {
|
|
612
|
-
return GEMINI_THINKING_LEVEL[effort];
|
|
504
|
+
function extractResponseIds(responses) {
|
|
505
|
+
const ids = responses
|
|
506
|
+
.map((response) => response && typeof response === "object"
|
|
507
|
+
? response.id
|
|
508
|
+
: undefined)
|
|
509
|
+
.filter((id) => typeof id === "string");
|
|
510
|
+
return ids.length ? ids : undefined;
|
|
613
511
|
}
|
|
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];
|
|
639
|
-
}
|
|
640
|
-
// OpenRouter `reasoning.effort` — accepts the full minimal..xhigh ladder and
|
|
641
|
-
// maps to the routed provider's nearest supported level itself, so nothing is
|
|
642
|
-
// papered here.
|
|
643
|
-
const OPENROUTER_EFFORT = {
|
|
644
|
-
[EFFORT.LOWEST]: "minimal",
|
|
645
|
-
[EFFORT.LOW]: "low",
|
|
646
|
-
[EFFORT.MEDIUM]: "medium",
|
|
647
|
-
[EFFORT.HIGH]: "high",
|
|
648
|
-
[EFFORT.HIGHEST]: "xhigh",
|
|
649
|
-
};
|
|
650
|
-
function toOpenRouterEffort(effort) {
|
|
651
|
-
return { papered: false, value: OPENROUTER_EFFORT[effort] };
|
|
652
|
-
}
|
|
653
|
-
|
|
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
512
|
//
|
|
694
513
|
//
|
|
695
|
-
//
|
|
514
|
+
// Main
|
|
696
515
|
//
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Assemble the serializable exchange envelope for one operate() settlement.
|
|
518
|
+
* The `resolution` block is stamped by the Llm facade, which is the layer
|
|
519
|
+
* that knows fallback outcome; the loop fills provider/model best-effort.
|
|
520
|
+
*/
|
|
521
|
+
function buildExchangeEnvelope({ duration, initialHistoryLength, input, options, response, startedAt, state, }) {
|
|
522
|
+
const historyDelta = response.history.slice(initialHistoryLength);
|
|
523
|
+
return {
|
|
524
|
+
ids: extractResponseIds(response.responses),
|
|
525
|
+
request: {
|
|
526
|
+
data: options.data,
|
|
527
|
+
effort: options.effort,
|
|
528
|
+
explain: options.explain,
|
|
529
|
+
format: state.formattedFormat,
|
|
530
|
+
input,
|
|
531
|
+
instructions: options.instructions,
|
|
532
|
+
model: options.model,
|
|
533
|
+
placeholders: options.placeholders,
|
|
534
|
+
providerOptions: options.providerOptions,
|
|
535
|
+
system: options.system,
|
|
536
|
+
temperature: options.temperature,
|
|
537
|
+
tools: state.formattedTools?.map(({ name }) => name),
|
|
538
|
+
turns: options.turns,
|
|
539
|
+
user: options.user,
|
|
540
|
+
},
|
|
541
|
+
resolution: {
|
|
542
|
+
model: response.model,
|
|
543
|
+
provider: response.provider,
|
|
544
|
+
retries: state.retries,
|
|
545
|
+
},
|
|
546
|
+
response: {
|
|
547
|
+
content: response.content,
|
|
548
|
+
error: response.error,
|
|
549
|
+
historyDelta,
|
|
550
|
+
reasoning: response.reasoning,
|
|
551
|
+
status: response.status,
|
|
552
|
+
stopReason: state.lastStopReason,
|
|
553
|
+
usage: response.usage,
|
|
554
|
+
usageTotals: sumUsageByProviderModel(response.usage),
|
|
555
|
+
},
|
|
556
|
+
timing: {
|
|
557
|
+
duration,
|
|
558
|
+
startedAt,
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
705
562
|
|
|
706
563
|
/**
|
|
707
564
|
* Type guard to check if an item is a dedicated reasoning item (OpenAI)
|
|
@@ -921,6 +778,45 @@ function fillFormatArrays({ content, format, }) {
|
|
|
921
778
|
return fillFromSchema(schema, structuredClone(content));
|
|
922
779
|
}
|
|
923
780
|
|
|
781
|
+
// Enums
|
|
782
|
+
var LlmMessageRole;
|
|
783
|
+
(function (LlmMessageRole) {
|
|
784
|
+
LlmMessageRole["Assistant"] = "assistant";
|
|
785
|
+
LlmMessageRole["Developer"] = "developer";
|
|
786
|
+
LlmMessageRole["System"] = "system";
|
|
787
|
+
LlmMessageRole["User"] = "user";
|
|
788
|
+
})(LlmMessageRole || (LlmMessageRole = {}));
|
|
789
|
+
var LlmMessageType;
|
|
790
|
+
(function (LlmMessageType) {
|
|
791
|
+
LlmMessageType["FunctionCall"] = "function_call";
|
|
792
|
+
LlmMessageType["FunctionCallOutput"] = "function_call_output";
|
|
793
|
+
LlmMessageType["InputFile"] = "input_file";
|
|
794
|
+
LlmMessageType["InputImage"] = "input_image";
|
|
795
|
+
LlmMessageType["InputText"] = "input_text";
|
|
796
|
+
LlmMessageType["ItemReference"] = "item_reference";
|
|
797
|
+
LlmMessageType["Message"] = "message";
|
|
798
|
+
LlmMessageType["OutputText"] = "output_text";
|
|
799
|
+
LlmMessageType["Refusal"] = "refusal";
|
|
800
|
+
})(LlmMessageType || (LlmMessageType = {}));
|
|
801
|
+
var LlmResponseStatus;
|
|
802
|
+
(function (LlmResponseStatus) {
|
|
803
|
+
LlmResponseStatus["Completed"] = "completed";
|
|
804
|
+
LlmResponseStatus["Incomplete"] = "incomplete";
|
|
805
|
+
LlmResponseStatus["InProgress"] = "in_progress";
|
|
806
|
+
})(LlmResponseStatus || (LlmResponseStatus = {}));
|
|
807
|
+
// Progress
|
|
808
|
+
var LlmProgressEventType;
|
|
809
|
+
(function (LlmProgressEventType) {
|
|
810
|
+
LlmProgressEventType["Done"] = "done";
|
|
811
|
+
LlmProgressEventType["ModelRequest"] = "model_request";
|
|
812
|
+
LlmProgressEventType["ModelResponse"] = "model_response";
|
|
813
|
+
LlmProgressEventType["Retry"] = "retry";
|
|
814
|
+
LlmProgressEventType["Start"] = "start";
|
|
815
|
+
LlmProgressEventType["ToolCall"] = "tool_call";
|
|
816
|
+
LlmProgressEventType["ToolError"] = "tool_error";
|
|
817
|
+
LlmProgressEventType["ToolResult"] = "tool_result";
|
|
818
|
+
})(LlmProgressEventType || (LlmProgressEventType = {}));
|
|
819
|
+
|
|
924
820
|
/**
|
|
925
821
|
* Converts a string to a standardized LlmInputMessage
|
|
926
822
|
* @param input - String to format
|
|
@@ -1518,61 +1414,357 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
|
|
|
1518
1414
|
}
|
|
1519
1415
|
llm.usage = usageByModel;
|
|
1520
1416
|
}
|
|
1521
|
-
log$2.tally({ llm });
|
|
1417
|
+
log$2.tally({ llm });
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Helper function to safely call a function that might throw
|
|
1422
|
+
* @param fn Function to call safely
|
|
1423
|
+
*/
|
|
1424
|
+
function callSafely(fn) {
|
|
1425
|
+
try {
|
|
1426
|
+
fn();
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
// Silently catch any errors from the function
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Attempts to parse a value as a number. Returns the original input if parsing fails or results in NaN.
|
|
1434
|
+
* @param input - The value to attempt to parse as a number
|
|
1435
|
+
* @param options - Optional configuration
|
|
1436
|
+
* @param options.defaultValue - Default value to return if parsing fails or results in NaN
|
|
1437
|
+
* @param options.warnFunction - Function to call with warning message if parsing fails or results in NaN
|
|
1438
|
+
* @returns The parsed number, defaultValue (if specified and parsing fails), or the original input
|
|
1439
|
+
*/
|
|
1440
|
+
function tryParseNumber(input, options) {
|
|
1441
|
+
if (input === null || input === undefined) {
|
|
1442
|
+
return input;
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
const parsed = Number(input);
|
|
1446
|
+
if (Number.isNaN(parsed)) {
|
|
1447
|
+
if (options?.warnFunction) {
|
|
1448
|
+
const warningMessage = `Failed to parse "${String(input)}" as number`;
|
|
1449
|
+
callSafely(() => options.warnFunction(warningMessage));
|
|
1450
|
+
}
|
|
1451
|
+
return typeof options?.defaultValue === "number"
|
|
1452
|
+
? options.defaultValue
|
|
1453
|
+
: input;
|
|
1454
|
+
}
|
|
1455
|
+
return parsed;
|
|
1456
|
+
}
|
|
1457
|
+
catch (error) {
|
|
1458
|
+
if (options?.warnFunction) {
|
|
1459
|
+
let errorMessage = "";
|
|
1460
|
+
if (error instanceof Error) {
|
|
1461
|
+
errorMessage = error.message;
|
|
1462
|
+
}
|
|
1463
|
+
const warningMessage = `Error parsing "${String(input)}" as number${errorMessage ? "; " + errorMessage : ""}`;
|
|
1464
|
+
callSafely(() => options.warnFunction(warningMessage));
|
|
1465
|
+
}
|
|
1466
|
+
return typeof options?.defaultValue === "number"
|
|
1467
|
+
? options.defaultValue
|
|
1468
|
+
: input;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Truthy-except-"false"/"0" gate on LLM_EXCHANGE_ENABLED, matching the
|
|
1474
|
+
* DD_LLMOBS_ENABLED convention in observability/llmobs.ts.
|
|
1475
|
+
*/
|
|
1476
|
+
function isExchangeStoreEnabled() {
|
|
1477
|
+
const flag = process.env.LLM_EXCHANGE_ENABLED;
|
|
1478
|
+
if (!flag) {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
return flag.toLowerCase() !== "false" && flag !== "0";
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Whether the loop should assemble an exchange envelope at settlement.
|
|
1485
|
+
*/
|
|
1486
|
+
function isExchangeRequested({ onExchange, }) {
|
|
1487
|
+
return Boolean(onExchange) || isExchangeStoreEnabled();
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Deliver an exchange envelope to a callback. Errors thrown by the callback
|
|
1491
|
+
* are logged and swallowed — exchange capture must never interrupt the call.
|
|
1492
|
+
*/
|
|
1493
|
+
async function emitExchange({ envelope, onExchange, }) {
|
|
1494
|
+
if (!onExchange) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
try {
|
|
1498
|
+
await onExchange(envelope);
|
|
1499
|
+
}
|
|
1500
|
+
catch (error) {
|
|
1501
|
+
const log = getLogger$7();
|
|
1502
|
+
log.warn("[operate] onExchange callback threw");
|
|
1503
|
+
log.var({ error });
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
//
|
|
1508
|
+
//
|
|
1509
|
+
// Constants
|
|
1510
|
+
//
|
|
1511
|
+
const MODULE$1 = {
|
|
1512
|
+
// Computed at runtime so bundlers (esbuild) do not attempt to include
|
|
1513
|
+
// @jaypie/dynamodb, which is an optional peer dependency.
|
|
1514
|
+
JAYPIE_DYNAMODB: ["@jaypie", "dynamodb"].join("/"),
|
|
1515
|
+
};
|
|
1516
|
+
//
|
|
1517
|
+
//
|
|
1518
|
+
// Helpers
|
|
1519
|
+
//
|
|
1520
|
+
// CJS/ESM compatible require - handles bundling to CJS where import.meta.url
|
|
1521
|
+
// becomes undefined (mirrors observability/llmobs.ts).
|
|
1522
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1523
|
+
// @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
|
|
1524
|
+
const requireModule$1 = typeof __filename !== "undefined"
|
|
1525
|
+
? createRequire(pathToFileURL(__filename).href)
|
|
1526
|
+
: createRequire(import.meta.url);
|
|
1527
|
+
let resolved$1 = false;
|
|
1528
|
+
let cachedSdk$2 = null;
|
|
1529
|
+
/**
|
|
1530
|
+
* Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
|
|
1531
|
+
* throws) when the peer is absent. Cached after the first attempt.
|
|
1532
|
+
*/
|
|
1533
|
+
function resolveExchangeStore() {
|
|
1534
|
+
if (resolved$1) {
|
|
1535
|
+
return cachedSdk$2;
|
|
1536
|
+
}
|
|
1537
|
+
resolved$1 = true;
|
|
1538
|
+
try {
|
|
1539
|
+
const dynamodb = requireModule$1(MODULE$1.JAYPIE_DYNAMODB);
|
|
1540
|
+
const sdk = dynamodb?.default ?? dynamodb;
|
|
1541
|
+
if (sdk && typeof sdk.storeExchange === "function") {
|
|
1542
|
+
cachedSdk$2 = sdk;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
catch {
|
|
1546
|
+
cachedSdk$2 = null;
|
|
1547
|
+
}
|
|
1548
|
+
return cachedSdk$2;
|
|
1549
|
+
}
|
|
1550
|
+
//
|
|
1551
|
+
//
|
|
1552
|
+
// Main
|
|
1553
|
+
//
|
|
1554
|
+
/**
|
|
1555
|
+
* Persist an exchange envelope via @jaypie/dynamodb when
|
|
1556
|
+
* LLM_EXCHANGE_ENABLED is set. Silent no-op when the flag is unset or the
|
|
1557
|
+
* peer is absent; persister failures are logged and never thrown.
|
|
1558
|
+
*/
|
|
1559
|
+
async function persistExchange(envelope) {
|
|
1560
|
+
if (!isExchangeStoreEnabled()) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const sdk = resolveExchangeStore();
|
|
1564
|
+
if (!sdk) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
await sdk.storeExchange(envelope);
|
|
1569
|
+
}
|
|
1570
|
+
catch (error) {
|
|
1571
|
+
const log = getLogger$7();
|
|
1572
|
+
log.warn("[operate] Exchange persistence failed");
|
|
1573
|
+
log.var({ error });
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
//
|
|
1578
|
+
//
|
|
1579
|
+
// Abstract Base Adapter
|
|
1580
|
+
//
|
|
1581
|
+
/**
|
|
1582
|
+
* BaseProviderAdapter provides default implementations for common adapter methods.
|
|
1583
|
+
* Providers can extend this class to reduce boilerplate.
|
|
1584
|
+
*/
|
|
1585
|
+
class BaseProviderAdapter {
|
|
1586
|
+
constructor() {
|
|
1587
|
+
/**
|
|
1588
|
+
* Whether OperateLoop may take a corrective turn when a `format` request
|
|
1589
|
+
* completes with prose instead of structured output. Providers whose
|
|
1590
|
+
* structured output rides a tool emulation (where compliance is a model
|
|
1591
|
+
* decision) opt in; native grammar-constrained providers do not need it.
|
|
1592
|
+
*/
|
|
1593
|
+
this.supportsStructuredOutputRetry = false;
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Default implementation checks if error is retryable via classifyError
|
|
1597
|
+
*/
|
|
1598
|
+
isRetryableError(error) {
|
|
1599
|
+
const classified = this.classifyError(error);
|
|
1600
|
+
return classified.shouldRetry;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Default implementation checks error category via classifyError
|
|
1604
|
+
*/
|
|
1605
|
+
isRateLimitError(error) {
|
|
1606
|
+
const classified = this.classifyError(error);
|
|
1607
|
+
return classified.category === "rate_limit";
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Default implementation returns false - override for providers with native structured output
|
|
1611
|
+
*/
|
|
1612
|
+
hasStructuredOutput(_response) {
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Default implementation returns undefined - override for providers with native structured output
|
|
1617
|
+
*/
|
|
1618
|
+
extractStructuredOutput(_response) {
|
|
1619
|
+
return undefined;
|
|
1620
|
+
}
|
|
1522
1621
|
}
|
|
1523
1622
|
|
|
1524
1623
|
/**
|
|
1525
|
-
*
|
|
1526
|
-
*
|
|
1624
|
+
* Per-provider translation of the provider-neutral {@link LlmEffort} scale.
|
|
1625
|
+
*
|
|
1626
|
+
* The neutral enum (lowest → low → medium → high → highest) is a relative
|
|
1627
|
+
* five-point scale. Each table below maps it onto the target provider's native
|
|
1628
|
+
* effort control, keeping `medium`/`high` semantically aligned across providers
|
|
1629
|
+
* and using each provider's extra bottom (`minimal`) or top (`xhigh`/`max`)
|
|
1630
|
+
* rung where one exists. Providers with fewer levels collapse the ends and mark
|
|
1631
|
+
* the result `papered`. A single `effort` value is therefore safe to reuse
|
|
1632
|
+
* across providers and fallback chains.
|
|
1527
1633
|
*/
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
catch {
|
|
1533
|
-
// Silently catch any errors from the function
|
|
1534
|
-
}
|
|
1634
|
+
/** Consistent debug message for a papered-over effort level. */
|
|
1635
|
+
function paperedEffortMessage({ model, provider, requested, value, }) {
|
|
1636
|
+
return `[llm] effort '${requested}' has no distinct tier on ${provider} model '${model}'; using '${value}'`;
|
|
1535
1637
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1638
|
+
// OpenAI Responses API `reasoning.effort`.
|
|
1639
|
+
// Full ladder: minimal | low | medium | high | xhigh (plus `none`). Availability
|
|
1640
|
+
// is not uniform across the gpt-5 line:
|
|
1641
|
+
// - `xhigh` was introduced at gpt-5.2 and has been continuous since, so it is
|
|
1642
|
+
// safe for our gpt-5.4 default and everything newer.
|
|
1643
|
+
// - `minimal` shipped on gpt-5/5.1, was dropped at gpt-5.2, and returned on
|
|
1644
|
+
// the current line; because that history is non-monotonic we only trust it
|
|
1645
|
+
// from gpt-5.4 (our default floor) onward.
|
|
1646
|
+
// Outside those windows (older gpt-5, o-series) the extreme rung is clamped and
|
|
1647
|
+
// the mapping reports `papered: true`.
|
|
1648
|
+
const OPENAI_EFFORT = {
|
|
1649
|
+
[EFFORT.LOWEST]: "minimal",
|
|
1650
|
+
[EFFORT.LOW]: "low",
|
|
1651
|
+
[EFFORT.MEDIUM]: "medium",
|
|
1652
|
+
[EFFORT.HIGH]: "high",
|
|
1653
|
+
[EFFORT.HIGHEST]: "xhigh",
|
|
1654
|
+
};
|
|
1655
|
+
function openAiGptVersion(model) {
|
|
1656
|
+
const match = model.match(/^gpt-(\d+)(?:\.(\d+))?/);
|
|
1657
|
+
if (!match)
|
|
1658
|
+
return null;
|
|
1659
|
+
return { major: Number(match[1]), minor: match[2] ? Number(match[2]) : 0 };
|
|
1660
|
+
}
|
|
1661
|
+
function atLeast(version, major, minor) {
|
|
1662
|
+
if (!version)
|
|
1663
|
+
return false;
|
|
1664
|
+
return (version.major > major || (version.major === major && version.minor >= minor));
|
|
1665
|
+
}
|
|
1666
|
+
function toOpenAiEffort(effort, { model }) {
|
|
1667
|
+
const native = OPENAI_EFFORT[effort];
|
|
1668
|
+
const version = openAiGptVersion(model);
|
|
1669
|
+
// `minimal` only from gpt-5.4 (non-monotonic history; absent on o-series)
|
|
1670
|
+
if (native === "minimal" && !atLeast(version, 5, 4)) {
|
|
1671
|
+
return { papered: true, value: "low" };
|
|
1560
1672
|
}
|
|
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;
|
|
1673
|
+
// `xhigh` from gpt-5.2 onward (absent on older gpt-5 and o-series)
|
|
1674
|
+
if (native === "xhigh" && !atLeast(version, 5, 2)) {
|
|
1675
|
+
return { papered: true, value: "high" };
|
|
1573
1676
|
}
|
|
1677
|
+
return { papered: false, value: native };
|
|
1678
|
+
}
|
|
1679
|
+
// xAI Grok `reasoning_effort` — low | medium | high. No sub-low or top rung, so
|
|
1680
|
+
// `lowest` collapses onto `low` and `highest` onto `high`.
|
|
1681
|
+
const XAI_EFFORT = {
|
|
1682
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1683
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1684
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1685
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1686
|
+
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
1687
|
+
};
|
|
1688
|
+
function toXaiEffort(effort) {
|
|
1689
|
+
return XAI_EFFORT[effort];
|
|
1690
|
+
}
|
|
1691
|
+
// Anthropic `output_config.effort` — low | medium | high | xhigh | max. No
|
|
1692
|
+
// sub-low rung, so `lowest` collapses onto `low`; `highest` reaches `max`.
|
|
1693
|
+
const ANTHROPIC_EFFORT = {
|
|
1694
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1695
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1696
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1697
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1698
|
+
[EFFORT.HIGHEST]: { papered: false, value: "max" },
|
|
1699
|
+
};
|
|
1700
|
+
function toAnthropicEffort(effort) {
|
|
1701
|
+
return ANTHROPIC_EFFORT[effort];
|
|
1702
|
+
}
|
|
1703
|
+
// Gemini 3.x `thinkingConfig.thinkingLevel` — MINIMAL | LOW | MEDIUM | HIGH. No
|
|
1704
|
+
// top rung above HIGH, so `highest` collapses onto HIGH.
|
|
1705
|
+
const GEMINI_THINKING_LEVEL = {
|
|
1706
|
+
[EFFORT.LOWEST]: { papered: false, value: "MINIMAL" },
|
|
1707
|
+
[EFFORT.LOW]: { papered: false, value: "LOW" },
|
|
1708
|
+
[EFFORT.MEDIUM]: { papered: false, value: "MEDIUM" },
|
|
1709
|
+
[EFFORT.HIGH]: { papered: false, value: "HIGH" },
|
|
1710
|
+
[EFFORT.HIGHEST]: { papered: true, value: "HIGH" },
|
|
1711
|
+
};
|
|
1712
|
+
function toGeminiThinkingLevel(effort) {
|
|
1713
|
+
return GEMINI_THINKING_LEVEL[effort];
|
|
1714
|
+
}
|
|
1715
|
+
// Gemini 2.5 `thinkingConfig.thinkingBudget` — every tier is a distinct token
|
|
1716
|
+
// budget, so nothing is papered over. Floor (512) clears every 2.5 minimum;
|
|
1717
|
+
// ceiling (24,576) is valid across 2.5 Pro (max 32,768) and Flash (max 24,576).
|
|
1718
|
+
const GEMINI_THINKING_BUDGET = {
|
|
1719
|
+
[EFFORT.LOWEST]: 512,
|
|
1720
|
+
[EFFORT.LOW]: 4096,
|
|
1721
|
+
[EFFORT.MEDIUM]: 8192,
|
|
1722
|
+
[EFFORT.HIGH]: 16384,
|
|
1723
|
+
[EFFORT.HIGHEST]: 24576,
|
|
1724
|
+
};
|
|
1725
|
+
function toGeminiThinkingBudget(effort) {
|
|
1726
|
+
return { papered: false, value: GEMINI_THINKING_BUDGET[effort] };
|
|
1727
|
+
}
|
|
1728
|
+
// Fireworks `reasoning_effort` — low | medium | high. Accepted on every model
|
|
1729
|
+
// (the API no-ops where unsupported). No sub-low or top rung, so `lowest`
|
|
1730
|
+
// collapses onto `low` and `highest` onto `high`.
|
|
1731
|
+
const FIREWORKS_EFFORT = {
|
|
1732
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1733
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1734
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1735
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1736
|
+
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
1737
|
+
};
|
|
1738
|
+
function toFireworksEffort(effort) {
|
|
1739
|
+
return FIREWORKS_EFFORT[effort];
|
|
1740
|
+
}
|
|
1741
|
+
// OpenRouter `reasoning.effort` — accepts the full minimal..xhigh ladder and
|
|
1742
|
+
// maps to the routed provider's nearest supported level itself, so nothing is
|
|
1743
|
+
// papered here.
|
|
1744
|
+
const OPENROUTER_EFFORT = {
|
|
1745
|
+
[EFFORT.LOWEST]: "minimal",
|
|
1746
|
+
[EFFORT.LOW]: "low",
|
|
1747
|
+
[EFFORT.MEDIUM]: "medium",
|
|
1748
|
+
[EFFORT.HIGH]: "high",
|
|
1749
|
+
[EFFORT.HIGHEST]: "xhigh",
|
|
1750
|
+
};
|
|
1751
|
+
function toOpenRouterEffort(effort) {
|
|
1752
|
+
return { papered: false, value: OPENROUTER_EFFORT[effort] };
|
|
1574
1753
|
}
|
|
1575
1754
|
|
|
1755
|
+
//
|
|
1756
|
+
//
|
|
1757
|
+
// Types
|
|
1758
|
+
//
|
|
1759
|
+
var LlmStreamChunkType;
|
|
1760
|
+
(function (LlmStreamChunkType) {
|
|
1761
|
+
LlmStreamChunkType["Done"] = "done";
|
|
1762
|
+
LlmStreamChunkType["Error"] = "error";
|
|
1763
|
+
LlmStreamChunkType["Text"] = "text";
|
|
1764
|
+
LlmStreamChunkType["ToolCall"] = "tool_call";
|
|
1765
|
+
LlmStreamChunkType["ToolResult"] = "tool_result";
|
|
1766
|
+
})(LlmStreamChunkType || (LlmStreamChunkType = {}));
|
|
1767
|
+
|
|
1576
1768
|
//
|
|
1577
1769
|
//
|
|
1578
1770
|
// Error Classification Types
|
|
@@ -8074,10 +8266,14 @@ class OperateLoop {
|
|
|
8074
8266
|
log.trace("[operate] Starting operate loop");
|
|
8075
8267
|
log.trace.var({ "operate.input": input });
|
|
8076
8268
|
log.trace.var({ "operate.options": options });
|
|
8269
|
+
const startedAt = new Date().toISOString();
|
|
8270
|
+
const startMs = Date.now();
|
|
8077
8271
|
// Initialize state
|
|
8078
8272
|
const state = await this.initializeState(input, options);
|
|
8079
8273
|
const context = this.createContext(options);
|
|
8080
8274
|
const modelName = options.model ?? this.adapter.defaultModel;
|
|
8275
|
+
const exchangeRequested = isExchangeRequested(options);
|
|
8276
|
+
const initialHistoryLength = state.responseBuilder.getHistory().length;
|
|
8081
8277
|
await emitProgress({
|
|
8082
8278
|
event: {
|
|
8083
8279
|
maxTurns: state.maxTurns,
|
|
@@ -8089,59 +8285,102 @@ class OperateLoop {
|
|
|
8089
8285
|
});
|
|
8090
8286
|
// Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
|
|
8091
8287
|
// 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
|
-
|
|
8288
|
+
try {
|
|
8289
|
+
return await withLlmObsSpan({
|
|
8290
|
+
kind: state.toolkit ? "agent" : "llm",
|
|
8291
|
+
modelName,
|
|
8292
|
+
modelProvider: this.adapter.name,
|
|
8293
|
+
name: "jaypie.llm.operate",
|
|
8294
|
+
}, async () => {
|
|
8295
|
+
// Build initial request
|
|
8296
|
+
let request = this.buildInitialRequest(state, options);
|
|
8297
|
+
// Multi-turn loop
|
|
8298
|
+
while (state.currentTurn < state.maxTurns) {
|
|
8299
|
+
state.currentTurn++;
|
|
8300
|
+
// Execute one turn with retry logic
|
|
8301
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
8302
|
+
if (!shouldContinue) {
|
|
8303
|
+
break;
|
|
8304
|
+
}
|
|
8305
|
+
// Rebuild request with updated history for next turn
|
|
8306
|
+
request = {
|
|
8307
|
+
effort: options.effort,
|
|
8308
|
+
format: state.formattedFormat,
|
|
8309
|
+
instructions: options.instructions,
|
|
8310
|
+
messages: state.currentInput,
|
|
8311
|
+
model: modelName,
|
|
8312
|
+
providerOptions: options.providerOptions,
|
|
8313
|
+
structuredOutputRetry: state.structuredOutputRetry,
|
|
8314
|
+
system: options.system,
|
|
8315
|
+
temperature: options.temperature,
|
|
8316
|
+
tools: state.formattedTools,
|
|
8317
|
+
user: options.user,
|
|
8318
|
+
};
|
|
8107
8319
|
}
|
|
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,
|
|
8320
|
+
const response = state.responseBuilder.build();
|
|
8321
|
+
if (exchangeRequested) {
|
|
8322
|
+
response.exchange = buildExchangeEnvelope({
|
|
8323
|
+
duration: Date.now() - startMs,
|
|
8324
|
+
initialHistoryLength,
|
|
8325
|
+
input,
|
|
8326
|
+
options,
|
|
8327
|
+
response,
|
|
8328
|
+
startedAt,
|
|
8329
|
+
state,
|
|
8330
|
+
});
|
|
8331
|
+
}
|
|
8332
|
+
annotateLlmObs({
|
|
8333
|
+
inputData: input,
|
|
8334
|
+
metrics: usageToLlmObsMetrics(response.usage),
|
|
8335
|
+
outputData: response.content,
|
|
8336
|
+
});
|
|
8337
|
+
tallyOperate({
|
|
8338
|
+
toolCallNames: state.toolCallNames,
|
|
8339
|
+
turns: state.currentTurn,
|
|
8139
8340
|
usage: response.usage,
|
|
8140
|
-
}
|
|
8141
|
-
|
|
8341
|
+
});
|
|
8342
|
+
await emitProgress({
|
|
8343
|
+
event: {
|
|
8344
|
+
content: response.content,
|
|
8345
|
+
turn: state.currentTurn,
|
|
8346
|
+
type: LlmProgressEventType.Done,
|
|
8347
|
+
usage: response.usage,
|
|
8348
|
+
},
|
|
8349
|
+
onProgress: options.onProgress,
|
|
8350
|
+
});
|
|
8351
|
+
return response;
|
|
8142
8352
|
});
|
|
8143
|
-
|
|
8144
|
-
|
|
8353
|
+
}
|
|
8354
|
+
catch (error) {
|
|
8355
|
+
// A hard failure (retry budget exhausted, unrecoverable) still settles
|
|
8356
|
+
// the exchange: attach the envelope to the thrown error so the facade
|
|
8357
|
+
// can emit it before rethrowing to the caller.
|
|
8358
|
+
if (exchangeRequested) {
|
|
8359
|
+
const response = state.responseBuilder.build();
|
|
8360
|
+
if (!response.error) {
|
|
8361
|
+
const thrown = error;
|
|
8362
|
+
response.error = {
|
|
8363
|
+
detail: thrown.detail ?? thrown.message,
|
|
8364
|
+
status: thrown.status ?? 500,
|
|
8365
|
+
title: thrown.title ?? thrown.name ?? "Error",
|
|
8366
|
+
};
|
|
8367
|
+
}
|
|
8368
|
+
if (response.status === LlmResponseStatus.InProgress) {
|
|
8369
|
+
response.status = LlmResponseStatus.Incomplete;
|
|
8370
|
+
}
|
|
8371
|
+
error.exchange =
|
|
8372
|
+
buildExchangeEnvelope({
|
|
8373
|
+
duration: Date.now() - startMs,
|
|
8374
|
+
initialHistoryLength,
|
|
8375
|
+
input,
|
|
8376
|
+
options,
|
|
8377
|
+
response,
|
|
8378
|
+
startedAt,
|
|
8379
|
+
state,
|
|
8380
|
+
});
|
|
8381
|
+
}
|
|
8382
|
+
throw error;
|
|
8383
|
+
}
|
|
8145
8384
|
}
|
|
8146
8385
|
//
|
|
8147
8386
|
// Private Methods
|
|
@@ -8200,6 +8439,7 @@ class OperateLoop {
|
|
|
8200
8439
|
formattedTools,
|
|
8201
8440
|
maxTurns,
|
|
8202
8441
|
responseBuilder,
|
|
8442
|
+
retries: 0,
|
|
8203
8443
|
toolCallNames: [],
|
|
8204
8444
|
toolkit,
|
|
8205
8445
|
};
|
|
@@ -8261,26 +8501,26 @@ class OperateLoop {
|
|
|
8261
8501
|
options,
|
|
8262
8502
|
providerRequest,
|
|
8263
8503
|
});
|
|
8264
|
-
//
|
|
8504
|
+
// Count retries for the exchange envelope and emit retry progress
|
|
8505
|
+
// alongside the caller's onRetryableModelError hook
|
|
8265
8506
|
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;
|
|
8507
|
+
const hooksWithProgress = {
|
|
8508
|
+
...hooks,
|
|
8509
|
+
onRetryableModelError: async (retryContext) => {
|
|
8510
|
+
state.retries++;
|
|
8511
|
+
await emitProgress({
|
|
8512
|
+
event: {
|
|
8513
|
+
error: retryContext.error instanceof Error
|
|
8514
|
+
? retryContext.error.message
|
|
8515
|
+
: String(retryContext.error),
|
|
8516
|
+
turn: state.currentTurn,
|
|
8517
|
+
type: LlmProgressEventType.Retry,
|
|
8518
|
+
},
|
|
8519
|
+
onProgress: options.onProgress,
|
|
8520
|
+
});
|
|
8521
|
+
return hooks?.onRetryableModelError?.(retryContext);
|
|
8522
|
+
},
|
|
8523
|
+
};
|
|
8284
8524
|
// Execute with retry inside a child llm span (no-op when llmobs disabled).
|
|
8285
8525
|
// RetryExecutor handles error hooks and throws appropriate errors.
|
|
8286
8526
|
const { parsed, response, toolCalls } = await withLlmObsSpan({
|
|
@@ -8339,6 +8579,10 @@ class OperateLoop {
|
|
|
8339
8579
|
if (parsed.usage) {
|
|
8340
8580
|
state.responseBuilder.addUsage(parsed.usage);
|
|
8341
8581
|
}
|
|
8582
|
+
// Track stop reason for the exchange envelope
|
|
8583
|
+
if (parsed.stopReason) {
|
|
8584
|
+
state.lastStopReason = parsed.stopReason;
|
|
8585
|
+
}
|
|
8342
8586
|
// Add raw response
|
|
8343
8587
|
state.responseBuilder.addResponse(parsed.raw);
|
|
8344
8588
|
// Execute afterEachModelResponse hook
|
|
@@ -10841,12 +11085,17 @@ class Llm {
|
|
|
10841
11085
|
attempts++;
|
|
10842
11086
|
try {
|
|
10843
11087
|
const response = await this._llm.operate(input, optionsWithoutFallback);
|
|
10844
|
-
|
|
11088
|
+
const settled = {
|
|
10845
11089
|
...response,
|
|
10846
11090
|
fallbackAttempts: attempts,
|
|
10847
11091
|
fallbackUsed: false,
|
|
10848
11092
|
provider: response.provider || this._provider,
|
|
10849
11093
|
};
|
|
11094
|
+
await this.settleExchange({
|
|
11095
|
+
onExchange: resolvedOptions.onExchange,
|
|
11096
|
+
response: settled,
|
|
11097
|
+
});
|
|
11098
|
+
return settled;
|
|
10850
11099
|
}
|
|
10851
11100
|
catch (error) {
|
|
10852
11101
|
lastError = error;
|
|
@@ -10855,18 +11104,28 @@ class Llm {
|
|
|
10855
11104
|
fallbacksRemaining: fallbackChain.length,
|
|
10856
11105
|
});
|
|
10857
11106
|
}
|
|
10858
|
-
// Try fallback providers
|
|
11107
|
+
// Try fallback providers. The fallback instance's underlying provider is
|
|
11108
|
+
// called directly so the nested facade does not settle the exchange —
|
|
11109
|
+
// exactly one settlement per operate() happens here.
|
|
10859
11110
|
for (const fallbackConfig of fallbackChain) {
|
|
10860
11111
|
attempts++;
|
|
10861
11112
|
try {
|
|
10862
11113
|
const fallbackInstance = this.createFallbackInstance(fallbackConfig);
|
|
10863
|
-
|
|
10864
|
-
|
|
11114
|
+
if (!fallbackInstance._llm.operate) {
|
|
11115
|
+
throw new NotImplementedError(`Provider ${fallbackConfig.provider} does not support operate method`);
|
|
11116
|
+
}
|
|
11117
|
+
const response = await fallbackInstance._llm.operate(input, optionsWithoutFallback);
|
|
11118
|
+
const settled = {
|
|
10865
11119
|
...response,
|
|
10866
11120
|
fallbackAttempts: attempts,
|
|
10867
11121
|
fallbackUsed: true,
|
|
10868
11122
|
provider: response.provider || fallbackConfig.provider,
|
|
10869
11123
|
};
|
|
11124
|
+
await this.settleExchange({
|
|
11125
|
+
onExchange: resolvedOptions.onExchange,
|
|
11126
|
+
response: settled,
|
|
11127
|
+
});
|
|
11128
|
+
return settled;
|
|
10870
11129
|
}
|
|
10871
11130
|
catch (error) {
|
|
10872
11131
|
lastError = error;
|
|
@@ -10876,9 +11135,44 @@ class Llm {
|
|
|
10876
11135
|
});
|
|
10877
11136
|
}
|
|
10878
11137
|
}
|
|
10879
|
-
// All providers failed
|
|
11138
|
+
// All providers failed: settle the exchange from the envelope the loop
|
|
11139
|
+
// attached to the last error, then throw
|
|
11140
|
+
const failureEnvelope = lastError
|
|
11141
|
+
?.exchange;
|
|
11142
|
+
if (failureEnvelope) {
|
|
11143
|
+
failureEnvelope.resolution = {
|
|
11144
|
+
...failureEnvelope.resolution,
|
|
11145
|
+
fallbackAttempts: attempts,
|
|
11146
|
+
fallbackUsed: attempts > 1,
|
|
11147
|
+
};
|
|
11148
|
+
await emitExchange({
|
|
11149
|
+
envelope: failureEnvelope,
|
|
11150
|
+
onExchange: resolvedOptions.onExchange,
|
|
11151
|
+
});
|
|
11152
|
+
await persistExchange(failureEnvelope);
|
|
11153
|
+
}
|
|
10880
11154
|
throw lastError;
|
|
10881
11155
|
}
|
|
11156
|
+
/**
|
|
11157
|
+
* Stamp fallback resolution onto the envelope the operate loop attached to
|
|
11158
|
+
* the response and deliver it to the caller's onExchange. Fires once per
|
|
11159
|
+
* operate() settlement; callback errors are logged and never thrown.
|
|
11160
|
+
*/
|
|
11161
|
+
async settleExchange({ onExchange, response, }) {
|
|
11162
|
+
const envelope = response.exchange;
|
|
11163
|
+
if (!envelope) {
|
|
11164
|
+
return;
|
|
11165
|
+
}
|
|
11166
|
+
envelope.resolution = {
|
|
11167
|
+
...envelope.resolution,
|
|
11168
|
+
fallbackAttempts: response.fallbackAttempts,
|
|
11169
|
+
fallbackUsed: response.fallbackUsed,
|
|
11170
|
+
model: response.model,
|
|
11171
|
+
provider: response.provider,
|
|
11172
|
+
};
|
|
11173
|
+
await emitExchange({ envelope, onExchange });
|
|
11174
|
+
await persistExchange(envelope);
|
|
11175
|
+
}
|
|
10882
11176
|
async *stream(input, options = {}) {
|
|
10883
11177
|
if (!this._llm.stream) {
|
|
10884
11178
|
throw new NotImplementedError(`Provider ${this._provider} does not support stream method`);
|