@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.cjs CHANGED
@@ -441,6 +441,34 @@ function estimateCallCostUsd({
441
441
  var import_node_os2 = __toESM(require("os"), 1);
442
442
  var import_node_util = require("util");
443
443
 
444
+ // src/utils/runtimeSingleton.ts
445
+ var runtimeSingletonStoreKey = /* @__PURE__ */ Symbol.for("@ljoukov/llm.runtimeSingletonStore");
446
+ function getRuntimeSingletonStore() {
447
+ const globalObject = globalThis;
448
+ const existingStore = globalObject[runtimeSingletonStoreKey];
449
+ if (existingStore) {
450
+ return existingStore;
451
+ }
452
+ const store = /* @__PURE__ */ new Map();
453
+ Object.defineProperty(globalObject, runtimeSingletonStoreKey, {
454
+ value: store,
455
+ enumerable: false,
456
+ configurable: false,
457
+ writable: false
458
+ });
459
+ return store;
460
+ }
461
+ function getRuntimeSingleton(key, create) {
462
+ const store = getRuntimeSingletonStore();
463
+ const existingValue = store.get(key);
464
+ if (existingValue !== void 0) {
465
+ return existingValue;
466
+ }
467
+ const createdValue = create();
468
+ store.set(key, createdValue);
469
+ return createdValue;
470
+ }
471
+
444
472
  // src/openai/chatgpt-auth.ts
445
473
  var import_node_buffer = require("buffer");
446
474
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -451,14 +479,16 @@ var import_zod = require("zod");
451
479
  // src/utils/env.ts
452
480
  var import_node_fs = __toESM(require("fs"), 1);
453
481
  var import_node_path = __toESM(require("path"), 1);
454
- var envLoaded = false;
482
+ var envState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.envState"), () => ({
483
+ envLoaded: false
484
+ }));
455
485
  function loadLocalEnv() {
456
- if (envLoaded) {
486
+ if (envState.envLoaded) {
457
487
  return;
458
488
  }
459
489
  const envPath = import_node_path.default.join(process.cwd(), ".env.local");
460
490
  loadEnvFromFile(envPath, { override: false });
461
- envLoaded = true;
491
+ envState.envLoaded = true;
462
492
  }
463
493
  function loadEnvFromFile(filePath, { override = false } = {}) {
464
494
  let content;
@@ -544,8 +574,10 @@ var ExchangeResponseSchema = import_zod.z.object({
544
574
  expires_in: import_zod.z.union([import_zod.z.number(), import_zod.z.string()]),
545
575
  id_token: import_zod.z.string().optional()
546
576
  });
547
- var cachedProfile = null;
548
- var refreshPromise = null;
577
+ var chatGptAuthState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.chatGptAuthState"), () => ({
578
+ cachedProfile: null,
579
+ refreshPromise: null
580
+ }));
549
581
  async function fetchChatGptAuthProfileFromTokenProvider(options) {
550
582
  const base = options.baseUrl.replace(/\/+$/u, "");
551
583
  const store = options.store?.trim() ? options.store.trim() : "kv";
@@ -646,13 +678,13 @@ async function getChatGptAuthProfile() {
646
678
  const tokenProviderUrl = process.env[CHATGPT_AUTH_TOKEN_PROVIDER_URL_ENV];
647
679
  const tokenProviderKey = process.env[CHATGPT_AUTH_TOKEN_PROVIDER_API_KEY_ENV] ?? process.env[CHATGPT_AUTH_API_KEY_ENV];
648
680
  if (tokenProviderUrl && tokenProviderUrl.trim().length > 0 && tokenProviderKey && tokenProviderKey.trim().length > 0) {
649
- if (cachedProfile && !isExpired(cachedProfile)) {
650
- return cachedProfile;
681
+ if (chatGptAuthState.cachedProfile && !isExpired(chatGptAuthState.cachedProfile)) {
682
+ return chatGptAuthState.cachedProfile;
651
683
  }
652
- if (refreshPromise) {
653
- return refreshPromise;
684
+ if (chatGptAuthState.refreshPromise) {
685
+ return chatGptAuthState.refreshPromise;
654
686
  }
655
- refreshPromise = (async () => {
687
+ chatGptAuthState.refreshPromise = (async () => {
656
688
  try {
657
689
  const store = process.env[CHATGPT_AUTH_TOKEN_PROVIDER_STORE_ENV];
658
690
  const profile = await fetchChatGptAuthProfileFromTokenProvider({
@@ -660,31 +692,31 @@ async function getChatGptAuthProfile() {
660
692
  apiKey: tokenProviderKey,
661
693
  store: store ?? void 0
662
694
  });
663
- cachedProfile = profile;
695
+ chatGptAuthState.cachedProfile = profile;
664
696
  return profile;
665
697
  } finally {
666
- refreshPromise = null;
698
+ chatGptAuthState.refreshPromise = null;
667
699
  }
668
700
  })();
669
- return refreshPromise;
701
+ return chatGptAuthState.refreshPromise;
670
702
  }
671
- if (cachedProfile && !isExpired(cachedProfile)) {
672
- return cachedProfile;
703
+ if (chatGptAuthState.cachedProfile && !isExpired(chatGptAuthState.cachedProfile)) {
704
+ return chatGptAuthState.cachedProfile;
673
705
  }
674
- if (refreshPromise) {
675
- return refreshPromise;
706
+ if (chatGptAuthState.refreshPromise) {
707
+ return chatGptAuthState.refreshPromise;
676
708
  }
677
- refreshPromise = (async () => {
709
+ chatGptAuthState.refreshPromise = (async () => {
678
710
  try {
679
- const baseProfile = cachedProfile ?? loadAuthProfileFromCodexStore();
711
+ const baseProfile = chatGptAuthState.cachedProfile ?? loadAuthProfileFromCodexStore();
680
712
  const profile = isExpired(baseProfile) ? await refreshAndPersistCodexProfile(baseProfile) : baseProfile;
681
- cachedProfile = profile;
713
+ chatGptAuthState.cachedProfile = profile;
682
714
  return profile;
683
715
  } finally {
684
- refreshPromise = null;
716
+ chatGptAuthState.refreshPromise = null;
685
717
  }
686
718
  })();
687
- return refreshPromise;
719
+ return chatGptAuthState.refreshPromise;
688
720
  }
689
721
  function resolveCodexHome() {
690
722
  const codexHome = process.env.CODEX_HOME;
@@ -1416,8 +1448,10 @@ function createAbortError(reason) {
1416
1448
  // src/openai/chatgpt-codex.ts
1417
1449
  var CHATGPT_CODEX_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
1418
1450
  var CHATGPT_RESPONSES_EXPERIMENTAL_HEADER = "responses=experimental";
1419
- var cachedResponsesWebSocketMode = null;
1420
- var chatGptResponsesWebSocketDisabled = false;
1451
+ var chatGptCodexState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.chatGptCodexState"), () => ({
1452
+ cachedResponsesWebSocketMode: null,
1453
+ chatGptResponsesWebSocketDisabled: false
1454
+ }));
1421
1455
  async function streamChatGptCodexResponse(options) {
1422
1456
  const { access, accountId } = await getChatGptAuthProfile();
1423
1457
  const mode = resolveChatGptResponsesWebSocketMode();
@@ -1443,7 +1477,7 @@ async function streamChatGptCodexResponse(options) {
1443
1477
  }
1444
1478
  };
1445
1479
  };
1446
- if (mode === "off" || chatGptResponsesWebSocketDisabled) {
1480
+ if (mode === "off" || chatGptCodexState.chatGptResponsesWebSocketDisabled) {
1447
1481
  return fallbackStreamFactory();
1448
1482
  }
1449
1483
  const websocketHeaders = buildChatGptCodexHeaders({
@@ -1462,7 +1496,7 @@ async function streamChatGptCodexResponse(options) {
1462
1496
  }),
1463
1497
  createFallbackStream: fallbackStreamFactory,
1464
1498
  onWebSocketFallback: () => {
1465
- chatGptResponsesWebSocketDisabled = true;
1499
+ chatGptCodexState.chatGptResponsesWebSocketDisabled = true;
1466
1500
  }
1467
1501
  });
1468
1502
  }
@@ -1492,14 +1526,14 @@ async function streamChatGptCodexResponseSse(options) {
1492
1526
  return parseEventStream(body);
1493
1527
  }
1494
1528
  function resolveChatGptResponsesWebSocketMode() {
1495
- if (cachedResponsesWebSocketMode) {
1496
- return cachedResponsesWebSocketMode;
1529
+ if (chatGptCodexState.cachedResponsesWebSocketMode) {
1530
+ return chatGptCodexState.cachedResponsesWebSocketMode;
1497
1531
  }
1498
- cachedResponsesWebSocketMode = resolveResponsesWebSocketMode(
1532
+ chatGptCodexState.cachedResponsesWebSocketMode = resolveResponsesWebSocketMode(
1499
1533
  process.env.CHATGPT_RESPONSES_WEBSOCKET_MODE ?? process.env.OPENAI_RESPONSES_WEBSOCKET_MODE,
1500
1534
  "auto"
1501
1535
  );
1502
- return cachedResponsesWebSocketMode;
1536
+ return chatGptCodexState.cachedResponsesWebSocketMode;
1503
1537
  }
1504
1538
  function buildChatGptCodexHeaders(options) {
1505
1539
  const openAiBeta = options.useWebSocket ? mergeOpenAiBetaHeader(
@@ -1537,8 +1571,8 @@ async function collectChatGptCodexResponse(options) {
1537
1571
  }
1538
1572
  });
1539
1573
  } catch (error) {
1540
- if (!sawAnyDelta && !retriedViaSseFallback && shouldRetryViaSseFallback(error) && !chatGptResponsesWebSocketDisabled) {
1541
- chatGptResponsesWebSocketDisabled = true;
1574
+ if (!sawAnyDelta && !retriedViaSseFallback && shouldRetryViaSseFallback(error) && !chatGptCodexState.chatGptResponsesWebSocketDisabled) {
1575
+ chatGptCodexState.chatGptResponsesWebSocketDisabled = true;
1542
1576
  retriedViaSseFallback = true;
1543
1577
  continue;
1544
1578
  }
@@ -1774,7 +1808,12 @@ var MODEL_CONCURRENCY_PROVIDERS = [
1774
1808
  "google",
1775
1809
  "fireworks"
1776
1810
  ];
1777
- var configuredModelConcurrency = normalizeModelConcurrencyConfig({});
1811
+ var modelConcurrencyState = getRuntimeSingleton(
1812
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.modelConcurrencyState"),
1813
+ () => ({
1814
+ configuredModelConcurrency: normalizeModelConcurrencyConfig({})
1815
+ })
1816
+ );
1778
1817
  function clampModelConcurrencyCap(value) {
1779
1818
  if (!Number.isFinite(value)) {
1780
1819
  return DEFAULT_MODEL_CONCURRENCY_CAP;
@@ -1848,14 +1887,14 @@ function resolveDefaultProviderCap(provider, modelId) {
1848
1887
  return DEFAULT_FIREWORKS_MODEL_CONCURRENCY_CAP;
1849
1888
  }
1850
1889
  function configureModelConcurrency(config = {}) {
1851
- configuredModelConcurrency = normalizeModelConcurrencyConfig(config);
1890
+ modelConcurrencyState.configuredModelConcurrency = normalizeModelConcurrencyConfig(config);
1852
1891
  }
1853
1892
  function resetModelConcurrencyConfig() {
1854
- configuredModelConcurrency = normalizeModelConcurrencyConfig({});
1893
+ modelConcurrencyState.configuredModelConcurrency = normalizeModelConcurrencyConfig({});
1855
1894
  }
1856
1895
  function resolveModelConcurrencyCap(options) {
1857
1896
  const modelId = options.modelId ? normalizeModelIdForConfig(options.modelId) : void 0;
1858
- const config = options.config ? normalizeModelConcurrencyConfig(options.config) : configuredModelConcurrency;
1897
+ const config = options.config ? normalizeModelConcurrencyConfig(options.config) : modelConcurrencyState.configuredModelConcurrency;
1859
1898
  const providerModelCap = modelId ? config.providerModelCaps[options.provider].get(modelId) : void 0;
1860
1899
  if (providerModelCap !== void 0) {
1861
1900
  return providerModelCap;
@@ -2089,32 +2128,37 @@ var import_openai = __toESM(require("openai"), 1);
2089
2128
  var import_undici = require("undici");
2090
2129
  var DEFAULT_FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
2091
2130
  var DEFAULT_FIREWORKS_TIMEOUT_MS = 15 * 6e4;
2092
- var cachedClient = null;
2093
- var cachedFetch = null;
2094
- var cachedBaseUrl = null;
2095
- var cachedApiKey = null;
2096
- var cachedTimeoutMs = null;
2131
+ var fireworksClientState = getRuntimeSingleton(
2132
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.fireworksClientState"),
2133
+ () => ({
2134
+ cachedClient: null,
2135
+ cachedFetch: null,
2136
+ cachedBaseUrl: null,
2137
+ cachedApiKey: null,
2138
+ cachedTimeoutMs: null
2139
+ })
2140
+ );
2097
2141
  function resolveTimeoutMs() {
2098
- if (cachedTimeoutMs !== null) {
2099
- return cachedTimeoutMs;
2142
+ if (fireworksClientState.cachedTimeoutMs !== null) {
2143
+ return fireworksClientState.cachedTimeoutMs;
2100
2144
  }
2101
2145
  const raw = process.env.FIREWORKS_TIMEOUT_MS;
2102
2146
  const parsed = raw ? Number(raw) : Number.NaN;
2103
- cachedTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FIREWORKS_TIMEOUT_MS;
2104
- return cachedTimeoutMs;
2147
+ fireworksClientState.cachedTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FIREWORKS_TIMEOUT_MS;
2148
+ return fireworksClientState.cachedTimeoutMs;
2105
2149
  }
2106
2150
  function resolveBaseUrl() {
2107
- if (cachedBaseUrl !== null) {
2108
- return cachedBaseUrl;
2151
+ if (fireworksClientState.cachedBaseUrl !== null) {
2152
+ return fireworksClientState.cachedBaseUrl;
2109
2153
  }
2110
2154
  loadLocalEnv();
2111
2155
  const raw = process.env.FIREWORKS_BASE_URL?.trim();
2112
- cachedBaseUrl = raw && raw.length > 0 ? raw : DEFAULT_FIREWORKS_BASE_URL;
2113
- return cachedBaseUrl;
2156
+ fireworksClientState.cachedBaseUrl = raw && raw.length > 0 ? raw : DEFAULT_FIREWORKS_BASE_URL;
2157
+ return fireworksClientState.cachedBaseUrl;
2114
2158
  }
2115
2159
  function resolveApiKey() {
2116
- if (cachedApiKey !== null) {
2117
- return cachedApiKey;
2160
+ if (fireworksClientState.cachedApiKey !== null) {
2161
+ return fireworksClientState.cachedApiKey;
2118
2162
  }
2119
2163
  loadLocalEnv();
2120
2164
  const raw = process.env.FIREWORKS_TOKEN ?? process.env.FIREWORKS_API_KEY;
@@ -2124,46 +2168,51 @@ function resolveApiKey() {
2124
2168
  "FIREWORKS_TOKEN (or FIREWORKS_API_KEY) must be provided to access Fireworks APIs."
2125
2169
  );
2126
2170
  }
2127
- cachedApiKey = token;
2128
- return cachedApiKey;
2171
+ fireworksClientState.cachedApiKey = token;
2172
+ return fireworksClientState.cachedApiKey;
2129
2173
  }
2130
2174
  function getFireworksFetch() {
2131
- if (cachedFetch) {
2132
- return cachedFetch;
2175
+ if (fireworksClientState.cachedFetch) {
2176
+ return fireworksClientState.cachedFetch;
2133
2177
  }
2134
2178
  const timeoutMs = resolveTimeoutMs();
2135
2179
  const dispatcher = new import_undici.Agent({
2136
2180
  bodyTimeout: timeoutMs,
2137
2181
  headersTimeout: timeoutMs
2138
2182
  });
2139
- cachedFetch = ((input, init) => {
2183
+ fireworksClientState.cachedFetch = ((input, init) => {
2140
2184
  return (0, import_undici.fetch)(input, {
2141
2185
  ...init ?? {},
2142
2186
  dispatcher
2143
2187
  });
2144
2188
  });
2145
- return cachedFetch;
2189
+ return fireworksClientState.cachedFetch;
2146
2190
  }
2147
2191
  function getFireworksClient() {
2148
- if (cachedClient) {
2149
- return cachedClient;
2192
+ if (fireworksClientState.cachedClient) {
2193
+ return fireworksClientState.cachedClient;
2150
2194
  }
2151
- cachedClient = new import_openai.default({
2195
+ fireworksClientState.cachedClient = new import_openai.default({
2152
2196
  apiKey: resolveApiKey(),
2153
2197
  baseURL: resolveBaseUrl(),
2154
2198
  timeout: resolveTimeoutMs(),
2155
2199
  fetch: getFireworksFetch()
2156
2200
  });
2157
- return cachedClient;
2201
+ return fireworksClientState.cachedClient;
2158
2202
  }
2159
2203
 
2160
2204
  // src/fireworks/calls.ts
2161
2205
  var DEFAULT_SCHEDULER_KEY = "__default__";
2162
- var schedulerByModel = /* @__PURE__ */ new Map();
2206
+ var fireworksCallState = getRuntimeSingleton(
2207
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.fireworksCallState"),
2208
+ () => ({
2209
+ schedulerByModel: /* @__PURE__ */ new Map()
2210
+ })
2211
+ );
2163
2212
  function getSchedulerForModel(modelId) {
2164
2213
  const normalizedModelId = modelId?.trim();
2165
2214
  const schedulerKey = normalizedModelId && normalizedModelId.length > 0 ? normalizedModelId : DEFAULT_SCHEDULER_KEY;
2166
- const existing = schedulerByModel.get(schedulerKey);
2215
+ const existing = fireworksCallState.schedulerByModel.get(schedulerKey);
2167
2216
  if (existing) {
2168
2217
  return existing;
2169
2218
  }
@@ -2175,7 +2224,7 @@ function getSchedulerForModel(modelId) {
2175
2224
  minIntervalBetweenStartMs: 200,
2176
2225
  startJitterMs: 200
2177
2226
  });
2178
- schedulerByModel.set(schedulerKey, created);
2227
+ fireworksCallState.schedulerByModel.set(schedulerKey, created);
2179
2228
  return created;
2180
2229
  }
2181
2230
  async function runFireworksCall(fn, modelId, runOptions) {
@@ -2222,7 +2271,10 @@ var ServiceAccountSchema = import_zod2.z.object({
2222
2271
  privateKey: private_key.replace(/\\n/g, "\n"),
2223
2272
  tokenUri: token_uri
2224
2273
  }));
2225
- var cachedServiceAccount = null;
2274
+ var googleAuthState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.googleAuthState"), () => ({
2275
+ cachedServiceAccount: null,
2276
+ authClientCache: /* @__PURE__ */ new Map()
2277
+ }));
2226
2278
  function parseGoogleServiceAccount(input) {
2227
2279
  let parsed;
2228
2280
  try {
@@ -2233,16 +2285,16 @@ function parseGoogleServiceAccount(input) {
2233
2285
  return ServiceAccountSchema.parse(parsed);
2234
2286
  }
2235
2287
  function getGoogleServiceAccount() {
2236
- if (cachedServiceAccount) {
2237
- return cachedServiceAccount;
2288
+ if (googleAuthState.cachedServiceAccount) {
2289
+ return googleAuthState.cachedServiceAccount;
2238
2290
  }
2239
2291
  loadLocalEnv();
2240
2292
  const raw = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
2241
2293
  if (!raw || raw.trim().length === 0) {
2242
2294
  throw new Error("GOOGLE_SERVICE_ACCOUNT_JSON must be provided for Google APIs access.");
2243
2295
  }
2244
- cachedServiceAccount = parseGoogleServiceAccount(raw);
2245
- return cachedServiceAccount;
2296
+ googleAuthState.cachedServiceAccount = parseGoogleServiceAccount(raw);
2297
+ return googleAuthState.cachedServiceAccount;
2246
2298
  }
2247
2299
  function normaliseScopes(scopes) {
2248
2300
  if (!scopes) {
@@ -2294,8 +2346,10 @@ function isGeminiImageModelId(value) {
2294
2346
  }
2295
2347
  var CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
2296
2348
  var DEFAULT_VERTEX_LOCATION = "global";
2297
- var geminiConfiguration = {};
2298
- var clientPromise;
2349
+ var geminiClientState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.geminiClientState"), () => ({
2350
+ geminiConfiguration: {},
2351
+ clientPromise: void 0
2352
+ }));
2299
2353
  function normaliseConfigValue(value) {
2300
2354
  if (value === void 0 || value === null) {
2301
2355
  return void 0;
@@ -2306,14 +2360,14 @@ function normaliseConfigValue(value) {
2306
2360
  function configureGemini(options = {}) {
2307
2361
  const nextProjectId = normaliseConfigValue(options.projectId);
2308
2362
  const nextLocation = normaliseConfigValue(options.location);
2309
- geminiConfiguration = {
2310
- projectId: nextProjectId !== void 0 ? nextProjectId : geminiConfiguration.projectId,
2311
- location: nextLocation !== void 0 ? nextLocation : geminiConfiguration.location
2363
+ geminiClientState.geminiConfiguration = {
2364
+ projectId: nextProjectId !== void 0 ? nextProjectId : geminiClientState.geminiConfiguration.projectId,
2365
+ location: nextLocation !== void 0 ? nextLocation : geminiClientState.geminiConfiguration.location
2312
2366
  };
2313
- clientPromise = void 0;
2367
+ geminiClientState.clientPromise = void 0;
2314
2368
  }
2315
2369
  function resolveProjectId() {
2316
- const override = geminiConfiguration.projectId;
2370
+ const override = geminiClientState.geminiConfiguration.projectId;
2317
2371
  if (override) {
2318
2372
  return override;
2319
2373
  }
@@ -2321,15 +2375,15 @@ function resolveProjectId() {
2321
2375
  return serviceAccount.projectId;
2322
2376
  }
2323
2377
  function resolveLocation() {
2324
- const override = geminiConfiguration.location;
2378
+ const override = geminiClientState.geminiConfiguration.location;
2325
2379
  if (override) {
2326
2380
  return override;
2327
2381
  }
2328
2382
  return DEFAULT_VERTEX_LOCATION;
2329
2383
  }
2330
2384
  async function getGeminiClient() {
2331
- if (!clientPromise) {
2332
- clientPromise = Promise.resolve().then(() => {
2385
+ if (!geminiClientState.clientPromise) {
2386
+ geminiClientState.clientPromise = Promise.resolve().then(() => {
2333
2387
  const projectId = resolveProjectId();
2334
2388
  const location = resolveLocation();
2335
2389
  const googleAuthOptions = getGoogleAuthOptions(CLOUD_PLATFORM_SCOPE);
@@ -2341,7 +2395,7 @@ async function getGeminiClient() {
2341
2395
  });
2342
2396
  });
2343
2397
  }
2344
- return clientPromise;
2398
+ return geminiClientState.clientPromise;
2345
2399
  }
2346
2400
 
2347
2401
  // src/google/calls.ts
@@ -2537,11 +2591,13 @@ function retryDelayMs(attempt) {
2537
2591
  return base + jitter;
2538
2592
  }
2539
2593
  var DEFAULT_SCHEDULER_KEY2 = "__default__";
2540
- var schedulerByModel2 = /* @__PURE__ */ new Map();
2594
+ var googleCallState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.googleCallState"), () => ({
2595
+ schedulerByModel: /* @__PURE__ */ new Map()
2596
+ }));
2541
2597
  function getSchedulerForModel2(modelId) {
2542
2598
  const normalizedModelId = modelId?.trim();
2543
2599
  const schedulerKey = normalizedModelId && normalizedModelId.length > 0 ? normalizedModelId : DEFAULT_SCHEDULER_KEY2;
2544
- const existing = schedulerByModel2.get(schedulerKey);
2600
+ const existing = googleCallState.schedulerByModel.get(schedulerKey);
2545
2601
  if (existing) {
2546
2602
  return existing;
2547
2603
  }
@@ -2564,7 +2620,7 @@ function getSchedulerForModel2(modelId) {
2564
2620
  }
2565
2621
  }
2566
2622
  });
2567
- schedulerByModel2.set(schedulerKey, created);
2623
+ googleCallState.schedulerByModel.set(schedulerKey, created);
2568
2624
  return created;
2569
2625
  }
2570
2626
  async function runGeminiCall(fn, modelId, runOptions) {
@@ -2574,53 +2630,55 @@ async function runGeminiCall(fn, modelId, runOptions) {
2574
2630
  // src/openai/client.ts
2575
2631
  var import_openai2 = __toESM(require("openai"), 1);
2576
2632
  var import_undici2 = require("undici");
2577
- var cachedApiKey2 = null;
2578
- var cachedClient2 = null;
2579
- var cachedFetch2 = null;
2580
- var cachedTimeoutMs2 = null;
2581
- var openAiResponsesWebSocketMode = null;
2582
- var openAiResponsesWebSocketDisabled = false;
2633
+ var openAiClientState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.openAiClientState"), () => ({
2634
+ cachedApiKey: null,
2635
+ cachedClient: null,
2636
+ cachedFetch: null,
2637
+ cachedTimeoutMs: null,
2638
+ openAiResponsesWebSocketMode: null,
2639
+ openAiResponsesWebSocketDisabled: false
2640
+ }));
2583
2641
  var DEFAULT_OPENAI_TIMEOUT_MS = 15 * 6e4;
2584
2642
  function resolveOpenAiTimeoutMs() {
2585
- if (cachedTimeoutMs2 !== null) {
2586
- return cachedTimeoutMs2;
2643
+ if (openAiClientState.cachedTimeoutMs !== null) {
2644
+ return openAiClientState.cachedTimeoutMs;
2587
2645
  }
2588
2646
  const raw = process.env.OPENAI_STREAM_TIMEOUT_MS ?? process.env.OPENAI_TIMEOUT_MS;
2589
2647
  const parsed = raw ? Number(raw) : Number.NaN;
2590
- cachedTimeoutMs2 = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OPENAI_TIMEOUT_MS;
2591
- return cachedTimeoutMs2;
2648
+ openAiClientState.cachedTimeoutMs = Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OPENAI_TIMEOUT_MS;
2649
+ return openAiClientState.cachedTimeoutMs;
2592
2650
  }
2593
2651
  function getOpenAiFetch() {
2594
- if (cachedFetch2) {
2595
- return cachedFetch2;
2652
+ if (openAiClientState.cachedFetch) {
2653
+ return openAiClientState.cachedFetch;
2596
2654
  }
2597
2655
  const timeoutMs = resolveOpenAiTimeoutMs();
2598
2656
  const dispatcher = new import_undici2.Agent({
2599
2657
  bodyTimeout: timeoutMs,
2600
2658
  headersTimeout: timeoutMs
2601
2659
  });
2602
- cachedFetch2 = ((input, init) => {
2660
+ openAiClientState.cachedFetch = ((input, init) => {
2603
2661
  return (0, import_undici2.fetch)(input, {
2604
2662
  ...init ?? {},
2605
2663
  dispatcher
2606
2664
  });
2607
2665
  });
2608
- return cachedFetch2;
2666
+ return openAiClientState.cachedFetch;
2609
2667
  }
2610
2668
  function resolveOpenAiBaseUrl() {
2611
2669
  loadLocalEnv();
2612
2670
  return process.env.OPENAI_BASE_URL?.trim() || "https://api.openai.com/v1";
2613
2671
  }
2614
2672
  function resolveOpenAiResponsesWebSocketMode() {
2615
- if (openAiResponsesWebSocketMode) {
2616
- return openAiResponsesWebSocketMode;
2673
+ if (openAiClientState.openAiResponsesWebSocketMode) {
2674
+ return openAiClientState.openAiResponsesWebSocketMode;
2617
2675
  }
2618
2676
  loadLocalEnv();
2619
- openAiResponsesWebSocketMode = resolveResponsesWebSocketMode(
2677
+ openAiClientState.openAiResponsesWebSocketMode = resolveResponsesWebSocketMode(
2620
2678
  process.env.OPENAI_RESPONSES_WEBSOCKET_MODE,
2621
2679
  "auto"
2622
2680
  );
2623
- return openAiResponsesWebSocketMode;
2681
+ return openAiClientState.openAiResponsesWebSocketMode;
2624
2682
  }
2625
2683
  function wrapFallbackStream(stream) {
2626
2684
  return {
@@ -2673,7 +2731,7 @@ function installResponsesWebSocketTransport(client, apiKey) {
2673
2731
  responsesApi.stream = (request, options) => {
2674
2732
  const mode = resolveOpenAiResponsesWebSocketMode();
2675
2733
  const fallbackStreamFactory = () => wrapFallbackStream(originalStream(request, options));
2676
- if (mode === "off" || openAiResponsesWebSocketDisabled) {
2734
+ if (mode === "off" || openAiClientState.openAiResponsesWebSocketDisabled) {
2677
2735
  return fallbackStreamFactory();
2678
2736
  }
2679
2737
  const signal = options && typeof options === "object" ? options.signal ?? void 0 : void 0;
@@ -2692,15 +2750,15 @@ function installResponsesWebSocketTransport(client, apiKey) {
2692
2750
  createFallbackStream: fallbackStreamFactory,
2693
2751
  onWebSocketFallback: (error) => {
2694
2752
  if (isResponsesWebSocketUnsupportedError(error)) {
2695
- openAiResponsesWebSocketDisabled = true;
2753
+ openAiClientState.openAiResponsesWebSocketDisabled = true;
2696
2754
  }
2697
2755
  }
2698
2756
  });
2699
2757
  };
2700
2758
  }
2701
2759
  function getOpenAiApiKey() {
2702
- if (cachedApiKey2 !== null) {
2703
- return cachedApiKey2;
2760
+ if (openAiClientState.cachedApiKey !== null) {
2761
+ return openAiClientState.cachedApiKey;
2704
2762
  }
2705
2763
  loadLocalEnv();
2706
2764
  const raw = process.env.OPENAI_API_KEY;
@@ -2708,33 +2766,35 @@ function getOpenAiApiKey() {
2708
2766
  if (!value) {
2709
2767
  throw new Error("OPENAI_API_KEY must be provided to access OpenAI APIs.");
2710
2768
  }
2711
- cachedApiKey2 = value;
2712
- return cachedApiKey2;
2769
+ openAiClientState.cachedApiKey = value;
2770
+ return openAiClientState.cachedApiKey;
2713
2771
  }
2714
2772
  function getOpenAiClient() {
2715
- if (cachedClient2) {
2716
- return cachedClient2;
2773
+ if (openAiClientState.cachedClient) {
2774
+ return openAiClientState.cachedClient;
2717
2775
  }
2718
2776
  loadLocalEnv();
2719
2777
  const apiKey = getOpenAiApiKey();
2720
2778
  const timeoutMs = resolveOpenAiTimeoutMs();
2721
- cachedClient2 = new import_openai2.default({
2779
+ openAiClientState.cachedClient = new import_openai2.default({
2722
2780
  apiKey,
2723
2781
  fetch: getOpenAiFetch(),
2724
2782
  timeout: timeoutMs
2725
2783
  });
2726
- installResponsesWebSocketTransport(cachedClient2, apiKey);
2727
- return cachedClient2;
2784
+ installResponsesWebSocketTransport(openAiClientState.cachedClient, apiKey);
2785
+ return openAiClientState.cachedClient;
2728
2786
  }
2729
2787
 
2730
2788
  // src/openai/calls.ts
2731
2789
  var DEFAULT_OPENAI_REASONING_EFFORT = "medium";
2732
2790
  var DEFAULT_SCHEDULER_KEY3 = "__default__";
2733
- var schedulerByModel3 = /* @__PURE__ */ new Map();
2791
+ var openAiCallState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.openAiCallState"), () => ({
2792
+ schedulerByModel: /* @__PURE__ */ new Map()
2793
+ }));
2734
2794
  function getSchedulerForModel3(modelId) {
2735
2795
  const normalizedModelId = modelId?.trim();
2736
2796
  const schedulerKey = normalizedModelId && normalizedModelId.length > 0 ? normalizedModelId : DEFAULT_SCHEDULER_KEY3;
2737
- const existing = schedulerByModel3.get(schedulerKey);
2797
+ const existing = openAiCallState.schedulerByModel.get(schedulerKey);
2738
2798
  if (existing) {
2739
2799
  return existing;
2740
2800
  }
@@ -2746,7 +2806,7 @@ function getSchedulerForModel3(modelId) {
2746
2806
  minIntervalBetweenStartMs: 200,
2747
2807
  startJitterMs: 200
2748
2808
  });
2749
- schedulerByModel3.set(schedulerKey, created);
2809
+ openAiCallState.schedulerByModel.set(schedulerKey, created);
2750
2810
  return created;
2751
2811
  }
2752
2812
  async function runOpenAiCall(fn, modelId, runOptions) {
@@ -2818,6 +2878,9 @@ function ensureTrailingNewline(value) {
2818
2878
  return value.endsWith("\n") ? value : `${value}
2819
2879
  `;
2820
2880
  }
2881
+ function hasNonEmptyText(value) {
2882
+ return typeof value === "string" && value.length > 0;
2883
+ }
2821
2884
  function redactDataUrlPayload(value) {
2822
2885
  if (!value.toLowerCase().startsWith("data:")) {
2823
2886
  return value;
@@ -3066,6 +3129,25 @@ var AgentLoggingSessionImpl = class {
3066
3129
  }
3067
3130
  this.enqueueLineWrite(timestamped);
3068
3131
  }
3132
+ async writeAttachments(baseDir, attachments) {
3133
+ const usedNames = /* @__PURE__ */ new Set();
3134
+ for (const attachment of attachments ?? []) {
3135
+ let filename = normalisePathSegment(attachment.filename);
3136
+ if (!filename.includes(".")) {
3137
+ filename = `${filename}.bin`;
3138
+ }
3139
+ const ext = import_node_path3.default.extname(filename);
3140
+ const base = ext.length > 0 ? filename.slice(0, -ext.length) : filename;
3141
+ let candidate = filename;
3142
+ let duplicateIndex = 2;
3143
+ while (usedNames.has(candidate)) {
3144
+ candidate = `${base}-${duplicateIndex.toString()}${ext}`;
3145
+ duplicateIndex += 1;
3146
+ }
3147
+ usedNames.add(candidate);
3148
+ await (0, import_promises.writeFile)(import_node_path3.default.join(baseDir, candidate), attachment.bytes);
3149
+ }
3150
+ }
3069
3151
  startLlmCall(input) {
3070
3152
  const callNumber = this.callCounter + 1;
3071
3153
  this.callCounter = callNumber;
@@ -3078,6 +3160,9 @@ var AgentLoggingSessionImpl = class {
3078
3160
  );
3079
3161
  const responsePath = import_node_path3.default.join(baseDir, "response.txt");
3080
3162
  const thoughtsPath = import_node_path3.default.join(baseDir, "thoughts.txt");
3163
+ const toolCallPath = import_node_path3.default.join(baseDir, "tool_call.txt");
3164
+ const toolCallResponsePath = import_node_path3.default.join(baseDir, "tool_call_response.txt");
3165
+ const errorPath = import_node_path3.default.join(baseDir, "error.txt");
3081
3166
  const responseMetadataPath = import_node_path3.default.join(baseDir, "response.metadata.json");
3082
3167
  let chain = this.ensureReady.then(async () => {
3083
3168
  await (0, import_promises.mkdir)(baseDir, { recursive: true });
@@ -3099,22 +3184,13 @@ var AgentLoggingSessionImpl = class {
3099
3184
  `,
3100
3185
  "utf8"
3101
3186
  );
3102
- const usedNames = /* @__PURE__ */ new Set();
3103
- for (const attachment of input.attachments ?? []) {
3104
- let filename = normalisePathSegment(attachment.filename);
3105
- if (!filename.includes(".")) {
3106
- filename = `${filename}.bin`;
3107
- }
3108
- const ext = import_node_path3.default.extname(filename);
3109
- const base = ext.length > 0 ? filename.slice(0, -ext.length) : filename;
3110
- let candidate = filename;
3111
- let duplicateIndex = 2;
3112
- while (usedNames.has(candidate)) {
3113
- candidate = `${base}-${duplicateIndex.toString()}${ext}`;
3114
- duplicateIndex += 1;
3115
- }
3116
- usedNames.add(candidate);
3117
- await (0, import_promises.writeFile)(import_node_path3.default.join(baseDir, candidate), attachment.bytes);
3187
+ await this.writeAttachments(baseDir, input.attachments);
3188
+ if (hasNonEmptyText(input.toolCallResponseText)) {
3189
+ await (0, import_promises.writeFile)(
3190
+ toolCallResponsePath,
3191
+ ensureTrailingNewline(input.toolCallResponseText),
3192
+ "utf8"
3193
+ );
3118
3194
  }
3119
3195
  }).catch(() => void 0);
3120
3196
  this.track(chain);
@@ -3142,18 +3218,25 @@ var AgentLoggingSessionImpl = class {
3142
3218
  await (0, import_promises.appendFile)(responsePath, text, "utf8");
3143
3219
  });
3144
3220
  },
3145
- complete: (metadata) => {
3221
+ complete: (options) => {
3146
3222
  if (closed) {
3147
3223
  return;
3148
3224
  }
3149
3225
  closed = true;
3150
3226
  enqueue(async () => {
3227
+ if (hasNonEmptyText(options?.responseText)) {
3228
+ await (0, import_promises.writeFile)(responsePath, options.responseText, "utf8");
3229
+ }
3230
+ if (hasNonEmptyText(options?.toolCallText)) {
3231
+ await (0, import_promises.writeFile)(toolCallPath, ensureTrailingNewline(options.toolCallText), "utf8");
3232
+ }
3233
+ await this.writeAttachments(baseDir, options?.attachments);
3151
3234
  const payload = {
3152
3235
  capturedAt: toIsoNow(),
3153
3236
  status: "completed"
3154
3237
  };
3155
- if (metadata) {
3156
- const sanitised = sanitiseLogValue(metadata);
3238
+ if (options?.metadata) {
3239
+ const sanitised = sanitiseLogValue(options.metadata);
3157
3240
  if (sanitised && typeof sanitised === "object" && !Array.isArray(sanitised)) {
3158
3241
  Object.assign(payload, sanitised);
3159
3242
  } else if (sanitised !== void 0) {
@@ -3164,19 +3247,27 @@ var AgentLoggingSessionImpl = class {
3164
3247
  `, "utf8");
3165
3248
  });
3166
3249
  },
3167
- fail: (error, metadata) => {
3250
+ fail: (error, options) => {
3168
3251
  if (closed) {
3169
3252
  return;
3170
3253
  }
3171
3254
  closed = true;
3172
3255
  enqueue(async () => {
3256
+ if (hasNonEmptyText(options?.responseText)) {
3257
+ await (0, import_promises.writeFile)(responsePath, options.responseText, "utf8");
3258
+ }
3259
+ if (hasNonEmptyText(options?.toolCallText)) {
3260
+ await (0, import_promises.writeFile)(toolCallPath, ensureTrailingNewline(options.toolCallText), "utf8");
3261
+ }
3262
+ await this.writeAttachments(baseDir, options?.attachments);
3263
+ await (0, import_promises.writeFile)(errorPath, ensureTrailingNewline(toErrorMessage(error)), "utf8");
3173
3264
  const payload = {
3174
3265
  capturedAt: toIsoNow(),
3175
3266
  status: "failed",
3176
3267
  error: toErrorMessage(error)
3177
3268
  };
3178
- if (metadata) {
3179
- const sanitised = sanitiseLogValue(metadata);
3269
+ if (options?.metadata) {
3270
+ const sanitised = sanitiseLogValue(options.metadata);
3180
3271
  if (sanitised && typeof sanitised === "object" && !Array.isArray(sanitised)) {
3181
3272
  Object.assign(payload, sanitised);
3182
3273
  } else if (sanitised !== void 0) {
@@ -3201,7 +3292,10 @@ var AgentLoggingSessionImpl = class {
3201
3292
  }
3202
3293
  }
3203
3294
  };
3204
- var loggingSessionStorage = new import_node_async_hooks.AsyncLocalStorage();
3295
+ var loggingSessionStorage = getRuntimeSingleton(
3296
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.agentLogging.sessionStorage"),
3297
+ () => new import_node_async_hooks.AsyncLocalStorage()
3298
+ );
3205
3299
  function createAgentLoggingSession(config) {
3206
3300
  return new AgentLoggingSessionImpl(config);
3207
3301
  }
@@ -3216,7 +3310,10 @@ function getCurrentAgentLoggingSession() {
3216
3310
  }
3217
3311
 
3218
3312
  // src/llm.ts
3219
- var toolCallContextStorage = new import_node_async_hooks2.AsyncLocalStorage();
3313
+ var toolCallContextStorage = getRuntimeSingleton(
3314
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.toolCallContextStorage"),
3315
+ () => new import_node_async_hooks2.AsyncLocalStorage()
3316
+ );
3220
3317
  function getCurrentToolCallContext() {
3221
3318
  return toolCallContextStorage.getStore() ?? null;
3222
3319
  }
@@ -5019,7 +5116,10 @@ function resolveAttachmentExtension(mimeType) {
5019
5116
  }
5020
5117
  }
5021
5118
  }
5022
- function decodeDataUrlAttachment(value, basename) {
5119
+ function buildLoggedAttachmentFilename(prefix, index, mimeType) {
5120
+ return `${prefix}-${index.toString()}.${resolveAttachmentExtension(mimeType)}`;
5121
+ }
5122
+ function decodeDataUrlAttachment(value, options) {
5023
5123
  const trimmed = value.trim();
5024
5124
  if (!trimmed.toLowerCase().startsWith("data:")) {
5025
5125
  return null;
@@ -5035,7 +5135,7 @@ function decodeDataUrlAttachment(value, basename) {
5035
5135
  try {
5036
5136
  const bytes = isBase64 ? import_node_buffer3.Buffer.from(payload, "base64") : import_node_buffer3.Buffer.from(decodeURIComponent(payload), "utf8");
5037
5137
  return {
5038
- filename: `${basename}.${resolveAttachmentExtension(mimeType)}`,
5138
+ filename: buildLoggedAttachmentFilename(options.prefix, options.index, mimeType),
5039
5139
  bytes
5040
5140
  };
5041
5141
  } catch {
@@ -5044,10 +5144,10 @@ function decodeDataUrlAttachment(value, basename) {
5044
5144
  }
5045
5145
  function collectPayloadAttachments(value, options) {
5046
5146
  if (typeof value === "string") {
5047
- const attachment = decodeDataUrlAttachment(
5048
- value,
5049
- `${options.prefix}-${options.counter.toString()}`
5050
- );
5147
+ const attachment = decodeDataUrlAttachment(value, {
5148
+ prefix: options.prefix,
5149
+ index: options.counter
5150
+ });
5051
5151
  if (attachment) {
5052
5152
  options.attachments.push(attachment);
5053
5153
  options.counter += 1;
@@ -5072,7 +5172,7 @@ function collectPayloadAttachments(value, options) {
5072
5172
  if (typeof record.data === "string" && mimeType) {
5073
5173
  try {
5074
5174
  options.attachments.push({
5075
- filename: `${options.prefix}-${options.counter.toString()}.${resolveAttachmentExtension(mimeType)}`,
5175
+ filename: buildLoggedAttachmentFilename(options.prefix, options.counter, mimeType),
5076
5176
  bytes: decodeInlineDataBuffer(record.data)
5077
5177
  });
5078
5178
  options.counter += 1;
@@ -5092,27 +5192,166 @@ function serialiseRequestPayloadForLogging(value) {
5092
5192
  `;
5093
5193
  }
5094
5194
  }
5195
+ function serialiseLogArtifactText(value) {
5196
+ if (value === null || value === void 0) {
5197
+ return void 0;
5198
+ }
5199
+ if (typeof value === "string") {
5200
+ if (value.length === 0) {
5201
+ return void 0;
5202
+ }
5203
+ return value.endsWith("\n") ? value : `${value}
5204
+ `;
5205
+ }
5206
+ if (Array.isArray(value) && value.length === 0) {
5207
+ return void 0;
5208
+ }
5209
+ if (isPlainRecord(value) && Object.keys(value).length === 0) {
5210
+ return void 0;
5211
+ }
5212
+ try {
5213
+ return `${JSON.stringify(sanitiseLogValue(value), null, 2)}
5214
+ `;
5215
+ } catch {
5216
+ return `${String(value)}
5217
+ `;
5218
+ }
5219
+ }
5220
+ function collectLoggedAttachmentsFromLlmParts(parts, prefix) {
5221
+ const attachments = [];
5222
+ let index = 1;
5223
+ for (const part of parts) {
5224
+ if (part.type !== "inlineData") {
5225
+ continue;
5226
+ }
5227
+ attachments.push({
5228
+ filename: buildLoggedAttachmentFilename(prefix, index, part.mimeType),
5229
+ bytes: decodeInlineDataBuffer(part.data)
5230
+ });
5231
+ index += 1;
5232
+ }
5233
+ return attachments;
5234
+ }
5235
+ function collectLoggedAttachmentsFromGeminiParts(parts, prefix) {
5236
+ return collectLoggedAttachmentsFromLlmParts(convertGooglePartsToLlmParts(parts), prefix);
5237
+ }
5238
+ function extractToolCallResponseTextFromOpenAiInput(input) {
5239
+ if (!Array.isArray(input)) {
5240
+ return void 0;
5241
+ }
5242
+ const responses = input.filter((item) => isPlainRecord(item)).flatMap((item) => {
5243
+ const type = typeof item.type === "string" ? item.type : "";
5244
+ if (type !== "function_call_output" && type !== "custom_tool_call_output") {
5245
+ return [];
5246
+ }
5247
+ return [
5248
+ {
5249
+ type,
5250
+ callId: typeof item.call_id === "string" ? item.call_id : void 0,
5251
+ output: "output" in item ? sanitiseLogValue(item.output) : void 0
5252
+ }
5253
+ ];
5254
+ });
5255
+ return serialiseLogArtifactText(responses);
5256
+ }
5257
+ function extractToolCallResponseTextFromFireworksMessages(messages) {
5258
+ if (!Array.isArray(messages)) {
5259
+ return void 0;
5260
+ }
5261
+ const responses = messages.filter((message) => isPlainRecord(message)).flatMap((message) => {
5262
+ if (message.role !== "tool") {
5263
+ return [];
5264
+ }
5265
+ return [
5266
+ {
5267
+ toolCallId: typeof message.tool_call_id === "string" ? message.tool_call_id : void 0,
5268
+ content: sanitiseLogValue(message.content)
5269
+ }
5270
+ ];
5271
+ });
5272
+ return serialiseLogArtifactText(responses);
5273
+ }
5274
+ function extractToolCallResponseTextFromGeminiContents(contents) {
5275
+ if (!Array.isArray(contents)) {
5276
+ return void 0;
5277
+ }
5278
+ const responses = [];
5279
+ for (const content of contents) {
5280
+ if (!content || typeof content !== "object") {
5281
+ continue;
5282
+ }
5283
+ const parts = content.parts;
5284
+ if (!Array.isArray(parts)) {
5285
+ continue;
5286
+ }
5287
+ for (const part of parts) {
5288
+ if (!part || typeof part !== "object") {
5289
+ continue;
5290
+ }
5291
+ const functionResponse = part.functionResponse;
5292
+ if (functionResponse) {
5293
+ responses.push(sanitiseLogValue(functionResponse));
5294
+ }
5295
+ }
5296
+ }
5297
+ return serialiseLogArtifactText(responses);
5298
+ }
5299
+ function serialiseOpenAiStyleToolCallsForLogging(calls) {
5300
+ return serialiseLogArtifactText(
5301
+ calls.map((call) => {
5302
+ if (call.kind === "custom") {
5303
+ return {
5304
+ kind: call.kind,
5305
+ name: call.name,
5306
+ callId: call.callId,
5307
+ itemId: call.itemId,
5308
+ input: call.input
5309
+ };
5310
+ }
5311
+ const { value, error } = parseOpenAiToolArguments(call.arguments);
5312
+ return {
5313
+ kind: call.kind,
5314
+ name: call.name,
5315
+ callId: call.callId,
5316
+ itemId: call.itemId,
5317
+ arguments: value,
5318
+ ...error ? { parseError: error, rawArguments: call.arguments } : {}
5319
+ };
5320
+ })
5321
+ );
5322
+ }
5323
+ function serialiseGeminiToolCallsForLogging(calls) {
5324
+ return serialiseLogArtifactText(
5325
+ calls.map((call) => ({
5326
+ name: call.name ?? "unknown",
5327
+ callId: typeof call.id === "string" ? call.id : void 0,
5328
+ arguments: sanitiseLogValue(call.args ?? {})
5329
+ }))
5330
+ );
5331
+ }
5095
5332
  function startLlmCallLoggerFromContents(options) {
5096
5333
  const session = getCurrentAgentLoggingSession();
5097
5334
  if (!session) {
5098
5335
  return void 0;
5099
5336
  }
5100
5337
  const attachments = [];
5338
+ let attachmentIndex = 1;
5101
5339
  const sections = [];
5102
5340
  for (const [messageIndex, message] of options.contents.entries()) {
5103
5341
  sections.push(`### message_${(messageIndex + 1).toString()} role=${message.role}`);
5104
- for (const [partIndex, part] of message.parts.entries()) {
5342
+ for (const part of message.parts) {
5105
5343
  if (part.type === "text") {
5106
5344
  const channel = part.thought === true ? "thought" : "response";
5107
5345
  sections.push(`[text:${channel}]`);
5108
5346
  sections.push(part.text);
5109
5347
  continue;
5110
5348
  }
5111
- const filename = `message-${(messageIndex + 1).toString()}-part-${(partIndex + 1).toString()}.${resolveAttachmentExtension(part.mimeType)}`;
5349
+ const filename = buildLoggedAttachmentFilename("input", attachmentIndex, part.mimeType);
5112
5350
  attachments.push({
5113
5351
  filename,
5114
5352
  bytes: decodeInlineDataBuffer(part.data)
5115
5353
  });
5354
+ attachmentIndex += 1;
5116
5355
  sections.push(
5117
5356
  `[inlineData] file=${filename} mime=${part.mimeType ?? "application/octet-stream"} bytes=${attachments[attachments.length - 1]?.bytes.byteLength ?? 0}`
5118
5357
  );
@@ -5156,11 +5395,18 @@ function startLlmCallLoggerFromPayload(options) {
5156
5395
  }
5157
5396
  const attachments = [];
5158
5397
  collectPayloadAttachments(options.requestPayload, {
5159
- prefix: `step-${options.step.toString()}`,
5398
+ prefix: "input",
5160
5399
  attachments,
5161
5400
  seen: /* @__PURE__ */ new WeakSet(),
5162
5401
  counter: 1
5163
5402
  });
5403
+ const toolCallResponseText = options.provider === "openai" || options.provider === "chatgpt" ? extractToolCallResponseTextFromOpenAiInput(
5404
+ options.requestPayload.input
5405
+ ) : options.provider === "fireworks" ? extractToolCallResponseTextFromFireworksMessages(
5406
+ options.requestPayload.messages
5407
+ ) : extractToolCallResponseTextFromGeminiContents(
5408
+ options.requestPayload.contents
5409
+ );
5164
5410
  return session.startLlmCall({
5165
5411
  provider: options.provider,
5166
5412
  modelId: options.modelId,
@@ -5169,7 +5415,8 @@ function startLlmCallLoggerFromPayload(options) {
5169
5415
  step: options.step,
5170
5416
  ...getCurrentToolCallContext() ? { toolContext: getCurrentToolCallContext() } : {}
5171
5417
  },
5172
- attachments
5418
+ attachments,
5419
+ toolCallResponseText
5173
5420
  });
5174
5421
  }
5175
5422
  async function runTextCall(params) {
@@ -5474,6 +5721,7 @@ async function runTextCall(params) {
5474
5721
  const mergedParts = mergeConsecutiveTextParts(responseParts);
5475
5722
  const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
5476
5723
  const { text, thoughts } = extractTextByChannel(content);
5724
+ const outputAttachments = collectLoggedAttachmentsFromLlmParts(mergedParts, "output");
5477
5725
  const costUsd = estimateCallCostUsd({
5478
5726
  modelId: modelVersion,
5479
5727
  tokens: latestUsage,
@@ -5484,16 +5732,20 @@ async function runTextCall(params) {
5484
5732
  queue.push({ type: "usage", usage: latestUsage, costUsd, modelVersion });
5485
5733
  }
5486
5734
  callLogger?.complete({
5487
- provider,
5488
- model: request.model,
5489
- modelVersion,
5490
- blocked,
5491
- costUsd,
5492
- usage: latestUsage,
5493
- grounding: grounding ? sanitiseLogValue(grounding) : void 0,
5494
- responseChars: text.length,
5495
- thoughtChars: thoughts.length,
5496
- responseImages
5735
+ responseText: text,
5736
+ attachments: outputAttachments,
5737
+ metadata: {
5738
+ provider,
5739
+ model: request.model,
5740
+ modelVersion,
5741
+ blocked,
5742
+ costUsd,
5743
+ usage: latestUsage,
5744
+ grounding: grounding ? sanitiseLogValue(grounding) : void 0,
5745
+ responseChars: text.length,
5746
+ thoughtChars: thoughts.length,
5747
+ responseImages
5748
+ }
5497
5749
  });
5498
5750
  return {
5499
5751
  provider,
@@ -5508,14 +5760,21 @@ async function runTextCall(params) {
5508
5760
  grounding
5509
5761
  };
5510
5762
  } catch (error) {
5763
+ const partialParts = mergeConsecutiveTextParts(responseParts);
5764
+ const partialContent = partialParts.length > 0 ? { role: responseRole ?? "assistant", parts: partialParts } : void 0;
5765
+ const { text: partialText } = extractTextByChannel(partialContent);
5511
5766
  callLogger?.fail(error, {
5512
- provider,
5513
- model: request.model,
5514
- modelVersion,
5515
- blocked,
5516
- usage: latestUsage,
5517
- partialResponseParts: responseParts.length,
5518
- responseImages
5767
+ responseText: partialText,
5768
+ attachments: collectLoggedAttachmentsFromLlmParts(partialParts, "output"),
5769
+ metadata: {
5770
+ provider,
5771
+ model: request.model,
5772
+ modelVersion,
5773
+ blocked,
5774
+ usage: latestUsage,
5775
+ partialResponseParts: responseParts.length,
5776
+ responseImages
5777
+ }
5519
5778
  });
5520
5779
  throw error;
5521
5780
  }
@@ -5722,7 +5981,10 @@ var DEFAULT_TOOL_LOOP_MAX_STEPS = 8;
5722
5981
  function resolveToolLoopContents(input) {
5723
5982
  return resolveTextContents(input);
5724
5983
  }
5725
- var toolLoopSteeringInternals = /* @__PURE__ */ new WeakMap();
5984
+ var toolLoopSteeringInternals = getRuntimeSingleton(
5985
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.toolLoopSteeringInternals"),
5986
+ () => /* @__PURE__ */ new WeakMap()
5987
+ );
5726
5988
  function createToolLoopSteeringChannel() {
5727
5989
  const pending = [];
5728
5990
  let closed = false;
@@ -5982,6 +6244,9 @@ async function runToolLoop(request) {
5982
6244
  let usageTokens;
5983
6245
  let thoughtDeltaEmitted = false;
5984
6246
  let blocked = false;
6247
+ let responseText = "";
6248
+ let reasoningSummary = "";
6249
+ let stepToolCallText;
5985
6250
  const stepRequestPayload = {
5986
6251
  model: providerInfo.model,
5987
6252
  input,
@@ -6074,8 +6339,8 @@ async function runToolLoop(request) {
6074
6339
  throw new Error(message);
6075
6340
  }
6076
6341
  usageTokens = extractOpenAiUsageTokens(finalResponse.usage);
6077
- const responseText = extractOpenAiResponseParts(finalResponse).parts.filter((p) => p.type === "text" && p.thought !== true).map((p) => p.text).join("").trim();
6078
- const reasoningSummary = extractOpenAiReasoningSummary(finalResponse).trim();
6342
+ responseText = extractOpenAiResponseParts(finalResponse).parts.filter((p) => p.type === "text" && p.thought !== true).map((p) => p.text).join("").trim();
6343
+ reasoningSummary = extractOpenAiReasoningSummary(finalResponse).trim();
6079
6344
  if (!thoughtDeltaEmitted && reasoningSummary.length > 0) {
6080
6345
  stepCallLogger?.appendThoughtDelta(reasoningSummary);
6081
6346
  emitEvent({ type: "delta", channel: "thought", text: reasoningSummary });
@@ -6091,6 +6356,23 @@ async function runToolLoop(request) {
6091
6356
  emitEvent({ type: "usage", usage: usageTokens, costUsd: stepCostUsd, modelVersion });
6092
6357
  }
6093
6358
  const responseToolCalls = extractOpenAiToolCalls(finalResponse.output);
6359
+ stepToolCallText = serialiseOpenAiStyleToolCallsForLogging(
6360
+ responseToolCalls.map(
6361
+ (call) => call.kind === "custom" ? {
6362
+ kind: call.kind,
6363
+ name: call.name,
6364
+ input: call.input,
6365
+ callId: call.call_id,
6366
+ itemId: call.id
6367
+ } : {
6368
+ kind: call.kind,
6369
+ name: call.name,
6370
+ arguments: call.arguments,
6371
+ callId: call.call_id,
6372
+ itemId: call.id
6373
+ }
6374
+ )
6375
+ );
6094
6376
  const stepToolCalls = [];
6095
6377
  if (responseToolCalls.length === 0) {
6096
6378
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
@@ -6118,17 +6400,20 @@ async function runToolLoop(request) {
6118
6400
  timing: timing2
6119
6401
  });
6120
6402
  stepCallLogger?.complete({
6121
- provider: "openai",
6122
- model: request.model,
6123
- modelVersion,
6124
- step: turn,
6125
- usage: usageTokens,
6126
- costUsd: stepCostUsd,
6127
- blocked,
6128
- responseChars: responseText.length,
6129
- thoughtChars: reasoningSummary.length,
6130
- toolCalls: 0,
6131
- finalStep: steeringItems2.length === 0
6403
+ responseText,
6404
+ metadata: {
6405
+ provider: "openai",
6406
+ model: request.model,
6407
+ modelVersion,
6408
+ step: turn,
6409
+ usage: usageTokens,
6410
+ costUsd: stepCostUsd,
6411
+ blocked,
6412
+ responseChars: responseText.length,
6413
+ thoughtChars: reasoningSummary.length,
6414
+ toolCalls: 0,
6415
+ finalStep: steeringItems2.length === 0
6416
+ }
6132
6417
  });
6133
6418
  if (steeringItems2.length === 0) {
6134
6419
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -6251,28 +6536,36 @@ async function runToolLoop(request) {
6251
6536
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
6252
6537
  const steeringItems = steeringInput.length > 0 ? toOpenAiInput(steeringInput) : [];
6253
6538
  stepCallLogger?.complete({
6254
- provider: "openai",
6255
- model: request.model,
6256
- modelVersion,
6257
- step: turn,
6258
- usage: usageTokens,
6259
- costUsd: stepCostUsd,
6260
- blocked,
6261
- responseChars: responseText.length,
6262
- thoughtChars: reasoningSummary.length,
6263
- toolCalls: stepToolCalls.length,
6264
- finalStep: false
6539
+ responseText,
6540
+ toolCallText: stepToolCallText,
6541
+ metadata: {
6542
+ provider: "openai",
6543
+ model: request.model,
6544
+ modelVersion,
6545
+ step: turn,
6546
+ usage: usageTokens,
6547
+ costUsd: stepCostUsd,
6548
+ blocked,
6549
+ responseChars: responseText.length,
6550
+ thoughtChars: reasoningSummary.length,
6551
+ toolCalls: stepToolCalls.length,
6552
+ finalStep: false
6553
+ }
6265
6554
  });
6266
6555
  previousResponseId = finalResponse.id;
6267
6556
  input = steeringItems.length > 0 ? toolOutputs.concat(steeringItems) : toolOutputs;
6268
6557
  } catch (error) {
6269
6558
  stepCallLogger?.fail(error, {
6270
- provider: "openai",
6271
- model: request.model,
6272
- modelVersion,
6273
- step: turn,
6274
- usage: usageTokens,
6275
- blocked
6559
+ responseText,
6560
+ toolCallText: stepToolCallText,
6561
+ metadata: {
6562
+ provider: "openai",
6563
+ model: request.model,
6564
+ modelVersion,
6565
+ step: turn,
6566
+ usage: usageTokens,
6567
+ blocked
6568
+ }
6276
6569
  });
6277
6570
  throw error;
6278
6571
  }
@@ -6298,6 +6591,7 @@ async function runToolLoop(request) {
6298
6591
  let usageTokens;
6299
6592
  let responseText = "";
6300
6593
  let reasoningSummaryText = "";
6594
+ let stepToolCallText;
6301
6595
  const markFirstModelEvent = () => {
6302
6596
  if (firstModelEventAtMs === void 0) {
6303
6597
  firstModelEventAtMs = Date.now();
@@ -6366,6 +6660,23 @@ async function runToolLoop(request) {
6366
6660
  stepCallLogger?.appendResponseDelta(responseText);
6367
6661
  }
6368
6662
  const responseToolCalls = response.toolCalls ?? [];
6663
+ stepToolCallText = serialiseOpenAiStyleToolCallsForLogging(
6664
+ responseToolCalls.map(
6665
+ (call) => call.kind === "custom" ? {
6666
+ kind: call.kind,
6667
+ name: call.name,
6668
+ input: call.input,
6669
+ callId: call.callId,
6670
+ itemId: call.id
6671
+ } : {
6672
+ kind: call.kind,
6673
+ name: call.name,
6674
+ arguments: call.arguments,
6675
+ callId: call.callId,
6676
+ itemId: call.id
6677
+ }
6678
+ )
6679
+ );
6369
6680
  if (responseToolCalls.length === 0) {
6370
6681
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
6371
6682
  const steeringItems2 = steeringInput2.length > 0 ? toChatGptInput(steeringInput2).input : [];
@@ -6391,16 +6702,19 @@ async function runToolLoop(request) {
6391
6702
  timing: timing2
6392
6703
  });
6393
6704
  stepCallLogger?.complete({
6394
- provider: "chatgpt",
6395
- model: request.model,
6396
- modelVersion,
6397
- step: turn,
6398
- usage: usageTokens,
6399
- costUsd: stepCostUsd,
6400
- responseChars: responseText.length,
6401
- thoughtChars: reasoningSummaryText.length,
6402
- toolCalls: 0,
6403
- finalStep: steeringItems2.length === 0
6705
+ responseText,
6706
+ metadata: {
6707
+ provider: "chatgpt",
6708
+ model: request.model,
6709
+ modelVersion,
6710
+ step: turn,
6711
+ usage: usageTokens,
6712
+ costUsd: stepCostUsd,
6713
+ responseChars: responseText.length,
6714
+ thoughtChars: reasoningSummaryText.length,
6715
+ toolCalls: 0,
6716
+ finalStep: steeringItems2.length === 0
6717
+ }
6404
6718
  });
6405
6719
  if (steeringItems2.length === 0) {
6406
6720
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -6533,25 +6847,33 @@ async function runToolLoop(request) {
6533
6847
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
6534
6848
  const steeringItems = steeringInput.length > 0 ? toChatGptInput(steeringInput).input : [];
6535
6849
  stepCallLogger?.complete({
6536
- provider: "chatgpt",
6537
- model: request.model,
6538
- modelVersion,
6539
- step: turn,
6540
- usage: usageTokens,
6541
- costUsd: stepCostUsd,
6542
- responseChars: responseText.length,
6543
- thoughtChars: reasoningSummaryText.length,
6544
- toolCalls: toolCalls.length,
6545
- finalStep: false
6850
+ responseText,
6851
+ toolCallText: stepToolCallText,
6852
+ metadata: {
6853
+ provider: "chatgpt",
6854
+ model: request.model,
6855
+ modelVersion,
6856
+ step: turn,
6857
+ usage: usageTokens,
6858
+ costUsd: stepCostUsd,
6859
+ responseChars: responseText.length,
6860
+ thoughtChars: reasoningSummaryText.length,
6861
+ toolCalls: toolCalls.length,
6862
+ finalStep: false
6863
+ }
6546
6864
  });
6547
6865
  input = steeringItems.length > 0 ? input.concat(toolOutputs, steeringItems) : input.concat(toolOutputs);
6548
6866
  } catch (error) {
6549
6867
  stepCallLogger?.fail(error, {
6550
- provider: "chatgpt",
6551
- model: request.model,
6552
- modelVersion,
6553
- step: turn,
6554
- usage: usageTokens
6868
+ responseText,
6869
+ toolCallText: stepToolCallText,
6870
+ metadata: {
6871
+ provider: "chatgpt",
6872
+ model: request.model,
6873
+ modelVersion,
6874
+ step: turn,
6875
+ usage: usageTokens
6876
+ }
6555
6877
  });
6556
6878
  throw error;
6557
6879
  }
@@ -6574,6 +6896,7 @@ async function runToolLoop(request) {
6574
6896
  let usageTokens;
6575
6897
  let responseText = "";
6576
6898
  let blocked = false;
6899
+ let stepToolCallText;
6577
6900
  const stepRequestPayload = {
6578
6901
  model: providerInfo.model,
6579
6902
  messages,
@@ -6638,6 +6961,14 @@ async function runToolLoop(request) {
6638
6961
  });
6639
6962
  }
6640
6963
  const responseToolCalls = extractFireworksToolCalls(message);
6964
+ stepToolCallText = serialiseOpenAiStyleToolCallsForLogging(
6965
+ responseToolCalls.map((call) => ({
6966
+ kind: "function",
6967
+ name: call.name,
6968
+ arguments: call.arguments,
6969
+ callId: call.id
6970
+ }))
6971
+ );
6641
6972
  if (responseToolCalls.length === 0) {
6642
6973
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
6643
6974
  const steeringMessages = steeringInput2.length > 0 ? toFireworksMessages(steeringInput2) : [];
@@ -6663,17 +6994,20 @@ async function runToolLoop(request) {
6663
6994
  timing: timing2
6664
6995
  });
6665
6996
  stepCallLogger?.complete({
6666
- provider: "fireworks",
6667
- model: request.model,
6668
- modelVersion,
6669
- step: turn,
6670
- usage: usageTokens,
6671
- costUsd: stepCostUsd,
6672
- blocked,
6673
- responseChars: responseText.length,
6674
- thoughtChars: 0,
6675
- toolCalls: 0,
6676
- finalStep: steeringMessages.length === 0
6997
+ responseText,
6998
+ metadata: {
6999
+ provider: "fireworks",
7000
+ model: request.model,
7001
+ modelVersion,
7002
+ step: turn,
7003
+ usage: usageTokens,
7004
+ costUsd: stepCostUsd,
7005
+ blocked,
7006
+ responseChars: responseText.length,
7007
+ thoughtChars: 0,
7008
+ toolCalls: 0,
7009
+ finalStep: steeringMessages.length === 0
7010
+ }
6677
7011
  });
6678
7012
  if (steeringMessages.length === 0) {
6679
7013
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -6785,17 +7119,21 @@ async function runToolLoop(request) {
6785
7119
  timing
6786
7120
  });
6787
7121
  stepCallLogger?.complete({
6788
- provider: "fireworks",
6789
- model: request.model,
6790
- modelVersion,
6791
- step: turn,
6792
- usage: usageTokens,
6793
- costUsd: stepCostUsd,
6794
- blocked,
6795
- responseChars: responseText.length,
6796
- thoughtChars: 0,
6797
- toolCalls: stepToolCalls.length,
6798
- finalStep: false
7122
+ responseText,
7123
+ toolCallText: stepToolCallText,
7124
+ metadata: {
7125
+ provider: "fireworks",
7126
+ model: request.model,
7127
+ modelVersion,
7128
+ step: turn,
7129
+ usage: usageTokens,
7130
+ costUsd: stepCostUsd,
7131
+ blocked,
7132
+ responseChars: responseText.length,
7133
+ thoughtChars: 0,
7134
+ toolCalls: stepToolCalls.length,
7135
+ finalStep: false
7136
+ }
6799
7137
  });
6800
7138
  messages.push({
6801
7139
  role: "assistant",
@@ -6809,12 +7147,16 @@ async function runToolLoop(request) {
6809
7147
  }
6810
7148
  } catch (error) {
6811
7149
  stepCallLogger?.fail(error, {
6812
- provider: "fireworks",
6813
- model: request.model,
6814
- modelVersion,
6815
- step: turn,
6816
- usage: usageTokens,
6817
- blocked
7150
+ responseText,
7151
+ toolCallText: stepToolCallText,
7152
+ metadata: {
7153
+ provider: "fireworks",
7154
+ model: request.model,
7155
+ modelVersion,
7156
+ step: turn,
7157
+ usage: usageTokens,
7158
+ blocked
7159
+ }
6818
7160
  });
6819
7161
  throw error;
6820
7162
  }
@@ -6834,6 +7176,7 @@ async function runToolLoop(request) {
6834
7176
  let usageTokens;
6835
7177
  let responseText = "";
6836
7178
  let thoughtsText = "";
7179
+ let stepToolCallText;
6837
7180
  const markFirstModelEvent = () => {
6838
7181
  if (firstModelEventAtMs === void 0) {
6839
7182
  firstModelEventAtMs = Date.now();
@@ -6954,12 +7297,17 @@ async function runToolLoop(request) {
6954
7297
  modelVersion = response.modelVersion ?? request.model;
6955
7298
  responseText = response.responseText.trim();
6956
7299
  thoughtsText = response.thoughtsText.trim();
7300
+ const responseOutputAttachments = collectLoggedAttachmentsFromGeminiParts(
7301
+ response.modelParts,
7302
+ "output"
7303
+ );
6957
7304
  const stepCostUsd = estimateCallCostUsd({
6958
7305
  modelId: modelVersion,
6959
7306
  tokens: usageTokens,
6960
7307
  responseImages: 0
6961
7308
  });
6962
7309
  totalCostUsd += stepCostUsd;
7310
+ stepToolCallText = serialiseGeminiToolCallsForLogging(response.functionCalls);
6963
7311
  if (response.functionCalls.length === 0) {
6964
7312
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
6965
7313
  finalText = responseText;
@@ -6985,16 +7333,20 @@ async function runToolLoop(request) {
6985
7333
  timing: timing2
6986
7334
  });
6987
7335
  stepCallLogger?.complete({
6988
- provider: "gemini",
6989
- model: request.model,
6990
- modelVersion,
6991
- step: turn,
6992
- usage: usageTokens,
6993
- costUsd: stepCostUsd,
6994
- responseChars: responseText.length,
6995
- thoughtChars: thoughtsText.length,
6996
- toolCalls: 0,
6997
- finalStep: steeringInput2.length === 0
7336
+ responseText,
7337
+ attachments: responseOutputAttachments,
7338
+ metadata: {
7339
+ provider: "gemini",
7340
+ model: request.model,
7341
+ modelVersion,
7342
+ step: turn,
7343
+ usage: usageTokens,
7344
+ costUsd: stepCostUsd,
7345
+ responseChars: responseText.length,
7346
+ thoughtChars: thoughtsText.length,
7347
+ toolCalls: 0,
7348
+ finalStep: steeringInput2.length === 0
7349
+ }
6998
7350
  });
6999
7351
  if (steeringInput2.length === 0) {
7000
7352
  return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
@@ -7121,16 +7473,21 @@ async function runToolLoop(request) {
7121
7473
  timing
7122
7474
  });
7123
7475
  stepCallLogger?.complete({
7124
- provider: "gemini",
7125
- model: request.model,
7126
- modelVersion,
7127
- step: turn,
7128
- usage: usageTokens,
7129
- costUsd: stepCostUsd,
7130
- responseChars: responseText.length,
7131
- thoughtChars: thoughtsText.length,
7132
- toolCalls: toolCalls.length,
7133
- finalStep: false
7476
+ responseText,
7477
+ attachments: responseOutputAttachments,
7478
+ toolCallText: stepToolCallText,
7479
+ metadata: {
7480
+ provider: "gemini",
7481
+ model: request.model,
7482
+ modelVersion,
7483
+ step: turn,
7484
+ usage: usageTokens,
7485
+ costUsd: stepCostUsd,
7486
+ responseChars: responseText.length,
7487
+ thoughtChars: thoughtsText.length,
7488
+ toolCalls: toolCalls.length,
7489
+ finalStep: false
7490
+ }
7134
7491
  });
7135
7492
  geminiContents.push({ role: "user", parts: responseParts });
7136
7493
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
@@ -7139,13 +7496,17 @@ async function runToolLoop(request) {
7139
7496
  }
7140
7497
  } catch (error) {
7141
7498
  stepCallLogger?.fail(error, {
7142
- provider: "gemini",
7143
- model: request.model,
7144
- modelVersion,
7145
- step: turn,
7146
- usage: usageTokens,
7147
- responseChars: responseText.length,
7148
- thoughtChars: thoughtsText.length
7499
+ responseText,
7500
+ toolCallText: stepToolCallText,
7501
+ metadata: {
7502
+ provider: "gemini",
7503
+ model: request.model,
7504
+ modelVersion,
7505
+ step: turn,
7506
+ usage: usageTokens,
7507
+ responseChars: responseText.length,
7508
+ thoughtChars: thoughtsText.length
7509
+ }
7149
7510
  });
7150
7511
  throw error;
7151
7512
  }