@adaptic/lumic-utils 1.0.17 → 1.0.19

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.
@@ -2106,15 +2106,33 @@ function resetLLMCostTracker() {
2106
2106
  // llm-openai.ts
2107
2107
  /**
2108
2108
  * Determines if an LLM error should be retried.
2109
- * Only retries on rate limit errors (429).
2109
+ *
2110
+ * Retries on:
2111
+ * - 429 / rate limit errors (transient capacity)
2112
+ * - "could not parse the JSON body" 400s — observed once in production for a
2113
+ * single symbol on the very first conversation turn (Wave 86, 2026-04-11).
2114
+ * The exact same call site succeeds millions of times before and after, and
2115
+ * the prior fix commit `6eaef52` in this repo already eliminated the only
2116
+ * known SDK-v5 cause (passing `tools: undefined/null`). The remaining cases
2117
+ * are virtually always proxy/network corruption of the request body in
2118
+ * flight (request truncated mid-flight, TLS renegotiation, edge proxy
2119
+ * buffer reset). Retrying once with a fresh connection has a high
2120
+ * probability of recovering, and a deterministic SDK-side defect would
2121
+ * re-fail on retry (so we still surface it).
2110
2122
  */
2111
2123
  const isRetryableLLMError = (error) => {
2112
2124
  if (error instanceof Error) {
2113
2125
  const message = error.message;
2114
- // Retry only on rate limits (429)
2126
+ // Retry on rate limits (429)
2115
2127
  if (message.includes('429') || message.includes('rate limit') || message.includes('Rate limit')) {
2116
2128
  return true;
2117
2129
  }
2130
+ // Retry on transient body-corruption 400s. Match the exact OpenAI error
2131
+ // string to avoid retrying genuine client-side validation 400s (which
2132
+ // would re-fail forever and waste retry budget).
2133
+ if (message.includes('could not parse the JSON body of your request')) {
2134
+ return true;
2135
+ }
2118
2136
  }
2119
2137
  return false;
2120
2138
  };
@@ -2310,12 +2328,52 @@ async function createCompletion(content, responseFormat, options = DEFAULT_OPTIO
2310
2328
  if (responseFormatOption.type !== 'text') {
2311
2329
  queryOptions.response_format = responseFormatOption;
2312
2330
  }
2313
- const completion = await withRetry(() => openai.chat.completions.create(queryOptions), {
2314
- maxRetries: 3,
2315
- baseDelayMs: 2000,
2316
- maxDelayMs: 30000,
2317
- retryableErrors: isRetryableLLMError,
2318
- }, `OpenAI:${normalizedModel}`);
2331
+ let completion;
2332
+ try {
2333
+ completion = await withRetry(() => openai.chat.completions.create(queryOptions), {
2334
+ maxRetries: 3,
2335
+ baseDelayMs: 2000,
2336
+ maxDelayMs: 30000,
2337
+ retryableErrors: isRetryableLLMError,
2338
+ }, `OpenAI:${normalizedModel}`);
2339
+ }
2340
+ catch (error) {
2341
+ // Defensive observability: when the OpenAI SDK rejects our request,
2342
+ // emit a structured snapshot of the queryOptions shape (NOT content) so
2343
+ // a future recurrence of the rare "could not parse JSON body" 400 can be
2344
+ // diagnosed without having to reproduce locally. We deliberately log
2345
+ // metadata only — no message content, no API key — so this is safe even
2346
+ // for production prompts containing sensitive context.
2347
+ const errorMessage = error instanceof Error ? error.message : String(error);
2348
+ const totalContentChars = messages.reduce((sum, msg) => {
2349
+ if (typeof msg.content === 'string')
2350
+ return sum + msg.content.length;
2351
+ if (Array.isArray(msg.content)) {
2352
+ return sum + msg.content.reduce((s, part) => {
2353
+ if (typeof part === 'object' && part !== null && 'text' in part && typeof part.text === 'string') {
2354
+ return s + part.text.length;
2355
+ }
2356
+ return s;
2357
+ }, 0);
2358
+ }
2359
+ return sum;
2360
+ }, 0);
2361
+ getLumicLogger().error(`OpenAI ChatCompletion call failed for model ${normalizedModel}`, {
2362
+ model: normalizedModel,
2363
+ errorMessage,
2364
+ messageCount: messages.length,
2365
+ roleBreakdown: messages.reduce((acc, msg) => {
2366
+ acc[msg.role] = (acc[msg.role] ?? 0) + 1;
2367
+ return acc;
2368
+ }, {}),
2369
+ totalContentChars,
2370
+ toolCount: queryOptions.tools?.length ?? 0,
2371
+ hasTemperature: queryOptions.temperature !== undefined,
2372
+ hasResponseFormat: queryOptions.response_format !== undefined,
2373
+ hasMaxCompletionTokens: queryOptions.max_completion_tokens !== undefined,
2374
+ });
2375
+ throw error;
2376
+ }
2319
2377
  const response = {
2320
2378
  id: completion.id,
2321
2379
  content: completion.choices[0]?.message?.content || '',
@@ -7921,7 +7979,35 @@ function translateContextToAnthropic(context) {
7921
7979
  continue;
7922
7980
  }
7923
7981
  }
7924
- return { messages, systemText: systemParts.join('\n\n') };
7982
+ // Anthropic requires alternating user/assistant roles — merge consecutive
7983
+ // same-role messages into a single message with combined content blocks.
7984
+ const merged = [];
7985
+ for (const msg of messages) {
7986
+ const prev = merged[merged.length - 1];
7987
+ if (prev && prev.role === msg.role) {
7988
+ // Merge into the previous message by combining content blocks
7989
+ const prevBlocks = toContentBlocks(prev.content);
7990
+ const curBlocks = toContentBlocks(msg.content);
7991
+ prev.content = [...prevBlocks, ...curBlocks];
7992
+ }
7993
+ else {
7994
+ // Ensure content is in block form for consistency (string → TextBlock)
7995
+ merged.push({ role: msg.role, content: toContentBlocks(msg.content) });
7996
+ }
7997
+ }
7998
+ return { messages: merged, systemText: systemParts.join('\n\n') };
7999
+ }
8000
+ /** Convert string or content block array to a uniform content block array. */
8001
+ function toContentBlocks(content) {
8002
+ if (typeof content === 'string') {
8003
+ const textBlock = {
8004
+ type: 'text',
8005
+ text: content,
8006
+ citations: null,
8007
+ };
8008
+ return [textBlock];
8009
+ }
8010
+ return content;
7925
8011
  }
7926
8012
  /**
7927
8013
  * Makes a call to the Anthropic Messages API.
@@ -22710,11 +22796,11 @@ let poolConfig = DEFAULT_POOL_CONFIG;
22710
22796
  async function loadApolloModules() {
22711
22797
  if (typeof window === "undefined" || process.env.AWS_EXECUTION_ENV) {
22712
22798
  // Server-side (or Lambda): load the CommonJS‑based implementation.
22713
- return (await Promise.resolve().then(function () { return require('./apollo-client.server-CS3TcmzK.js'); }));
22799
+ return (await Promise.resolve().then(function () { return require('./apollo-client.server-HwHIFnVk.js'); }));
22714
22800
  }
22715
22801
  else {
22716
22802
  // Client-side: load the ESM‑based implementation.
22717
- return (await Promise.resolve().then(function () { return require('./apollo-client.client-NpMY129A.js'); }));
22803
+ return (await Promise.resolve().then(function () { return require('./apollo-client.client-guxMwplM.js'); }));
22718
22804
  }
22719
22805
  }
22720
22806
  /**
@@ -81425,4 +81511,4 @@ exports.withCorrelationId = withCorrelationId;
81425
81511
  exports.withMetrics = withMetrics;
81426
81512
  exports.withRateLimit = withRateLimit;
81427
81513
  exports.withRetry = withRetry;
81428
- //# sourceMappingURL=index-Y9dzs7p_.js.map
81514
+ //# sourceMappingURL=index-Dr85zRZC.js.map