@jaypie/llm 1.3.12 → 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 +703 -333
- 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/adapters/FireworksAdapter.d.ts +1 -0
- package/dist/cjs/operate/adapters/ProviderAdapter.interface.d.ts +13 -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 +13 -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 +703 -333
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/exchangeStore.d.ts +20 -0
- package/dist/esm/operate/adapters/FireworksAdapter.d.ts +1 -0
- package/dist/esm/operate/adapters/ProviderAdapter.interface.d.ts +13 -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 +13 -0
- package/dist/esm/types/LlmProvider.interface.d.ts +87 -1
- package/package.json +5 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -478,224 +478,90 @@ function resolveModelChain(model) {
|
|
|
478
478
|
|
|
479
479
|
//
|
|
480
480
|
//
|
|
481
|
-
//
|
|
481
|
+
// Helpers
|
|
482
482
|
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
* Providers can extend this class to reduce boilerplate.
|
|
486
|
-
*/
|
|
487
|
-
class BaseProviderAdapter {
|
|
488
|
-
/**
|
|
489
|
-
* Default implementation checks if error is retryable via classifyError
|
|
490
|
-
*/
|
|
491
|
-
isRetryableError(error) {
|
|
492
|
-
const classified = this.classifyError(error);
|
|
493
|
-
return classified.shouldRetry;
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Default implementation checks error category via classifyError
|
|
497
|
-
*/
|
|
498
|
-
isRateLimitError(error) {
|
|
499
|
-
const classified = this.classifyError(error);
|
|
500
|
-
return classified.category === "rate_limit";
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Default implementation returns false - override for providers with native structured output
|
|
504
|
-
*/
|
|
505
|
-
hasStructuredOutput(_response) {
|
|
506
|
-
return false;
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Default implementation returns undefined - override for providers with native structured output
|
|
510
|
-
*/
|
|
511
|
-
extractStructuredOutput(_response) {
|
|
483
|
+
function sumUsageByProviderModel(usage) {
|
|
484
|
+
if (!usage.length) {
|
|
512
485
|
return undefined;
|
|
513
486
|
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
// OpenAI Responses API `reasoning.effort`.
|
|
532
|
-
// Full ladder: minimal | low | medium | high | xhigh (plus `none`). Availability
|
|
533
|
-
// is not uniform across the gpt-5 line:
|
|
534
|
-
// - `xhigh` was introduced at gpt-5.2 and has been continuous since, so it is
|
|
535
|
-
// safe for our gpt-5.4 default and everything newer.
|
|
536
|
-
// - `minimal` shipped on gpt-5/5.1, was dropped at gpt-5.2, and returned on
|
|
537
|
-
// the current line; because that history is non-monotonic we only trust it
|
|
538
|
-
// from gpt-5.4 (our default floor) onward.
|
|
539
|
-
// Outside those windows (older gpt-5, o-series) the extreme rung is clamped and
|
|
540
|
-
// the mapping reports `papered: true`.
|
|
541
|
-
const OPENAI_EFFORT = {
|
|
542
|
-
[EFFORT.LOWEST]: "minimal",
|
|
543
|
-
[EFFORT.LOW]: "low",
|
|
544
|
-
[EFFORT.MEDIUM]: "medium",
|
|
545
|
-
[EFFORT.HIGH]: "high",
|
|
546
|
-
[EFFORT.HIGHEST]: "xhigh",
|
|
547
|
-
};
|
|
548
|
-
function openAiGptVersion(model) {
|
|
549
|
-
const match = model.match(/^gpt-(\d+)(?:\.(\d+))?/);
|
|
550
|
-
if (!match)
|
|
551
|
-
return null;
|
|
552
|
-
return { major: Number(match[1]), minor: match[2] ? Number(match[2]) : 0 };
|
|
553
|
-
}
|
|
554
|
-
function atLeast(version, major, minor) {
|
|
555
|
-
if (!version)
|
|
556
|
-
return false;
|
|
557
|
-
return (version.major > major || (version.major === major && version.minor >= minor));
|
|
558
|
-
}
|
|
559
|
-
function toOpenAiEffort(effort, { model }) {
|
|
560
|
-
const native = OPENAI_EFFORT[effort];
|
|
561
|
-
const version = openAiGptVersion(model);
|
|
562
|
-
// `minimal` only from gpt-5.4 (non-monotonic history; absent on o-series)
|
|
563
|
-
if (native === "minimal" && !atLeast(version, 5, 4)) {
|
|
564
|
-
return { papered: true, value: "low" };
|
|
565
|
-
}
|
|
566
|
-
// `xhigh` from gpt-5.2 onward (absent on older gpt-5 and o-series)
|
|
567
|
-
if (native === "xhigh" && !atLeast(version, 5, 2)) {
|
|
568
|
-
return { papered: true, value: "high" };
|
|
487
|
+
const totals = {};
|
|
488
|
+
for (const item of usage) {
|
|
489
|
+
const key = `${item.provider ?? "unknown"}:${item.model ?? "unknown"}`;
|
|
490
|
+
if (!totals[key]) {
|
|
491
|
+
totals[key] = {
|
|
492
|
+
input: 0,
|
|
493
|
+
model: item.model,
|
|
494
|
+
output: 0,
|
|
495
|
+
provider: item.provider,
|
|
496
|
+
reasoning: 0,
|
|
497
|
+
total: 0,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
totals[key].input += item.input;
|
|
501
|
+
totals[key].output += item.output;
|
|
502
|
+
totals[key].reasoning += item.reasoning;
|
|
503
|
+
totals[key].total += item.total;
|
|
569
504
|
}
|
|
570
|
-
return
|
|
571
|
-
}
|
|
572
|
-
// xAI Grok `reasoning_effort` — low | medium | high. No sub-low or top rung, so
|
|
573
|
-
// `lowest` collapses onto `low` and `highest` onto `high`.
|
|
574
|
-
const XAI_EFFORT = {
|
|
575
|
-
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
576
|
-
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
577
|
-
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
578
|
-
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
579
|
-
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
580
|
-
};
|
|
581
|
-
function toXaiEffort(effort) {
|
|
582
|
-
return XAI_EFFORT[effort];
|
|
583
|
-
}
|
|
584
|
-
// Anthropic `output_config.effort` — low | medium | high | xhigh | max. No
|
|
585
|
-
// sub-low rung, so `lowest` collapses onto `low`; `highest` reaches `max`.
|
|
586
|
-
const ANTHROPIC_EFFORT = {
|
|
587
|
-
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
588
|
-
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
589
|
-
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
590
|
-
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
591
|
-
[EFFORT.HIGHEST]: { papered: false, value: "max" },
|
|
592
|
-
};
|
|
593
|
-
function toAnthropicEffort(effort) {
|
|
594
|
-
return ANTHROPIC_EFFORT[effort];
|
|
595
|
-
}
|
|
596
|
-
// Gemini 3.x `thinkingConfig.thinkingLevel` — MINIMAL | LOW | MEDIUM | HIGH. No
|
|
597
|
-
// top rung above HIGH, so `highest` collapses onto HIGH.
|
|
598
|
-
const GEMINI_THINKING_LEVEL = {
|
|
599
|
-
[EFFORT.LOWEST]: { papered: false, value: "MINIMAL" },
|
|
600
|
-
[EFFORT.LOW]: { papered: false, value: "LOW" },
|
|
601
|
-
[EFFORT.MEDIUM]: { papered: false, value: "MEDIUM" },
|
|
602
|
-
[EFFORT.HIGH]: { papered: false, value: "HIGH" },
|
|
603
|
-
[EFFORT.HIGHEST]: { papered: true, value: "HIGH" },
|
|
604
|
-
};
|
|
605
|
-
function toGeminiThinkingLevel(effort) {
|
|
606
|
-
return GEMINI_THINKING_LEVEL[effort];
|
|
607
|
-
}
|
|
608
|
-
// Gemini 2.5 `thinkingConfig.thinkingBudget` — every tier is a distinct token
|
|
609
|
-
// budget, so nothing is papered over. Floor (512) clears every 2.5 minimum;
|
|
610
|
-
// ceiling (24,576) is valid across 2.5 Pro (max 32,768) and Flash (max 24,576).
|
|
611
|
-
const GEMINI_THINKING_BUDGET = {
|
|
612
|
-
[EFFORT.LOWEST]: 512,
|
|
613
|
-
[EFFORT.LOW]: 4096,
|
|
614
|
-
[EFFORT.MEDIUM]: 8192,
|
|
615
|
-
[EFFORT.HIGH]: 16384,
|
|
616
|
-
[EFFORT.HIGHEST]: 24576,
|
|
617
|
-
};
|
|
618
|
-
function toGeminiThinkingBudget(effort) {
|
|
619
|
-
return { papered: false, value: GEMINI_THINKING_BUDGET[effort] };
|
|
620
|
-
}
|
|
621
|
-
// Fireworks `reasoning_effort` — low | medium | high. Accepted on every model
|
|
622
|
-
// (the API no-ops where unsupported). No sub-low or top rung, so `lowest`
|
|
623
|
-
// collapses onto `low` and `highest` onto `high`.
|
|
624
|
-
const FIREWORKS_EFFORT = {
|
|
625
|
-
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
626
|
-
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
627
|
-
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
628
|
-
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
629
|
-
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
630
|
-
};
|
|
631
|
-
function toFireworksEffort(effort) {
|
|
632
|
-
return FIREWORKS_EFFORT[effort];
|
|
505
|
+
return totals;
|
|
633
506
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
[EFFORT.HIGH]: "high",
|
|
642
|
-
[EFFORT.HIGHEST]: "xhigh",
|
|
643
|
-
};
|
|
644
|
-
function toOpenRouterEffort(effort) {
|
|
645
|
-
return { papered: false, value: OPENROUTER_EFFORT[effort] };
|
|
507
|
+
function extractResponseIds(responses) {
|
|
508
|
+
const ids = responses
|
|
509
|
+
.map((response) => response && typeof response === "object"
|
|
510
|
+
? response.id
|
|
511
|
+
: undefined)
|
|
512
|
+
.filter((id) => typeof id === "string");
|
|
513
|
+
return ids.length ? ids : undefined;
|
|
646
514
|
}
|
|
647
|
-
|
|
648
|
-
// Enums
|
|
649
|
-
exports.LlmMessageRole = void 0;
|
|
650
|
-
(function (LlmMessageRole) {
|
|
651
|
-
LlmMessageRole["Assistant"] = "assistant";
|
|
652
|
-
LlmMessageRole["Developer"] = "developer";
|
|
653
|
-
LlmMessageRole["System"] = "system";
|
|
654
|
-
LlmMessageRole["User"] = "user";
|
|
655
|
-
})(exports.LlmMessageRole || (exports.LlmMessageRole = {}));
|
|
656
|
-
exports.LlmMessageType = void 0;
|
|
657
|
-
(function (LlmMessageType) {
|
|
658
|
-
LlmMessageType["FunctionCall"] = "function_call";
|
|
659
|
-
LlmMessageType["FunctionCallOutput"] = "function_call_output";
|
|
660
|
-
LlmMessageType["InputFile"] = "input_file";
|
|
661
|
-
LlmMessageType["InputImage"] = "input_image";
|
|
662
|
-
LlmMessageType["InputText"] = "input_text";
|
|
663
|
-
LlmMessageType["ItemReference"] = "item_reference";
|
|
664
|
-
LlmMessageType["Message"] = "message";
|
|
665
|
-
LlmMessageType["OutputText"] = "output_text";
|
|
666
|
-
LlmMessageType["Refusal"] = "refusal";
|
|
667
|
-
})(exports.LlmMessageType || (exports.LlmMessageType = {}));
|
|
668
|
-
var LlmResponseStatus;
|
|
669
|
-
(function (LlmResponseStatus) {
|
|
670
|
-
LlmResponseStatus["Completed"] = "completed";
|
|
671
|
-
LlmResponseStatus["Incomplete"] = "incomplete";
|
|
672
|
-
LlmResponseStatus["InProgress"] = "in_progress";
|
|
673
|
-
})(LlmResponseStatus || (LlmResponseStatus = {}));
|
|
674
|
-
// Progress
|
|
675
|
-
exports.LlmProgressEventType = void 0;
|
|
676
|
-
(function (LlmProgressEventType) {
|
|
677
|
-
LlmProgressEventType["Done"] = "done";
|
|
678
|
-
LlmProgressEventType["ModelRequest"] = "model_request";
|
|
679
|
-
LlmProgressEventType["ModelResponse"] = "model_response";
|
|
680
|
-
LlmProgressEventType["Retry"] = "retry";
|
|
681
|
-
LlmProgressEventType["Start"] = "start";
|
|
682
|
-
LlmProgressEventType["ToolCall"] = "tool_call";
|
|
683
|
-
LlmProgressEventType["ToolError"] = "tool_error";
|
|
684
|
-
LlmProgressEventType["ToolResult"] = "tool_result";
|
|
685
|
-
})(exports.LlmProgressEventType || (exports.LlmProgressEventType = {}));
|
|
686
|
-
|
|
687
515
|
//
|
|
688
516
|
//
|
|
689
|
-
//
|
|
517
|
+
// Main
|
|
690
518
|
//
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Assemble the serializable exchange envelope for one operate() settlement.
|
|
521
|
+
* The `resolution` block is stamped by the Llm facade, which is the layer
|
|
522
|
+
* that knows fallback outcome; the loop fills provider/model best-effort.
|
|
523
|
+
*/
|
|
524
|
+
function buildExchangeEnvelope({ duration, initialHistoryLength, input, options, response, startedAt, state, }) {
|
|
525
|
+
const historyDelta = response.history.slice(initialHistoryLength);
|
|
526
|
+
return {
|
|
527
|
+
ids: extractResponseIds(response.responses),
|
|
528
|
+
request: {
|
|
529
|
+
data: options.data,
|
|
530
|
+
effort: options.effort,
|
|
531
|
+
explain: options.explain,
|
|
532
|
+
format: state.formattedFormat,
|
|
533
|
+
input,
|
|
534
|
+
instructions: options.instructions,
|
|
535
|
+
model: options.model,
|
|
536
|
+
placeholders: options.placeholders,
|
|
537
|
+
providerOptions: options.providerOptions,
|
|
538
|
+
system: options.system,
|
|
539
|
+
temperature: options.temperature,
|
|
540
|
+
tools: state.formattedTools?.map(({ name }) => name),
|
|
541
|
+
turns: options.turns,
|
|
542
|
+
user: options.user,
|
|
543
|
+
},
|
|
544
|
+
resolution: {
|
|
545
|
+
model: response.model,
|
|
546
|
+
provider: response.provider,
|
|
547
|
+
retries: state.retries,
|
|
548
|
+
},
|
|
549
|
+
response: {
|
|
550
|
+
content: response.content,
|
|
551
|
+
error: response.error,
|
|
552
|
+
historyDelta,
|
|
553
|
+
reasoning: response.reasoning,
|
|
554
|
+
status: response.status,
|
|
555
|
+
stopReason: state.lastStopReason,
|
|
556
|
+
usage: response.usage,
|
|
557
|
+
usageTotals: sumUsageByProviderModel(response.usage),
|
|
558
|
+
},
|
|
559
|
+
timing: {
|
|
560
|
+
duration,
|
|
561
|
+
startedAt,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
699
565
|
|
|
700
566
|
/**
|
|
701
567
|
* Type guard to check if an item is a dedicated reasoning item (OpenAI)
|
|
@@ -915,6 +781,45 @@ function fillFormatArrays({ content, format, }) {
|
|
|
915
781
|
return fillFromSchema(schema, structuredClone(content));
|
|
916
782
|
}
|
|
917
783
|
|
|
784
|
+
// Enums
|
|
785
|
+
exports.LlmMessageRole = void 0;
|
|
786
|
+
(function (LlmMessageRole) {
|
|
787
|
+
LlmMessageRole["Assistant"] = "assistant";
|
|
788
|
+
LlmMessageRole["Developer"] = "developer";
|
|
789
|
+
LlmMessageRole["System"] = "system";
|
|
790
|
+
LlmMessageRole["User"] = "user";
|
|
791
|
+
})(exports.LlmMessageRole || (exports.LlmMessageRole = {}));
|
|
792
|
+
exports.LlmMessageType = void 0;
|
|
793
|
+
(function (LlmMessageType) {
|
|
794
|
+
LlmMessageType["FunctionCall"] = "function_call";
|
|
795
|
+
LlmMessageType["FunctionCallOutput"] = "function_call_output";
|
|
796
|
+
LlmMessageType["InputFile"] = "input_file";
|
|
797
|
+
LlmMessageType["InputImage"] = "input_image";
|
|
798
|
+
LlmMessageType["InputText"] = "input_text";
|
|
799
|
+
LlmMessageType["ItemReference"] = "item_reference";
|
|
800
|
+
LlmMessageType["Message"] = "message";
|
|
801
|
+
LlmMessageType["OutputText"] = "output_text";
|
|
802
|
+
LlmMessageType["Refusal"] = "refusal";
|
|
803
|
+
})(exports.LlmMessageType || (exports.LlmMessageType = {}));
|
|
804
|
+
var LlmResponseStatus;
|
|
805
|
+
(function (LlmResponseStatus) {
|
|
806
|
+
LlmResponseStatus["Completed"] = "completed";
|
|
807
|
+
LlmResponseStatus["Incomplete"] = "incomplete";
|
|
808
|
+
LlmResponseStatus["InProgress"] = "in_progress";
|
|
809
|
+
})(LlmResponseStatus || (LlmResponseStatus = {}));
|
|
810
|
+
// Progress
|
|
811
|
+
exports.LlmProgressEventType = void 0;
|
|
812
|
+
(function (LlmProgressEventType) {
|
|
813
|
+
LlmProgressEventType["Done"] = "done";
|
|
814
|
+
LlmProgressEventType["ModelRequest"] = "model_request";
|
|
815
|
+
LlmProgressEventType["ModelResponse"] = "model_response";
|
|
816
|
+
LlmProgressEventType["Retry"] = "retry";
|
|
817
|
+
LlmProgressEventType["Start"] = "start";
|
|
818
|
+
LlmProgressEventType["ToolCall"] = "tool_call";
|
|
819
|
+
LlmProgressEventType["ToolError"] = "tool_error";
|
|
820
|
+
LlmProgressEventType["ToolResult"] = "tool_result";
|
|
821
|
+
})(exports.LlmProgressEventType || (exports.LlmProgressEventType = {}));
|
|
822
|
+
|
|
918
823
|
/**
|
|
919
824
|
* Converts a string to a standardized LlmInputMessage
|
|
920
825
|
* @param input - String to format
|
|
@@ -1514,58 +1419,354 @@ function tallyOperate({ toolCallNames = [], turns, usage = [], }) {
|
|
|
1514
1419
|
}
|
|
1515
1420
|
log$1.log.tally({ llm });
|
|
1516
1421
|
}
|
|
1517
|
-
|
|
1518
|
-
/**
|
|
1519
|
-
* Helper function to safely call a function that might throw
|
|
1520
|
-
* @param fn Function to call safely
|
|
1521
|
-
*/
|
|
1522
|
-
function callSafely(fn) {
|
|
1523
|
-
try {
|
|
1524
|
-
fn();
|
|
1525
|
-
}
|
|
1526
|
-
catch {
|
|
1527
|
-
// Silently catch any errors from the function
|
|
1528
|
-
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Helper function to safely call a function that might throw
|
|
1425
|
+
* @param fn Function to call safely
|
|
1426
|
+
*/
|
|
1427
|
+
function callSafely(fn) {
|
|
1428
|
+
try {
|
|
1429
|
+
fn();
|
|
1430
|
+
}
|
|
1431
|
+
catch {
|
|
1432
|
+
// Silently catch any errors from the function
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Attempts to parse a value as a number. Returns the original input if parsing fails or results in NaN.
|
|
1437
|
+
* @param input - The value to attempt to parse as a number
|
|
1438
|
+
* @param options - Optional configuration
|
|
1439
|
+
* @param options.defaultValue - Default value to return if parsing fails or results in NaN
|
|
1440
|
+
* @param options.warnFunction - Function to call with warning message if parsing fails or results in NaN
|
|
1441
|
+
* @returns The parsed number, defaultValue (if specified and parsing fails), or the original input
|
|
1442
|
+
*/
|
|
1443
|
+
function tryParseNumber(input, options) {
|
|
1444
|
+
if (input === null || input === undefined) {
|
|
1445
|
+
return input;
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
const parsed = Number(input);
|
|
1449
|
+
if (Number.isNaN(parsed)) {
|
|
1450
|
+
if (options?.warnFunction) {
|
|
1451
|
+
const warningMessage = `Failed to parse "${String(input)}" as number`;
|
|
1452
|
+
callSafely(() => options.warnFunction(warningMessage));
|
|
1453
|
+
}
|
|
1454
|
+
return typeof options?.defaultValue === "number"
|
|
1455
|
+
? options.defaultValue
|
|
1456
|
+
: input;
|
|
1457
|
+
}
|
|
1458
|
+
return parsed;
|
|
1459
|
+
}
|
|
1460
|
+
catch (error) {
|
|
1461
|
+
if (options?.warnFunction) {
|
|
1462
|
+
let errorMessage = "";
|
|
1463
|
+
if (error instanceof Error) {
|
|
1464
|
+
errorMessage = error.message;
|
|
1465
|
+
}
|
|
1466
|
+
const warningMessage = `Error parsing "${String(input)}" as number${errorMessage ? "; " + errorMessage : ""}`;
|
|
1467
|
+
callSafely(() => options.warnFunction(warningMessage));
|
|
1468
|
+
}
|
|
1469
|
+
return typeof options?.defaultValue === "number"
|
|
1470
|
+
? options.defaultValue
|
|
1471
|
+
: input;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Truthy-except-"false"/"0" gate on LLM_EXCHANGE_ENABLED, matching the
|
|
1477
|
+
* DD_LLMOBS_ENABLED convention in observability/llmobs.ts.
|
|
1478
|
+
*/
|
|
1479
|
+
function isExchangeStoreEnabled() {
|
|
1480
|
+
const flag = process.env.LLM_EXCHANGE_ENABLED;
|
|
1481
|
+
if (!flag) {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
return flag.toLowerCase() !== "false" && flag !== "0";
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Whether the loop should assemble an exchange envelope at settlement.
|
|
1488
|
+
*/
|
|
1489
|
+
function isExchangeRequested({ onExchange, }) {
|
|
1490
|
+
return Boolean(onExchange) || isExchangeStoreEnabled();
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Deliver an exchange envelope to a callback. Errors thrown by the callback
|
|
1494
|
+
* are logged and swallowed — exchange capture must never interrupt the call.
|
|
1495
|
+
*/
|
|
1496
|
+
async function emitExchange({ envelope, onExchange, }) {
|
|
1497
|
+
if (!onExchange) {
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
await onExchange(envelope);
|
|
1502
|
+
}
|
|
1503
|
+
catch (error) {
|
|
1504
|
+
const log = getLogger$7();
|
|
1505
|
+
log.warn("[operate] onExchange callback threw");
|
|
1506
|
+
log.var({ error });
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
//
|
|
1511
|
+
//
|
|
1512
|
+
// Constants
|
|
1513
|
+
//
|
|
1514
|
+
const MODULE$1 = {
|
|
1515
|
+
// Computed at runtime so bundlers (esbuild) do not attempt to include
|
|
1516
|
+
// @jaypie/dynamodb, which is an optional peer dependency.
|
|
1517
|
+
JAYPIE_DYNAMODB: ["@jaypie", "dynamodb"].join("/"),
|
|
1518
|
+
};
|
|
1519
|
+
//
|
|
1520
|
+
//
|
|
1521
|
+
// Helpers
|
|
1522
|
+
//
|
|
1523
|
+
// CJS/ESM compatible require - handles bundling to CJS where import.meta.url
|
|
1524
|
+
// becomes undefined (mirrors observability/llmobs.ts).
|
|
1525
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1526
|
+
// @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
|
|
1527
|
+
const requireModule$1 = typeof __filename !== "undefined"
|
|
1528
|
+
? module$1.createRequire(url.pathToFileURL(__filename).href)
|
|
1529
|
+
: module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
1530
|
+
let resolved$1 = false;
|
|
1531
|
+
let cachedSdk$2 = null;
|
|
1532
|
+
/**
|
|
1533
|
+
* Lazily resolve @jaypie/dynamodb's storeExchange. Returns null (and never
|
|
1534
|
+
* throws) when the peer is absent. Cached after the first attempt.
|
|
1535
|
+
*/
|
|
1536
|
+
function resolveExchangeStore() {
|
|
1537
|
+
if (resolved$1) {
|
|
1538
|
+
return cachedSdk$2;
|
|
1539
|
+
}
|
|
1540
|
+
resolved$1 = true;
|
|
1541
|
+
try {
|
|
1542
|
+
const dynamodb = requireModule$1(MODULE$1.JAYPIE_DYNAMODB);
|
|
1543
|
+
const sdk = dynamodb?.default ?? dynamodb;
|
|
1544
|
+
if (sdk && typeof sdk.storeExchange === "function") {
|
|
1545
|
+
cachedSdk$2 = sdk;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
catch {
|
|
1549
|
+
cachedSdk$2 = null;
|
|
1550
|
+
}
|
|
1551
|
+
return cachedSdk$2;
|
|
1552
|
+
}
|
|
1553
|
+
//
|
|
1554
|
+
//
|
|
1555
|
+
// Main
|
|
1556
|
+
//
|
|
1557
|
+
/**
|
|
1558
|
+
* Persist an exchange envelope via @jaypie/dynamodb when
|
|
1559
|
+
* LLM_EXCHANGE_ENABLED is set. Silent no-op when the flag is unset or the
|
|
1560
|
+
* peer is absent; persister failures are logged and never thrown.
|
|
1561
|
+
*/
|
|
1562
|
+
async function persistExchange(envelope) {
|
|
1563
|
+
if (!isExchangeStoreEnabled()) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const sdk = resolveExchangeStore();
|
|
1567
|
+
if (!sdk) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
await sdk.storeExchange(envelope);
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
const log = getLogger$7();
|
|
1575
|
+
log.warn("[operate] Exchange persistence failed");
|
|
1576
|
+
log.var({ error });
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
//
|
|
1581
|
+
//
|
|
1582
|
+
// Abstract Base Adapter
|
|
1583
|
+
//
|
|
1584
|
+
/**
|
|
1585
|
+
* BaseProviderAdapter provides default implementations for common adapter methods.
|
|
1586
|
+
* Providers can extend this class to reduce boilerplate.
|
|
1587
|
+
*/
|
|
1588
|
+
class BaseProviderAdapter {
|
|
1589
|
+
constructor() {
|
|
1590
|
+
/**
|
|
1591
|
+
* Whether OperateLoop may take a corrective turn when a `format` request
|
|
1592
|
+
* completes with prose instead of structured output. Providers whose
|
|
1593
|
+
* structured output rides a tool emulation (where compliance is a model
|
|
1594
|
+
* decision) opt in; native grammar-constrained providers do not need it.
|
|
1595
|
+
*/
|
|
1596
|
+
this.supportsStructuredOutputRetry = false;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Default implementation checks if error is retryable via classifyError
|
|
1600
|
+
*/
|
|
1601
|
+
isRetryableError(error) {
|
|
1602
|
+
const classified = this.classifyError(error);
|
|
1603
|
+
return classified.shouldRetry;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Default implementation checks error category via classifyError
|
|
1607
|
+
*/
|
|
1608
|
+
isRateLimitError(error) {
|
|
1609
|
+
const classified = this.classifyError(error);
|
|
1610
|
+
return classified.category === "rate_limit";
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Default implementation returns false - override for providers with native structured output
|
|
1614
|
+
*/
|
|
1615
|
+
hasStructuredOutput(_response) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Default implementation returns undefined - override for providers with native structured output
|
|
1620
|
+
*/
|
|
1621
|
+
extractStructuredOutput(_response) {
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Per-provider translation of the provider-neutral {@link LlmEffort} scale.
|
|
1628
|
+
*
|
|
1629
|
+
* The neutral enum (lowest → low → medium → high → highest) is a relative
|
|
1630
|
+
* five-point scale. Each table below maps it onto the target provider's native
|
|
1631
|
+
* effort control, keeping `medium`/`high` semantically aligned across providers
|
|
1632
|
+
* and using each provider's extra bottom (`minimal`) or top (`xhigh`/`max`)
|
|
1633
|
+
* rung where one exists. Providers with fewer levels collapse the ends and mark
|
|
1634
|
+
* the result `papered`. A single `effort` value is therefore safe to reuse
|
|
1635
|
+
* across providers and fallback chains.
|
|
1636
|
+
*/
|
|
1637
|
+
/** Consistent debug message for a papered-over effort level. */
|
|
1638
|
+
function paperedEffortMessage({ model, provider, requested, value, }) {
|
|
1639
|
+
return `[llm] effort '${requested}' has no distinct tier on ${provider} model '${model}'; using '${value}'`;
|
|
1640
|
+
}
|
|
1641
|
+
// OpenAI Responses API `reasoning.effort`.
|
|
1642
|
+
// Full ladder: minimal | low | medium | high | xhigh (plus `none`). Availability
|
|
1643
|
+
// is not uniform across the gpt-5 line:
|
|
1644
|
+
// - `xhigh` was introduced at gpt-5.2 and has been continuous since, so it is
|
|
1645
|
+
// safe for our gpt-5.4 default and everything newer.
|
|
1646
|
+
// - `minimal` shipped on gpt-5/5.1, was dropped at gpt-5.2, and returned on
|
|
1647
|
+
// the current line; because that history is non-monotonic we only trust it
|
|
1648
|
+
// from gpt-5.4 (our default floor) onward.
|
|
1649
|
+
// Outside those windows (older gpt-5, o-series) the extreme rung is clamped and
|
|
1650
|
+
// the mapping reports `papered: true`.
|
|
1651
|
+
const OPENAI_EFFORT = {
|
|
1652
|
+
[EFFORT.LOWEST]: "minimal",
|
|
1653
|
+
[EFFORT.LOW]: "low",
|
|
1654
|
+
[EFFORT.MEDIUM]: "medium",
|
|
1655
|
+
[EFFORT.HIGH]: "high",
|
|
1656
|
+
[EFFORT.HIGHEST]: "xhigh",
|
|
1657
|
+
};
|
|
1658
|
+
function openAiGptVersion(model) {
|
|
1659
|
+
const match = model.match(/^gpt-(\d+)(?:\.(\d+))?/);
|
|
1660
|
+
if (!match)
|
|
1661
|
+
return null;
|
|
1662
|
+
return { major: Number(match[1]), minor: match[2] ? Number(match[2]) : 0 };
|
|
1663
|
+
}
|
|
1664
|
+
function atLeast(version, major, minor) {
|
|
1665
|
+
if (!version)
|
|
1666
|
+
return false;
|
|
1667
|
+
return (version.major > major || (version.major === major && version.minor >= minor));
|
|
1529
1668
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
* @returns The parsed number, defaultValue (if specified and parsing fails), or the original input
|
|
1537
|
-
*/
|
|
1538
|
-
function tryParseNumber(input, options) {
|
|
1539
|
-
if (input === null || input === undefined) {
|
|
1540
|
-
return input;
|
|
1541
|
-
}
|
|
1542
|
-
try {
|
|
1543
|
-
const parsed = Number(input);
|
|
1544
|
-
if (Number.isNaN(parsed)) {
|
|
1545
|
-
if (options?.warnFunction) {
|
|
1546
|
-
const warningMessage = `Failed to parse "${String(input)}" as number`;
|
|
1547
|
-
callSafely(() => options.warnFunction(warningMessage));
|
|
1548
|
-
}
|
|
1549
|
-
return typeof options?.defaultValue === "number"
|
|
1550
|
-
? options.defaultValue
|
|
1551
|
-
: input;
|
|
1552
|
-
}
|
|
1553
|
-
return parsed;
|
|
1669
|
+
function toOpenAiEffort(effort, { model }) {
|
|
1670
|
+
const native = OPENAI_EFFORT[effort];
|
|
1671
|
+
const version = openAiGptVersion(model);
|
|
1672
|
+
// `minimal` only from gpt-5.4 (non-monotonic history; absent on o-series)
|
|
1673
|
+
if (native === "minimal" && !atLeast(version, 5, 4)) {
|
|
1674
|
+
return { papered: true, value: "low" };
|
|
1554
1675
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
if (error instanceof Error) {
|
|
1559
|
-
errorMessage = error.message;
|
|
1560
|
-
}
|
|
1561
|
-
const warningMessage = `Error parsing "${String(input)}" as number${errorMessage ? "; " + errorMessage : ""}`;
|
|
1562
|
-
callSafely(() => options.warnFunction(warningMessage));
|
|
1563
|
-
}
|
|
1564
|
-
return typeof options?.defaultValue === "number"
|
|
1565
|
-
? options.defaultValue
|
|
1566
|
-
: input;
|
|
1676
|
+
// `xhigh` from gpt-5.2 onward (absent on older gpt-5 and o-series)
|
|
1677
|
+
if (native === "xhigh" && !atLeast(version, 5, 2)) {
|
|
1678
|
+
return { papered: true, value: "high" };
|
|
1567
1679
|
}
|
|
1680
|
+
return { papered: false, value: native };
|
|
1681
|
+
}
|
|
1682
|
+
// xAI Grok `reasoning_effort` — low | medium | high. No sub-low or top rung, so
|
|
1683
|
+
// `lowest` collapses onto `low` and `highest` onto `high`.
|
|
1684
|
+
const XAI_EFFORT = {
|
|
1685
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1686
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1687
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1688
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1689
|
+
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
1690
|
+
};
|
|
1691
|
+
function toXaiEffort(effort) {
|
|
1692
|
+
return XAI_EFFORT[effort];
|
|
1693
|
+
}
|
|
1694
|
+
// Anthropic `output_config.effort` — low | medium | high | xhigh | max. No
|
|
1695
|
+
// sub-low rung, so `lowest` collapses onto `low`; `highest` reaches `max`.
|
|
1696
|
+
const ANTHROPIC_EFFORT = {
|
|
1697
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1698
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1699
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1700
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1701
|
+
[EFFORT.HIGHEST]: { papered: false, value: "max" },
|
|
1702
|
+
};
|
|
1703
|
+
function toAnthropicEffort(effort) {
|
|
1704
|
+
return ANTHROPIC_EFFORT[effort];
|
|
1705
|
+
}
|
|
1706
|
+
// Gemini 3.x `thinkingConfig.thinkingLevel` — MINIMAL | LOW | MEDIUM | HIGH. No
|
|
1707
|
+
// top rung above HIGH, so `highest` collapses onto HIGH.
|
|
1708
|
+
const GEMINI_THINKING_LEVEL = {
|
|
1709
|
+
[EFFORT.LOWEST]: { papered: false, value: "MINIMAL" },
|
|
1710
|
+
[EFFORT.LOW]: { papered: false, value: "LOW" },
|
|
1711
|
+
[EFFORT.MEDIUM]: { papered: false, value: "MEDIUM" },
|
|
1712
|
+
[EFFORT.HIGH]: { papered: false, value: "HIGH" },
|
|
1713
|
+
[EFFORT.HIGHEST]: { papered: true, value: "HIGH" },
|
|
1714
|
+
};
|
|
1715
|
+
function toGeminiThinkingLevel(effort) {
|
|
1716
|
+
return GEMINI_THINKING_LEVEL[effort];
|
|
1717
|
+
}
|
|
1718
|
+
// Gemini 2.5 `thinkingConfig.thinkingBudget` — every tier is a distinct token
|
|
1719
|
+
// budget, so nothing is papered over. Floor (512) clears every 2.5 minimum;
|
|
1720
|
+
// ceiling (24,576) is valid across 2.5 Pro (max 32,768) and Flash (max 24,576).
|
|
1721
|
+
const GEMINI_THINKING_BUDGET = {
|
|
1722
|
+
[EFFORT.LOWEST]: 512,
|
|
1723
|
+
[EFFORT.LOW]: 4096,
|
|
1724
|
+
[EFFORT.MEDIUM]: 8192,
|
|
1725
|
+
[EFFORT.HIGH]: 16384,
|
|
1726
|
+
[EFFORT.HIGHEST]: 24576,
|
|
1727
|
+
};
|
|
1728
|
+
function toGeminiThinkingBudget(effort) {
|
|
1729
|
+
return { papered: false, value: GEMINI_THINKING_BUDGET[effort] };
|
|
1568
1730
|
}
|
|
1731
|
+
// Fireworks `reasoning_effort` — low | medium | high. Accepted on every model
|
|
1732
|
+
// (the API no-ops where unsupported). No sub-low or top rung, so `lowest`
|
|
1733
|
+
// collapses onto `low` and `highest` onto `high`.
|
|
1734
|
+
const FIREWORKS_EFFORT = {
|
|
1735
|
+
[EFFORT.LOWEST]: { papered: true, value: "low" },
|
|
1736
|
+
[EFFORT.LOW]: { papered: false, value: "low" },
|
|
1737
|
+
[EFFORT.MEDIUM]: { papered: false, value: "medium" },
|
|
1738
|
+
[EFFORT.HIGH]: { papered: false, value: "high" },
|
|
1739
|
+
[EFFORT.HIGHEST]: { papered: true, value: "high" },
|
|
1740
|
+
};
|
|
1741
|
+
function toFireworksEffort(effort) {
|
|
1742
|
+
return FIREWORKS_EFFORT[effort];
|
|
1743
|
+
}
|
|
1744
|
+
// OpenRouter `reasoning.effort` — accepts the full minimal..xhigh ladder and
|
|
1745
|
+
// maps to the routed provider's nearest supported level itself, so nothing is
|
|
1746
|
+
// papered here.
|
|
1747
|
+
const OPENROUTER_EFFORT = {
|
|
1748
|
+
[EFFORT.LOWEST]: "minimal",
|
|
1749
|
+
[EFFORT.LOW]: "low",
|
|
1750
|
+
[EFFORT.MEDIUM]: "medium",
|
|
1751
|
+
[EFFORT.HIGH]: "high",
|
|
1752
|
+
[EFFORT.HIGHEST]: "xhigh",
|
|
1753
|
+
};
|
|
1754
|
+
function toOpenRouterEffort(effort) {
|
|
1755
|
+
return { papered: false, value: OPENROUTER_EFFORT[effort] };
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
//
|
|
1759
|
+
//
|
|
1760
|
+
// Types
|
|
1761
|
+
//
|
|
1762
|
+
exports.LlmStreamChunkType = void 0;
|
|
1763
|
+
(function (LlmStreamChunkType) {
|
|
1764
|
+
LlmStreamChunkType["Done"] = "done";
|
|
1765
|
+
LlmStreamChunkType["Error"] = "error";
|
|
1766
|
+
LlmStreamChunkType["Text"] = "text";
|
|
1767
|
+
LlmStreamChunkType["ToolCall"] = "tool_call";
|
|
1768
|
+
LlmStreamChunkType["ToolResult"] = "tool_result";
|
|
1769
|
+
})(exports.LlmStreamChunkType || (exports.LlmStreamChunkType = {}));
|
|
1569
1770
|
|
|
1570
1771
|
//
|
|
1571
1772
|
//
|
|
@@ -3412,6 +3613,10 @@ class FireworksAdapter extends BaseProviderAdapter {
|
|
|
3412
3613
|
super(...arguments);
|
|
3413
3614
|
this.name = PROVIDER.FIREWORKS.NAME;
|
|
3414
3615
|
this.defaultModel = PROVIDER.FIREWORKS.DEFAULT;
|
|
3616
|
+
// Structured output with tools rides the structured_output tool emulation
|
|
3617
|
+
// (Fireworks rejects response_format + tools), and emulation compliance is
|
|
3618
|
+
// a model decision — opt in to OperateLoop's corrective retry turn.
|
|
3619
|
+
this.supportsStructuredOutputRetry = true;
|
|
3415
3620
|
// Session-level cache of models observed to reject native
|
|
3416
3621
|
// `response_format: json_schema`. When a model is in this set, buildRequest
|
|
3417
3622
|
// engages the legacy fake-tool path instead of native structured output.
|
|
@@ -3466,9 +3671,10 @@ class FireworksAdapter extends BaseProviderAdapter {
|
|
|
3466
3671
|
const useFallbackStructuredOutput = Boolean(request.format) &&
|
|
3467
3672
|
(hasCallerTools ||
|
|
3468
3673
|
!this.supportsStructuredOutput(fireworksRequest.model));
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3674
|
+
// On a corrective retry turn (the model answered a format request with
|
|
3675
|
+
// prose), offer only the structured_output tool so the demanded call is
|
|
3676
|
+
// the sole option.
|
|
3677
|
+
const allTools = request.tools && !request.structuredOutputRetry ? [...request.tools] : [];
|
|
3472
3678
|
if (useFallbackStructuredOutput && request.format) {
|
|
3473
3679
|
log$1.log.warn(hasCallerTools
|
|
3474
3680
|
? `[FireworksAdapter] Fireworks does not support response_format combined with tools; using structured_output tool emulation for model ${fireworksRequest.model}.`
|
|
@@ -8009,6 +8215,31 @@ function createErrorClassifier(adapter) {
|
|
|
8009
8215
|
},
|
|
8010
8216
|
};
|
|
8011
8217
|
}
|
|
8218
|
+
/**
|
|
8219
|
+
* Attempt to read a prose response as the structured payload itself: parse the
|
|
8220
|
+
* text (stripping a Markdown code fence if present) and return the object, or
|
|
8221
|
+
* undefined when the text is not a JSON object.
|
|
8222
|
+
*/
|
|
8223
|
+
function tryParseJsonObject(text) {
|
|
8224
|
+
let candidate = text.trim();
|
|
8225
|
+
const fence = candidate.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
|
|
8226
|
+
if (fence) {
|
|
8227
|
+
candidate = fence[1].trim();
|
|
8228
|
+
}
|
|
8229
|
+
if (!candidate.startsWith("{")) {
|
|
8230
|
+
return undefined;
|
|
8231
|
+
}
|
|
8232
|
+
try {
|
|
8233
|
+
const parsed = JSON.parse(candidate);
|
|
8234
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
8235
|
+
return parsed;
|
|
8236
|
+
}
|
|
8237
|
+
}
|
|
8238
|
+
catch {
|
|
8239
|
+
// Not JSON; caller falls through to the corrective-turn path.
|
|
8240
|
+
}
|
|
8241
|
+
return undefined;
|
|
8242
|
+
}
|
|
8012
8243
|
//
|
|
8013
8244
|
//
|
|
8014
8245
|
// Main
|
|
@@ -8038,10 +8269,14 @@ class OperateLoop {
|
|
|
8038
8269
|
log.trace("[operate] Starting operate loop");
|
|
8039
8270
|
log.trace.var({ "operate.input": input });
|
|
8040
8271
|
log.trace.var({ "operate.options": options });
|
|
8272
|
+
const startedAt = new Date().toISOString();
|
|
8273
|
+
const startMs = Date.now();
|
|
8041
8274
|
// Initialize state
|
|
8042
8275
|
const state = await this.initializeState(input, options);
|
|
8043
8276
|
const context = this.createContext(options);
|
|
8044
8277
|
const modelName = options.model ?? this.adapter.defaultModel;
|
|
8278
|
+
const exchangeRequested = isExchangeRequested(options);
|
|
8279
|
+
const initialHistoryLength = state.responseBuilder.getHistory().length;
|
|
8045
8280
|
await emitProgress({
|
|
8046
8281
|
event: {
|
|
8047
8282
|
maxTurns: state.maxTurns,
|
|
@@ -8053,58 +8288,102 @@ class OperateLoop {
|
|
|
8053
8288
|
});
|
|
8054
8289
|
// Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
|
|
8055
8290
|
// Child llm/tool spans nest under it via the SDK's active-span context.
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
state.currentTurn
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8291
|
+
try {
|
|
8292
|
+
return await withLlmObsSpan({
|
|
8293
|
+
kind: state.toolkit ? "agent" : "llm",
|
|
8294
|
+
modelName,
|
|
8295
|
+
modelProvider: this.adapter.name,
|
|
8296
|
+
name: "jaypie.llm.operate",
|
|
8297
|
+
}, async () => {
|
|
8298
|
+
// Build initial request
|
|
8299
|
+
let request = this.buildInitialRequest(state, options);
|
|
8300
|
+
// Multi-turn loop
|
|
8301
|
+
while (state.currentTurn < state.maxTurns) {
|
|
8302
|
+
state.currentTurn++;
|
|
8303
|
+
// Execute one turn with retry logic
|
|
8304
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
8305
|
+
if (!shouldContinue) {
|
|
8306
|
+
break;
|
|
8307
|
+
}
|
|
8308
|
+
// Rebuild request with updated history for next turn
|
|
8309
|
+
request = {
|
|
8310
|
+
effort: options.effort,
|
|
8311
|
+
format: state.formattedFormat,
|
|
8312
|
+
instructions: options.instructions,
|
|
8313
|
+
messages: state.currentInput,
|
|
8314
|
+
model: modelName,
|
|
8315
|
+
providerOptions: options.providerOptions,
|
|
8316
|
+
structuredOutputRetry: state.structuredOutputRetry,
|
|
8317
|
+
system: options.system,
|
|
8318
|
+
temperature: options.temperature,
|
|
8319
|
+
tools: state.formattedTools,
|
|
8320
|
+
user: options.user,
|
|
8321
|
+
};
|
|
8071
8322
|
}
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
tallyOperate({
|
|
8093
|
-
toolCallNames: state.toolCallNames,
|
|
8094
|
-
turns: state.currentTurn,
|
|
8095
|
-
usage: response.usage,
|
|
8096
|
-
});
|
|
8097
|
-
await emitProgress({
|
|
8098
|
-
event: {
|
|
8099
|
-
content: response.content,
|
|
8100
|
-
turn: state.currentTurn,
|
|
8101
|
-
type: exports.LlmProgressEventType.Done,
|
|
8323
|
+
const response = state.responseBuilder.build();
|
|
8324
|
+
if (exchangeRequested) {
|
|
8325
|
+
response.exchange = buildExchangeEnvelope({
|
|
8326
|
+
duration: Date.now() - startMs,
|
|
8327
|
+
initialHistoryLength,
|
|
8328
|
+
input,
|
|
8329
|
+
options,
|
|
8330
|
+
response,
|
|
8331
|
+
startedAt,
|
|
8332
|
+
state,
|
|
8333
|
+
});
|
|
8334
|
+
}
|
|
8335
|
+
annotateLlmObs({
|
|
8336
|
+
inputData: input,
|
|
8337
|
+
metrics: usageToLlmObsMetrics(response.usage),
|
|
8338
|
+
outputData: response.content,
|
|
8339
|
+
});
|
|
8340
|
+
tallyOperate({
|
|
8341
|
+
toolCallNames: state.toolCallNames,
|
|
8342
|
+
turns: state.currentTurn,
|
|
8102
8343
|
usage: response.usage,
|
|
8103
|
-
}
|
|
8104
|
-
|
|
8344
|
+
});
|
|
8345
|
+
await emitProgress({
|
|
8346
|
+
event: {
|
|
8347
|
+
content: response.content,
|
|
8348
|
+
turn: state.currentTurn,
|
|
8349
|
+
type: exports.LlmProgressEventType.Done,
|
|
8350
|
+
usage: response.usage,
|
|
8351
|
+
},
|
|
8352
|
+
onProgress: options.onProgress,
|
|
8353
|
+
});
|
|
8354
|
+
return response;
|
|
8105
8355
|
});
|
|
8106
|
-
|
|
8107
|
-
|
|
8356
|
+
}
|
|
8357
|
+
catch (error) {
|
|
8358
|
+
// A hard failure (retry budget exhausted, unrecoverable) still settles
|
|
8359
|
+
// the exchange: attach the envelope to the thrown error so the facade
|
|
8360
|
+
// can emit it before rethrowing to the caller.
|
|
8361
|
+
if (exchangeRequested) {
|
|
8362
|
+
const response = state.responseBuilder.build();
|
|
8363
|
+
if (!response.error) {
|
|
8364
|
+
const thrown = error;
|
|
8365
|
+
response.error = {
|
|
8366
|
+
detail: thrown.detail ?? thrown.message,
|
|
8367
|
+
status: thrown.status ?? 500,
|
|
8368
|
+
title: thrown.title ?? thrown.name ?? "Error",
|
|
8369
|
+
};
|
|
8370
|
+
}
|
|
8371
|
+
if (response.status === LlmResponseStatus.InProgress) {
|
|
8372
|
+
response.status = LlmResponseStatus.Incomplete;
|
|
8373
|
+
}
|
|
8374
|
+
error.exchange =
|
|
8375
|
+
buildExchangeEnvelope({
|
|
8376
|
+
duration: Date.now() - startMs,
|
|
8377
|
+
initialHistoryLength,
|
|
8378
|
+
input,
|
|
8379
|
+
options,
|
|
8380
|
+
response,
|
|
8381
|
+
startedAt,
|
|
8382
|
+
state,
|
|
8383
|
+
});
|
|
8384
|
+
}
|
|
8385
|
+
throw error;
|
|
8386
|
+
}
|
|
8108
8387
|
}
|
|
8109
8388
|
//
|
|
8110
8389
|
// Private Methods
|
|
@@ -8163,6 +8442,7 @@ class OperateLoop {
|
|
|
8163
8442
|
formattedTools,
|
|
8164
8443
|
maxTurns,
|
|
8165
8444
|
responseBuilder,
|
|
8445
|
+
retries: 0,
|
|
8166
8446
|
toolCallNames: [],
|
|
8167
8447
|
toolkit,
|
|
8168
8448
|
};
|
|
@@ -8224,26 +8504,26 @@ class OperateLoop {
|
|
|
8224
8504
|
options,
|
|
8225
8505
|
providerRequest,
|
|
8226
8506
|
});
|
|
8227
|
-
//
|
|
8507
|
+
// Count retries for the exchange envelope and emit retry progress
|
|
8508
|
+
// alongside the caller's onRetryableModelError hook
|
|
8228
8509
|
const hooks = context.hooks;
|
|
8229
|
-
const hooksWithProgress =
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
: hooks;
|
|
8510
|
+
const hooksWithProgress = {
|
|
8511
|
+
...hooks,
|
|
8512
|
+
onRetryableModelError: async (retryContext) => {
|
|
8513
|
+
state.retries++;
|
|
8514
|
+
await emitProgress({
|
|
8515
|
+
event: {
|
|
8516
|
+
error: retryContext.error instanceof Error
|
|
8517
|
+
? retryContext.error.message
|
|
8518
|
+
: String(retryContext.error),
|
|
8519
|
+
turn: state.currentTurn,
|
|
8520
|
+
type: exports.LlmProgressEventType.Retry,
|
|
8521
|
+
},
|
|
8522
|
+
onProgress: options.onProgress,
|
|
8523
|
+
});
|
|
8524
|
+
return hooks?.onRetryableModelError?.(retryContext);
|
|
8525
|
+
},
|
|
8526
|
+
};
|
|
8247
8527
|
// Execute with retry inside a child llm span (no-op when llmobs disabled).
|
|
8248
8528
|
// RetryExecutor handles error hooks and throws appropriate errors.
|
|
8249
8529
|
const { parsed, response, toolCalls } = await withLlmObsSpan({
|
|
@@ -8302,6 +8582,10 @@ class OperateLoop {
|
|
|
8302
8582
|
if (parsed.usage) {
|
|
8303
8583
|
state.responseBuilder.addUsage(parsed.usage);
|
|
8304
8584
|
}
|
|
8585
|
+
// Track stop reason for the exchange envelope
|
|
8586
|
+
if (parsed.stopReason) {
|
|
8587
|
+
state.lastStopReason = parsed.stopReason;
|
|
8588
|
+
}
|
|
8305
8589
|
// Add raw response
|
|
8306
8590
|
state.responseBuilder.addResponse(parsed.raw);
|
|
8307
8591
|
// Execute afterEachModelResponse hook
|
|
@@ -8480,6 +8764,42 @@ class OperateLoop {
|
|
|
8480
8764
|
return true; // Continue to next turn
|
|
8481
8765
|
}
|
|
8482
8766
|
}
|
|
8767
|
+
// Format contract enforcement: the loop is about to complete but the
|
|
8768
|
+
// model answered with prose instead of structured output.
|
|
8769
|
+
if (state.formattedFormat && typeof parsed.content === "string") {
|
|
8770
|
+
// First salvage attempt: the text may be the JSON itself (with or
|
|
8771
|
+
// without a code fence).
|
|
8772
|
+
const salvaged = tryParseJsonObject(parsed.content);
|
|
8773
|
+
if (salvaged) {
|
|
8774
|
+
state.responseBuilder.setContent(this.applyFormatArrayDefaults(salvaged, options));
|
|
8775
|
+
state.responseBuilder.complete();
|
|
8776
|
+
for (const item of this.adapter.responseToHistoryItems(parsed.raw)) {
|
|
8777
|
+
state.responseBuilder.appendToHistory(item);
|
|
8778
|
+
}
|
|
8779
|
+
return false; // Stop loop
|
|
8780
|
+
}
|
|
8781
|
+
// Corrective turn: for adapters whose structured output rides a tool
|
|
8782
|
+
// emulation, take another turn offering only the structured_output tool
|
|
8783
|
+
// and demand it be called. Bounded by maxTurns.
|
|
8784
|
+
if (this.adapter.supportsStructuredOutputRetry &&
|
|
8785
|
+
state.currentTurn < state.maxTurns) {
|
|
8786
|
+
log.warn(`[operate] Model returned text despite format on turn ${state.currentTurn}; retrying with structured_output tool only`);
|
|
8787
|
+
for (const item of this.adapter.responseToHistoryItems(parsed.raw)) {
|
|
8788
|
+
state.currentInput.push(item);
|
|
8789
|
+
state.responseBuilder.appendToHistory(item);
|
|
8790
|
+
}
|
|
8791
|
+
const corrective = {
|
|
8792
|
+
content: "You must provide your final answer by calling the structured_output tool " +
|
|
8793
|
+
"with arguments matching the required schema. Do not respond with text.",
|
|
8794
|
+
role: exports.LlmMessageRole.User,
|
|
8795
|
+
type: exports.LlmMessageType.Message,
|
|
8796
|
+
};
|
|
8797
|
+
state.currentInput.push(corrective);
|
|
8798
|
+
state.responseBuilder.appendToHistory(corrective);
|
|
8799
|
+
state.structuredOutputRetry = true;
|
|
8800
|
+
return true; // Continue to corrective turn
|
|
8801
|
+
}
|
|
8802
|
+
}
|
|
8483
8803
|
// No tool calls or no toolkit - we're done
|
|
8484
8804
|
state.responseBuilder.setContent(this.applyFormatArrayDefaults(parsed.content, options));
|
|
8485
8805
|
state.responseBuilder.complete();
|
|
@@ -10768,12 +11088,17 @@ class Llm {
|
|
|
10768
11088
|
attempts++;
|
|
10769
11089
|
try {
|
|
10770
11090
|
const response = await this._llm.operate(input, optionsWithoutFallback);
|
|
10771
|
-
|
|
11091
|
+
const settled = {
|
|
10772
11092
|
...response,
|
|
10773
11093
|
fallbackAttempts: attempts,
|
|
10774
11094
|
fallbackUsed: false,
|
|
10775
11095
|
provider: response.provider || this._provider,
|
|
10776
11096
|
};
|
|
11097
|
+
await this.settleExchange({
|
|
11098
|
+
onExchange: resolvedOptions.onExchange,
|
|
11099
|
+
response: settled,
|
|
11100
|
+
});
|
|
11101
|
+
return settled;
|
|
10777
11102
|
}
|
|
10778
11103
|
catch (error) {
|
|
10779
11104
|
lastError = error;
|
|
@@ -10782,18 +11107,28 @@ class Llm {
|
|
|
10782
11107
|
fallbacksRemaining: fallbackChain.length,
|
|
10783
11108
|
});
|
|
10784
11109
|
}
|
|
10785
|
-
// Try fallback providers
|
|
11110
|
+
// Try fallback providers. The fallback instance's underlying provider is
|
|
11111
|
+
// called directly so the nested facade does not settle the exchange —
|
|
11112
|
+
// exactly one settlement per operate() happens here.
|
|
10786
11113
|
for (const fallbackConfig of fallbackChain) {
|
|
10787
11114
|
attempts++;
|
|
10788
11115
|
try {
|
|
10789
11116
|
const fallbackInstance = this.createFallbackInstance(fallbackConfig);
|
|
10790
|
-
|
|
10791
|
-
|
|
11117
|
+
if (!fallbackInstance._llm.operate) {
|
|
11118
|
+
throw new errors.NotImplementedError(`Provider ${fallbackConfig.provider} does not support operate method`);
|
|
11119
|
+
}
|
|
11120
|
+
const response = await fallbackInstance._llm.operate(input, optionsWithoutFallback);
|
|
11121
|
+
const settled = {
|
|
10792
11122
|
...response,
|
|
10793
11123
|
fallbackAttempts: attempts,
|
|
10794
11124
|
fallbackUsed: true,
|
|
10795
11125
|
provider: response.provider || fallbackConfig.provider,
|
|
10796
11126
|
};
|
|
11127
|
+
await this.settleExchange({
|
|
11128
|
+
onExchange: resolvedOptions.onExchange,
|
|
11129
|
+
response: settled,
|
|
11130
|
+
});
|
|
11131
|
+
return settled;
|
|
10797
11132
|
}
|
|
10798
11133
|
catch (error) {
|
|
10799
11134
|
lastError = error;
|
|
@@ -10803,9 +11138,44 @@ class Llm {
|
|
|
10803
11138
|
});
|
|
10804
11139
|
}
|
|
10805
11140
|
}
|
|
10806
|
-
// All providers failed
|
|
11141
|
+
// All providers failed: settle the exchange from the envelope the loop
|
|
11142
|
+
// attached to the last error, then throw
|
|
11143
|
+
const failureEnvelope = lastError
|
|
11144
|
+
?.exchange;
|
|
11145
|
+
if (failureEnvelope) {
|
|
11146
|
+
failureEnvelope.resolution = {
|
|
11147
|
+
...failureEnvelope.resolution,
|
|
11148
|
+
fallbackAttempts: attempts,
|
|
11149
|
+
fallbackUsed: attempts > 1,
|
|
11150
|
+
};
|
|
11151
|
+
await emitExchange({
|
|
11152
|
+
envelope: failureEnvelope,
|
|
11153
|
+
onExchange: resolvedOptions.onExchange,
|
|
11154
|
+
});
|
|
11155
|
+
await persistExchange(failureEnvelope);
|
|
11156
|
+
}
|
|
10807
11157
|
throw lastError;
|
|
10808
11158
|
}
|
|
11159
|
+
/**
|
|
11160
|
+
* Stamp fallback resolution onto the envelope the operate loop attached to
|
|
11161
|
+
* the response and deliver it to the caller's onExchange. Fires once per
|
|
11162
|
+
* operate() settlement; callback errors are logged and never thrown.
|
|
11163
|
+
*/
|
|
11164
|
+
async settleExchange({ onExchange, response, }) {
|
|
11165
|
+
const envelope = response.exchange;
|
|
11166
|
+
if (!envelope) {
|
|
11167
|
+
return;
|
|
11168
|
+
}
|
|
11169
|
+
envelope.resolution = {
|
|
11170
|
+
...envelope.resolution,
|
|
11171
|
+
fallbackAttempts: response.fallbackAttempts,
|
|
11172
|
+
fallbackUsed: response.fallbackUsed,
|
|
11173
|
+
model: response.model,
|
|
11174
|
+
provider: response.provider,
|
|
11175
|
+
};
|
|
11176
|
+
await emitExchange({ envelope, onExchange });
|
|
11177
|
+
await persistExchange(envelope);
|
|
11178
|
+
}
|
|
10809
11179
|
async *stream(input, options = {}) {
|
|
10810
11180
|
if (!this._llm.stream) {
|
|
10811
11181
|
throw new errors.NotImplementedError(`Provider ${this._provider} does not support stream method`);
|