@ljoukov/llm 4.1.1 → 5.0.2

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/index.cjs CHANGED
@@ -53,6 +53,7 @@ __export(index_exports, {
53
53
  applyPatch: () => applyPatch,
54
54
  configureGemini: () => configureGemini,
55
55
  configureModelConcurrency: () => configureModelConcurrency,
56
+ configureTelemetry: () => configureTelemetry,
56
57
  convertGooglePartsToLlmParts: () => convertGooglePartsToLlmParts,
57
58
  createApplyPatchTool: () => createApplyPatchTool,
58
59
  createCodexApplyPatchTool: () => createCodexApplyPatchTool,
@@ -101,6 +102,7 @@ __export(index_exports, {
101
102
  parseJsonFromLlmText: () => parseJsonFromLlmText,
102
103
  refreshChatGptOauthToken: () => refreshChatGptOauthToken,
103
104
  resetModelConcurrencyConfig: () => resetModelConcurrencyConfig,
105
+ resetTelemetry: () => resetTelemetry,
104
106
  resolveFilesystemToolProfile: () => resolveFilesystemToolProfile,
105
107
  resolveFireworksModelId: () => resolveFireworksModelId,
106
108
  runAgentLoop: () => runAgentLoop,
@@ -4165,6 +4167,71 @@ var files = {
4165
4167
  content: filesContent
4166
4168
  };
4167
4169
 
4170
+ // src/telemetry.ts
4171
+ var telemetryState = getRuntimeSingleton(
4172
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.telemetryState"),
4173
+ () => ({
4174
+ configuredTelemetry: void 0
4175
+ })
4176
+ );
4177
+ function configureTelemetry(telemetry = void 0) {
4178
+ telemetryState.configuredTelemetry = telemetry === void 0 || telemetry === false ? void 0 : telemetry;
4179
+ }
4180
+ function resetTelemetry() {
4181
+ telemetryState.configuredTelemetry = void 0;
4182
+ }
4183
+ function isPromiseLike2(value) {
4184
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
4185
+ }
4186
+ function resolveTelemetrySelection(telemetry) {
4187
+ if (telemetry === false) {
4188
+ return void 0;
4189
+ }
4190
+ if (telemetry !== void 0) {
4191
+ return telemetry;
4192
+ }
4193
+ return telemetryState.configuredTelemetry;
4194
+ }
4195
+ function createTelemetrySession(telemetry) {
4196
+ const config = resolveTelemetrySelection(telemetry);
4197
+ if (!config) {
4198
+ return void 0;
4199
+ }
4200
+ const pending = /* @__PURE__ */ new Set();
4201
+ const trackPromise = (promise) => {
4202
+ pending.add(promise);
4203
+ promise.finally(() => {
4204
+ pending.delete(promise);
4205
+ });
4206
+ };
4207
+ const emit = (event) => {
4208
+ try {
4209
+ const output = config.sink.emit(event);
4210
+ if (isPromiseLike2(output)) {
4211
+ const task = Promise.resolve(output).then(() => void 0).catch(() => void 0);
4212
+ trackPromise(task);
4213
+ }
4214
+ } catch {
4215
+ }
4216
+ };
4217
+ const flush = async () => {
4218
+ while (pending.size > 0) {
4219
+ await Promise.allSettled([...pending]);
4220
+ }
4221
+ if (typeof config.sink.flush === "function") {
4222
+ try {
4223
+ await config.sink.flush();
4224
+ } catch {
4225
+ }
4226
+ }
4227
+ };
4228
+ return {
4229
+ includeStreamEvents: config.includeStreamEvents === true,
4230
+ emit,
4231
+ flush
4232
+ };
4233
+ }
4234
+
4168
4235
  // src/llm.ts
4169
4236
  var toolCallContextStorage = getRuntimeSingleton(
4170
4237
  /* @__PURE__ */ Symbol.for("@ljoukov/llm.toolCallContextStorage"),
@@ -5751,6 +5818,65 @@ function mergeTokenUpdates(current, next) {
5751
5818
  toolUsePromptTokens: next.toolUsePromptTokens ?? current.toolUsePromptTokens
5752
5819
  };
5753
5820
  }
5821
+ function sumUsageValue(current, next) {
5822
+ if (typeof next !== "number" || !Number.isFinite(next)) {
5823
+ return current;
5824
+ }
5825
+ const normalizedNext = Math.max(0, next);
5826
+ if (typeof current !== "number" || !Number.isFinite(current)) {
5827
+ return normalizedNext;
5828
+ }
5829
+ return Math.max(0, current) + normalizedNext;
5830
+ }
5831
+ function sumUsageTokens(current, next) {
5832
+ if (!next) {
5833
+ return current;
5834
+ }
5835
+ return {
5836
+ promptTokens: sumUsageValue(current?.promptTokens, next.promptTokens),
5837
+ cachedTokens: sumUsageValue(current?.cachedTokens, next.cachedTokens),
5838
+ responseTokens: sumUsageValue(current?.responseTokens, next.responseTokens),
5839
+ responseImageTokens: sumUsageValue(current?.responseImageTokens, next.responseImageTokens),
5840
+ thinkingTokens: sumUsageValue(current?.thinkingTokens, next.thinkingTokens),
5841
+ totalTokens: sumUsageValue(current?.totalTokens, next.totalTokens),
5842
+ toolUsePromptTokens: sumUsageValue(current?.toolUsePromptTokens, next.toolUsePromptTokens)
5843
+ };
5844
+ }
5845
+ function countInlineImagesInContent(content) {
5846
+ if (!content) {
5847
+ return 0;
5848
+ }
5849
+ let count = 0;
5850
+ for (const part of content.parts) {
5851
+ if (part.type === "inlineData" && isInlineImageMime(part.mimeType)) {
5852
+ count += 1;
5853
+ }
5854
+ }
5855
+ return count;
5856
+ }
5857
+ function createLlmTelemetryEmitter(params) {
5858
+ const session = createTelemetrySession(params.telemetry);
5859
+ const callId = (0, import_node_crypto2.randomBytes)(8).toString("hex");
5860
+ return {
5861
+ includeStreamEvents: session?.includeStreamEvents === true,
5862
+ emit: (event) => {
5863
+ if (!session) {
5864
+ return;
5865
+ }
5866
+ session.emit({
5867
+ ...event,
5868
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5869
+ callId,
5870
+ operation: params.operation,
5871
+ provider: params.provider,
5872
+ model: params.model
5873
+ });
5874
+ },
5875
+ flush: async () => {
5876
+ await session?.flush();
5877
+ }
5878
+ };
5879
+ }
5754
5880
  function toMaybeNumber(value) {
5755
5881
  if (typeof value === "number" && Number.isFinite(value)) {
5756
5882
  return value;
@@ -5971,6 +6097,23 @@ function toOpenAiToolOutput(value) {
5971
6097
  }
5972
6098
  return mergeToolOutput(value);
5973
6099
  }
6100
+ function toChatGptToolOutput(value) {
6101
+ const toolOutput = toOpenAiToolOutput(value);
6102
+ if (typeof toolOutput === "string") {
6103
+ return toolOutput;
6104
+ }
6105
+ return toolOutput.map((item) => {
6106
+ if (item.type !== "input_image") {
6107
+ return item;
6108
+ }
6109
+ return {
6110
+ type: "input_image",
6111
+ ...item.file_id ? { file_id: item.file_id } : {},
6112
+ ...item.image_url ? { image_url: item.image_url } : {},
6113
+ ...item.detail ? { detail: item.detail } : {}
6114
+ };
6115
+ });
6116
+ }
5974
6117
  function toGeminiToolOutputItems(value) {
5975
6118
  if (isLlmToolOutputContentItem(value)) {
5976
6119
  return [value];
@@ -7160,6 +7303,10 @@ async function runTextCall(params) {
7160
7303
  let responseRole;
7161
7304
  let latestUsage;
7162
7305
  let responseImages = 0;
7306
+ const pushEvent = (event) => {
7307
+ queue.push(event);
7308
+ params.onEvent?.(event);
7309
+ };
7163
7310
  const pushDelta = (channel, text) => {
7164
7311
  if (!text) {
7165
7312
  return;
@@ -7170,7 +7317,7 @@ async function runTextCall(params) {
7170
7317
  } else {
7171
7318
  callLogger?.appendResponseDelta(text);
7172
7319
  }
7173
- queue.push({ type: "delta", channel, text });
7320
+ pushEvent({ type: "delta", channel, text });
7174
7321
  };
7175
7322
  const pushInline = (data, mimeType) => {
7176
7323
  if (!data) {
@@ -7240,7 +7387,7 @@ async function runTextCall(params) {
7240
7387
  }
7241
7388
  case "response.refusal.delta": {
7242
7389
  blocked = true;
7243
- queue.push({ type: "blocked" });
7390
+ pushEvent({ type: "blocked" });
7244
7391
  break;
7245
7392
  }
7246
7393
  default:
@@ -7249,7 +7396,7 @@ async function runTextCall(params) {
7249
7396
  }
7250
7397
  const finalResponse = await stream.finalResponse();
7251
7398
  modelVersion = typeof finalResponse.model === "string" ? finalResponse.model : request.model;
7252
- queue.push({ type: "model", modelVersion });
7399
+ pushEvent({ type: "model", modelVersion });
7253
7400
  if (finalResponse.error) {
7254
7401
  const message = typeof finalResponse.error.message === "string" ? finalResponse.error.message : "OpenAI response failed";
7255
7402
  throw new Error(message);
@@ -7313,11 +7460,11 @@ async function runTextCall(params) {
7313
7460
  });
7314
7461
  blocked = blocked || result2.blocked;
7315
7462
  if (blocked) {
7316
- queue.push({ type: "blocked" });
7463
+ pushEvent({ type: "blocked" });
7317
7464
  }
7318
7465
  if (result2.model) {
7319
7466
  modelVersion = providerInfo.serviceTier ? request.model : `chatgpt-${result2.model}`;
7320
- queue.push({ type: "model", modelVersion });
7467
+ pushEvent({ type: "model", modelVersion });
7321
7468
  }
7322
7469
  latestUsage = extractChatGptUsageTokens(result2.usage);
7323
7470
  const fallbackText = typeof result2.text === "string" ? result2.text : "";
@@ -7355,11 +7502,11 @@ async function runTextCall(params) {
7355
7502
  { signal }
7356
7503
  );
7357
7504
  modelVersion = typeof response.model === "string" ? response.model : request.model;
7358
- queue.push({ type: "model", modelVersion });
7505
+ pushEvent({ type: "model", modelVersion });
7359
7506
  const choice = Array.isArray(response.choices) ? response.choices[0] : void 0;
7360
7507
  if (choice?.finish_reason === "content_filter") {
7361
7508
  blocked = true;
7362
- queue.push({ type: "blocked" });
7509
+ pushEvent({ type: "blocked" });
7363
7510
  }
7364
7511
  const textOutput = extractFireworksMessageText(
7365
7512
  choice?.message
@@ -7401,11 +7548,11 @@ async function runTextCall(params) {
7401
7548
  for await (const chunk of stream) {
7402
7549
  if (chunk.modelVersion) {
7403
7550
  modelVersion = chunk.modelVersion;
7404
- queue.push({ type: "model", modelVersion });
7551
+ pushEvent({ type: "model", modelVersion });
7405
7552
  }
7406
7553
  if (chunk.promptFeedback?.blockReason) {
7407
7554
  blocked = true;
7408
- queue.push({ type: "blocked" });
7555
+ pushEvent({ type: "blocked" });
7409
7556
  }
7410
7557
  latestUsage = mergeTokenUpdates(
7411
7558
  latestUsage,
@@ -7418,7 +7565,7 @@ async function runTextCall(params) {
7418
7565
  const primary = candidates[0];
7419
7566
  if (primary && isModerationFinish(primary.finishReason)) {
7420
7567
  blocked = true;
7421
- queue.push({ type: "blocked" });
7568
+ pushEvent({ type: "blocked" });
7422
7569
  }
7423
7570
  for (const candidate of candidates) {
7424
7571
  const candidateContent = candidate.content;
@@ -7455,7 +7602,7 @@ async function runTextCall(params) {
7455
7602
  imageSize: request.imageSize
7456
7603
  });
7457
7604
  if (latestUsage) {
7458
- queue.push({ type: "usage", usage: latestUsage, costUsd, modelVersion });
7605
+ pushEvent({ type: "usage", usage: latestUsage, costUsd, modelVersion });
7459
7606
  }
7460
7607
  callLogger?.complete({
7461
7608
  responseText: text,
@@ -7509,18 +7656,76 @@ async function runTextCall(params) {
7509
7656
  });
7510
7657
  return result;
7511
7658
  }
7512
- function streamText(request) {
7659
+ function startTextStream(request, operation) {
7513
7660
  const queue = createAsyncQueue();
7514
7661
  const abortController = new AbortController();
7662
+ const provider = resolveProvider(request.model).provider;
7663
+ const telemetry = createLlmTelemetryEmitter({
7664
+ telemetry: request.telemetry,
7665
+ operation,
7666
+ provider,
7667
+ model: request.model
7668
+ });
7669
+ const startedAtMs = Date.now();
7670
+ telemetry.emit({
7671
+ type: "llm.call.started",
7672
+ inputMode: typeof request.input === "string" ? "string" : "messages",
7673
+ toolCount: request.tools?.length ?? 0,
7674
+ responseModalities: request.responseModalities
7675
+ });
7515
7676
  const result = (async () => {
7677
+ let uploadMetrics = emptyFileUploadMetrics();
7516
7678
  try {
7517
- const output = await runTextCall({ request, queue, abortController });
7679
+ let output;
7680
+ await collectFileUploadMetrics(async () => {
7681
+ try {
7682
+ output = await runTextCall({
7683
+ request,
7684
+ queue,
7685
+ abortController,
7686
+ onEvent: telemetry.includeStreamEvents ? (event) => {
7687
+ telemetry.emit({ type: "llm.call.stream", event });
7688
+ } : void 0
7689
+ });
7690
+ } finally {
7691
+ uploadMetrics = getCurrentFileUploadMetrics();
7692
+ }
7693
+ });
7694
+ if (!output) {
7695
+ throw new Error("LLM text call returned no result.");
7696
+ }
7697
+ telemetry.emit({
7698
+ type: "llm.call.completed",
7699
+ success: true,
7700
+ durationMs: Math.max(0, Date.now() - startedAtMs),
7701
+ modelVersion: output.modelVersion,
7702
+ blocked: output.blocked,
7703
+ usage: output.usage,
7704
+ costUsd: output.costUsd,
7705
+ outputTextChars: output.text.length,
7706
+ thoughtChars: output.thoughts.length,
7707
+ responseImages: countInlineImagesInContent(output.content),
7708
+ uploadCount: uploadMetrics.count,
7709
+ uploadBytes: uploadMetrics.totalBytes,
7710
+ uploadLatencyMs: uploadMetrics.totalLatencyMs
7711
+ });
7518
7712
  queue.close();
7519
7713
  return output;
7520
7714
  } catch (error) {
7521
7715
  const err = error instanceof Error ? error : new Error(String(error));
7716
+ telemetry.emit({
7717
+ type: "llm.call.completed",
7718
+ success: false,
7719
+ durationMs: Math.max(0, Date.now() - startedAtMs),
7720
+ uploadCount: uploadMetrics.count,
7721
+ uploadBytes: uploadMetrics.totalBytes,
7722
+ uploadLatencyMs: uploadMetrics.totalLatencyMs,
7723
+ error: err.message
7724
+ });
7522
7725
  queue.fail(err);
7523
7726
  throw err;
7727
+ } finally {
7728
+ await telemetry.flush();
7524
7729
  }
7525
7730
  })();
7526
7731
  return {
@@ -7529,8 +7734,11 @@ function streamText(request) {
7529
7734
  abort: () => abortController.abort()
7530
7735
  };
7531
7736
  }
7737
+ function streamText(request) {
7738
+ return startTextStream(request, "streamText");
7739
+ }
7532
7740
  async function generateText(request) {
7533
- const call = streamText(request);
7741
+ const call = startTextStream(request, "generateText");
7534
7742
  for await (const _event of call.events) {
7535
7743
  }
7536
7744
  return await call.result;
@@ -7556,9 +7764,26 @@ function buildJsonSchemaConfig(request) {
7556
7764
  } : void 0;
7557
7765
  return { providerInfo, responseJsonSchema, openAiTextFormat };
7558
7766
  }
7559
- function streamJson(request) {
7767
+ function startJsonStream(request, operation) {
7560
7768
  const queue = createAsyncQueue();
7561
7769
  const abortController = new AbortController();
7770
+ const provider = resolveProvider(request.model).provider;
7771
+ const telemetry = createLlmTelemetryEmitter({
7772
+ telemetry: request.telemetry,
7773
+ operation,
7774
+ provider,
7775
+ model: request.model
7776
+ });
7777
+ const startedAtMs = Date.now();
7778
+ const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
7779
+ const streamMode = request.streamMode ?? "partial";
7780
+ telemetry.emit({
7781
+ type: "llm.call.started",
7782
+ inputMode: typeof request.input === "string" ? "string" : "messages",
7783
+ toolCount: request.tools?.length ?? 0,
7784
+ maxAttempts,
7785
+ streamMode
7786
+ });
7562
7787
  const resolveAbortSignal = () => {
7563
7788
  if (!request.signal) {
7564
7789
  return abortController.signal;
@@ -7577,135 +7802,155 @@ function streamJson(request) {
7577
7802
  return abortController.signal;
7578
7803
  };
7579
7804
  const result = (async () => {
7580
- const signal = resolveAbortSignal();
7581
- const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
7582
- const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
7583
- const streamMode = request.streamMode ?? "partial";
7584
- const failures = [];
7585
- let openAiTextFormatForAttempt = openAiTextFormat;
7586
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
7587
- let rawText = "";
7588
- let lastPartial = "";
7589
- try {
7590
- const call = streamText({
7591
- model: request.model,
7592
- input: request.input,
7593
- instructions: request.instructions,
7594
- tools: request.tools,
7595
- responseMimeType: request.responseMimeType ?? "application/json",
7596
- responseJsonSchema,
7597
- thinkingLevel: request.thinkingLevel,
7598
- ...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
7599
- signal
7600
- });
7805
+ let uploadMetrics = emptyFileUploadMetrics();
7806
+ let attemptsUsed = 0;
7807
+ try {
7808
+ let output;
7809
+ await collectFileUploadMetrics(async () => {
7601
7810
  try {
7602
- for await (const event of call.events) {
7603
- queue.push(event);
7604
- if (event.type === "delta" && event.channel === "response") {
7605
- rawText += event.text;
7606
- if (streamMode === "partial") {
7607
- const partial = parsePartialJsonFromLlmText(rawText);
7608
- if (partial !== null) {
7609
- const serialized = JSON.stringify(partial);
7610
- if (serialized !== lastPartial) {
7611
- lastPartial = serialized;
7612
- queue.push({
7613
- type: "json",
7614
- stage: "partial",
7615
- value: partial
7616
- });
7811
+ const signal = resolveAbortSignal();
7812
+ const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
7813
+ const failures = [];
7814
+ let openAiTextFormatForAttempt = openAiTextFormat;
7815
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
7816
+ attemptsUsed = attempt;
7817
+ let rawText = "";
7818
+ let lastPartial = "";
7819
+ try {
7820
+ const call = streamText({
7821
+ model: request.model,
7822
+ input: request.input,
7823
+ instructions: request.instructions,
7824
+ tools: request.tools,
7825
+ responseMimeType: request.responseMimeType ?? "application/json",
7826
+ responseJsonSchema,
7827
+ thinkingLevel: request.thinkingLevel,
7828
+ ...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
7829
+ telemetry: false,
7830
+ signal
7831
+ });
7832
+ try {
7833
+ for await (const event of call.events) {
7834
+ queue.push(event);
7835
+ if (telemetry.includeStreamEvents) {
7836
+ telemetry.emit({ type: "llm.call.stream", event });
7837
+ }
7838
+ if (event.type === "delta" && event.channel === "response") {
7839
+ rawText += event.text;
7840
+ if (streamMode === "partial") {
7841
+ const partial = parsePartialJsonFromLlmText(rawText);
7842
+ if (partial !== null) {
7843
+ const serialized = JSON.stringify(partial);
7844
+ if (serialized !== lastPartial) {
7845
+ lastPartial = serialized;
7846
+ queue.push({
7847
+ type: "json",
7848
+ stage: "partial",
7849
+ value: partial
7850
+ });
7851
+ }
7852
+ }
7853
+ }
7617
7854
  }
7618
7855
  }
7856
+ } catch (streamError) {
7857
+ await call.result.catch(() => void 0);
7858
+ throw streamError;
7859
+ }
7860
+ const result2 = await call.result;
7861
+ rawText = rawText || result2.text;
7862
+ const cleanedText = normalizeJsonText(rawText);
7863
+ const repairedText = escapeNewlinesInStrings(cleanedText);
7864
+ const payload = JSON.parse(repairedText);
7865
+ const normalized = typeof request.normalizeJson === "function" ? request.normalizeJson(payload) : payload;
7866
+ const parsed = request.schema.parse(normalized);
7867
+ queue.push({ type: "json", stage: "final", value: parsed });
7868
+ output = { value: parsed, rawText, result: result2 };
7869
+ return;
7870
+ } catch (error) {
7871
+ const handled = error instanceof Error ? error : new Error(String(error));
7872
+ failures.push({ attempt, rawText, error: handled });
7873
+ if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
7874
+ openAiTextFormatForAttempt = void 0;
7875
+ }
7876
+ if (attempt >= maxAttempts) {
7877
+ throw new LlmJsonCallError(
7878
+ `LLM JSON call failed after ${attempt} attempt(s)`,
7879
+ failures
7880
+ );
7619
7881
  }
7620
7882
  }
7621
7883
  }
7622
- } catch (streamError) {
7623
- await call.result.catch(() => void 0);
7624
- throw streamError;
7625
- }
7626
- const result2 = await call.result;
7627
- rawText = rawText || result2.text;
7628
- const cleanedText = normalizeJsonText(rawText);
7629
- const repairedText = escapeNewlinesInStrings(cleanedText);
7630
- const payload = JSON.parse(repairedText);
7631
- const normalized = typeof request.normalizeJson === "function" ? request.normalizeJson(payload) : payload;
7632
- const parsed = request.schema.parse(normalized);
7633
- queue.push({ type: "json", stage: "final", value: parsed });
7634
- queue.close();
7635
- return { value: parsed, rawText, result: result2 };
7636
- } catch (error) {
7637
- const handled = error instanceof Error ? error : new Error(String(error));
7638
- failures.push({ attempt, rawText, error: handled });
7639
- if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
7640
- openAiTextFormatForAttempt = void 0;
7641
- }
7642
- if (attempt >= maxAttempts) {
7643
- throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
7884
+ throw new LlmJsonCallError("LLM JSON call failed", failures);
7885
+ } finally {
7886
+ uploadMetrics = getCurrentFileUploadMetrics();
7644
7887
  }
7645
- }
7888
+ });
7889
+ if (!output) {
7890
+ throw new Error("LLM JSON call returned no result.");
7891
+ }
7892
+ telemetry.emit({
7893
+ type: "llm.call.completed",
7894
+ success: true,
7895
+ durationMs: Math.max(0, Date.now() - startedAtMs),
7896
+ modelVersion: output.result.modelVersion,
7897
+ blocked: output.result.blocked,
7898
+ usage: output.result.usage,
7899
+ costUsd: output.result.costUsd,
7900
+ rawTextChars: output.rawText.length,
7901
+ attempts: attemptsUsed,
7902
+ uploadCount: uploadMetrics.count,
7903
+ uploadBytes: uploadMetrics.totalBytes,
7904
+ uploadLatencyMs: uploadMetrics.totalLatencyMs
7905
+ });
7906
+ queue.close();
7907
+ return output;
7908
+ } catch (error) {
7909
+ const err = error instanceof Error ? error : new Error(String(error));
7910
+ telemetry.emit({
7911
+ type: "llm.call.completed",
7912
+ success: false,
7913
+ durationMs: Math.max(0, Date.now() - startedAtMs),
7914
+ attempts: attemptsUsed > 0 ? attemptsUsed : void 0,
7915
+ uploadCount: uploadMetrics.count,
7916
+ uploadBytes: uploadMetrics.totalBytes,
7917
+ uploadLatencyMs: uploadMetrics.totalLatencyMs,
7918
+ error: err.message
7919
+ });
7920
+ queue.fail(err);
7921
+ throw err;
7922
+ } finally {
7923
+ await telemetry.flush();
7646
7924
  }
7647
- throw new LlmJsonCallError("LLM JSON call failed", failures);
7648
- })().catch((error) => {
7649
- const err = error instanceof Error ? error : new Error(String(error));
7650
- queue.fail(err);
7651
- throw err;
7652
- });
7925
+ })();
7653
7926
  return {
7654
7927
  events: queue.iterable,
7655
7928
  result,
7656
7929
  abort: () => abortController.abort()
7657
7930
  };
7658
7931
  }
7932
+ function streamJson(request) {
7933
+ return startJsonStream(request, "streamJson");
7934
+ }
7659
7935
  async function generateJson(request) {
7660
- const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 2));
7661
- const { providerInfo, responseJsonSchema, openAiTextFormat } = buildJsonSchemaConfig(request);
7662
- let openAiTextFormatForAttempt = openAiTextFormat;
7663
- const failures = [];
7664
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
7665
- let rawText = "";
7666
- try {
7667
- const call = streamText({
7668
- model: request.model,
7669
- input: request.input,
7670
- instructions: request.instructions,
7671
- tools: request.tools,
7672
- responseMimeType: request.responseMimeType ?? "application/json",
7673
- responseJsonSchema,
7674
- thinkingLevel: request.thinkingLevel,
7675
- ...openAiTextFormatForAttempt ? { openAiTextFormat: openAiTextFormatForAttempt } : {},
7676
- signal: request.signal
7677
- });
7678
- try {
7679
- for await (const event of call.events) {
7680
- request.onEvent?.(event);
7681
- if (event.type === "delta" && event.channel === "response") {
7682
- rawText += event.text;
7683
- }
7684
- }
7685
- } catch (streamError) {
7686
- await call.result.catch(() => void 0);
7687
- throw streamError;
7688
- }
7689
- const result = await call.result;
7690
- rawText = rawText || result.text;
7691
- const cleanedText = normalizeJsonText(rawText);
7692
- const repairedText = escapeNewlinesInStrings(cleanedText);
7693
- const payload = JSON.parse(repairedText);
7694
- const normalized = typeof request.normalizeJson === "function" ? request.normalizeJson(payload) : payload;
7695
- const parsed = request.schema.parse(normalized);
7696
- return { value: parsed, rawText, result };
7697
- } catch (error) {
7698
- const handled = error instanceof Error ? error : new Error(String(error));
7699
- failures.push({ attempt, rawText, error: handled });
7700
- if (providerInfo.provider === "chatgpt" && openAiTextFormatForAttempt) {
7701
- openAiTextFormatForAttempt = void 0;
7702
- }
7703
- if (attempt >= maxAttempts) {
7704
- throw new LlmJsonCallError(`LLM JSON call failed after ${attempt} attempt(s)`, failures);
7936
+ const call = startJsonStream(
7937
+ {
7938
+ ...request,
7939
+ streamMode: "final"
7940
+ },
7941
+ "generateJson"
7942
+ );
7943
+ try {
7944
+ for await (const event of call.events) {
7945
+ if (event.type !== "json") {
7946
+ request.onEvent?.(event);
7705
7947
  }
7706
7948
  }
7949
+ } catch (streamError) {
7950
+ await call.result.catch(() => void 0);
7951
+ throw streamError;
7707
7952
  }
7708
- throw new LlmJsonCallError("LLM JSON call failed", failures);
7953
+ return await call.result;
7709
7954
  }
7710
7955
  var DEFAULT_TOOL_LOOP_MAX_STEPS = 8;
7711
7956
  function resolveToolLoopContents(input) {
@@ -8553,7 +8798,7 @@ async function runToolLoop(request) {
8553
8798
  toolOutputs.push({
8554
8799
  type: "custom_tool_call_output",
8555
8800
  call_id: entry.ids.callId,
8556
- output: toOpenAiToolOutput(outputPayload)
8801
+ output: toChatGptToolOutput(outputPayload)
8557
8802
  });
8558
8803
  } else {
8559
8804
  toolOutputs.push({
@@ -8567,7 +8812,7 @@ async function runToolLoop(request) {
8567
8812
  toolOutputs.push({
8568
8813
  type: "function_call_output",
8569
8814
  call_id: entry.ids.callId,
8570
- output: toOpenAiToolOutput(outputPayload)
8815
+ output: toChatGptToolOutput(outputPayload)
8571
8816
  });
8572
8817
  }
8573
8818
  }
@@ -9321,7 +9566,10 @@ function streamToolLoop(request) {
9321
9566
  abort: () => abortController.abort()
9322
9567
  };
9323
9568
  }
9324
- var IMAGE_GRADE_SCHEMA = import_zod3.z.enum(["pass", "fail"]);
9569
+ var IMAGE_GRADE_VALUE_SCHEMA = import_zod3.z.enum(["pass", "fail"]);
9570
+ var IMAGE_GRADE_SCHEMA = import_zod3.z.object({
9571
+ grade: IMAGE_GRADE_VALUE_SCHEMA
9572
+ });
9325
9573
  async function gradeGeneratedImage(params) {
9326
9574
  const parts = [
9327
9575
  {
@@ -9332,7 +9580,7 @@ async function gradeGeneratedImage(params) {
9332
9580
  "Image prompt to grade:",
9333
9581
  params.imagePrompt,
9334
9582
  "",
9335
- 'Respond with the JSON string "pass" or "fail".'
9583
+ 'Respond with JSON like {"grade":"pass"} or {"grade":"fail"}.'
9336
9584
  ].join("\\n")
9337
9585
  },
9338
9586
  {
@@ -9341,12 +9589,13 @@ async function gradeGeneratedImage(params) {
9341
9589
  mimeType: params.image.mimeType ?? "image/png"
9342
9590
  }
9343
9591
  ];
9344
- const { value } = await generateJson({
9592
+ const { value, result } = await generateJson({
9345
9593
  model: params.model,
9346
9594
  input: [{ role: "user", content: parts }],
9347
- schema: IMAGE_GRADE_SCHEMA
9595
+ schema: IMAGE_GRADE_SCHEMA,
9596
+ telemetry: false
9348
9597
  });
9349
- return value;
9598
+ return { grade: value.grade, result };
9350
9599
  }
9351
9600
  async function generateImages(request) {
9352
9601
  const maxAttempts = Math.max(1, Math.floor(request.maxAttempts ?? 4));
@@ -9366,6 +9615,19 @@ async function generateImages(request) {
9366
9615
  if (!gradingPrompt) {
9367
9616
  throw new Error("imageGradingPrompt must be a non-empty string");
9368
9617
  }
9618
+ const telemetry = createLlmTelemetryEmitter({
9619
+ telemetry: request.telemetry,
9620
+ operation: "generateImages",
9621
+ provider: resolveProvider(request.model).provider,
9622
+ model: request.model
9623
+ });
9624
+ const startedAtMs = Date.now();
9625
+ telemetry.emit({
9626
+ type: "llm.call.started",
9627
+ imagePromptCount: promptList.length,
9628
+ styleImageCount: request.styleImages?.length ?? 0,
9629
+ maxAttempts
9630
+ });
9369
9631
  const addText = (parts, text) => {
9370
9632
  const lastPart = parts[parts.length - 1];
9371
9633
  if (lastPart !== void 0 && lastPart.type === "text") {
@@ -9423,6 +9685,9 @@ async function generateImages(request) {
9423
9685
  const inputMessages = [{ role: "user", content: buildInitialPromptParts() }];
9424
9686
  const orderedEntries = [...promptEntries];
9425
9687
  const resolvedImages = /* @__PURE__ */ new Map();
9688
+ let totalCostUsd = 0;
9689
+ let totalUsage;
9690
+ let attemptsUsed = 0;
9426
9691
  const removeResolvedEntries = (resolved) => {
9427
9692
  if (resolved.size === 0) {
9428
9693
  return;
@@ -9437,70 +9702,118 @@ async function generateImages(request) {
9437
9702
  }
9438
9703
  }
9439
9704
  };
9440
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
9441
- const result = await generateText({
9442
- model: request.model,
9443
- input: inputMessages,
9444
- responseModalities: ["IMAGE", "TEXT"],
9445
- imageAspectRatio: request.imageAspectRatio,
9446
- imageSize: request.imageSize ?? "2K"
9447
- });
9448
- if (result.blocked || !result.content) {
9449
- continue;
9450
- }
9451
- const images = extractImages(result.content);
9452
- if (images.length > 0 && promptEntries.length > 0) {
9453
- const assignedCount = Math.min(images.length, promptEntries.length);
9454
- const pendingAssignments = promptEntries.slice(0, assignedCount);
9455
- const assignedImages = images.slice(0, assignedCount);
9456
- const gradeResults = await Promise.all(
9457
- pendingAssignments.map(
9458
- (entry, index) => gradeGeneratedImage({
9459
- gradingPrompt,
9460
- imagePrompt: entry.prompt,
9461
- image: (() => {
9462
- const image = assignedImages[index];
9463
- if (!image) {
9464
- throw new Error("Image generation returned fewer images than expected.");
9705
+ let uploadMetrics = emptyFileUploadMetrics();
9706
+ try {
9707
+ await collectFileUploadMetrics(async () => {
9708
+ try {
9709
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
9710
+ attemptsUsed = attempt;
9711
+ const result = await generateText({
9712
+ model: request.model,
9713
+ input: inputMessages,
9714
+ responseModalities: ["IMAGE", "TEXT"],
9715
+ imageAspectRatio: request.imageAspectRatio,
9716
+ imageSize: request.imageSize ?? "2K",
9717
+ telemetry: false
9718
+ });
9719
+ totalCostUsd += result.costUsd;
9720
+ totalUsage = sumUsageTokens(totalUsage, result.usage);
9721
+ if (result.blocked || !result.content) {
9722
+ continue;
9723
+ }
9724
+ const images = extractImages(result.content);
9725
+ if (images.length > 0 && promptEntries.length > 0) {
9726
+ const assignedCount = Math.min(images.length, promptEntries.length);
9727
+ const pendingAssignments = promptEntries.slice(0, assignedCount);
9728
+ const assignedImages = images.slice(0, assignedCount);
9729
+ const gradeResults = await Promise.all(
9730
+ pendingAssignments.map(
9731
+ (entry, index) => gradeGeneratedImage({
9732
+ gradingPrompt,
9733
+ imagePrompt: entry.prompt,
9734
+ image: (() => {
9735
+ const image = assignedImages[index];
9736
+ if (!image) {
9737
+ throw new Error("Image generation returned fewer images than expected.");
9738
+ }
9739
+ return image;
9740
+ })(),
9741
+ model: "gpt-5.2"
9742
+ })
9743
+ )
9744
+ );
9745
+ const passedEntries = /* @__PURE__ */ new Set();
9746
+ for (let i = 0; i < gradeResults.length; i += 1) {
9747
+ const gradeResult = gradeResults[i];
9748
+ const entry = pendingAssignments[i];
9749
+ const image = assignedImages[i];
9750
+ if (!gradeResult || !entry || !image) {
9751
+ continue;
9465
9752
  }
9466
- return image;
9467
- })(),
9468
- model: "gpt-5.2"
9469
- })
9470
- )
9471
- );
9472
- const passedEntries = /* @__PURE__ */ new Set();
9473
- for (let i = 0; i < gradeResults.length; i += 1) {
9474
- const grade = gradeResults[i];
9475
- const entry = pendingAssignments[i];
9476
- const image = assignedImages[i];
9477
- if (!grade || !entry || !image) {
9478
- continue;
9479
- }
9480
- if (grade === "pass") {
9481
- resolvedImages.set(entry.index, image);
9482
- passedEntries.add(entry.index);
9753
+ totalCostUsd += gradeResult.result.costUsd;
9754
+ totalUsage = sumUsageTokens(totalUsage, gradeResult.result.usage);
9755
+ if (gradeResult.grade === "pass") {
9756
+ resolvedImages.set(entry.index, image);
9757
+ passedEntries.add(entry.index);
9758
+ }
9759
+ }
9760
+ removeResolvedEntries(passedEntries);
9761
+ }
9762
+ if (promptEntries.length === 0) {
9763
+ break;
9764
+ }
9765
+ inputMessages.push({
9766
+ role: "assistant",
9767
+ content: result.content.parts
9768
+ });
9769
+ inputMessages.push({
9770
+ role: "user",
9771
+ content: buildContinuationPromptParts(promptEntries)
9772
+ });
9483
9773
  }
9774
+ } finally {
9775
+ uploadMetrics = getCurrentFileUploadMetrics();
9484
9776
  }
9485
- removeResolvedEntries(passedEntries);
9486
- }
9487
- if (promptEntries.length === 0) {
9488
- break;
9489
- }
9490
- inputMessages.push({
9491
- role: "assistant",
9492
- content: result.content.parts
9493
9777
  });
9494
- inputMessages.push({ role: "user", content: buildContinuationPromptParts(promptEntries) });
9495
- }
9496
- const orderedImages = [];
9497
- for (const entry of orderedEntries) {
9498
- const image = resolvedImages.get(entry.index);
9499
- if (image) {
9500
- orderedImages.push(image);
9778
+ const orderedImages = [];
9779
+ for (const entry of orderedEntries) {
9780
+ const image = resolvedImages.get(entry.index);
9781
+ if (image) {
9782
+ orderedImages.push(image);
9783
+ }
9501
9784
  }
9785
+ const outputImages = orderedImages.slice(0, numImages);
9786
+ telemetry.emit({
9787
+ type: "llm.call.completed",
9788
+ success: true,
9789
+ durationMs: Math.max(0, Date.now() - startedAtMs),
9790
+ usage: totalUsage,
9791
+ costUsd: totalCostUsd,
9792
+ imageCount: outputImages.length,
9793
+ attempts: attemptsUsed,
9794
+ uploadCount: uploadMetrics.count,
9795
+ uploadBytes: uploadMetrics.totalBytes,
9796
+ uploadLatencyMs: uploadMetrics.totalLatencyMs
9797
+ });
9798
+ return outputImages;
9799
+ } catch (error) {
9800
+ const err = error instanceof Error ? error : new Error(String(error));
9801
+ telemetry.emit({
9802
+ type: "llm.call.completed",
9803
+ success: false,
9804
+ durationMs: Math.max(0, Date.now() - startedAtMs),
9805
+ usage: totalUsage,
9806
+ costUsd: totalCostUsd,
9807
+ attempts: attemptsUsed > 0 ? attemptsUsed : void 0,
9808
+ uploadCount: uploadMetrics.count,
9809
+ uploadBytes: uploadMetrics.totalBytes,
9810
+ uploadLatencyMs: uploadMetrics.totalLatencyMs,
9811
+ error: err.message
9812
+ });
9813
+ throw err;
9814
+ } finally {
9815
+ await telemetry.flush();
9502
9816
  }
9503
- return orderedImages.slice(0, numImages);
9504
9817
  }
9505
9818
  async function generateImageInBatches(request) {
9506
9819
  const {
@@ -12151,7 +12464,7 @@ function isNoEntError(error) {
12151
12464
 
12152
12465
  // src/agent.ts
12153
12466
  async function runAgentLoop(request) {
12154
- const telemetry = createAgentTelemetrySession(request.telemetry);
12467
+ const telemetry = createTelemetrySession(request.telemetry);
12155
12468
  const logging = createRootAgentLoggingSession(request);
12156
12469
  try {
12157
12470
  return await runWithAgentLoggingSession(logging, async () => {
@@ -12237,7 +12550,7 @@ async function runAgentLoopInternal(request, context) {
12237
12550
  logging: _logging,
12238
12551
  ...toolLoopRequest
12239
12552
  } = request;
12240
- const telemetrySession = context.telemetry ?? createAgentTelemetrySession(telemetry);
12553
+ const telemetrySession = context.telemetry ?? createTelemetrySession(telemetry);
12241
12554
  const loggingSession = context.logging;
12242
12555
  const runId = randomRunId();
12243
12556
  const startedAtMs = Date.now();
@@ -12300,15 +12613,15 @@ async function runAgentLoopInternal(request, context) {
12300
12613
  ].join(" ")
12301
12614
  );
12302
12615
  const sourceOnEvent = toolLoopRequestWithSteering.onEvent;
12303
- const includeLlmStreamEvents = telemetrySession?.includeLlmStreamEvents === true;
12616
+ const includeStreamEvents = telemetrySession?.includeStreamEvents === true;
12304
12617
  const streamEventLogger = loggingSession ? createAgentStreamEventLogger({
12305
12618
  append: (line) => {
12306
12619
  loggingSession.logLine(`[agent:${runId}] ${line}`);
12307
12620
  }
12308
12621
  }) : void 0;
12309
- const wrappedOnEvent = sourceOnEvent || includeLlmStreamEvents ? (event) => {
12622
+ const wrappedOnEvent = sourceOnEvent || includeStreamEvents ? (event) => {
12310
12623
  sourceOnEvent?.(event);
12311
- if (includeLlmStreamEvents) {
12624
+ if (includeStreamEvents) {
12312
12625
  emitTelemetry({ type: "agent.run.stream", event });
12313
12626
  }
12314
12627
  streamEventLogger?.appendEvent(event);
@@ -12546,7 +12859,7 @@ function countToolCalls(result) {
12546
12859
  }
12547
12860
  return count;
12548
12861
  }
12549
- function sumUsageValue(current, next) {
12862
+ function sumUsageValue2(current, next) {
12550
12863
  if (typeof next !== "number" || !Number.isFinite(next)) {
12551
12864
  return current;
12552
12865
  }
@@ -12564,20 +12877,17 @@ function summarizeResultUsage(result) {
12564
12877
  continue;
12565
12878
  }
12566
12879
  summary = {
12567
- promptTokens: sumUsageValue(summary?.promptTokens, usage.promptTokens),
12568
- cachedTokens: sumUsageValue(summary?.cachedTokens, usage.cachedTokens),
12569
- responseTokens: sumUsageValue(summary?.responseTokens, usage.responseTokens),
12570
- responseImageTokens: sumUsageValue(summary?.responseImageTokens, usage.responseImageTokens),
12571
- thinkingTokens: sumUsageValue(summary?.thinkingTokens, usage.thinkingTokens),
12572
- totalTokens: sumUsageValue(summary?.totalTokens, usage.totalTokens),
12573
- toolUsePromptTokens: sumUsageValue(summary?.toolUsePromptTokens, usage.toolUsePromptTokens)
12880
+ promptTokens: sumUsageValue2(summary?.promptTokens, usage.promptTokens),
12881
+ cachedTokens: sumUsageValue2(summary?.cachedTokens, usage.cachedTokens),
12882
+ responseTokens: sumUsageValue2(summary?.responseTokens, usage.responseTokens),
12883
+ responseImageTokens: sumUsageValue2(summary?.responseImageTokens, usage.responseImageTokens),
12884
+ thinkingTokens: sumUsageValue2(summary?.thinkingTokens, usage.thinkingTokens),
12885
+ totalTokens: sumUsageValue2(summary?.totalTokens, usage.totalTokens),
12886
+ toolUsePromptTokens: sumUsageValue2(summary?.toolUsePromptTokens, usage.toolUsePromptTokens)
12574
12887
  };
12575
12888
  }
12576
12889
  return summary;
12577
12890
  }
12578
- function isPromiseLike2(value) {
12579
- return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
12580
- }
12581
12891
  function resolveAgentLoggingSelection(value) {
12582
12892
  if (value === false) {
12583
12893
  return void 0;
@@ -12611,60 +12921,6 @@ function createRootAgentLoggingSession(request) {
12611
12921
  mirrorToConsole: selected.mirrorToConsole !== false
12612
12922
  });
12613
12923
  }
12614
- function isAgentTelemetrySink(value) {
12615
- return typeof value === "object" && value !== null && typeof value.emit === "function";
12616
- }
12617
- function resolveTelemetrySelection(telemetry) {
12618
- if (!telemetry) {
12619
- return void 0;
12620
- }
12621
- if (isAgentTelemetrySink(telemetry)) {
12622
- return { sink: telemetry };
12623
- }
12624
- if (isAgentTelemetrySink(telemetry.sink)) {
12625
- return telemetry;
12626
- }
12627
- throw new Error("Invalid runAgentLoop telemetry config: expected a sink with emit(event).");
12628
- }
12629
- function createAgentTelemetrySession(telemetry) {
12630
- const config = resolveTelemetrySelection(telemetry);
12631
- if (!config) {
12632
- return void 0;
12633
- }
12634
- const pending = /* @__PURE__ */ new Set();
12635
- const trackPromise = (promise) => {
12636
- pending.add(promise);
12637
- promise.finally(() => {
12638
- pending.delete(promise);
12639
- });
12640
- };
12641
- const emit = (event) => {
12642
- try {
12643
- const output = config.sink.emit(event);
12644
- if (isPromiseLike2(output)) {
12645
- const task = Promise.resolve(output).then(() => void 0).catch(() => void 0);
12646
- trackPromise(task);
12647
- }
12648
- } catch {
12649
- }
12650
- };
12651
- const flush = async () => {
12652
- while (pending.size > 0) {
12653
- await Promise.allSettled([...pending]);
12654
- }
12655
- if (typeof config.sink.flush === "function") {
12656
- try {
12657
- await config.sink.flush();
12658
- } catch {
12659
- }
12660
- }
12661
- };
12662
- return {
12663
- includeLlmStreamEvents: config.includeLlmStreamEvents === true,
12664
- emit,
12665
- flush
12666
- };
12667
- }
12668
12924
  function createAgentTelemetryEmitter(params) {
12669
12925
  return (event) => {
12670
12926
  if (!params.session) {
@@ -13358,6 +13614,7 @@ async function runCandidateEvolution(options) {
13358
13614
  applyPatch,
13359
13615
  configureGemini,
13360
13616
  configureModelConcurrency,
13617
+ configureTelemetry,
13361
13618
  convertGooglePartsToLlmParts,
13362
13619
  createApplyPatchTool,
13363
13620
  createCodexApplyPatchTool,
@@ -13406,6 +13663,7 @@ async function runCandidateEvolution(options) {
13406
13663
  parseJsonFromLlmText,
13407
13664
  refreshChatGptOauthToken,
13408
13665
  resetModelConcurrencyConfig,
13666
+ resetTelemetry,
13409
13667
  resolveFilesystemToolProfile,
13410
13668
  resolveFireworksModelId,
13411
13669
  runAgentLoop,