@jaypie/llm 1.2.38 → 1.2.40
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 +322 -57
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/observability/llmobs.d.ts +72 -0
- package/dist/esm/index.js +321 -57
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/observability/llmobs.d.ts +72 -0
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Observability span kinds supported by Datadog's `llmobs` SDK.
|
|
3
|
+
* @see https://docs.datadoghq.com/llm_observability/setup/sdk/nodejs/
|
|
4
|
+
*/
|
|
5
|
+
export type LlmObsSpanKind = "agent" | "embedding" | "llm" | "retrieval" | "task" | "tool" | "workflow";
|
|
6
|
+
export interface LlmObsSpanOptions {
|
|
7
|
+
kind: LlmObsSpanKind;
|
|
8
|
+
modelName?: string;
|
|
9
|
+
modelProvider?: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
export interface LlmObsAnnotation {
|
|
13
|
+
inputData?: unknown;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
metrics?: Record<string, number>;
|
|
16
|
+
outputData?: unknown;
|
|
17
|
+
tags?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/** Manual span handle for streaming, where a callback cannot enclose the work. */
|
|
20
|
+
export interface LlmObsSpanHandle {
|
|
21
|
+
annotate: (annotation: LlmObsAnnotation) => void;
|
|
22
|
+
finish: () => void;
|
|
23
|
+
}
|
|
24
|
+
/** Minimal shape of the dd-trace `llmobs` SDK surface this module uses. */
|
|
25
|
+
interface LlmObsSdk {
|
|
26
|
+
annotate: (span: unknown, annotation: LlmObsAnnotation) => void;
|
|
27
|
+
trace: (options: LlmObsSpanOptions, fn: (span: unknown) => unknown) => unknown;
|
|
28
|
+
}
|
|
29
|
+
/** Reset the cached SDK resolution. Exposed for tests. */
|
|
30
|
+
export declare function _resetLlmObs(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Inject an SDK to bypass dd-trace resolution. Test-only: dd-trace is not a
|
|
33
|
+
* dependency, so the enabled path cannot otherwise be exercised in unit tests.
|
|
34
|
+
*/
|
|
35
|
+
export declare function _setLlmObs(sdk: LlmObsSdk | null): void;
|
|
36
|
+
/**
|
|
37
|
+
* True when LLM Observability emission is opted in via `DD_LLMOBS_ENABLED`.
|
|
38
|
+
* Accepts any truthy value except "false"/"0".
|
|
39
|
+
*/
|
|
40
|
+
export declare function isLlmObsEnabled(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Run `fn` inside an LLM Observability span. When emission is disabled or
|
|
43
|
+
* dd-trace is absent, `fn` is invoked directly with no overhead. The active
|
|
44
|
+
* span is established for the duration of `fn`, so nested `withLlmObsSpan`
|
|
45
|
+
* calls (and `annotateLlmObs`) attach as children/annotations automatically.
|
|
46
|
+
*/
|
|
47
|
+
export declare function withLlmObsSpan<T>(options: LlmObsSpanOptions, fn: () => Promise<T>): Promise<T>;
|
|
48
|
+
/**
|
|
49
|
+
* Annotate the currently active LLM Observability span. No-op when disabled.
|
|
50
|
+
*/
|
|
51
|
+
export declare function annotateLlmObs(annotation: LlmObsAnnotation): void;
|
|
52
|
+
/**
|
|
53
|
+
* Open a manual span that is finished later. Used by streaming, where work is
|
|
54
|
+
* yielded incrementally and cannot be enclosed in a single callback. The span
|
|
55
|
+
* is kept open via a deferred promise resolved by `finish()`. Returns null when
|
|
56
|
+
* emission is disabled or dd-trace is absent.
|
|
57
|
+
*
|
|
58
|
+
* Note: because the span is held open across `yield` boundaries (outside the
|
|
59
|
+
* SDK's AsyncLocalStorage scope), manual spans are emitted flat rather than
|
|
60
|
+
* nested. Annotations are applied to the explicit span reference.
|
|
61
|
+
*/
|
|
62
|
+
export declare function openLlmObsSpan(options: LlmObsSpanOptions): LlmObsSpanHandle | null;
|
|
63
|
+
/**
|
|
64
|
+
* Sum an `LlmUsage` array into LLM Observability metric keys. Returns undefined
|
|
65
|
+
* when there is no usage data so callers can omit the `metrics` field.
|
|
66
|
+
*/
|
|
67
|
+
export declare function usageToLlmObsMetrics(usage?: Array<{
|
|
68
|
+
input?: number;
|
|
69
|
+
output?: number;
|
|
70
|
+
total?: number;
|
|
71
|
+
}>): Record<string, number> | undefined;
|
|
72
|
+
export {};
|
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';
|
|
@@ -54,8 +56,8 @@ const MODEL = {
|
|
|
54
56
|
GEMINI_PRO: "gemini-3.1-pro-preview",
|
|
55
57
|
// OpenAI
|
|
56
58
|
GPT: "gpt-5.5",
|
|
57
|
-
GPT_MINI: "gpt-5.
|
|
58
|
-
GPT_NANO: "gpt-5.
|
|
59
|
+
GPT_MINI: "gpt-5.4-mini",
|
|
60
|
+
GPT_NANO: "gpt-5.4-nano",
|
|
59
61
|
// xAI
|
|
60
62
|
GROK: "grok-latest",
|
|
61
63
|
};
|
|
@@ -2068,8 +2070,7 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2068
2070
|
if (request.system) {
|
|
2069
2071
|
bedrockRequest.system = [{ text: request.system }];
|
|
2070
2072
|
}
|
|
2071
|
-
if (request.temperature !== undefined &&
|
|
2072
|
-
this.supportsTemperature(model)) {
|
|
2073
|
+
if (request.temperature !== undefined && this.supportsTemperature(model)) {
|
|
2073
2074
|
bedrockRequest.inferenceConfig = {
|
|
2074
2075
|
...bedrockRequest.inferenceConfig,
|
|
2075
2076
|
temperature: request.temperature,
|
|
@@ -2210,10 +2211,7 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2210
2211
|
return {
|
|
2211
2212
|
...rest,
|
|
2212
2213
|
toolConfig: {
|
|
2213
|
-
tools: [
|
|
2214
|
-
...(rest.toolConfig?.tools ?? []),
|
|
2215
|
-
fakeTool,
|
|
2216
|
-
],
|
|
2214
|
+
tools: [...(rest.toolConfig?.tools ?? []), fakeTool],
|
|
2217
2215
|
},
|
|
2218
2216
|
};
|
|
2219
2217
|
}
|
|
@@ -2437,7 +2435,8 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2437
2435
|
return this.extractStructuredOutput(response) !== undefined;
|
|
2438
2436
|
}
|
|
2439
2437
|
// Fake-tool path: last content block is a structured_output toolUse
|
|
2440
|
-
const content = (bedrockResponse.output?.message?.content ??
|
|
2438
|
+
const content = (bedrockResponse.output?.message?.content ??
|
|
2439
|
+
[]);
|
|
2441
2440
|
const last = content[content.length - 1];
|
|
2442
2441
|
return (!!last &&
|
|
2443
2442
|
"toolUse" in last &&
|
|
@@ -2446,14 +2445,16 @@ class BedrockAdapter extends BaseProviderAdapter {
|
|
|
2446
2445
|
extractStructuredOutput(response) {
|
|
2447
2446
|
const bedrockResponse = response;
|
|
2448
2447
|
if (bedrockResponse.__jaypieStructuredOutput) {
|
|
2449
|
-
const content = (bedrockResponse.output?.message?.content ??
|
|
2448
|
+
const content = (bedrockResponse.output?.message?.content ??
|
|
2449
|
+
[]);
|
|
2450
2450
|
const textBlock = content.find((b) => "text" in b);
|
|
2451
2451
|
if (!textBlock)
|
|
2452
2452
|
return undefined;
|
|
2453
2453
|
return extractJson(textBlock.text);
|
|
2454
2454
|
}
|
|
2455
2455
|
// Fake-tool path
|
|
2456
|
-
const content = (bedrockResponse.output?.message?.content ??
|
|
2456
|
+
const content = (bedrockResponse.output?.message?.content ??
|
|
2457
|
+
[]);
|
|
2457
2458
|
const last = content[content.length - 1];
|
|
2458
2459
|
if (last &&
|
|
2459
2460
|
"toolUse" in last &&
|
|
@@ -4836,6 +4837,198 @@ class Toolkit {
|
|
|
4836
4837
|
}
|
|
4837
4838
|
}
|
|
4838
4839
|
|
|
4840
|
+
//
|
|
4841
|
+
//
|
|
4842
|
+
// Constants
|
|
4843
|
+
//
|
|
4844
|
+
const ENV = {
|
|
4845
|
+
DD_LLMOBS_ENABLED: "DD_LLMOBS_ENABLED",
|
|
4846
|
+
};
|
|
4847
|
+
const MODULE = {
|
|
4848
|
+
// Computed at runtime so bundlers (esbuild) do not attempt to include
|
|
4849
|
+
// dd-trace, which is provided by the Datadog Lambda layer at runtime.
|
|
4850
|
+
DD_TRACE: ["dd", "trace"].join("-"),
|
|
4851
|
+
};
|
|
4852
|
+
//
|
|
4853
|
+
//
|
|
4854
|
+
// Helpers
|
|
4855
|
+
//
|
|
4856
|
+
// CJS/ESM compatible require - handles bundling to CJS where import.meta.url
|
|
4857
|
+
// becomes undefined (mirrors packages/testkit/src/mock/original.ts).
|
|
4858
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4859
|
+
// @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
|
|
4860
|
+
const requireModule = typeof __filename !== "undefined"
|
|
4861
|
+
? createRequire(pathToFileURL(__filename).href)
|
|
4862
|
+
: createRequire(import.meta.url);
|
|
4863
|
+
let resolved = false;
|
|
4864
|
+
let cachedSdk$4 = null;
|
|
4865
|
+
/**
|
|
4866
|
+
* Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
|
|
4867
|
+
* when dd-trace is absent or the SDK surface is unexpected. Cached after the
|
|
4868
|
+
* first attempt.
|
|
4869
|
+
*/
|
|
4870
|
+
function resolveLlmObs() {
|
|
4871
|
+
if (resolved) {
|
|
4872
|
+
return cachedSdk$4;
|
|
4873
|
+
}
|
|
4874
|
+
resolved = true;
|
|
4875
|
+
try {
|
|
4876
|
+
const ddTrace = requireModule(MODULE.DD_TRACE);
|
|
4877
|
+
const tracer = ddTrace?.default ?? ddTrace;
|
|
4878
|
+
const llmobs = tracer?.llmobs;
|
|
4879
|
+
if (llmobs &&
|
|
4880
|
+
typeof llmobs.trace === "function" &&
|
|
4881
|
+
typeof llmobs.annotate === "function") {
|
|
4882
|
+
cachedSdk$4 = llmobs;
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
catch {
|
|
4886
|
+
cachedSdk$4 = null;
|
|
4887
|
+
}
|
|
4888
|
+
return cachedSdk$4;
|
|
4889
|
+
}
|
|
4890
|
+
//
|
|
4891
|
+
//
|
|
4892
|
+
// Main
|
|
4893
|
+
//
|
|
4894
|
+
/**
|
|
4895
|
+
* True when LLM Observability emission is opted in via `DD_LLMOBS_ENABLED`.
|
|
4896
|
+
* Accepts any truthy value except "false"/"0".
|
|
4897
|
+
*/
|
|
4898
|
+
function isLlmObsEnabled() {
|
|
4899
|
+
const value = process.env[ENV.DD_LLMOBS_ENABLED];
|
|
4900
|
+
if (!value) {
|
|
4901
|
+
return false;
|
|
4902
|
+
}
|
|
4903
|
+
const normalized = value.trim().toLowerCase();
|
|
4904
|
+
return normalized !== "" && normalized !== "false" && normalized !== "0";
|
|
4905
|
+
}
|
|
4906
|
+
/** Resolve the SDK only when enabled and available. */
|
|
4907
|
+
function activeSdk() {
|
|
4908
|
+
if (!isLlmObsEnabled()) {
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
return resolveLlmObs();
|
|
4912
|
+
}
|
|
4913
|
+
/**
|
|
4914
|
+
* Run `fn` inside an LLM Observability span. When emission is disabled or
|
|
4915
|
+
* dd-trace is absent, `fn` is invoked directly with no overhead. The active
|
|
4916
|
+
* span is established for the duration of `fn`, so nested `withLlmObsSpan`
|
|
4917
|
+
* calls (and `annotateLlmObs`) attach as children/annotations automatically.
|
|
4918
|
+
*/
|
|
4919
|
+
async function withLlmObsSpan(options, fn) {
|
|
4920
|
+
const sdk = activeSdk();
|
|
4921
|
+
if (!sdk) {
|
|
4922
|
+
return fn();
|
|
4923
|
+
}
|
|
4924
|
+
let started = false;
|
|
4925
|
+
try {
|
|
4926
|
+
return (await sdk.trace(options, () => {
|
|
4927
|
+
started = true;
|
|
4928
|
+
return fn();
|
|
4929
|
+
}));
|
|
4930
|
+
}
|
|
4931
|
+
catch (error) {
|
|
4932
|
+
// If `fn` ran, propagate its error (the SDK re-throws after finishing the
|
|
4933
|
+
// span). Only swallow failures from the instrumentation itself, re-running
|
|
4934
|
+
// `fn` so observability never breaks the underlying call.
|
|
4935
|
+
if (started) {
|
|
4936
|
+
throw error;
|
|
4937
|
+
}
|
|
4938
|
+
getLogger$6().warn("[llmobs] span emission failed");
|
|
4939
|
+
getLogger$6().var({ error });
|
|
4940
|
+
return fn();
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
/**
|
|
4944
|
+
* Annotate the currently active LLM Observability span. No-op when disabled.
|
|
4945
|
+
*/
|
|
4946
|
+
function annotateLlmObs(annotation) {
|
|
4947
|
+
const sdk = activeSdk();
|
|
4948
|
+
if (!sdk) {
|
|
4949
|
+
return;
|
|
4950
|
+
}
|
|
4951
|
+
try {
|
|
4952
|
+
sdk.annotate(undefined, annotation);
|
|
4953
|
+
}
|
|
4954
|
+
catch (error) {
|
|
4955
|
+
getLogger$6().warn("[llmobs] annotate failed");
|
|
4956
|
+
getLogger$6().var({ error });
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
/**
|
|
4960
|
+
* Open a manual span that is finished later. Used by streaming, where work is
|
|
4961
|
+
* yielded incrementally and cannot be enclosed in a single callback. The span
|
|
4962
|
+
* is kept open via a deferred promise resolved by `finish()`. Returns null when
|
|
4963
|
+
* emission is disabled or dd-trace is absent.
|
|
4964
|
+
*
|
|
4965
|
+
* Note: because the span is held open across `yield` boundaries (outside the
|
|
4966
|
+
* SDK's AsyncLocalStorage scope), manual spans are emitted flat rather than
|
|
4967
|
+
* nested. Annotations are applied to the explicit span reference.
|
|
4968
|
+
*/
|
|
4969
|
+
function openLlmObsSpan(options) {
|
|
4970
|
+
const sdk = activeSdk();
|
|
4971
|
+
if (!sdk) {
|
|
4972
|
+
return null;
|
|
4973
|
+
}
|
|
4974
|
+
try {
|
|
4975
|
+
let capturedSpan;
|
|
4976
|
+
let release;
|
|
4977
|
+
const held = new Promise((resolve) => {
|
|
4978
|
+
release = resolve;
|
|
4979
|
+
});
|
|
4980
|
+
// trace() runs the callback synchronously, capturing the span; the returned
|
|
4981
|
+
// pending promise keeps the span open until finish() resolves it.
|
|
4982
|
+
void sdk.trace(options, (span) => {
|
|
4983
|
+
capturedSpan = span;
|
|
4984
|
+
return held;
|
|
4985
|
+
});
|
|
4986
|
+
let finished = false;
|
|
4987
|
+
return {
|
|
4988
|
+
annotate: (annotation) => {
|
|
4989
|
+
try {
|
|
4990
|
+
sdk.annotate(capturedSpan, annotation);
|
|
4991
|
+
}
|
|
4992
|
+
catch (error) {
|
|
4993
|
+
getLogger$6().warn("[llmobs] annotate failed");
|
|
4994
|
+
getLogger$6().var({ error });
|
|
4995
|
+
}
|
|
4996
|
+
},
|
|
4997
|
+
finish: () => {
|
|
4998
|
+
if (finished) {
|
|
4999
|
+
return;
|
|
5000
|
+
}
|
|
5001
|
+
finished = true;
|
|
5002
|
+
release?.();
|
|
5003
|
+
},
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
catch (error) {
|
|
5007
|
+
getLogger$6().warn("[llmobs] span emission failed");
|
|
5008
|
+
getLogger$6().var({ error });
|
|
5009
|
+
return null;
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
//
|
|
5013
|
+
//
|
|
5014
|
+
// Mapping Helpers
|
|
5015
|
+
//
|
|
5016
|
+
/**
|
|
5017
|
+
* Sum an `LlmUsage` array into LLM Observability metric keys. Returns undefined
|
|
5018
|
+
* when there is no usage data so callers can omit the `metrics` field.
|
|
5019
|
+
*/
|
|
5020
|
+
function usageToLlmObsMetrics(usage) {
|
|
5021
|
+
if (!usage || usage.length === 0) {
|
|
5022
|
+
return undefined;
|
|
5023
|
+
}
|
|
5024
|
+
const metrics = usage.reduce((sum, item) => ({
|
|
5025
|
+
input_tokens: sum.input_tokens + (item.input ?? 0),
|
|
5026
|
+
output_tokens: sum.output_tokens + (item.output ?? 0),
|
|
5027
|
+
total_tokens: sum.total_tokens + (item.total ?? 0),
|
|
5028
|
+
}), { input_tokens: 0, output_tokens: 0, total_tokens: 0 });
|
|
5029
|
+
return metrics;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
4839
5032
|
//
|
|
4840
5033
|
//
|
|
4841
5034
|
// Main
|
|
@@ -5707,30 +5900,46 @@ class OperateLoop {
|
|
|
5707
5900
|
// Initialize state
|
|
5708
5901
|
const state = await this.initializeState(input, options);
|
|
5709
5902
|
const context = this.createContext(options);
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
//
|
|
5713
|
-
|
|
5714
|
-
state.
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5903
|
+
const modelName = options.model ?? this.adapter.defaultModel;
|
|
5904
|
+
// Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
|
|
5905
|
+
// Child llm/tool spans nest under it via the SDK's active-span context.
|
|
5906
|
+
return withLlmObsSpan({
|
|
5907
|
+
kind: state.toolkit ? "agent" : "llm",
|
|
5908
|
+
modelName,
|
|
5909
|
+
modelProvider: this.adapter.name,
|
|
5910
|
+
name: "jaypie.llm.operate",
|
|
5911
|
+
}, async () => {
|
|
5912
|
+
// Build initial request
|
|
5913
|
+
let request = this.buildInitialRequest(state, options);
|
|
5914
|
+
// Multi-turn loop
|
|
5915
|
+
while (state.currentTurn < state.maxTurns) {
|
|
5916
|
+
state.currentTurn++;
|
|
5917
|
+
// Execute one turn with retry logic
|
|
5918
|
+
const shouldContinue = await this.executeOneTurn(request, state, context, options);
|
|
5919
|
+
if (!shouldContinue) {
|
|
5920
|
+
break;
|
|
5921
|
+
}
|
|
5922
|
+
// Rebuild request with updated history for next turn
|
|
5923
|
+
request = {
|
|
5924
|
+
format: state.formattedFormat,
|
|
5925
|
+
instructions: options.instructions,
|
|
5926
|
+
messages: state.currentInput,
|
|
5927
|
+
model: modelName,
|
|
5928
|
+
providerOptions: options.providerOptions,
|
|
5929
|
+
system: options.system,
|
|
5930
|
+
temperature: options.temperature,
|
|
5931
|
+
tools: state.formattedTools,
|
|
5932
|
+
user: options.user,
|
|
5933
|
+
};
|
|
5719
5934
|
}
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
temperature: options.temperature,
|
|
5729
|
-
tools: state.formattedTools,
|
|
5730
|
-
user: options.user,
|
|
5731
|
-
};
|
|
5732
|
-
}
|
|
5733
|
-
return state.responseBuilder.build();
|
|
5935
|
+
const response = state.responseBuilder.build();
|
|
5936
|
+
annotateLlmObs({
|
|
5937
|
+
inputData: input,
|
|
5938
|
+
metrics: usageToLlmObsMetrics(response.usage),
|
|
5939
|
+
outputData: response.content,
|
|
5940
|
+
});
|
|
5941
|
+
return response;
|
|
5942
|
+
});
|
|
5734
5943
|
}
|
|
5735
5944
|
//
|
|
5736
5945
|
// Private Methods
|
|
@@ -5832,20 +6041,34 @@ class OperateLoop {
|
|
|
5832
6041
|
options,
|
|
5833
6042
|
providerRequest,
|
|
5834
6043
|
});
|
|
5835
|
-
// Execute with retry
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
6044
|
+
// Execute with retry inside a child llm span (no-op when llmobs disabled).
|
|
6045
|
+
// RetryExecutor handles error hooks and throws appropriate errors.
|
|
6046
|
+
const { parsed, response } = await withLlmObsSpan({
|
|
6047
|
+
kind: "llm",
|
|
6048
|
+
modelName: options.model ?? this.adapter.defaultModel,
|
|
6049
|
+
modelProvider: this.adapter.name,
|
|
6050
|
+
name: "jaypie.llm.model",
|
|
6051
|
+
}, async () => {
|
|
6052
|
+
const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
|
|
6053
|
+
context: {
|
|
6054
|
+
input: state.currentInput,
|
|
6055
|
+
options,
|
|
6056
|
+
providerRequest,
|
|
6057
|
+
},
|
|
6058
|
+
hooks: context.hooks,
|
|
6059
|
+
});
|
|
6060
|
+
// Log what was returned from the model
|
|
6061
|
+
log.trace("[operate] Model response received");
|
|
6062
|
+
log.var({ "operate.response": response });
|
|
6063
|
+
// Parse response
|
|
6064
|
+
const parsed = this.adapter.parseResponse(response, options);
|
|
6065
|
+
annotateLlmObs({
|
|
6066
|
+
inputData: state.currentInput,
|
|
6067
|
+
metrics: usageToLlmObsMetrics(parsed.usage ? [parsed.usage] : undefined),
|
|
6068
|
+
outputData: parsed.content ?? "",
|
|
6069
|
+
});
|
|
6070
|
+
return { parsed, response };
|
|
5843
6071
|
});
|
|
5844
|
-
// Log what was returned from the model
|
|
5845
|
-
log.trace("[operate] Model response received");
|
|
5846
|
-
log.var({ "operate.response": response });
|
|
5847
|
-
// Parse response
|
|
5848
|
-
const parsed = this.adapter.parseResponse(response, options);
|
|
5849
6072
|
// Track usage
|
|
5850
6073
|
if (parsed.usage) {
|
|
5851
6074
|
state.responseBuilder.addUsage(parsed.usage);
|
|
@@ -5890,11 +6113,19 @@ class OperateLoop {
|
|
|
5890
6113
|
args: toolCall.arguments,
|
|
5891
6114
|
toolName: toolCall.name,
|
|
5892
6115
|
});
|
|
5893
|
-
// Call the tool
|
|
6116
|
+
// Call the tool inside a child tool span (no-op when disabled)
|
|
5894
6117
|
log.trace(`[operate] Calling tool - ${toolCall.name}`);
|
|
5895
|
-
const result = await
|
|
5896
|
-
|
|
5897
|
-
|
|
6118
|
+
const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
|
|
6119
|
+
const result = await state.toolkit.call({
|
|
6120
|
+
arguments: toolCall.arguments,
|
|
6121
|
+
name: toolCall.name,
|
|
6122
|
+
});
|
|
6123
|
+
annotateLlmObs({
|
|
6124
|
+
inputData: toolCall.arguments,
|
|
6125
|
+
metadata: { tool: toolCall.name },
|
|
6126
|
+
outputData: result,
|
|
6127
|
+
});
|
|
6128
|
+
return result;
|
|
5898
6129
|
});
|
|
5899
6130
|
// Execute afterEachTool hook
|
|
5900
6131
|
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|
|
@@ -6144,7 +6375,7 @@ class StreamLoop {
|
|
|
6144
6375
|
}
|
|
6145
6376
|
// If we have tool calls, process them
|
|
6146
6377
|
if (toolCalls && toolCalls.length > 0 && state.toolkit) {
|
|
6147
|
-
yield* this.processToolCalls(toolCalls, state, context
|
|
6378
|
+
yield* this.processToolCalls(toolCalls, state, context);
|
|
6148
6379
|
// Check if we've reached max turns
|
|
6149
6380
|
if (state.currentTurn >= state.maxTurns) {
|
|
6150
6381
|
const error = new TooManyRequestsError();
|
|
@@ -6251,6 +6482,16 @@ class StreamLoop {
|
|
|
6251
6482
|
options,
|
|
6252
6483
|
providerRequest,
|
|
6253
6484
|
});
|
|
6485
|
+
// Open a manual llm span held open across the streamed turn. Flat (not
|
|
6486
|
+
// nested) because it spans yield boundaries; no-op when llmobs disabled.
|
|
6487
|
+
const llmSpan = openLlmObsSpan({
|
|
6488
|
+
kind: "llm",
|
|
6489
|
+
modelName: options.model ?? this.adapter.defaultModel,
|
|
6490
|
+
modelProvider: this.adapter.name,
|
|
6491
|
+
name: "jaypie.llm.model",
|
|
6492
|
+
});
|
|
6493
|
+
const inputSnapshot = [...state.currentInput];
|
|
6494
|
+
let streamedText = "";
|
|
6254
6495
|
// Collect tool calls from the stream
|
|
6255
6496
|
const collectedToolCalls = [];
|
|
6256
6497
|
// Retry loop for connection-level failures
|
|
@@ -6270,6 +6511,7 @@ class StreamLoop {
|
|
|
6270
6511
|
// Pass through text chunks
|
|
6271
6512
|
if (chunk.type === LlmStreamChunkType.Text) {
|
|
6272
6513
|
chunksYielded = true;
|
|
6514
|
+
streamedText += chunk.content ?? "";
|
|
6273
6515
|
yield chunk;
|
|
6274
6516
|
}
|
|
6275
6517
|
// Collect tool calls
|
|
@@ -6316,6 +6558,12 @@ class StreamLoop {
|
|
|
6316
6558
|
title: "Stream Error",
|
|
6317
6559
|
},
|
|
6318
6560
|
};
|
|
6561
|
+
llmSpan?.annotate({
|
|
6562
|
+
inputData: inputSnapshot,
|
|
6563
|
+
metrics: usageToLlmObsMetrics(state.usageItems),
|
|
6564
|
+
outputData: streamedText,
|
|
6565
|
+
});
|
|
6566
|
+
llmSpan?.finish();
|
|
6319
6567
|
return { shouldContinue: false };
|
|
6320
6568
|
}
|
|
6321
6569
|
// Check if we've exhausted retries or error is not retryable
|
|
@@ -6324,6 +6572,7 @@ class StreamLoop {
|
|
|
6324
6572
|
log.error(`Stream request failed after ${this.retryPolicy.maxRetries} retries`);
|
|
6325
6573
|
log.var({ error });
|
|
6326
6574
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6575
|
+
llmSpan?.finish();
|
|
6327
6576
|
throw new BadGatewayError(errorMessage);
|
|
6328
6577
|
}
|
|
6329
6578
|
const delay = this.retryPolicy.getDelayForAttempt(attempt);
|
|
@@ -6337,6 +6586,13 @@ class StreamLoop {
|
|
|
6337
6586
|
finally {
|
|
6338
6587
|
guard.remove();
|
|
6339
6588
|
}
|
|
6589
|
+
// Annotate and finish the streamed llm span (no-op when disabled)
|
|
6590
|
+
llmSpan?.annotate({
|
|
6591
|
+
inputData: inputSnapshot,
|
|
6592
|
+
metrics: usageToLlmObsMetrics(state.usageItems),
|
|
6593
|
+
outputData: streamedText,
|
|
6594
|
+
});
|
|
6595
|
+
llmSpan?.finish();
|
|
6340
6596
|
// Execute afterEachModelResponse hook
|
|
6341
6597
|
await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
|
|
6342
6598
|
content: "",
|
|
@@ -6371,7 +6627,7 @@ class StreamLoop {
|
|
|
6371
6627
|
}
|
|
6372
6628
|
return { shouldContinue: false };
|
|
6373
6629
|
}
|
|
6374
|
-
async *processToolCalls(toolCalls, state, context
|
|
6630
|
+
async *processToolCalls(toolCalls, state, context) {
|
|
6375
6631
|
const log = getLogger$6();
|
|
6376
6632
|
for (const toolCall of toolCalls) {
|
|
6377
6633
|
try {
|
|
@@ -6380,11 +6636,19 @@ class StreamLoop {
|
|
|
6380
6636
|
args: toolCall.arguments,
|
|
6381
6637
|
toolName: toolCall.name,
|
|
6382
6638
|
});
|
|
6383
|
-
// Call the tool
|
|
6639
|
+
// Call the tool inside a child tool span (no-op when disabled)
|
|
6384
6640
|
log.trace(`[stream] Calling tool - ${toolCall.name}`);
|
|
6385
|
-
const result = await
|
|
6386
|
-
|
|
6387
|
-
|
|
6641
|
+
const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
|
|
6642
|
+
const result = await state.toolkit.call({
|
|
6643
|
+
arguments: toolCall.arguments,
|
|
6644
|
+
name: toolCall.name,
|
|
6645
|
+
});
|
|
6646
|
+
annotateLlmObs({
|
|
6647
|
+
inputData: toolCall.arguments,
|
|
6648
|
+
metadata: { tool: toolCall.name },
|
|
6649
|
+
outputData: result,
|
|
6650
|
+
});
|
|
6651
|
+
return result;
|
|
6388
6652
|
});
|
|
6389
6653
|
// Execute afterEachTool hook
|
|
6390
6654
|
await this.hookRunnerInstance.runAfterTool(context.hooks, {
|