@jaypie/llm 1.2.39 → 1.2.41
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/index.cjs +476 -110
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/observability/llmobs.d.ts +72 -0
- package/dist/cjs/operate/OperateLoop.d.ts +6 -0
- package/dist/cjs/util/fillFormatArrays.d.ts +17 -0
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/esm/index.js +475 -110
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/llmobs.d.ts +72 -0
- package/dist/esm/operate/OperateLoop.d.ts +6 -0
- package/dist/esm/util/fillFormatArrays.d.ts +17 -0
- package/dist/esm/util/index.d.ts +1 -0
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { placeholders, JAYPIE, resolveValue, sleep } from '@jaypie/kit';
|
|
|
5
5
|
import RandomLib from 'random';
|
|
6
6
|
import { RateLimitError, APIConnectionError, APIConnectionTimeoutError, InternalServerError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, NotFoundError, PermissionDeniedError, UnprocessableEntityError, OpenAI } from 'openai';
|
|
7
7
|
import { zodResponseFormat } from 'openai/helpers/zod';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import { pathToFileURL } from 'url';
|
|
8
10
|
import { PDFDocument } from 'pdf-lib';
|
|
9
11
|
import { readFile } from 'fs/promises';
|
|
10
12
|
import { resolve } from 'path';
|
|
@@ -570,6 +572,154 @@ function extractReasoning(history) {
|
|
|
570
572
|
return reasoningTexts;
|
|
571
573
|
}
|
|
572
574
|
|
|
575
|
+
function naturalZodSchema(definition) {
|
|
576
|
+
if (Array.isArray(definition)) {
|
|
577
|
+
if (definition.length === 0) {
|
|
578
|
+
// Handle empty array - accept any[]
|
|
579
|
+
return z.array(z.any());
|
|
580
|
+
}
|
|
581
|
+
else if (definition.length === 1) {
|
|
582
|
+
// Handle array types
|
|
583
|
+
const itemType = definition[0];
|
|
584
|
+
switch (itemType) {
|
|
585
|
+
case String:
|
|
586
|
+
return z.array(z.string());
|
|
587
|
+
case Number:
|
|
588
|
+
return z.array(z.number());
|
|
589
|
+
case Boolean:
|
|
590
|
+
return z.array(z.boolean());
|
|
591
|
+
default:
|
|
592
|
+
if (typeof itemType === "object") {
|
|
593
|
+
// Handle array of objects
|
|
594
|
+
return z.array(naturalZodSchema(itemType));
|
|
595
|
+
}
|
|
596
|
+
// Handle enum arrays
|
|
597
|
+
return z.enum(definition);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
// Handle enum arrays
|
|
602
|
+
return z.enum(definition);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else if (definition && typeof definition === "object") {
|
|
606
|
+
if (Object.keys(definition).length === 0) {
|
|
607
|
+
// Handle empty object - accept any key-value pairs
|
|
608
|
+
return z.record(z.string(), z.any());
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// Handle object with properties
|
|
612
|
+
const schemaShape = {};
|
|
613
|
+
for (const [key, value] of Object.entries(definition)) {
|
|
614
|
+
schemaShape[key] = naturalZodSchema(value);
|
|
615
|
+
}
|
|
616
|
+
return z.object(schemaShape);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
switch (definition) {
|
|
621
|
+
case String:
|
|
622
|
+
return z.string();
|
|
623
|
+
case Number:
|
|
624
|
+
return z.number();
|
|
625
|
+
case Boolean:
|
|
626
|
+
return z.boolean();
|
|
627
|
+
case Object:
|
|
628
|
+
return z.record(z.string(), z.any());
|
|
629
|
+
case Array:
|
|
630
|
+
return z.array(z.any());
|
|
631
|
+
default:
|
|
632
|
+
throw new Error(`Unsupported type: ${definition}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
//
|
|
638
|
+
//
|
|
639
|
+
// Helpers
|
|
640
|
+
//
|
|
641
|
+
function isPlainObject(value) {
|
|
642
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Convert a `format` declaration (Zod schema, NaturalSchema, or a JSON Schema
|
|
646
|
+
* JsonObject) into a plain JSON Schema we can walk. Mirrors the conversion the
|
|
647
|
+
* provider adapters perform in `formatOutputSchema`, but without any
|
|
648
|
+
* provider-specific sanitization.
|
|
649
|
+
*/
|
|
650
|
+
function formatToJsonSchema(format) {
|
|
651
|
+
if (format instanceof z.ZodType) {
|
|
652
|
+
return z.toJSONSchema(format);
|
|
653
|
+
}
|
|
654
|
+
if (isPlainObject(format) && format.type === "json_schema") {
|
|
655
|
+
const clone = structuredClone(format);
|
|
656
|
+
clone.type = "object";
|
|
657
|
+
return clone;
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
return z.toJSONSchema(naturalZodSchema(format));
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Walk a JSON Schema alongside a parsed value, filling any declared array field
|
|
668
|
+
* that is absent (`undefined`/`null`) with `[]`. Recurses into object
|
|
669
|
+
* properties and array items so nested declared arrays are also backfilled.
|
|
670
|
+
*/
|
|
671
|
+
function fillFromSchema(schema, value) {
|
|
672
|
+
if (!isPlainObject(schema)) {
|
|
673
|
+
return value;
|
|
674
|
+
}
|
|
675
|
+
const type = schema.type;
|
|
676
|
+
const isArray = type === "array" || (type === undefined && "items" in schema);
|
|
677
|
+
if (isArray) {
|
|
678
|
+
if (value === undefined || value === null) {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
const items = schema.items;
|
|
682
|
+
if (Array.isArray(value) && isPlainObject(items)) {
|
|
683
|
+
return value.map((entry) => fillFromSchema(items, entry));
|
|
684
|
+
}
|
|
685
|
+
return value;
|
|
686
|
+
}
|
|
687
|
+
const isObject = type === "object" || (type === undefined && "properties" in schema);
|
|
688
|
+
if (isObject) {
|
|
689
|
+
if (!isPlainObject(value)) {
|
|
690
|
+
return value;
|
|
691
|
+
}
|
|
692
|
+
const properties = schema.properties;
|
|
693
|
+
if (isPlainObject(properties)) {
|
|
694
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
695
|
+
value[key] = fillFromSchema(propSchema, value[key]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
//
|
|
703
|
+
//
|
|
704
|
+
// Main
|
|
705
|
+
//
|
|
706
|
+
/**
|
|
707
|
+
* Ensure every array field declared in `format` is present in `content` as an
|
|
708
|
+
* array. A declared `format` is a schema contract: an empty list should surface
|
|
709
|
+
* as `[]`, not be dropped from the response. Some providers/models omit empty
|
|
710
|
+
* array fields entirely, leaving consumers to read `.length` on `undefined`.
|
|
711
|
+
*
|
|
712
|
+
* Only mutates a (cloned) structured object; strings and non-objects pass
|
|
713
|
+
* through untouched.
|
|
714
|
+
*/
|
|
715
|
+
function fillFormatArrays({ content, format, }) {
|
|
716
|
+
const schema = formatToJsonSchema(format);
|
|
717
|
+
if (!schema) {
|
|
718
|
+
return content;
|
|
719
|
+
}
|
|
720
|
+
return fillFromSchema(schema, structuredClone(content));
|
|
721
|
+
}
|
|
722
|
+
|
|
573
723
|
/**
|
|
574
724
|
* Converts a string to a standardized LlmInputMessage
|
|
575
725
|
* @param input - String to format
|
|
@@ -697,68 +847,6 @@ function maxTurnsFromOptions(options) {
|
|
|
697
847
|
return 1;
|
|
698
848
|
}
|
|
699
849
|
|
|
700
|
-
function naturalZodSchema(definition) {
|
|
701
|
-
if (Array.isArray(definition)) {
|
|
702
|
-
if (definition.length === 0) {
|
|
703
|
-
// Handle empty array - accept any[]
|
|
704
|
-
return z.array(z.any());
|
|
705
|
-
}
|
|
706
|
-
else if (definition.length === 1) {
|
|
707
|
-
// Handle array types
|
|
708
|
-
const itemType = definition[0];
|
|
709
|
-
switch (itemType) {
|
|
710
|
-
case String:
|
|
711
|
-
return z.array(z.string());
|
|
712
|
-
case Number:
|
|
713
|
-
return z.array(z.number());
|
|
714
|
-
case Boolean:
|
|
715
|
-
return z.array(z.boolean());
|
|
716
|
-
default:
|
|
717
|
-
if (typeof itemType === "object") {
|
|
718
|
-
// Handle array of objects
|
|
719
|
-
return z.array(naturalZodSchema(itemType));
|
|
720
|
-
}
|
|
721
|
-
// Handle enum arrays
|
|
722
|
-
return z.enum(definition);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
// Handle enum arrays
|
|
727
|
-
return z.enum(definition);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
else if (definition && typeof definition === "object") {
|
|
731
|
-
if (Object.keys(definition).length === 0) {
|
|
732
|
-
// Handle empty object - accept any key-value pairs
|
|
733
|
-
return z.record(z.string(), z.any());
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
// Handle object with properties
|
|
737
|
-
const schemaShape = {};
|
|
738
|
-
for (const [key, value] of Object.entries(definition)) {
|
|
739
|
-
schemaShape[key] = naturalZodSchema(value);
|
|
740
|
-
}
|
|
741
|
-
return z.object(schemaShape);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
else {
|
|
745
|
-
switch (definition) {
|
|
746
|
-
case String:
|
|
747
|
-
return z.string();
|
|
748
|
-
case Number:
|
|
749
|
-
return z.number();
|
|
750
|
-
case Boolean:
|
|
751
|
-
return z.boolean();
|
|
752
|
-
case Object:
|
|
753
|
-
return z.record(z.string(), z.any());
|
|
754
|
-
case Array:
|
|
755
|
-
return z.array(z.any());
|
|
756
|
-
default:
|
|
757
|
-
throw new Error(`Unsupported type: ${definition}`);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
850
|
//
|
|
763
851
|
// Constants
|
|
764
852
|
//
|
|
@@ -4835,6 +4923,198 @@ class Toolkit {
|
|
|
4835
4923
|
}
|
|
4836
4924
|
}
|
|
4837
4925
|
|
|
4926
|
+
//
|
|
4927
|
+
//
|
|
4928
|
+
// Constants
|
|
4929
|
+
//
|
|
4930
|
+
const ENV = {
|
|
4931
|
+
DD_LLMOBS_ENABLED: "DD_LLMOBS_ENABLED",
|
|
4932
|
+
};
|
|
4933
|
+
const MODULE = {
|
|
4934
|
+
// Computed at runtime so bundlers (esbuild) do not attempt to include
|
|
4935
|
+
// dd-trace, which is provided by the Datadog Lambda layer at runtime.
|
|
4936
|
+
DD_TRACE: ["dd", "trace"].join("-"),
|
|
4937
|
+
};
|
|
4938
|
+
//
|
|
4939
|
+
//
|
|
4940
|
+
// Helpers
|
|
4941
|
+
//
|
|
4942
|
+
// CJS/ESM compatible require - handles bundling to CJS where import.meta.url
|
|
4943
|
+
// becomes undefined (mirrors packages/testkit/src/mock/original.ts).
|
|
4944
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4945
|
+
// @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
|
|
4946
|
+
const requireModule = typeof __filename !== "undefined"
|
|
4947
|
+
? createRequire(pathToFileURL(__filename).href)
|
|
4948
|
+
: createRequire(import.meta.url);
|
|
4949
|
+
let resolved = false;
|
|
4950
|
+
let cachedSdk$4 = null;
|
|
4951
|
+
/**
|
|
4952
|
+
* Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
|
|
4953
|
+
* when dd-trace is absent or the SDK surface is unexpected. Cached after the
|
|
4954
|
+
* first attempt.
|
|
4955
|
+
*/
|
|
4956
|
+
function resolveLlmObs() {
|
|
4957
|
+
if (resolved) {
|
|
4958
|
+
return cachedSdk$4;
|
|
4959
|
+
}
|
|
4960
|
+
resolved = true;
|
|
4961
|
+
try {
|
|
4962
|
+
const ddTrace = requireModule(MODULE.DD_TRACE);
|
|
4963
|
+
const tracer = ddTrace?.default ?? ddTrace;
|
|
4964
|
+
const llmobs = tracer?.llmobs;
|
|
4965
|
+
if (llmobs &&
|
|
4966
|
+
typeof llmobs.trace === "function" &&
|
|
4967
|
+
typeof llmobs.annotate === "function") {
|
|
4968
|
+
cachedSdk$4 = llmobs;
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
catch {
|
|
4972
|
+
cachedSdk$4 = null;
|
|
4973
|
+
}
|
|
4974
|
+
return cachedSdk$4;
|
|
4975
|
+
}
|
|
4976
|
+
//
|
|
4977
|
+
//
|
|
4978
|
+
// Main
|
|
4979
|
+
//
|
|
4980
|
+
/**
|
|
4981
|
+
* True when LLM Observability emission is opted in via `DD_LLMOBS_ENABLED`.
|
|
4982
|
+
* Accepts any truthy value except "false"/"0".
|
|
4983
|
+
*/
|
|
4984
|
+
function isLlmObsEnabled() {
|
|
4985
|
+
const value = process.env[ENV.DD_LLMOBS_ENABLED];
|
|
4986
|
+
if (!value) {
|
|
4987
|
+
return false;
|
|
4988
|
+
}
|
|
4989
|
+
const normalized = value.trim().toLowerCase();
|
|
4990
|
+
return normalized !== "" && normalized !== "false" && normalized !== "0";
|
|
4991
|
+
}
|
|
4992
|
+
/** Resolve the SDK only when enabled and available. */
|
|
4993
|
+
function activeSdk() {
|
|
4994
|
+
if (!isLlmObsEnabled()) {
|
|
4995
|
+
return null;
|
|
4996
|
+
}
|
|
4997
|
+
return resolveLlmObs();
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Run `fn` inside an LLM Observability span. When emission is disabled or
|
|
5001
|
+
* dd-trace is absent, `fn` is invoked directly with no overhead. The active
|
|
5002
|
+
* span is established for the duration of `fn`, so nested `withLlmObsSpan`
|
|
5003
|
+
* calls (and `annotateLlmObs`) attach as children/annotations automatically.
|
|
5004
|
+
*/
|
|
5005
|
+
async function withLlmObsSpan(options, fn) {
|
|
5006
|
+
const sdk = activeSdk();
|
|
5007
|
+
if (!sdk) {
|
|
5008
|
+
return fn();
|
|
5009
|
+
}
|
|
5010
|
+
let started = false;
|
|
5011
|
+
try {
|
|
5012
|
+
return (await sdk.trace(options, () => {
|
|
5013
|
+
started = true;
|
|
5014
|
+
return fn();
|
|
5015
|
+
}));
|
|
5016
|
+
}
|
|
5017
|
+
catch (error) {
|
|
5018
|
+
// If `fn` ran, propagate its error (the SDK re-throws after finishing the
|
|
5019
|
+
// span). Only swallow failures from the instrumentation itself, re-running
|
|
5020
|
+
// `fn` so observability never breaks the underlying call.
|
|
5021
|
+
if (started) {
|
|
5022
|
+
throw error;
|
|
5023
|
+
}
|
|
5024
|
+
getLogger$6().warn("[llmobs] span emission failed");
|
|
5025
|
+
getLogger$6().var({ error });
|
|
5026
|
+
return fn();
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
/**
|
|
5030
|
+
* Annotate the currently active LLM Observability span. No-op when disabled.
|
|
5031
|
+
*/
|
|
5032
|
+
function annotateLlmObs(annotation) {
|
|
5033
|
+
const sdk = activeSdk();
|
|
5034
|
+
if (!sdk) {
|
|
5035
|
+
return;
|
|
5036
|
+
}
|
|
5037
|
+
try {
|
|
5038
|
+
sdk.annotate(undefined, annotation);
|
|
5039
|
+
}
|
|
5040
|
+
catch (error) {
|
|
5041
|
+
getLogger$6().warn("[llmobs] annotate failed");
|
|
5042
|
+
getLogger$6().var({ error });
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
/**
|
|
5046
|
+
* Open a manual span that is finished later. Used by streaming, where work is
|
|
5047
|
+
* yielded incrementally and cannot be enclosed in a single callback. The span
|
|
5048
|
+
* is kept open via a deferred promise resolved by `finish()`. Returns null when
|
|
5049
|
+
* emission is disabled or dd-trace is absent.
|
|
5050
|
+
*
|
|
5051
|
+
* Note: because the span is held open across `yield` boundaries (outside the
|
|
5052
|
+
* SDK's AsyncLocalStorage scope), manual spans are emitted flat rather than
|
|
5053
|
+
* nested. Annotations are applied to the explicit span reference.
|
|
5054
|
+
*/
|
|
5055
|
+
function openLlmObsSpan(options) {
|
|
5056
|
+
const sdk = activeSdk();
|
|
5057
|
+
if (!sdk) {
|
|
5058
|
+
return null;
|
|
5059
|
+
}
|
|
5060
|
+
try {
|
|
5061
|
+
let capturedSpan;
|
|
5062
|
+
let release;
|
|
5063
|
+
const held = new Promise((resolve) => {
|
|
5064
|
+
release = resolve;
|
|
5065
|
+
});
|
|
5066
|
+
// trace() runs the callback synchronously, capturing the span; the returned
|
|
5067
|
+
// pending promise keeps the span open until finish() resolves it.
|
|
5068
|
+
void sdk.trace(options, (span) => {
|
|
5069
|
+
capturedSpan = span;
|
|
5070
|
+
return held;
|
|
5071
|
+
});
|
|
5072
|
+
let finished = false;
|
|
5073
|
+
return {
|
|
5074
|
+
annotate: (annotation) => {
|
|
5075
|
+
try {
|
|
5076
|
+
sdk.annotate(capturedSpan, annotation);
|
|
5077
|
+
}
|
|
5078
|
+
catch (error) {
|
|
5079
|
+
getLogger$6().warn("[llmobs] annotate failed");
|
|
5080
|
+
getLogger$6().var({ error });
|
|
5081
|
+
}
|
|
5082
|
+
},
|
|
5083
|
+
finish: () => {
|
|
5084
|
+
if (finished) {
|
|
5085
|
+
return;
|
|
5086
|
+
}
|
|
5087
|
+
finished = true;
|
|
5088
|
+
release?.();
|
|
5089
|
+
},
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
catch (error) {
|
|
5093
|
+
getLogger$6().warn("[llmobs] span emission failed");
|
|
5094
|
+
getLogger$6().var({ error });
|
|
5095
|
+
return null;
|
|
5096
|
+
}
|
|
5097
|
+
}
|
|
5098
|
+
//
|
|
5099
|
+
//
|
|
5100
|
+
// Mapping Helpers
|
|
5101
|
+
//
|
|
5102
|
+
/**
|
|
5103
|
+
* Sum an `LlmUsage` array into LLM Observability metric keys. Returns undefined
|
|
5104
|
+
* when there is no usage data so callers can omit the `metrics` field.
|
|
5105
|
+
*/
|
|
5106
|
+
function usageToLlmObsMetrics(usage) {
|
|
5107
|
+
if (!usage || usage.length === 0) {
|
|
5108
|
+
return undefined;
|
|
5109
|
+
}
|
|
5110
|
+
const metrics = usage.reduce((sum, item) => ({
|
|
5111
|
+
input_tokens: sum.input_tokens + (item.input ?? 0),
|
|
5112
|
+
output_tokens: sum.output_tokens + (item.output ?? 0),
|
|
5113
|
+
total_tokens: sum.total_tokens + (item.total ?? 0),
|
|
5114
|
+
}), { input_tokens: 0, output_tokens: 0, total_tokens: 0 });
|
|
5115
|
+
return metrics;
|
|
5116
|
+
}
|
|
5117
|
+
|
|
4838
5118
|
//
|
|
4839
5119
|
//
|
|
4840
5120
|
// Main
|
|
@@ -5706,30 +5986,46 @@ class OperateLoop {
|
|
|
5706
5986
|
// Initialize state
|
|
5707
5987
|
const state = await this.initializeState(input, options);
|
|
5708
5988
|
const context = this.createContext(options);
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
//
|
|
5712
|
-
|
|
5713
|
-
state.
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5989
|
+
const modelName = options.model ?? this.adapter.defaultModel;
|
|
5990
|
+
// Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
|
|
5991
|
+
// Child llm/tool spans nest under it via the SDK's active-span context.
|
|
5992
|
+
return withLlmObsSpan({
|
|
5993
|
+
kind: state.toolkit ? "agent" : "llm",
|
|
5994
|
+
modelName,
|
|
5995
|
+
modelProvider: this.adapter.name,
|
|
5996
|
+
name: "jaypie.llm.operate",
|
|
5997
|
+
}, async () => {
|
|
5998
|
+
// Build initial request
|
|
5999
|
+
let request = this.buildInitialRequest(state, options);
|
|
6000
|
+
// Multi-turn loop
|
|
6001
|
+
while (state.currentTurn < state.maxTurns) {
|
|
6002
|
+
state.currentTurn++;
|
|
6003
|
+
// Execute one turn with retry logic
|
|
6004
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
6005
|
+
if (!shouldContinue) {
|
|
6006
|
+
break;
|
|
6007
|
+
}
|
|
6008
|
+
// Rebuild request with updated history for next turn
|
|
6009
|
+
request = {
|
|
6010
|
+
format: state.formattedFormat,
|
|
6011
|
+
instructions: options.instructions,
|
|
6012
|
+
messages: state.currentInput,
|
|
6013
|
+
model: modelName,
|
|
6014
|
+
providerOptions: options.providerOptions,
|
|
6015
|
+
system: options.system,
|
|
6016
|
+
temperature: options.temperature,
|
|
6017
|
+
tools: state.formattedTools,
|
|
6018
|
+
user: options.user,
|
|
6019
|
+
};
|
|
5718
6020
|
}
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
temperature: options.temperature,
|
|
5728
|
-
tools: state.formattedTools,
|
|
5729
|
-
user: options.user,
|
|
5730
|
-
};
|
|
5731
|
-
}
|
|
5732
|
-
return state.responseBuilder.build();
|
|
6021
|
+
const response = state.responseBuilder.build();
|
|
6022
|
+
annotateLlmObs({
|
|
6023
|
+
inputData: input,
|
|
6024
|
+
metrics: usageToLlmObsMetrics(response.usage),
|
|
6025
|
+
outputData: response.content,
|
|
6026
|
+
});
|
|
6027
|
+
return response;
|
|
6028
|
+
});
|
|
5733
6029
|
}
|
|
5734
6030
|
//
|
|
5735
6031
|
// Private Methods
|
|
@@ -5831,20 +6127,34 @@ class OperateLoop {
|
|
|
5831
6127
|
options,
|
|
5832
6128
|
providerRequest,
|
|
5833
6129
|
});
|
|
5834
|
-
// Execute with retry
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
6130
|
+
// Execute with retry inside a child llm span (no-op when llmobs disabled).
|
|
6131
|
+
// RetryExecutor handles error hooks and throws appropriate errors.
|
|
6132
|
+
const { parsed, response } = await withLlmObsSpan({
|
|
6133
|
+
kind: "llm",
|
|
6134
|
+
modelName: options.model ?? this.adapter.defaultModel,
|
|
6135
|
+
modelProvider: this.adapter.name,
|
|
6136
|
+
name: "jaypie.llm.model",
|
|
6137
|
+
}, async () => {
|
|
6138
|
+
const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
|
|
6139
|
+
context: {
|
|
6140
|
+
input: state.currentInput,
|
|
6141
|
+
options,
|
|
6142
|
+
providerRequest,
|
|
6143
|
+
},
|
|
6144
|
+
hooks: context.hooks,
|
|
6145
|
+
});
|
|
6146
|
+
// Log what was returned from the model
|
|
6147
|
+
log.trace("[operate] Model response received");
|
|
6148
|
+
log.var({ "operate.response": response });
|
|
6149
|
+
// Parse response
|
|
6150
|
+
const parsed = this.adapter.parseResponse(response, options);
|
|
6151
|
+
annotateLlmObs({
|
|
6152
|
+
inputData: state.currentInput,
|
|
6153
|
+
metrics: usageToLlmObsMetrics(parsed.usage ? [parsed.usage] : undefined),
|
|
6154
|
+
outputData: parsed.content ?? "",
|
|
6155
|
+
});
|
|
6156
|
+
return { parsed, response };
|
|
5842
6157
|
});
|
|
5843
|
-
// Log what was returned from the model
|
|
5844
|
-
log.trace("[operate] Model response received");
|
|
5845
|
-
log.var({ "operate.response": response });
|
|
5846
|
-
// Parse response
|
|
5847
|
-
const parsed = this.adapter.parseResponse(response, options);
|
|
5848
6158
|
// Track usage
|
|
5849
6159
|
if (parsed.usage) {
|
|
5850
6160
|
state.responseBuilder.addUsage(parsed.usage);
|
|
@@ -5865,7 +6175,7 @@ class OperateLoop {
|
|
|
5865
6175
|
if (this.adapter.hasStructuredOutput(response)) {
|
|
5866
6176
|
const structuredOutput = this.adapter.extractStructuredOutput(response);
|
|
5867
6177
|
if (structuredOutput) {
|
|
5868
|
-
state.responseBuilder.setContent(structuredOutput);
|
|
6178
|
+
state.responseBuilder.setContent(this.applyFormatArrayDefaults(structuredOutput, options));
|
|
5869
6179
|
state.responseBuilder.complete();
|
|
5870
6180
|
return false; // Stop loop
|
|
5871
6181
|
}
|
|
@@ -5889,11 +6199,19 @@ class OperateLoop {
|
|
|
5889
6199
|
args: toolCall.arguments,
|
|
5890
6200
|
toolName: toolCall.name,
|
|
5891
6201
|
});
|
|
5892
|
-
// Call the tool
|
|
6202
|
+
// Call the tool inside a child tool span (no-op when disabled)
|
|
5893
6203
|
log.trace(`[operate] Calling tool - ${toolCall.name}`);
|
|
5894
|
-
const result = await
|
|
5895
|
-
|
|
5896
|
-
|
|
6204
|
+
const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
|
|
6205
|
+
const result = await state.toolkit.call({
|
|
6206
|
+
arguments: toolCall.arguments,
|
|
6207
|
+
name: toolCall.name,
|
|
6208
|
+
});
|
|
6209
|
+
annotateLlmObs({
|
|
6210
|
+
inputData: toolCall.arguments,
|
|
6211
|
+
metadata: { tool: toolCall.name },
|
|
6212
|
+
outputData: result,
|
|
6213
|
+
});
|
|
6214
|
+
return result;
|
|
5897
6215
|
});
|
|
5898
6216
|
// Execute afterEachTool hook
|
|
5899
6217
|
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|
|
@@ -5982,7 +6300,7 @@ class OperateLoop {
|
|
|
5982
6300
|
}
|
|
5983
6301
|
}
|
|
5984
6302
|
// No tool calls or no toolkit - we're done
|
|
5985
|
-
state.responseBuilder.setContent(parsed.content);
|
|
6303
|
+
state.responseBuilder.setContent(this.applyFormatArrayDefaults(parsed.content, options));
|
|
5986
6304
|
state.responseBuilder.complete();
|
|
5987
6305
|
// Add final history items
|
|
5988
6306
|
const historyItems = this.adapter.responseToHistoryItems(parsed.raw);
|
|
@@ -5991,6 +6309,20 @@ class OperateLoop {
|
|
|
5991
6309
|
}
|
|
5992
6310
|
return false; // Stop loop
|
|
5993
6311
|
}
|
|
6312
|
+
/**
|
|
6313
|
+
* Backfill declared array fields when a `format` is supplied. A declared
|
|
6314
|
+
* `format` is a schema contract: an empty array field should surface as `[]`
|
|
6315
|
+
* rather than be dropped by a provider/model that omits empty lists.
|
|
6316
|
+
*/
|
|
6317
|
+
applyFormatArrayDefaults(content, options) {
|
|
6318
|
+
if (!options.format) {
|
|
6319
|
+
return content;
|
|
6320
|
+
}
|
|
6321
|
+
if (typeof content !== "object" || content === null) {
|
|
6322
|
+
return content;
|
|
6323
|
+
}
|
|
6324
|
+
return fillFormatArrays({ content, format: options.format });
|
|
6325
|
+
}
|
|
5994
6326
|
/**
|
|
5995
6327
|
* Sync the current input state from the updated provider request.
|
|
5996
6328
|
* This is necessary because appendToolResult modifies the provider-specific request,
|
|
@@ -6143,7 +6475,7 @@ class StreamLoop {
|
|
|
6143
6475
|
}
|
|
6144
6476
|
// If we have tool calls, process them
|
|
6145
6477
|
if (toolCalls && toolCalls.length > 0 && state.toolkit) {
|
|
6146
|
-
yield* this.processToolCalls(toolCalls, state, context
|
|
6478
|
+
yield* this.processToolCalls(toolCalls, state, context);
|
|
6147
6479
|
// Check if we've reached max turns
|
|
6148
6480
|
if (state.currentTurn >= state.maxTurns) {
|
|
6149
6481
|
const error = new TooManyRequestsError();
|
|
@@ -6250,6 +6582,16 @@ class StreamLoop {
|
|
|
6250
6582
|
options,
|
|
6251
6583
|
providerRequest,
|
|
6252
6584
|
});
|
|
6585
|
+
// Open a manual llm span held open across the streamed turn. Flat (not
|
|
6586
|
+
// nested) because it spans yield boundaries; no-op when llmobs disabled.
|
|
6587
|
+
const llmSpan = openLlmObsSpan({
|
|
6588
|
+
kind: "llm",
|
|
6589
|
+
modelName: options.model ?? this.adapter.defaultModel,
|
|
6590
|
+
modelProvider: this.adapter.name,
|
|
6591
|
+
name: "jaypie.llm.model",
|
|
6592
|
+
});
|
|
6593
|
+
const inputSnapshot = [...state.currentInput];
|
|
6594
|
+
let streamedText = "";
|
|
6253
6595
|
// Collect tool calls from the stream
|
|
6254
6596
|
const collectedToolCalls = [];
|
|
6255
6597
|
// Retry loop for connection-level failures
|
|
@@ -6269,6 +6611,7 @@ class StreamLoop {
|
|
|
6269
6611
|
// Pass through text chunks
|
|
6270
6612
|
if (chunk.type === LlmStreamChunkType.Text) {
|
|
6271
6613
|
chunksYielded = true;
|
|
6614
|
+
streamedText += chunk.content ?? "";
|
|
6272
6615
|
yield chunk;
|
|
6273
6616
|
}
|
|
6274
6617
|
// Collect tool calls
|
|
@@ -6315,6 +6658,12 @@ class StreamLoop {
|
|
|
6315
6658
|
title: "Stream Error",
|
|
6316
6659
|
},
|
|
6317
6660
|
};
|
|
6661
|
+
llmSpan?.annotate({
|
|
6662
|
+
inputData: inputSnapshot,
|
|
6663
|
+
metrics: usageToLlmObsMetrics(state.usageItems),
|
|
6664
|
+
outputData: streamedText,
|
|
6665
|
+
});
|
|
6666
|
+
llmSpan?.finish();
|
|
6318
6667
|
return { shouldContinue: false };
|
|
6319
6668
|
}
|
|
6320
6669
|
// Check if we've exhausted retries or error is not retryable
|
|
@@ -6323,6 +6672,7 @@ class StreamLoop {
|
|
|
6323
6672
|
log.error(`Stream request failed after ${this.retryPolicy.maxRetries} retries`);
|
|
6324
6673
|
log.var({ error });
|
|
6325
6674
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6675
|
+
llmSpan?.finish();
|
|
6326
6676
|
throw new BadGatewayError(errorMessage);
|
|
6327
6677
|
}
|
|
6328
6678
|
const delay = this.retryPolicy.getDelayForAttempt(attempt);
|
|
@@ -6336,6 +6686,13 @@ class StreamLoop {
|
|
|
6336
6686
|
finally {
|
|
6337
6687
|
guard.remove();
|
|
6338
6688
|
}
|
|
6689
|
+
// Annotate and finish the streamed llm span (no-op when disabled)
|
|
6690
|
+
llmSpan?.annotate({
|
|
6691
|
+
inputData: inputSnapshot,
|
|
6692
|
+
metrics: usageToLlmObsMetrics(state.usageItems),
|
|
6693
|
+
outputData: streamedText,
|
|
6694
|
+
});
|
|
6695
|
+
llmSpan?.finish();
|
|
6339
6696
|
// Execute afterEachModelResponse hook
|
|
6340
6697
|
await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
|
|
6341
6698
|
content: "",
|
|
@@ -6370,7 +6727,7 @@ class StreamLoop {
|
|
|
6370
6727
|
}
|
|
6371
6728
|
return { shouldContinue: false };
|
|
6372
6729
|
}
|
|
6373
|
-
async *processToolCalls(toolCalls, state, context
|
|
6730
|
+
async *processToolCalls(toolCalls, state, context) {
|
|
6374
6731
|
const log = getLogger$6();
|
|
6375
6732
|
for (const toolCall of toolCalls) {
|
|
6376
6733
|
try {
|
|
@@ -6379,11 +6736,19 @@ class StreamLoop {
|
|
|
6379
6736
|
args: toolCall.arguments,
|
|
6380
6737
|
toolName: toolCall.name,
|
|
6381
6738
|
});
|
|
6382
|
-
// Call the tool
|
|
6739
|
+
// Call the tool inside a child tool span (no-op when disabled)
|
|
6383
6740
|
log.trace(`[stream] Calling tool - ${toolCall.name}`);
|
|
6384
|
-
const result = await
|
|
6385
|
-
|
|
6386
|
-
|
|
6741
|
+
const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
|
|
6742
|
+
const result = await state.toolkit.call({
|
|
6743
|
+
arguments: toolCall.arguments,
|
|
6744
|
+
name: toolCall.name,
|
|
6745
|
+
});
|
|
6746
|
+
annotateLlmObs({
|
|
6747
|
+
inputData: toolCall.arguments,
|
|
6748
|
+
metadata: { tool: toolCall.name },
|
|
6749
|
+
outputData: result,
|
|
6750
|
+
});
|
|
6751
|
+
return result;
|
|
6387
6752
|
});
|
|
6388
6753
|
// Execute afterEachTool hook
|
|
6389
6754
|
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|