@ljoukov/llm 4.0.6 → 4.0.8

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.js CHANGED
@@ -329,6 +329,34 @@ function estimateCallCostUsd({
329
329
  import os2 from "os";
330
330
  import { TextDecoder } from "util";
331
331
 
332
+ // src/utils/runtimeSingleton.ts
333
+ var runtimeSingletonStoreKey = /* @__PURE__ */ Symbol.for("@ljoukov/llm.runtimeSingletonStore");
334
+ function getRuntimeSingletonStore() {
335
+ const globalObject = globalThis;
336
+ const existingStore = globalObject[runtimeSingletonStoreKey];
337
+ if (existingStore) {
338
+ return existingStore;
339
+ }
340
+ const store = /* @__PURE__ */ new Map();
341
+ Object.defineProperty(globalObject, runtimeSingletonStoreKey, {
342
+ value: store,
343
+ enumerable: false,
344
+ configurable: false,
345
+ writable: false
346
+ });
347
+ return store;
348
+ }
349
+ function getRuntimeSingleton(key, create) {
350
+ const store = getRuntimeSingletonStore();
351
+ const existingValue = store.get(key);
352
+ if (existingValue !== void 0) {
353
+ return existingValue;
354
+ }
355
+ const createdValue = create();
356
+ store.set(key, createdValue);
357
+ return createdValue;
358
+ }
359
+
332
360
  // src/openai/chatgpt-auth.ts
333
361
  import { Buffer as Buffer2 } from "buffer";
334
362
  import fs2 from "fs";
@@ -339,14 +367,16 @@ import { z } from "zod";
339
367
  // src/utils/env.ts
340
368
  import fs from "fs";
341
369
  import path from "path";
342
- var envLoaded = false;
370
+ var envState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.envState"), () => ({
371
+ envLoaded: false
372
+ }));
343
373
  function loadLocalEnv() {
344
- if (envLoaded) {
374
+ if (envState.envLoaded) {
345
375
  return;
346
376
  }
347
377
  const envPath = path.join(process.cwd(), ".env.local");
348
378
  loadEnvFromFile(envPath, { override: false });
349
- envLoaded = true;
379
+ envState.envLoaded = true;
350
380
  }
351
381
  function loadEnvFromFile(filePath, { override = false } = {}) {
352
382
  let content;
@@ -432,8 +462,10 @@ var ExchangeResponseSchema = z.object({
432
462
  expires_in: z.union([z.number(), z.string()]),
433
463
  id_token: z.string().optional()
434
464
  });
435
- var cachedProfile = null;
436
- var refreshPromise = null;
465
+ var chatGptAuthState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.chatGptAuthState"), () => ({
466
+ cachedProfile: null,
467
+ refreshPromise: null
468
+ }));
437
469
  async function fetchChatGptAuthProfileFromTokenProvider(options) {
438
470
  const base = options.baseUrl.replace(/\/+$/u, "");
439
471
  const store = options.store?.trim() ? options.store.trim() : "kv";
@@ -534,13 +566,13 @@ async function getChatGptAuthProfile() {
534
566
  const tokenProviderUrl = process.env[CHATGPT_AUTH_TOKEN_PROVIDER_URL_ENV];
535
567
  const tokenProviderKey = process.env[CHATGPT_AUTH_TOKEN_PROVIDER_API_KEY_ENV] ?? process.env[CHATGPT_AUTH_API_KEY_ENV];
536
568
  if (tokenProviderUrl && tokenProviderUrl.trim().length > 0 && tokenProviderKey && tokenProviderKey.trim().length > 0) {
537
- if (cachedProfile && !isExpired(cachedProfile)) {
538
- return cachedProfile;
569
+ if (chatGptAuthState.cachedProfile && !isExpired(chatGptAuthState.cachedProfile)) {
570
+ return chatGptAuthState.cachedProfile;
539
571
  }
540
- if (refreshPromise) {
541
- return refreshPromise;
572
+ if (chatGptAuthState.refreshPromise) {
573
+ return chatGptAuthState.refreshPromise;
542
574
  }
543
- refreshPromise = (async () => {
575
+ chatGptAuthState.refreshPromise = (async () => {
544
576
  try {
545
577
  const store = process.env[CHATGPT_AUTH_TOKEN_PROVIDER_STORE_ENV];
546
578
  const profile = await fetchChatGptAuthProfileFromTokenProvider({
@@ -548,31 +580,31 @@ async function getChatGptAuthProfile() {
548
580
  apiKey: tokenProviderKey,
549
581
  store: store ?? void 0
550
582
  });
551
- cachedProfile = profile;
583
+ chatGptAuthState.cachedProfile = profile;
552
584
  return profile;
553
585
  } finally {
554
- refreshPromise = null;
586
+ chatGptAuthState.refreshPromise = null;
555
587
  }
556
588
  })();
557
- return refreshPromise;
589
+ return chatGptAuthState.refreshPromise;
558
590
  }
559
- if (cachedProfile && !isExpired(cachedProfile)) {
560
- return cachedProfile;
591
+ if (chatGptAuthState.cachedProfile && !isExpired(chatGptAuthState.cachedProfile)) {
592
+ return chatGptAuthState.cachedProfile;
561
593
  }
562
- if (refreshPromise) {
563
- return refreshPromise;
594
+ if (chatGptAuthState.refreshPromise) {
595
+ return chatGptAuthState.refreshPromise;
564
596
  }
565
- refreshPromise = (async () => {
597
+ chatGptAuthState.refreshPromise = (async () => {
566
598
  try {
567
- const baseProfile = cachedProfile ?? loadAuthProfileFromCodexStore();
599
+ const baseProfile = chatGptAuthState.cachedProfile ?? loadAuthProfileFromCodexStore();
568
600
  const profile = isExpired(baseProfile) ? await refreshAndPersistCodexProfile(baseProfile) : baseProfile;
569
- cachedProfile = profile;
601
+ chatGptAuthState.cachedProfile = profile;
570
602
  return profile;
571
603
  } finally {
572
- refreshPromise = null;
604
+ chatGptAuthState.refreshPromise = null;
573
605
  }
574
606
  })();
575
- return refreshPromise;
607
+ return chatGptAuthState.refreshPromise;
576
608
  }
577
609
  function resolveCodexHome() {
578
610
  const codexHome = process.env.CODEX_HOME;
@@ -1304,8 +1336,10 @@ function createAbortError(reason) {
1304
1336
  // src/openai/chatgpt-codex.ts
1305
1337
  var CHATGPT_CODEX_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
1306
1338
  var CHATGPT_RESPONSES_EXPERIMENTAL_HEADER = "responses=experimental";
1307
- var cachedResponsesWebSocketMode = null;
1308
- var chatGptResponsesWebSocketDisabled = false;
1339
+ var chatGptCodexState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.chatGptCodexState"), () => ({
1340
+ cachedResponsesWebSocketMode: null,
1341
+ chatGptResponsesWebSocketDisabled: false
1342
+ }));
1309
1343
  async function streamChatGptCodexResponse(options) {
1310
1344
  const { access, accountId } = await getChatGptAuthProfile();
1311
1345
  const mode = resolveChatGptResponsesWebSocketMode();
@@ -1331,7 +1365,7 @@ async function streamChatGptCodexResponse(options) {
1331
1365
  }
1332
1366
  };
1333
1367
  };
1334
- if (mode === "off" || chatGptResponsesWebSocketDisabled) {
1368
+ if (mode === "off" || chatGptCodexState.chatGptResponsesWebSocketDisabled) {
1335
1369
  return fallbackStreamFactory();
1336
1370
  }
1337
1371
  const websocketHeaders = buildChatGptCodexHeaders({
@@ -1350,7 +1384,7 @@ async function streamChatGptCodexResponse(options) {
1350
1384
  }),
1351
1385
  createFallbackStream: fallbackStreamFactory,
1352
1386
  onWebSocketFallback: () => {
1353
- chatGptResponsesWebSocketDisabled = true;
1387
+ chatGptCodexState.chatGptResponsesWebSocketDisabled = true;
1354
1388
  }
1355
1389
  });
1356
1390
  }
@@ -1380,14 +1414,14 @@ async function streamChatGptCodexResponseSse(options) {
1380
1414
  return parseEventStream(body);
1381
1415
  }
1382
1416
  function resolveChatGptResponsesWebSocketMode() {
1383
- if (cachedResponsesWebSocketMode) {
1384
- return cachedResponsesWebSocketMode;
1417
+ if (chatGptCodexState.cachedResponsesWebSocketMode) {
1418
+ return chatGptCodexState.cachedResponsesWebSocketMode;
1385
1419
  }
1386
- cachedResponsesWebSocketMode = resolveResponsesWebSocketMode(
1420
+ chatGptCodexState.cachedResponsesWebSocketMode = resolveResponsesWebSocketMode(
1387
1421
  process.env.CHATGPT_RESPONSES_WEBSOCKET_MODE ?? process.env.OPENAI_RESPONSES_WEBSOCKET_MODE,
1388
1422
  "auto"
1389
1423
  );
1390
- return cachedResponsesWebSocketMode;
1424
+ return chatGptCodexState.cachedResponsesWebSocketMode;
1391
1425
  }
1392
1426
  function buildChatGptCodexHeaders(options) {
1393
1427
  const openAiBeta = options.useWebSocket ? mergeOpenAiBetaHeader(
@@ -1425,8 +1459,8 @@ async function collectChatGptCodexResponse(options) {
1425
1459
  }
1426
1460
  });
1427
1461
  } catch (error) {
1428
- if (!sawAnyDelta && !retriedViaSseFallback && shouldRetryViaSseFallback(error) && !chatGptResponsesWebSocketDisabled) {
1429
- chatGptResponsesWebSocketDisabled = true;
1462
+ if (!sawAnyDelta && !retriedViaSseFallback && shouldRetryViaSseFallback(error) && !chatGptCodexState.chatGptResponsesWebSocketDisabled) {
1463
+ chatGptCodexState.chatGptResponsesWebSocketDisabled = true;
1430
1464
  retriedViaSseFallback = true;
1431
1465
  continue;
1432
1466
  }
@@ -1662,7 +1696,12 @@ var MODEL_CONCURRENCY_PROVIDERS = [
1662
1696
  "google",
1663
1697
  "fireworks"
1664
1698
  ];
1665
- var configuredModelConcurrency = normalizeModelConcurrencyConfig({});
1699
+ var modelConcurrencyState = getRuntimeSingleton(
1700
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.modelConcurrencyState"),
1701
+ () => ({
1702
+ configuredModelConcurrency: normalizeModelConcurrencyConfig({})
1703
+ })
1704
+ );
1666
1705
  function clampModelConcurrencyCap(value) {
1667
1706
  if (!Number.isFinite(value)) {
1668
1707
  return DEFAULT_MODEL_CONCURRENCY_CAP;
@@ -1736,14 +1775,14 @@ function resolveDefaultProviderCap(provider, modelId) {
1736
1775
  return DEFAULT_FIREWORKS_MODEL_CONCURRENCY_CAP;
1737
1776
  }
1738
1777
  function configureModelConcurrency(config = {}) {
1739
- configuredModelConcurrency = normalizeModelConcurrencyConfig(config);
1778
+ modelConcurrencyState.configuredModelConcurrency = normalizeModelConcurrencyConfig(config);
1740
1779
  }
1741
1780
  function resetModelConcurrencyConfig() {
1742
- configuredModelConcurrency = normalizeModelConcurrencyConfig({});
1781
+ modelConcurrencyState.configuredModelConcurrency = normalizeModelConcurrencyConfig({});
1743
1782
  }
1744
1783
  function resolveModelConcurrencyCap(options) {
1745
1784
  const modelId = options.modelId ? normalizeModelIdForConfig(options.modelId) : void 0;
1746
- const config = options.config ? normalizeModelConcurrencyConfig(options.config) : configuredModelConcurrency;
1785
+ const config = options.config ? normalizeModelConcurrencyConfig(options.config) : modelConcurrencyState.configuredModelConcurrency;
1747
1786
  const providerModelCap = modelId ? config.providerModelCaps[options.provider].get(modelId) : void 0;
1748
1787
  if (providerModelCap !== void 0) {
1749
1788
  return providerModelCap;
@@ -1977,32 +2016,37 @@ import OpenAI from "openai";
1977
2016
  import { Agent, fetch as undiciFetch } from "undici";
1978
2017
  var DEFAULT_FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
1979
2018
  var DEFAULT_FIREWORKS_TIMEOUT_MS = 15 * 6e4;
1980
- var cachedClient = null;
1981
- var cachedFetch = null;
1982
- var cachedBaseUrl = null;
1983
- var cachedApiKey = null;
1984
- var cachedTimeoutMs = null;
2019
+ var fireworksClientState = getRuntimeSingleton(
2020
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.fireworksClientState"),
2021
+ () => ({
2022
+ cachedClient: null,
2023
+ cachedFetch: null,
2024
+ cachedBaseUrl: null,
2025
+ cachedApiKey: null,
2026
+ cachedTimeoutMs: null
2027
+ })
2028
+ );
1985
2029
  function resolveTimeoutMs() {
1986
- if (cachedTimeoutMs !== null) {
1987
- return cachedTimeoutMs;
2030
+ if (fireworksClientState.cachedTimeoutMs !== null) {
2031
+ return fireworksClientState.cachedTimeoutMs;
1988
2032
  }
1989
2033
  const raw = process.env.FIREWORKS_TIMEOUT_MS;
1990
2034
  const parsed = raw ? Number(raw) : Number.NaN;
1991
- cachedTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FIREWORKS_TIMEOUT_MS;
1992
- return cachedTimeoutMs;
2035
+ fireworksClientState.cachedTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FIREWORKS_TIMEOUT_MS;
2036
+ return fireworksClientState.cachedTimeoutMs;
1993
2037
  }
1994
2038
  function resolveBaseUrl() {
1995
- if (cachedBaseUrl !== null) {
1996
- return cachedBaseUrl;
2039
+ if (fireworksClientState.cachedBaseUrl !== null) {
2040
+ return fireworksClientState.cachedBaseUrl;
1997
2041
  }
1998
2042
  loadLocalEnv();
1999
2043
  const raw = process.env.FIREWORKS_BASE_URL?.trim();
2000
- cachedBaseUrl = raw && raw.length > 0 ? raw : DEFAULT_FIREWORKS_BASE_URL;
2001
- return cachedBaseUrl;
2044
+ fireworksClientState.cachedBaseUrl = raw && raw.length > 0 ? raw : DEFAULT_FIREWORKS_BASE_URL;
2045
+ return fireworksClientState.cachedBaseUrl;
2002
2046
  }
2003
2047
  function resolveApiKey() {
2004
- if (cachedApiKey !== null) {
2005
- return cachedApiKey;
2048
+ if (fireworksClientState.cachedApiKey !== null) {
2049
+ return fireworksClientState.cachedApiKey;
2006
2050
  }
2007
2051
  loadLocalEnv();
2008
2052
  const raw = process.env.FIREWORKS_TOKEN ?? process.env.FIREWORKS_API_KEY;
@@ -2012,46 +2056,51 @@ function resolveApiKey() {
2012
2056
  "FIREWORKS_TOKEN (or FIREWORKS_API_KEY) must be provided to access Fireworks APIs."
2013
2057
  );
2014
2058
  }
2015
- cachedApiKey = token;
2016
- return cachedApiKey;
2059
+ fireworksClientState.cachedApiKey = token;
2060
+ return fireworksClientState.cachedApiKey;
2017
2061
  }
2018
2062
  function getFireworksFetch() {
2019
- if (cachedFetch) {
2020
- return cachedFetch;
2063
+ if (fireworksClientState.cachedFetch) {
2064
+ return fireworksClientState.cachedFetch;
2021
2065
  }
2022
2066
  const timeoutMs = resolveTimeoutMs();
2023
2067
  const dispatcher = new Agent({
2024
2068
  bodyTimeout: timeoutMs,
2025
2069
  headersTimeout: timeoutMs
2026
2070
  });
2027
- cachedFetch = ((input, init) => {
2071
+ fireworksClientState.cachedFetch = ((input, init) => {
2028
2072
  return undiciFetch(input, {
2029
2073
  ...init ?? {},
2030
2074
  dispatcher
2031
2075
  });
2032
2076
  });
2033
- return cachedFetch;
2077
+ return fireworksClientState.cachedFetch;
2034
2078
  }
2035
2079
  function getFireworksClient() {
2036
- if (cachedClient) {
2037
- return cachedClient;
2080
+ if (fireworksClientState.cachedClient) {
2081
+ return fireworksClientState.cachedClient;
2038
2082
  }
2039
- cachedClient = new OpenAI({
2083
+ fireworksClientState.cachedClient = new OpenAI({
2040
2084
  apiKey: resolveApiKey(),
2041
2085
  baseURL: resolveBaseUrl(),
2042
2086
  timeout: resolveTimeoutMs(),
2043
2087
  fetch: getFireworksFetch()
2044
2088
  });
2045
- return cachedClient;
2089
+ return fireworksClientState.cachedClient;
2046
2090
  }
2047
2091
 
2048
2092
  // src/fireworks/calls.ts
2049
2093
  var DEFAULT_SCHEDULER_KEY = "__default__";
2050
- var schedulerByModel = /* @__PURE__ */ new Map();
2094
+ var fireworksCallState = getRuntimeSingleton(
2095
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.fireworksCallState"),
2096
+ () => ({
2097
+ schedulerByModel: /* @__PURE__ */ new Map()
2098
+ })
2099
+ );
2051
2100
  function getSchedulerForModel(modelId) {
2052
2101
  const normalizedModelId = modelId?.trim();
2053
2102
  const schedulerKey = normalizedModelId && normalizedModelId.length > 0 ? normalizedModelId : DEFAULT_SCHEDULER_KEY;
2054
- const existing = schedulerByModel.get(schedulerKey);
2103
+ const existing = fireworksCallState.schedulerByModel.get(schedulerKey);
2055
2104
  if (existing) {
2056
2105
  return existing;
2057
2106
  }
@@ -2063,7 +2112,7 @@ function getSchedulerForModel(modelId) {
2063
2112
  minIntervalBetweenStartMs: 200,
2064
2113
  startJitterMs: 200
2065
2114
  });
2066
- schedulerByModel.set(schedulerKey, created);
2115
+ fireworksCallState.schedulerByModel.set(schedulerKey, created);
2067
2116
  return created;
2068
2117
  }
2069
2118
  async function runFireworksCall(fn, modelId, runOptions) {
@@ -2110,7 +2159,10 @@ var ServiceAccountSchema = z2.object({
2110
2159
  privateKey: private_key.replace(/\\n/g, "\n"),
2111
2160
  tokenUri: token_uri
2112
2161
  }));
2113
- var cachedServiceAccount = null;
2162
+ var googleAuthState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.googleAuthState"), () => ({
2163
+ cachedServiceAccount: null,
2164
+ authClientCache: /* @__PURE__ */ new Map()
2165
+ }));
2114
2166
  function parseGoogleServiceAccount(input) {
2115
2167
  let parsed;
2116
2168
  try {
@@ -2121,16 +2173,16 @@ function parseGoogleServiceAccount(input) {
2121
2173
  return ServiceAccountSchema.parse(parsed);
2122
2174
  }
2123
2175
  function getGoogleServiceAccount() {
2124
- if (cachedServiceAccount) {
2125
- return cachedServiceAccount;
2176
+ if (googleAuthState.cachedServiceAccount) {
2177
+ return googleAuthState.cachedServiceAccount;
2126
2178
  }
2127
2179
  loadLocalEnv();
2128
2180
  const raw = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
2129
2181
  if (!raw || raw.trim().length === 0) {
2130
2182
  throw new Error("GOOGLE_SERVICE_ACCOUNT_JSON must be provided for Google APIs access.");
2131
2183
  }
2132
- cachedServiceAccount = parseGoogleServiceAccount(raw);
2133
- return cachedServiceAccount;
2184
+ googleAuthState.cachedServiceAccount = parseGoogleServiceAccount(raw);
2185
+ return googleAuthState.cachedServiceAccount;
2134
2186
  }
2135
2187
  function normaliseScopes(scopes) {
2136
2188
  if (!scopes) {
@@ -2182,8 +2234,10 @@ function isGeminiImageModelId(value) {
2182
2234
  }
2183
2235
  var CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
2184
2236
  var DEFAULT_VERTEX_LOCATION = "global";
2185
- var geminiConfiguration = {};
2186
- var clientPromise;
2237
+ var geminiClientState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.geminiClientState"), () => ({
2238
+ geminiConfiguration: {},
2239
+ clientPromise: void 0
2240
+ }));
2187
2241
  function normaliseConfigValue(value) {
2188
2242
  if (value === void 0 || value === null) {
2189
2243
  return void 0;
@@ -2194,14 +2248,14 @@ function normaliseConfigValue(value) {
2194
2248
  function configureGemini(options = {}) {
2195
2249
  const nextProjectId = normaliseConfigValue(options.projectId);
2196
2250
  const nextLocation = normaliseConfigValue(options.location);
2197
- geminiConfiguration = {
2198
- projectId: nextProjectId !== void 0 ? nextProjectId : geminiConfiguration.projectId,
2199
- location: nextLocation !== void 0 ? nextLocation : geminiConfiguration.location
2251
+ geminiClientState.geminiConfiguration = {
2252
+ projectId: nextProjectId !== void 0 ? nextProjectId : geminiClientState.geminiConfiguration.projectId,
2253
+ location: nextLocation !== void 0 ? nextLocation : geminiClientState.geminiConfiguration.location
2200
2254
  };
2201
- clientPromise = void 0;
2255
+ geminiClientState.clientPromise = void 0;
2202
2256
  }
2203
2257
  function resolveProjectId() {
2204
- const override = geminiConfiguration.projectId;
2258
+ const override = geminiClientState.geminiConfiguration.projectId;
2205
2259
  if (override) {
2206
2260
  return override;
2207
2261
  }
@@ -2209,15 +2263,15 @@ function resolveProjectId() {
2209
2263
  return serviceAccount.projectId;
2210
2264
  }
2211
2265
  function resolveLocation() {
2212
- const override = geminiConfiguration.location;
2266
+ const override = geminiClientState.geminiConfiguration.location;
2213
2267
  if (override) {
2214
2268
  return override;
2215
2269
  }
2216
2270
  return DEFAULT_VERTEX_LOCATION;
2217
2271
  }
2218
2272
  async function getGeminiClient() {
2219
- if (!clientPromise) {
2220
- clientPromise = Promise.resolve().then(() => {
2273
+ if (!geminiClientState.clientPromise) {
2274
+ geminiClientState.clientPromise = Promise.resolve().then(() => {
2221
2275
  const projectId = resolveProjectId();
2222
2276
  const location = resolveLocation();
2223
2277
  const googleAuthOptions = getGoogleAuthOptions(CLOUD_PLATFORM_SCOPE);
@@ -2229,7 +2283,7 @@ async function getGeminiClient() {
2229
2283
  });
2230
2284
  });
2231
2285
  }
2232
- return clientPromise;
2286
+ return geminiClientState.clientPromise;
2233
2287
  }
2234
2288
 
2235
2289
  // src/google/calls.ts
@@ -2425,11 +2479,13 @@ function retryDelayMs(attempt) {
2425
2479
  return base + jitter;
2426
2480
  }
2427
2481
  var DEFAULT_SCHEDULER_KEY2 = "__default__";
2428
- var schedulerByModel2 = /* @__PURE__ */ new Map();
2482
+ var googleCallState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.googleCallState"), () => ({
2483
+ schedulerByModel: /* @__PURE__ */ new Map()
2484
+ }));
2429
2485
  function getSchedulerForModel2(modelId) {
2430
2486
  const normalizedModelId = modelId?.trim();
2431
2487
  const schedulerKey = normalizedModelId && normalizedModelId.length > 0 ? normalizedModelId : DEFAULT_SCHEDULER_KEY2;
2432
- const existing = schedulerByModel2.get(schedulerKey);
2488
+ const existing = googleCallState.schedulerByModel.get(schedulerKey);
2433
2489
  if (existing) {
2434
2490
  return existing;
2435
2491
  }
@@ -2452,7 +2508,7 @@ function getSchedulerForModel2(modelId) {
2452
2508
  }
2453
2509
  }
2454
2510
  });
2455
- schedulerByModel2.set(schedulerKey, created);
2511
+ googleCallState.schedulerByModel.set(schedulerKey, created);
2456
2512
  return created;
2457
2513
  }
2458
2514
  async function runGeminiCall(fn, modelId, runOptions) {
@@ -2462,53 +2518,55 @@ async function runGeminiCall(fn, modelId, runOptions) {
2462
2518
  // src/openai/client.ts
2463
2519
  import OpenAI2 from "openai";
2464
2520
  import { Agent as Agent2, fetch as undiciFetch2 } from "undici";
2465
- var cachedApiKey2 = null;
2466
- var cachedClient2 = null;
2467
- var cachedFetch2 = null;
2468
- var cachedTimeoutMs2 = null;
2469
- var openAiResponsesWebSocketMode = null;
2470
- var openAiResponsesWebSocketDisabled = false;
2521
+ var openAiClientState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.openAiClientState"), () => ({
2522
+ cachedApiKey: null,
2523
+ cachedClient: null,
2524
+ cachedFetch: null,
2525
+ cachedTimeoutMs: null,
2526
+ openAiResponsesWebSocketMode: null,
2527
+ openAiResponsesWebSocketDisabled: false
2528
+ }));
2471
2529
  var DEFAULT_OPENAI_TIMEOUT_MS = 15 * 6e4;
2472
2530
  function resolveOpenAiTimeoutMs() {
2473
- if (cachedTimeoutMs2 !== null) {
2474
- return cachedTimeoutMs2;
2531
+ if (openAiClientState.cachedTimeoutMs !== null) {
2532
+ return openAiClientState.cachedTimeoutMs;
2475
2533
  }
2476
2534
  const raw = process.env.OPENAI_STREAM_TIMEOUT_MS ?? process.env.OPENAI_TIMEOUT_MS;
2477
2535
  const parsed = raw ? Number(raw) : Number.NaN;
2478
- cachedTimeoutMs2 = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OPENAI_TIMEOUT_MS;
2479
- return cachedTimeoutMs2;
2536
+ openAiClientState.cachedTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OPENAI_TIMEOUT_MS;
2537
+ return openAiClientState.cachedTimeoutMs;
2480
2538
  }
2481
2539
  function getOpenAiFetch() {
2482
- if (cachedFetch2) {
2483
- return cachedFetch2;
2540
+ if (openAiClientState.cachedFetch) {
2541
+ return openAiClientState.cachedFetch;
2484
2542
  }
2485
2543
  const timeoutMs = resolveOpenAiTimeoutMs();
2486
2544
  const dispatcher = new Agent2({
2487
2545
  bodyTimeout: timeoutMs,
2488
2546
  headersTimeout: timeoutMs
2489
2547
  });
2490
- cachedFetch2 = ((input, init) => {
2548
+ openAiClientState.cachedFetch = ((input, init) => {
2491
2549
  return undiciFetch2(input, {
2492
2550
  ...init ?? {},
2493
2551
  dispatcher
2494
2552
  });
2495
2553
  });
2496
- return cachedFetch2;
2554
+ return openAiClientState.cachedFetch;
2497
2555
  }
2498
2556
  function resolveOpenAiBaseUrl() {
2499
2557
  loadLocalEnv();
2500
2558
  return process.env.OPENAI_BASE_URL?.trim() || "https://api.openai.com/v1";
2501
2559
  }
2502
2560
  function resolveOpenAiResponsesWebSocketMode() {
2503
- if (openAiResponsesWebSocketMode) {
2504
- return openAiResponsesWebSocketMode;
2561
+ if (openAiClientState.openAiResponsesWebSocketMode) {
2562
+ return openAiClientState.openAiResponsesWebSocketMode;
2505
2563
  }
2506
2564
  loadLocalEnv();
2507
- openAiResponsesWebSocketMode = resolveResponsesWebSocketMode(
2565
+ openAiClientState.openAiResponsesWebSocketMode = resolveResponsesWebSocketMode(
2508
2566
  process.env.OPENAI_RESPONSES_WEBSOCKET_MODE,
2509
2567
  "auto"
2510
2568
  );
2511
- return openAiResponsesWebSocketMode;
2569
+ return openAiClientState.openAiResponsesWebSocketMode;
2512
2570
  }
2513
2571
  function wrapFallbackStream(stream) {
2514
2572
  return {
@@ -2561,7 +2619,7 @@ function installResponsesWebSocketTransport(client, apiKey) {
2561
2619
  responsesApi.stream = (request, options) => {
2562
2620
  const mode = resolveOpenAiResponsesWebSocketMode();
2563
2621
  const fallbackStreamFactory = () => wrapFallbackStream(originalStream(request, options));
2564
- if (mode === "off" || openAiResponsesWebSocketDisabled) {
2622
+ if (mode === "off" || openAiClientState.openAiResponsesWebSocketDisabled) {
2565
2623
  return fallbackStreamFactory();
2566
2624
  }
2567
2625
  const signal = options && typeof options === "object" ? options.signal ?? void 0 : void 0;
@@ -2580,15 +2638,15 @@ function installResponsesWebSocketTransport(client, apiKey) {
2580
2638
  createFallbackStream: fallbackStreamFactory,
2581
2639
  onWebSocketFallback: (error) => {
2582
2640
  if (isResponsesWebSocketUnsupportedError(error)) {
2583
- openAiResponsesWebSocketDisabled = true;
2641
+ openAiClientState.openAiResponsesWebSocketDisabled = true;
2584
2642
  }
2585
2643
  }
2586
2644
  });
2587
2645
  };
2588
2646
  }
2589
2647
  function getOpenAiApiKey() {
2590
- if (cachedApiKey2 !== null) {
2591
- return cachedApiKey2;
2648
+ if (openAiClientState.cachedApiKey !== null) {
2649
+ return openAiClientState.cachedApiKey;
2592
2650
  }
2593
2651
  loadLocalEnv();
2594
2652
  const raw = process.env.OPENAI_API_KEY;
@@ -2596,33 +2654,35 @@ function getOpenAiApiKey() {
2596
2654
  if (!value) {
2597
2655
  throw new Error("OPENAI_API_KEY must be provided to access OpenAI APIs.");
2598
2656
  }
2599
- cachedApiKey2 = value;
2600
- return cachedApiKey2;
2657
+ openAiClientState.cachedApiKey = value;
2658
+ return openAiClientState.cachedApiKey;
2601
2659
  }
2602
2660
  function getOpenAiClient() {
2603
- if (cachedClient2) {
2604
- return cachedClient2;
2661
+ if (openAiClientState.cachedClient) {
2662
+ return openAiClientState.cachedClient;
2605
2663
  }
2606
2664
  loadLocalEnv();
2607
2665
  const apiKey = getOpenAiApiKey();
2608
2666
  const timeoutMs = resolveOpenAiTimeoutMs();
2609
- cachedClient2 = new OpenAI2({
2667
+ openAiClientState.cachedClient = new OpenAI2({
2610
2668
  apiKey,
2611
2669
  fetch: getOpenAiFetch(),
2612
2670
  timeout: timeoutMs
2613
2671
  });
2614
- installResponsesWebSocketTransport(cachedClient2, apiKey);
2615
- return cachedClient2;
2672
+ installResponsesWebSocketTransport(openAiClientState.cachedClient, apiKey);
2673
+ return openAiClientState.cachedClient;
2616
2674
  }
2617
2675
 
2618
2676
  // src/openai/calls.ts
2619
2677
  var DEFAULT_OPENAI_REASONING_EFFORT = "medium";
2620
2678
  var DEFAULT_SCHEDULER_KEY3 = "__default__";
2621
- var schedulerByModel3 = /* @__PURE__ */ new Map();
2679
+ var openAiCallState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.openAiCallState"), () => ({
2680
+ schedulerByModel: /* @__PURE__ */ new Map()
2681
+ }));
2622
2682
  function getSchedulerForModel3(modelId) {
2623
2683
  const normalizedModelId = modelId?.trim();
2624
2684
  const schedulerKey = normalizedModelId && normalizedModelId.length > 0 ? normalizedModelId : DEFAULT_SCHEDULER_KEY3;
2625
- const existing = schedulerByModel3.get(schedulerKey);
2685
+ const existing = openAiCallState.schedulerByModel.get(schedulerKey);
2626
2686
  if (existing) {
2627
2687
  return existing;
2628
2688
  }
@@ -2634,7 +2694,7 @@ function getSchedulerForModel3(modelId) {
2634
2694
  minIntervalBetweenStartMs: 200,
2635
2695
  startJitterMs: 200
2636
2696
  });
2637
- schedulerByModel3.set(schedulerKey, created);
2697
+ openAiCallState.schedulerByModel.set(schedulerKey, created);
2638
2698
  return created;
2639
2699
  }
2640
2700
  async function runOpenAiCall(fn, modelId, runOptions) {
@@ -2706,6 +2766,9 @@ function ensureTrailingNewline(value) {
2706
2766
  return value.endsWith("\n") ? value : `${value}
2707
2767
  `;
2708
2768
  }
2769
+ function hasNonEmptyText(value) {
2770
+ return typeof value === "string" && value.length > 0;
2771
+ }
2709
2772
  function redactDataUrlPayload(value) {
2710
2773
  if (!value.toLowerCase().startsWith("data:")) {
2711
2774
  return value;
@@ -2954,6 +3017,25 @@ var AgentLoggingSessionImpl = class {
2954
3017
  }
2955
3018
  this.enqueueLineWrite(timestamped);
2956
3019
  }
3020
+ async writeAttachments(baseDir, attachments) {
3021
+ const usedNames = /* @__PURE__ */ new Set();
3022
+ for (const attachment of attachments ?? []) {
3023
+ let filename = normalisePathSegment(attachment.filename);
3024
+ if (!filename.includes(".")) {
3025
+ filename = `${filename}.bin`;
3026
+ }
3027
+ const ext = path3.extname(filename);
3028
+ const base = ext.length > 0 ? filename.slice(0, -ext.length) : filename;
3029
+ let candidate = filename;
3030
+ let duplicateIndex = 2;
3031
+ while (usedNames.has(candidate)) {
3032
+ candidate = `${base}-${duplicateIndex.toString()}${ext}`;
3033
+ duplicateIndex += 1;
3034
+ }
3035
+ usedNames.add(candidate);
3036
+ await writeFile(path3.join(baseDir, candidate), attachment.bytes);
3037
+ }
3038
+ }
2957
3039
  startLlmCall(input) {
2958
3040
  const callNumber = this.callCounter + 1;
2959
3041
  this.callCounter = callNumber;
@@ -2966,6 +3048,9 @@ var AgentLoggingSessionImpl = class {
2966
3048
  );
2967
3049
  const responsePath = path3.join(baseDir, "response.txt");
2968
3050
  const thoughtsPath = path3.join(baseDir, "thoughts.txt");
3051
+ const toolCallPath = path3.join(baseDir, "tool_call.txt");
3052
+ const toolCallResponsePath = path3.join(baseDir, "tool_call_response.txt");
3053
+ const errorPath = path3.join(baseDir, "error.txt");
2969
3054
  const responseMetadataPath = path3.join(baseDir, "response.metadata.json");
2970
3055
  let chain = this.ensureReady.then(async () => {
2971
3056
  await mkdir(baseDir, { recursive: true });
@@ -2987,22 +3072,13 @@ var AgentLoggingSessionImpl = class {
2987
3072
  `,
2988
3073
  "utf8"
2989
3074
  );
2990
- const usedNames = /* @__PURE__ */ new Set();
2991
- for (const attachment of input.attachments ?? []) {
2992
- let filename = normalisePathSegment(attachment.filename);
2993
- if (!filename.includes(".")) {
2994
- filename = `${filename}.bin`;
2995
- }
2996
- const ext = path3.extname(filename);
2997
- const base = ext.length > 0 ? filename.slice(0, -ext.length) : filename;
2998
- let candidate = filename;
2999
- let duplicateIndex = 2;
3000
- while (usedNames.has(candidate)) {
3001
- candidate = `${base}-${duplicateIndex.toString()}${ext}`;
3002
- duplicateIndex += 1;
3003
- }
3004
- usedNames.add(candidate);
3005
- await writeFile(path3.join(baseDir, candidate), attachment.bytes);
3075
+ await this.writeAttachments(baseDir, input.attachments);
3076
+ if (hasNonEmptyText(input.toolCallResponseText)) {
3077
+ await writeFile(
3078
+ toolCallResponsePath,
3079
+ ensureTrailingNewline(input.toolCallResponseText),
3080
+ "utf8"
3081
+ );
3006
3082
  }
3007
3083
  }).catch(() => void 0);
3008
3084
  this.track(chain);
@@ -3030,18 +3106,25 @@ var AgentLoggingSessionImpl = class {
3030
3106
  await appendFile(responsePath, text, "utf8");
3031
3107
  });
3032
3108
  },
3033
- complete: (metadata) => {
3109
+ complete: (options) => {
3034
3110
  if (closed) {
3035
3111
  return;
3036
3112
  }
3037
3113
  closed = true;
3038
3114
  enqueue(async () => {
3115
+ if (hasNonEmptyText(options?.responseText)) {
3116
+ await writeFile(responsePath, options.responseText, "utf8");
3117
+ }
3118
+ if (hasNonEmptyText(options?.toolCallText)) {
3119
+ await writeFile(toolCallPath, ensureTrailingNewline(options.toolCallText), "utf8");
3120
+ }
3121
+ await this.writeAttachments(baseDir, options?.attachments);
3039
3122
  const payload = {
3040
3123
  capturedAt: toIsoNow(),
3041
3124
  status: "completed"
3042
3125
  };
3043
- if (metadata) {
3044
- const sanitised = sanitiseLogValue(metadata);
3126
+ if (options?.metadata) {
3127
+ const sanitised = sanitiseLogValue(options.metadata);
3045
3128
  if (sanitised && typeof sanitised === "object" && !Array.isArray(sanitised)) {
3046
3129
  Object.assign(payload, sanitised);
3047
3130
  } else if (sanitised !== void 0) {
@@ -3052,19 +3135,27 @@ var AgentLoggingSessionImpl = class {
3052
3135
  `, "utf8");
3053
3136
  });
3054
3137
  },
3055
- fail: (error, metadata) => {
3138
+ fail: (error, options) => {
3056
3139
  if (closed) {
3057
3140
  return;
3058
3141
  }
3059
3142
  closed = true;
3060
3143
  enqueue(async () => {
3144
+ if (hasNonEmptyText(options?.responseText)) {
3145
+ await writeFile(responsePath, options.responseText, "utf8");
3146
+ }
3147
+ if (hasNonEmptyText(options?.toolCallText)) {
3148
+ await writeFile(toolCallPath, ensureTrailingNewline(options.toolCallText), "utf8");
3149
+ }
3150
+ await this.writeAttachments(baseDir, options?.attachments);
3151
+ await writeFile(errorPath, ensureTrailingNewline(toErrorMessage(error)), "utf8");
3061
3152
  const payload = {
3062
3153
  capturedAt: toIsoNow(),
3063
3154
  status: "failed",
3064
3155
  error: toErrorMessage(error)
3065
3156
  };
3066
- if (metadata) {
3067
- const sanitised = sanitiseLogValue(metadata);
3157
+ if (options?.metadata) {
3158
+ const sanitised = sanitiseLogValue(options.metadata);
3068
3159
  if (sanitised && typeof sanitised === "object" && !Array.isArray(sanitised)) {
3069
3160
  Object.assign(payload, sanitised);
3070
3161
  } else if (sanitised !== void 0) {
@@ -3089,7 +3180,10 @@ var AgentLoggingSessionImpl = class {
3089
3180
  }
3090
3181
  }
3091
3182
  };
3092
- var loggingSessionStorage = new AsyncLocalStorage();
3183
+ var loggingSessionStorage = getRuntimeSingleton(
3184
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.agentLogging.sessionStorage"),
3185
+ () => new AsyncLocalStorage()
3186
+ );
3093
3187
  function createAgentLoggingSession(config) {
3094
3188
  return new AgentLoggingSessionImpl(config);
3095
3189
  }
@@ -3104,7 +3198,10 @@ function getCurrentAgentLoggingSession() {
3104
3198
  }
3105
3199
 
3106
3200
  // src/llm.ts
3107
- var toolCallContextStorage = new AsyncLocalStorage2();
3201
+ var toolCallContextStorage = getRuntimeSingleton(
3202
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.toolCallContextStorage"),
3203
+ () => new AsyncLocalStorage2()
3204
+ );
3108
3205
  function getCurrentToolCallContext() {
3109
3206
  return toolCallContextStorage.getStore() ?? null;
3110
3207
  }
@@ -4907,7 +5004,10 @@ function resolveAttachmentExtension(mimeType) {
4907
5004
  }
4908
5005
  }
4909
5006
  }
4910
- function decodeDataUrlAttachment(value, basename) {
5007
+ function buildLoggedAttachmentFilename(prefix, index, mimeType) {
5008
+ return `${prefix}-${index.toString()}.${resolveAttachmentExtension(mimeType)}`;
5009
+ }
5010
+ function decodeDataUrlAttachment(value, options) {
4911
5011
  const trimmed = value.trim();
4912
5012
  if (!trimmed.toLowerCase().startsWith("data:")) {
4913
5013
  return null;
@@ -4923,7 +5023,7 @@ function decodeDataUrlAttachment(value, basename) {
4923
5023
  try {
4924
5024
  const bytes = isBase64 ? Buffer4.from(payload, "base64") : Buffer4.from(decodeURIComponent(payload), "utf8");
4925
5025
  return {
4926
- filename: `${basename}.${resolveAttachmentExtension(mimeType)}`,
5026
+ filename: buildLoggedAttachmentFilename(options.prefix, options.index, mimeType),
4927
5027
  bytes
4928
5028
  };
4929
5029
  } catch {
@@ -4932,10 +5032,10 @@ function decodeDataUrlAttachment(value, basename) {
4932
5032
  }
4933
5033
  function collectPayloadAttachments(value, options) {
4934
5034
  if (typeof value === "string") {
4935
- const attachment = decodeDataUrlAttachment(
4936
- value,
4937
- `${options.prefix}-${options.counter.toString()}`
4938
- );
5035
+ const attachment = decodeDataUrlAttachment(value, {
5036
+ prefix: options.prefix,
5037
+ index: options.counter
5038
+ });
4939
5039
  if (attachment) {
4940
5040
  options.attachments.push(attachment);
4941
5041
  options.counter += 1;
@@ -4960,7 +5060,7 @@ function collectPayloadAttachments(value, options) {
4960
5060
  if (typeof record.data === "string" && mimeType) {
4961
5061
  try {
4962
5062
  options.attachments.push({
4963
- filename: `${options.prefix}-${options.counter.toString()}.${resolveAttachmentExtension(mimeType)}`,
5063
+ filename: buildLoggedAttachmentFilename(options.prefix, options.counter, mimeType),
4964
5064
  bytes: decodeInlineDataBuffer(record.data)
4965
5065
  });
4966
5066
  options.counter += 1;
@@ -4980,27 +5080,166 @@ function serialiseRequestPayloadForLogging(value) {
4980
5080
  `;
4981
5081
  }
4982
5082
  }
5083
+ function serialiseLogArtifactText(value) {
5084
+ if (value === null || value === void 0) {
5085
+ return void 0;
5086
+ }
5087
+ if (typeof value === "string") {
5088
+ if (value.length === 0) {
5089
+ return void 0;
5090
+ }
5091
+ return value.endsWith("\n") ? value : `${value}
5092
+ `;
5093
+ }
5094
+ if (Array.isArray(value) && value.length === 0) {
5095
+ return void 0;
5096
+ }
5097
+ if (isPlainRecord(value) && Object.keys(value).length === 0) {
5098
+ return void 0;
5099
+ }
5100
+ try {
5101
+ return `${JSON.stringify(sanitiseLogValue(value), null, 2)}
5102
+ `;
5103
+ } catch {
5104
+ return `${String(value)}
5105
+ `;
5106
+ }
5107
+ }
5108
+ function collectLoggedAttachmentsFromLlmParts(parts, prefix) {
5109
+ const attachments = [];
5110
+ let index = 1;
5111
+ for (const part of parts) {
5112
+ if (part.type !== "inlineData") {
5113
+ continue;
5114
+ }
5115
+ attachments.push({
5116
+ filename: buildLoggedAttachmentFilename(prefix, index, part.mimeType),
5117
+ bytes: decodeInlineDataBuffer(part.data)
5118
+ });
5119
+ index += 1;
5120
+ }
5121
+ return attachments;
5122
+ }
5123
+ function collectLoggedAttachmentsFromGeminiParts(parts, prefix) {
5124
+ return collectLoggedAttachmentsFromLlmParts(convertGooglePartsToLlmParts(parts), prefix);
5125
+ }
5126
+ function extractToolCallResponseTextFromOpenAiInput(input) {
5127
+ if (!Array.isArray(input)) {
5128
+ return void 0;
5129
+ }
5130
+ const responses = input.filter((item) => isPlainRecord(item)).flatMap((item) => {
5131
+ const type = typeof item.type === "string" ? item.type : "";
5132
+ if (type !== "function_call_output" && type !== "custom_tool_call_output") {
5133
+ return [];
5134
+ }
5135
+ return [
5136
+ {
5137
+ type,
5138
+ callId: typeof item.call_id === "string" ? item.call_id : void 0,
5139
+ output: "output" in item ? sanitiseLogValue(item.output) : void 0
5140
+ }
5141
+ ];
5142
+ });
5143
+ return serialiseLogArtifactText(responses);
5144
+ }
5145
+ function extractToolCallResponseTextFromFireworksMessages(messages) {
5146
+ if (!Array.isArray(messages)) {
5147
+ return void 0;
5148
+ }
5149
+ const responses = messages.filter((message) => isPlainRecord(message)).flatMap((message) => {
5150
+ if (message.role !== "tool") {
5151
+ return [];
5152
+ }
5153
+ return [
5154
+ {
5155
+ toolCallId: typeof message.tool_call_id === "string" ? message.tool_call_id : void 0,
5156
+ content: sanitiseLogValue(message.content)
5157
+ }
5158
+ ];
5159
+ });
5160
+ return serialiseLogArtifactText(responses);
5161
+ }
5162
+ function extractToolCallResponseTextFromGeminiContents(contents) {
5163
+ if (!Array.isArray(contents)) {
5164
+ return void 0;
5165
+ }
5166
+ const responses = [];
5167
+ for (const content of contents) {
5168
+ if (!content || typeof content !== "object") {
5169
+ continue;
5170
+ }
5171
+ const parts = content.parts;
5172
+ if (!Array.isArray(parts)) {
5173
+ continue;
5174
+ }
5175
+ for (const part of parts) {
5176
+ if (!part || typeof part !== "object") {
5177
+ continue;
5178
+ }
5179
+ const functionResponse = part.functionResponse;
5180
+ if (functionResponse) {
5181
+ responses.push(sanitiseLogValue(functionResponse));
5182
+ }
5183
+ }
5184
+ }
5185
+ return serialiseLogArtifactText(responses);
5186
+ }
5187
+ function serialiseOpenAiStyleToolCallsForLogging(calls) {
5188
+ return serialiseLogArtifactText(
5189
+ calls.map((call) => {
5190
+ if (call.kind === "custom") {
5191
+ return {
5192
+ kind: call.kind,
5193
+ name: call.name,
5194
+ callId: call.callId,
5195
+ itemId: call.itemId,
5196
+ input: call.input
5197
+ };
5198
+ }
5199
+ const { value, error } = parseOpenAiToolArguments(call.arguments);
5200
+ return {
5201
+ kind: call.kind,
5202
+ name: call.name,
5203
+ callId: call.callId,
5204
+ itemId: call.itemId,
5205
+ arguments: value,
5206
+ ...error ? { parseError: error, rawArguments: call.arguments } : {}
5207
+ };
5208
+ })
5209
+ );
5210
+ }
5211
+ function serialiseGeminiToolCallsForLogging(calls) {
5212
+ return serialiseLogArtifactText(
5213
+ calls.map((call) => ({
5214
+ name: call.name ?? "unknown",
5215
+ callId: typeof call.id === "string" ? call.id : void 0,
5216
+ arguments: sanitiseLogValue(call.args ?? {})
5217
+ }))
5218
+ );
5219
+ }
4983
5220
  function startLlmCallLoggerFromContents(options) {
4984
5221
  const session = getCurrentAgentLoggingSession();
4985
5222
  if (!session) {
4986
5223
  return void 0;
4987
5224
  }
4988
5225
  const attachments = [];
5226
+ let attachmentIndex = 1;
4989
5227
  const sections = [];
4990
5228
  for (const [messageIndex, message] of options.contents.entries()) {
4991
5229
  sections.push(`### message_${(messageIndex + 1).toString()} role=${message.role}`);
4992
- for (const [partIndex, part] of message.parts.entries()) {
5230
+ for (const part of message.parts) {
4993
5231
  if (part.type === "text") {
4994
5232
  const channel = part.thought === true ? "thought" : "response";
4995
5233
  sections.push(`[text:${channel}]`);
4996
5234
  sections.push(part.text);
4997
5235
  continue;
4998
5236
  }
4999
- const filename = `message-${(messageIndex + 1).toString()}-part-${(partIndex + 1).toString()}.${resolveAttachmentExtension(part.mimeType)}`;
5237
+ const filename = buildLoggedAttachmentFilename("input", attachmentIndex, part.mimeType);
5000
5238
  attachments.push({
5001
5239
  filename,
5002
5240
  bytes: decodeInlineDataBuffer(part.data)
5003
5241
  });
5242
+ attachmentIndex += 1;
5004
5243
  sections.push(
5005
5244
  `[inlineData] file=${filename} mime=${part.mimeType ?? "application/octet-stream"} bytes=${attachments[attachments.length - 1]?.bytes.byteLength ?? 0}`
5006
5245
  );
@@ -5044,11 +5283,18 @@ function startLlmCallLoggerFromPayload(options) {
5044
5283
  }
5045
5284
  const attachments = [];
5046
5285
  collectPayloadAttachments(options.requestPayload, {
5047
- prefix: `step-${options.step.toString()}`,
5286
+ prefix: "input",
5048
5287
  attachments,
5049
5288
  seen: /* @__PURE__ */ new WeakSet(),
5050
5289
  counter: 1
5051
5290
  });
5291
+ const toolCallResponseText = options.provider === "openai" || options.provider === "chatgpt" ? extractToolCallResponseTextFromOpenAiInput(
5292
+ options.requestPayload.input
5293
+ ) : options.provider === "fireworks" ? extractToolCallResponseTextFromFireworksMessages(
5294
+ options.requestPayload.messages
5295
+ ) : extractToolCallResponseTextFromGeminiContents(
5296
+ options.requestPayload.contents
5297
+ );
5052
5298
  return session.startLlmCall({
5053
5299
  provider: options.provider,
5054
5300
  modelId: options.modelId,
@@ -5057,7 +5303,8 @@ function startLlmCallLoggerFromPayload(options) {
5057
5303
  step: options.step,
5058
5304
  ...getCurrentToolCallContext() ? { toolContext: getCurrentToolCallContext() } : {}
5059
5305
  },
5060
- attachments
5306
+ attachments,
5307
+ toolCallResponseText
5061
5308
  });
5062
5309
  }
5063
5310
  async function runTextCall(params) {
@@ -5362,6 +5609,7 @@ async function runTextCall(params) {
5362
5609
  const mergedParts = mergeConsecutiveTextParts(responseParts);
5363
5610
  const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
5364
5611
  const { text, thoughts } = extractTextByChannel(content);
5612
+ const outputAttachments = collectLoggedAttachmentsFromLlmParts(mergedParts, "output");
5365
5613
  const costUsd = estimateCallCostUsd({
5366
5614
  modelId: modelVersion,
5367
5615
  tokens: latestUsage,
@@ -5372,16 +5620,20 @@ async function runTextCall(params) {
5372
5620
  queue.push({ type: "usage", usage: latestUsage, costUsd, modelVersion });
5373
5621
  }
5374
5622
  callLogger?.complete({
5375
- provider,
5376
- model: request.model,
5377
- modelVersion,
5378
- blocked,
5379
- costUsd,
5380
- usage: latestUsage,
5381
- grounding: grounding ? sanitiseLogValue(grounding) : void 0,
5382
- responseChars: text.length,
5383
- thoughtChars: thoughts.length,
5384
- responseImages
5623
+ responseText: text,
5624
+ attachments: outputAttachments,
5625
+ metadata: {
5626
+ provider,
5627
+ model: request.model,
5628
+ modelVersion,
5629
+ blocked,
5630
+ costUsd,
5631
+ usage: latestUsage,
5632
+ grounding: grounding ? sanitiseLogValue(grounding) : void 0,
5633
+ responseChars: text.length,
5634
+ thoughtChars: thoughts.length,
5635
+ responseImages
5636
+ }
5385
5637
  });
5386
5638
  return {
5387
5639
  provider,
@@ -5396,14 +5648,21 @@ async function runTextCall(params) {
5396
5648
  grounding
5397
5649
  };
5398
5650
  } catch (error) {
5651
+ const partialParts = mergeConsecutiveTextParts(responseParts);
5652
+ const partialContent = partialParts.length > 0 ? { role: responseRole ?? "assistant", parts: partialParts } : void 0;
5653
+ const { text: partialText } = extractTextByChannel(partialContent);
5399
5654
  callLogger?.fail(error, {
5400
- provider,
5401
- model: request.model,
5402
- modelVersion,
5403
- blocked,
5404
- usage: latestUsage,
5405
- partialResponseParts: responseParts.length,
5406
- responseImages
5655
+ responseText: partialText,
5656
+ attachments: collectLoggedAttachmentsFromLlmParts(partialParts, "output"),
5657
+ metadata: {
5658
+ provider,
5659
+ model: request.model,
5660
+ modelVersion,
5661
+ blocked,
5662
+ usage: latestUsage,
5663
+ partialResponseParts: responseParts.length,
5664
+ responseImages
5665
+ }
5407
5666
  });
5408
5667
  throw error;
5409
5668
  }
@@ -5610,7 +5869,10 @@ var DEFAULT_TOOL_LOOP_MAX_STEPS = 8;
5610
5869
  function resolveToolLoopContents(input) {
5611
5870
  return resolveTextContents(input);
5612
5871
  }
5613
- var toolLoopSteeringInternals = /* @__PURE__ */ new WeakMap();
5872
+ var toolLoopSteeringInternals = getRuntimeSingleton(
5873
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.toolLoopSteeringInternals"),
5874
+ () => /* @__PURE__ */ new WeakMap()
5875
+ );
5614
5876
  function createToolLoopSteeringChannel() {
5615
5877
  const pending = [];
5616
5878
  let closed = false;
@@ -5870,6 +6132,9 @@ async function runToolLoop(request) {
5870
6132
  let usageTokens;
5871
6133
  let thoughtDeltaEmitted = false;
5872
6134
  let blocked = false;
6135
+ let responseText = "";
6136
+ let reasoningSummary = "";
6137
+ let stepToolCallText;
5873
6138
  const stepRequestPayload = {
5874
6139
  model: providerInfo.model,
5875
6140
  input,
@@ -5962,8 +6227,8 @@ async function runToolLoop(request) {
5962
6227
  throw new Error(message);
5963
6228
  }
5964
6229
  usageTokens = extractOpenAiUsageTokens(finalResponse.usage);
5965
- const responseText = extractOpenAiResponseParts(finalResponse).parts.filter((p) => p.type === "text" && p.thought !== true).map((p) => p.text).join("").trim();
5966
- const reasoningSummary = extractOpenAiReasoningSummary(finalResponse).trim();
6230
+ responseText = extractOpenAiResponseParts(finalResponse).parts.filter((p) => p.type === "text" && p.thought !== true).map((p) => p.text).join("").trim();
6231
+ reasoningSummary = extractOpenAiReasoningSummary(finalResponse).trim();
5967
6232
  if (!thoughtDeltaEmitted && reasoningSummary.length > 0) {
5968
6233
  stepCallLogger?.appendThoughtDelta(reasoningSummary);
5969
6234
  emitEvent({ type: "delta", channel: "thought", text: reasoningSummary });
@@ -5979,6 +6244,23 @@ async function runToolLoop(request) {
5979
6244
  emitEvent({ type: "usage", usage: usageTokens, costUsd: stepCostUsd, modelVersion });
5980
6245
  }
5981
6246
  const responseToolCalls = extractOpenAiToolCalls(finalResponse.output);
6247
+ stepToolCallText = serialiseOpenAiStyleToolCallsForLogging(
6248
+ responseToolCalls.map(
6249
+ (call) => call.kind === "custom" ? {
6250
+ kind: call.kind,
6251
+ name: call.name,
6252
+ input: call.input,
6253
+ callId: call.call_id,
6254
+ itemId: call.id
6255
+ } : {
6256
+ kind: call.kind,
6257
+ name: call.name,
6258
+ arguments: call.arguments,
6259
+ callId: call.call_id,
6260
+ itemId: call.id
6261
+ }
6262
+ )
6263
+ );
5982
6264
  const stepToolCalls = [];
5983
6265
  if (responseToolCalls.length === 0) {
5984
6266
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
@@ -6006,17 +6288,20 @@ async function runToolLoop(request) {
6006
6288
  timing: timing2
6007
6289
  });
6008
6290
  stepCallLogger?.complete({
6009
- provider: "openai",
6010
- model: request.model,
6011
- modelVersion,
6012
- step: turn,
6013
- usage: usageTokens,
6014
- costUsd: stepCostUsd,
6015
- blocked,
6016
- responseChars: responseText.length,
6017
- thoughtChars: reasoningSummary.length,
6018
- toolCalls: 0,
6019
- finalStep: steeringItems2.length === 0
6291
+ responseText,
6292
+ metadata: {
6293
+ provider: "openai",
6294
+ model: request.model,
6295
+ modelVersion,
6296
+ step: turn,
6297
+ usage: usageTokens,
6298
+ costUsd: stepCostUsd,
6299
+ blocked,
6300
+ responseChars: responseText.length,
6301
+ thoughtChars: reasoningSummary.length,
6302
+ toolCalls: 0,
6303
+ finalStep: steeringItems2.length === 0
6304
+ }
6020
6305
  });
6021
6306
  if (steeringItems2.length === 0) {
6022
6307
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -6139,28 +6424,36 @@ async function runToolLoop(request) {
6139
6424
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
6140
6425
  const steeringItems = steeringInput.length > 0 ? toOpenAiInput(steeringInput) : [];
6141
6426
  stepCallLogger?.complete({
6142
- provider: "openai",
6143
- model: request.model,
6144
- modelVersion,
6145
- step: turn,
6146
- usage: usageTokens,
6147
- costUsd: stepCostUsd,
6148
- blocked,
6149
- responseChars: responseText.length,
6150
- thoughtChars: reasoningSummary.length,
6151
- toolCalls: stepToolCalls.length,
6152
- finalStep: false
6427
+ responseText,
6428
+ toolCallText: stepToolCallText,
6429
+ metadata: {
6430
+ provider: "openai",
6431
+ model: request.model,
6432
+ modelVersion,
6433
+ step: turn,
6434
+ usage: usageTokens,
6435
+ costUsd: stepCostUsd,
6436
+ blocked,
6437
+ responseChars: responseText.length,
6438
+ thoughtChars: reasoningSummary.length,
6439
+ toolCalls: stepToolCalls.length,
6440
+ finalStep: false
6441
+ }
6153
6442
  });
6154
6443
  previousResponseId = finalResponse.id;
6155
6444
  input = steeringItems.length > 0 ? toolOutputs.concat(steeringItems) : toolOutputs;
6156
6445
  } catch (error) {
6157
6446
  stepCallLogger?.fail(error, {
6158
- provider: "openai",
6159
- model: request.model,
6160
- modelVersion,
6161
- step: turn,
6162
- usage: usageTokens,
6163
- blocked
6447
+ responseText,
6448
+ toolCallText: stepToolCallText,
6449
+ metadata: {
6450
+ provider: "openai",
6451
+ model: request.model,
6452
+ modelVersion,
6453
+ step: turn,
6454
+ usage: usageTokens,
6455
+ blocked
6456
+ }
6164
6457
  });
6165
6458
  throw error;
6166
6459
  }
@@ -6186,6 +6479,7 @@ async function runToolLoop(request) {
6186
6479
  let usageTokens;
6187
6480
  let responseText = "";
6188
6481
  let reasoningSummaryText = "";
6482
+ let stepToolCallText;
6189
6483
  const markFirstModelEvent = () => {
6190
6484
  if (firstModelEventAtMs === void 0) {
6191
6485
  firstModelEventAtMs = Date.now();
@@ -6254,6 +6548,23 @@ async function runToolLoop(request) {
6254
6548
  stepCallLogger?.appendResponseDelta(responseText);
6255
6549
  }
6256
6550
  const responseToolCalls = response.toolCalls ?? [];
6551
+ stepToolCallText = serialiseOpenAiStyleToolCallsForLogging(
6552
+ responseToolCalls.map(
6553
+ (call) => call.kind === "custom" ? {
6554
+ kind: call.kind,
6555
+ name: call.name,
6556
+ input: call.input,
6557
+ callId: call.callId,
6558
+ itemId: call.id
6559
+ } : {
6560
+ kind: call.kind,
6561
+ name: call.name,
6562
+ arguments: call.arguments,
6563
+ callId: call.callId,
6564
+ itemId: call.id
6565
+ }
6566
+ )
6567
+ );
6257
6568
  if (responseToolCalls.length === 0) {
6258
6569
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
6259
6570
  const steeringItems2 = steeringInput2.length > 0 ? toChatGptInput(steeringInput2).input : [];
@@ -6279,16 +6590,19 @@ async function runToolLoop(request) {
6279
6590
  timing: timing2
6280
6591
  });
6281
6592
  stepCallLogger?.complete({
6282
- provider: "chatgpt",
6283
- model: request.model,
6284
- modelVersion,
6285
- step: turn,
6286
- usage: usageTokens,
6287
- costUsd: stepCostUsd,
6288
- responseChars: responseText.length,
6289
- thoughtChars: reasoningSummaryText.length,
6290
- toolCalls: 0,
6291
- finalStep: steeringItems2.length === 0
6593
+ responseText,
6594
+ metadata: {
6595
+ provider: "chatgpt",
6596
+ model: request.model,
6597
+ modelVersion,
6598
+ step: turn,
6599
+ usage: usageTokens,
6600
+ costUsd: stepCostUsd,
6601
+ responseChars: responseText.length,
6602
+ thoughtChars: reasoningSummaryText.length,
6603
+ toolCalls: 0,
6604
+ finalStep: steeringItems2.length === 0
6605
+ }
6292
6606
  });
6293
6607
  if (steeringItems2.length === 0) {
6294
6608
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -6421,25 +6735,33 @@ async function runToolLoop(request) {
6421
6735
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
6422
6736
  const steeringItems = steeringInput.length > 0 ? toChatGptInput(steeringInput).input : [];
6423
6737
  stepCallLogger?.complete({
6424
- provider: "chatgpt",
6425
- model: request.model,
6426
- modelVersion,
6427
- step: turn,
6428
- usage: usageTokens,
6429
- costUsd: stepCostUsd,
6430
- responseChars: responseText.length,
6431
- thoughtChars: reasoningSummaryText.length,
6432
- toolCalls: toolCalls.length,
6433
- finalStep: false
6738
+ responseText,
6739
+ toolCallText: stepToolCallText,
6740
+ metadata: {
6741
+ provider: "chatgpt",
6742
+ model: request.model,
6743
+ modelVersion,
6744
+ step: turn,
6745
+ usage: usageTokens,
6746
+ costUsd: stepCostUsd,
6747
+ responseChars: responseText.length,
6748
+ thoughtChars: reasoningSummaryText.length,
6749
+ toolCalls: toolCalls.length,
6750
+ finalStep: false
6751
+ }
6434
6752
  });
6435
6753
  input = steeringItems.length > 0 ? input.concat(toolOutputs, steeringItems) : input.concat(toolOutputs);
6436
6754
  } catch (error) {
6437
6755
  stepCallLogger?.fail(error, {
6438
- provider: "chatgpt",
6439
- model: request.model,
6440
- modelVersion,
6441
- step: turn,
6442
- usage: usageTokens
6756
+ responseText,
6757
+ toolCallText: stepToolCallText,
6758
+ metadata: {
6759
+ provider: "chatgpt",
6760
+ model: request.model,
6761
+ modelVersion,
6762
+ step: turn,
6763
+ usage: usageTokens
6764
+ }
6443
6765
  });
6444
6766
  throw error;
6445
6767
  }
@@ -6462,6 +6784,7 @@ async function runToolLoop(request) {
6462
6784
  let usageTokens;
6463
6785
  let responseText = "";
6464
6786
  let blocked = false;
6787
+ let stepToolCallText;
6465
6788
  const stepRequestPayload = {
6466
6789
  model: providerInfo.model,
6467
6790
  messages,
@@ -6526,6 +6849,14 @@ async function runToolLoop(request) {
6526
6849
  });
6527
6850
  }
6528
6851
  const responseToolCalls = extractFireworksToolCalls(message);
6852
+ stepToolCallText = serialiseOpenAiStyleToolCallsForLogging(
6853
+ responseToolCalls.map((call) => ({
6854
+ kind: "function",
6855
+ name: call.name,
6856
+ arguments: call.arguments,
6857
+ callId: call.id
6858
+ }))
6859
+ );
6529
6860
  if (responseToolCalls.length === 0) {
6530
6861
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
6531
6862
  const steeringMessages = steeringInput2.length > 0 ? toFireworksMessages(steeringInput2) : [];
@@ -6551,17 +6882,20 @@ async function runToolLoop(request) {
6551
6882
  timing: timing2
6552
6883
  });
6553
6884
  stepCallLogger?.complete({
6554
- provider: "fireworks",
6555
- model: request.model,
6556
- modelVersion,
6557
- step: turn,
6558
- usage: usageTokens,
6559
- costUsd: stepCostUsd,
6560
- blocked,
6561
- responseChars: responseText.length,
6562
- thoughtChars: 0,
6563
- toolCalls: 0,
6564
- finalStep: steeringMessages.length === 0
6885
+ responseText,
6886
+ metadata: {
6887
+ provider: "fireworks",
6888
+ model: request.model,
6889
+ modelVersion,
6890
+ step: turn,
6891
+ usage: usageTokens,
6892
+ costUsd: stepCostUsd,
6893
+ blocked,
6894
+ responseChars: responseText.length,
6895
+ thoughtChars: 0,
6896
+ toolCalls: 0,
6897
+ finalStep: steeringMessages.length === 0
6898
+ }
6565
6899
  });
6566
6900
  if (steeringMessages.length === 0) {
6567
6901
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -6673,17 +7007,21 @@ async function runToolLoop(request) {
6673
7007
  timing
6674
7008
  });
6675
7009
  stepCallLogger?.complete({
6676
- provider: "fireworks",
6677
- model: request.model,
6678
- modelVersion,
6679
- step: turn,
6680
- usage: usageTokens,
6681
- costUsd: stepCostUsd,
6682
- blocked,
6683
- responseChars: responseText.length,
6684
- thoughtChars: 0,
6685
- toolCalls: stepToolCalls.length,
6686
- finalStep: false
7010
+ responseText,
7011
+ toolCallText: stepToolCallText,
7012
+ metadata: {
7013
+ provider: "fireworks",
7014
+ model: request.model,
7015
+ modelVersion,
7016
+ step: turn,
7017
+ usage: usageTokens,
7018
+ costUsd: stepCostUsd,
7019
+ blocked,
7020
+ responseChars: responseText.length,
7021
+ thoughtChars: 0,
7022
+ toolCalls: stepToolCalls.length,
7023
+ finalStep: false
7024
+ }
6687
7025
  });
6688
7026
  messages.push({
6689
7027
  role: "assistant",
@@ -6697,12 +7035,16 @@ async function runToolLoop(request) {
6697
7035
  }
6698
7036
  } catch (error) {
6699
7037
  stepCallLogger?.fail(error, {
6700
- provider: "fireworks",
6701
- model: request.model,
6702
- modelVersion,
6703
- step: turn,
6704
- usage: usageTokens,
6705
- blocked
7038
+ responseText,
7039
+ toolCallText: stepToolCallText,
7040
+ metadata: {
7041
+ provider: "fireworks",
7042
+ model: request.model,
7043
+ modelVersion,
7044
+ step: turn,
7045
+ usage: usageTokens,
7046
+ blocked
7047
+ }
6706
7048
  });
6707
7049
  throw error;
6708
7050
  }
@@ -6722,6 +7064,7 @@ async function runToolLoop(request) {
6722
7064
  let usageTokens;
6723
7065
  let responseText = "";
6724
7066
  let thoughtsText = "";
7067
+ let stepToolCallText;
6725
7068
  const markFirstModelEvent = () => {
6726
7069
  if (firstModelEventAtMs === void 0) {
6727
7070
  firstModelEventAtMs = Date.now();
@@ -6842,12 +7185,17 @@ async function runToolLoop(request) {
6842
7185
  modelVersion = response.modelVersion ?? request.model;
6843
7186
  responseText = response.responseText.trim();
6844
7187
  thoughtsText = response.thoughtsText.trim();
7188
+ const responseOutputAttachments = collectLoggedAttachmentsFromGeminiParts(
7189
+ response.modelParts,
7190
+ "output"
7191
+ );
6845
7192
  const stepCostUsd = estimateCallCostUsd({
6846
7193
  modelId: modelVersion,
6847
7194
  tokens: usageTokens,
6848
7195
  responseImages: 0
6849
7196
  });
6850
7197
  totalCostUsd += stepCostUsd;
7198
+ stepToolCallText = serialiseGeminiToolCallsForLogging(response.functionCalls);
6851
7199
  if (response.functionCalls.length === 0) {
6852
7200
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
6853
7201
  finalText = responseText;
@@ -6873,16 +7221,20 @@ async function runToolLoop(request) {
6873
7221
  timing: timing2
6874
7222
  });
6875
7223
  stepCallLogger?.complete({
6876
- provider: "gemini",
6877
- model: request.model,
6878
- modelVersion,
6879
- step: turn,
6880
- usage: usageTokens,
6881
- costUsd: stepCostUsd,
6882
- responseChars: responseText.length,
6883
- thoughtChars: thoughtsText.length,
6884
- toolCalls: 0,
6885
- finalStep: steeringInput2.length === 0
7224
+ responseText,
7225
+ attachments: responseOutputAttachments,
7226
+ metadata: {
7227
+ provider: "gemini",
7228
+ model: request.model,
7229
+ modelVersion,
7230
+ step: turn,
7231
+ usage: usageTokens,
7232
+ costUsd: stepCostUsd,
7233
+ responseChars: responseText.length,
7234
+ thoughtChars: thoughtsText.length,
7235
+ toolCalls: 0,
7236
+ finalStep: steeringInput2.length === 0
7237
+ }
6886
7238
  });
6887
7239
  if (steeringInput2.length === 0) {
6888
7240
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -7009,16 +7361,21 @@ async function runToolLoop(request) {
7009
7361
  timing
7010
7362
  });
7011
7363
  stepCallLogger?.complete({
7012
- provider: "gemini",
7013
- model: request.model,
7014
- modelVersion,
7015
- step: turn,
7016
- usage: usageTokens,
7017
- costUsd: stepCostUsd,
7018
- responseChars: responseText.length,
7019
- thoughtChars: thoughtsText.length,
7020
- toolCalls: toolCalls.length,
7021
- finalStep: false
7364
+ responseText,
7365
+ attachments: responseOutputAttachments,
7366
+ toolCallText: stepToolCallText,
7367
+ metadata: {
7368
+ provider: "gemini",
7369
+ model: request.model,
7370
+ modelVersion,
7371
+ step: turn,
7372
+ usage: usageTokens,
7373
+ costUsd: stepCostUsd,
7374
+ responseChars: responseText.length,
7375
+ thoughtChars: thoughtsText.length,
7376
+ toolCalls: toolCalls.length,
7377
+ finalStep: false
7378
+ }
7022
7379
  });
7023
7380
  geminiContents.push({ role: "user", parts: responseParts });
7024
7381
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
@@ -7027,13 +7384,17 @@ async function runToolLoop(request) {
7027
7384
  }
7028
7385
  } catch (error) {
7029
7386
  stepCallLogger?.fail(error, {
7030
- provider: "gemini",
7031
- model: request.model,
7032
- modelVersion,
7033
- step: turn,
7034
- usage: usageTokens,
7035
- responseChars: responseText.length,
7036
- thoughtChars: thoughtsText.length
7387
+ responseText,
7388
+ toolCallText: stepToolCallText,
7389
+ metadata: {
7390
+ provider: "gemini",
7391
+ model: request.model,
7392
+ modelVersion,
7393
+ step: turn,
7394
+ usage: usageTokens,
7395
+ responseChars: responseText.length,
7396
+ thoughtChars: thoughtsText.length
7397
+ }
7037
7398
  });
7038
7399
  throw error;
7039
7400
  }