@genesislcap/foundation-ai 15.6.2 → 15.7.0-ts7.1

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.
Files changed (34) hide show
  1. package/dist/dts/ai-provider.d.ts.map +1 -1
  2. package/dist/dts/index.d.ts +1 -0
  3. package/dist/dts/index.d.ts.map +1 -1
  4. package/dist/dts/interactions/criteria-interpretation.d.ts +40 -40
  5. package/dist/dts/interactions/criteria-interpretation.d.ts.map +1 -1
  6. package/dist/dts/providers/anthropic-provider.d.ts.map +1 -1
  7. package/dist/dts/providers/chrome-provider.d.ts.map +1 -1
  8. package/dist/dts/providers/default-provider.d.ts.map +1 -1
  9. package/dist/dts/providers/gemini-provider.d.ts.map +1 -1
  10. package/dist/dts/providers/openai-provider.d.ts.map +1 -1
  11. package/dist/dts/transports/anthropic-transport.d.ts.map +1 -1
  12. package/dist/dts/transports/budget-exhausted-error.d.ts +254 -0
  13. package/dist/dts/transports/budget-exhausted-error.d.ts.map +1 -0
  14. package/dist/dts/transports/chrome-prompt-transport.d.ts.map +1 -1
  15. package/dist/dts/transports/default-transport.d.ts.map +1 -1
  16. package/dist/dts/transports/gemini-transport.d.ts.map +1 -1
  17. package/dist/dts/transports/post-with-retry.d.ts +6 -1
  18. package/dist/dts/transports/post-with-retry.d.ts.map +1 -1
  19. package/dist/dts/transports/server-openai-transport.d.ts.map +1 -1
  20. package/dist/dts/types/chat.types.d.ts +74 -2
  21. package/dist/dts/types/chat.types.d.ts.map +1 -1
  22. package/dist/dts/utils/temperature.d.ts.map +1 -1
  23. package/dist/esm/ai-provider.js +14 -14
  24. package/dist/esm/index.js +1 -0
  25. package/dist/esm/interactions/criteria-interpretation.js +3 -6
  26. package/dist/esm/transports/anthropic-transport.js +6 -4
  27. package/dist/esm/transports/budget-exhausted-error.js +277 -0
  28. package/dist/esm/transports/chrome-prompt-transport.js +3 -2
  29. package/dist/esm/transports/gemini-transport.js +18 -13
  30. package/dist/esm/transports/post-with-retry.js +49 -9
  31. package/dist/esm/transports/server-openai-transport.js +19 -0
  32. package/dist/foundation-ai.api.json +533 -6
  33. package/dist/foundation-ai.d.ts +247 -2
  34. package/package.json +11 -11
@@ -3,6 +3,7 @@ import { SUPPORTED_GEMINI_MODEL_IDS, } from '../types';
3
3
  import { logger } from '../utils/logger';
4
4
  import { scaleTemperature } from '../utils/temperature';
5
5
  import { ResponseTruncatedError } from './anthropic-transport';
6
+ import { VENDOR_LABELS } from './budget-exhausted-error';
6
7
  import { repairMalformedFunctionCall } from './gemini-malformed-call';
7
8
  import { postWithRetry } from './post-with-retry';
8
9
  const GEMINI_DIRECT_URL = (model) => `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
@@ -207,7 +208,8 @@ export class GeminiTransport {
207
208
  // ── ChatTransport (multi-turn chat) ────────────────────────────────────
208
209
  sendChatMessage(history, userMessage, options) {
209
210
  return __awaiter(this, void 0, void 0, function* () {
210
- var _a, _b;
211
+ var _a;
212
+ var _b;
211
213
  // NOTE: `options.cachePolicy` is intentionally NOT read here. Gemini 2.5+ does *implicit*
212
214
  // caching automatically and always-on — it caches as much of the stable request prefix as it
213
215
  // can on every call, regardless of the policy's `scope` (including `'default'`). We issue no
@@ -309,7 +311,8 @@ export class GeminiTransport {
309
311
  return totalCost;
310
312
  }
311
313
  toGeminiContents(history, userMessage, attachments) {
312
- var _a, _b, _c;
314
+ var _a, _b;
315
+ var _c;
313
316
  const contents = [];
314
317
  // Gemini requires functionResponse.name to match the original functionCall.name.
315
318
  // Our internal IDs are UUIDs, so build a lookup from ID → function name as we go.
@@ -326,7 +329,7 @@ export class GeminiTransport {
326
329
  continue;
327
330
  const role = msg.role === 'user' ? 'user' : 'model';
328
331
  if (msg.toolResult) {
329
- const fnName = (_a = toolCallNameById.get(msg.toolResult.toolCallId)) !== null && _a !== void 0 ? _a : msg.toolResult.toolCallId;
332
+ const fnName = (_c = toolCallNameById.get(msg.toolResult.toolCallId)) !== null && _c !== void 0 ? _c : msg.toolResult.toolCallId;
330
333
  contents.push({
331
334
  role: 'user',
332
335
  parts: [
@@ -339,7 +342,7 @@ export class GeminiTransport {
339
342
  ],
340
343
  });
341
344
  }
342
- else if ((_b = msg.toolCalls) === null || _b === void 0 ? void 0 : _b.length) {
345
+ else if ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
343
346
  for (const tc of msg.toolCalls) {
344
347
  toolCallNameById.set(tc.id, tc.name);
345
348
  }
@@ -366,7 +369,7 @@ export class GeminiTransport {
366
369
  }),
367
370
  });
368
371
  }
369
- else if (role === 'user' && ((_c = msg.attachments) === null || _c === void 0 ? void 0 : _c.length)) {
372
+ else if (role === 'user' && ((_b = msg.attachments) === null || _b === void 0 ? void 0 : _b.length)) {
370
373
  const msgAttachmentParts = msg.attachments.map((att) => ({
371
374
  text: `[File: ${att.name}]\n${att.content}`,
372
375
  }));
@@ -420,11 +423,12 @@ export class GeminiTransport {
420
423
  };
421
424
  }
422
425
  fromGeminiResponse(response, offeredToolNames = new Set()) {
423
- var _a, _b, _c, _d;
426
+ var _a, _b;
427
+ var _c, _d;
424
428
  const { inputTokens, outputTokens, thoughtsTokens, cacheReadTokens, cost } = this.usageFromGemini(response.usageMetadata);
425
429
  const candidates = response === null || response === void 0 ? void 0 : response.candidates;
426
430
  const firstCandidate = candidates === null || candidates === void 0 ? void 0 : candidates[0];
427
- const parts = (_b = (_a = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : [];
431
+ const parts = (_c = (_a = firstCandidate === null || firstCandidate === void 0 ? void 0 : firstCandidate.content) === null || _a === void 0 ? void 0 : _a.parts) !== null && _c !== void 0 ? _c : [];
428
432
  const toolCalls = [];
429
433
  const thoughtParts = [];
430
434
  const textParts = [];
@@ -441,7 +445,7 @@ export class GeminiTransport {
441
445
  toolCalls.push({
442
446
  id: crypto.randomUUID(),
443
447
  name: part.functionCall.name,
444
- args: (_c = part.functionCall.args) !== null && _c !== void 0 ? _c : {},
448
+ args: (_d = part.functionCall.args) !== null && _d !== void 0 ? _d : {},
445
449
  providerMetadata: { [GEMINI_PROVIDER_KEY]: state },
446
450
  });
447
451
  }
@@ -502,7 +506,7 @@ export class GeminiTransport {
502
506
  // Surface the provider diagnostic on the message so the driver can fold it
503
507
  // into the debug-log meta events (the console log above is dev-only). Only
504
508
  // when there's signal — a finish reason, thinking tokens, or a block reason.
505
- const blockReason = (_d = response.promptFeedback) === null || _d === void 0 ? void 0 : _d.blockReason;
509
+ const blockReason = (_b = response.promptFeedback) === null || _b === void 0 ? void 0 : _b.blockReason;
506
510
  if (finishReason != null || thoughtsTokens != null || blockReason != null) {
507
511
  base.responseMeta = {
508
512
  finishReason,
@@ -550,9 +554,10 @@ export class GeminiTransport {
550
554
  * without answering" signature.
551
555
  */
552
556
  logAbnormalResponse(response, parsed) {
553
- var _a, _b, _c;
554
- const usage = ((_a = response.usageMetadata) !== null && _a !== void 0 ? _a : {});
555
- const candidate = (_c = (_b = response.candidates) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : {};
557
+ var _a;
558
+ var _b, _c;
559
+ const usage = ((_b = response.usageMetadata) !== null && _b !== void 0 ? _b : {});
560
+ const candidate = (_c = (_a = response.candidates) === null || _a === void 0 ? void 0 : _a[0]) !== null && _c !== void 0 ? _c : {};
556
561
  logger.warn('GeminiTransport: blank/non-STOP response', {
557
562
  model: this.model,
558
563
  finishReason: parsed.finishReason,
@@ -604,7 +609,7 @@ export class GeminiTransport {
604
609
  headers,
605
610
  body: payload,
606
611
  credentials,
607
- vendorLabel: 'Gemini',
612
+ vendorLabel: VENDOR_LABELS.gemini,
608
613
  retryableStatuses: GeminiTransport.RETRYABLE_STATUSES,
609
614
  timeout: this.timeout,
610
615
  stallTimeout: this.stallTimeout,
@@ -3,6 +3,7 @@ import { authoritativeAbortReason } from '../utils/abort-reason';
3
3
  import { abortableDelay } from '../utils/abortable-delay';
4
4
  import { combineSignals } from '../utils/combine-signals';
5
5
  import { logger } from '../utils/logger';
6
+ import { budgetExhaustedFrom, codeOf, isBudgetRejection, parseJsonOrUndefined, } from './budget-exhausted-error';
6
7
  import { readFramedBody } from './ndjson-frames';
7
8
  const MAX_RETRIES = 5;
8
9
  /**
@@ -15,7 +16,8 @@ const MAX_RETRIES = 5;
15
16
  */
16
17
  export function postWithRetry(options) {
17
18
  return __awaiter(this, void 0, void 0, function* () {
18
- var _a, _b, _c, _d;
19
+ var _a, _b;
20
+ var _c, _d;
19
21
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal, } = options;
20
22
  // Exponential backoff between retries: base, 2×, 4×, 8×, 16×. Abortable on the
21
23
  // CALLER signal only — the timeout is cleared before each backoff, so a caller
@@ -56,15 +58,44 @@ export function postWithRetry(options) {
56
58
  signal: combinedSignal,
57
59
  credentials,
58
60
  });
59
- if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
60
- logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
61
- clearTimeout(timeoutId); // this attempt is over — don't let its timer tick through the backoff
62
- yield backoff(attempt);
63
- continue;
64
- }
65
61
  if (!response.ok) {
66
- const err = yield response.text();
67
- throw new Error(`${vendorLabel} request error ${response.status}: ${err}`);
62
+ // One read of the error body per failing attempt — it feeds BOTH the
63
+ // budget classification and the thrown message, and the response is
64
+ // discarded either way. Note this means a retryable attempt now reads
65
+ // the (small, proxy-authored) error body before backing off. That is the
66
+ // price of the classification winning over the ladder in this branch as
67
+ // well as the framed one; the attempt's wall-clock timer is still armed
68
+ // and the read runs on the fetch's combined signal, so a pathological
69
+ // non-terminating body times out rather than hanging.
70
+ //
71
+ // That timeout is a real behaviour change for a RETRYABLE status, and the
72
+ // accepted cost of the symmetry: a 503 used to reach the ladder without
73
+ // the body being touched, so a non-terminating one still backed off and
74
+ // retried. Now the abort propagates out of `postWithRetry` instead.
75
+ // Acceptable because these bodies are proxy-authored and small — if that
76
+ // ever stops being true, read the body only when the status is
77
+ // non-retryable or already budget-shaped.
78
+ const errText = yield response.text();
79
+ const parsed = parseJsonOrUndefined(errText);
80
+ // The budget wall (GENC-1464) wins over ANY status-based retry — the same
81
+ // rule the framed branch below applies, now symmetric. It must be
82
+ // classified BEFORE the ladder, not after it: 402 is absent from every
83
+ // RETRYABLE_STATUSES today, but a proxy that maps the refusal onto a
84
+ // retryable status (or a future set that includes 402) would otherwise
85
+ // cost five doomed requests and ~31s of backoff before an opaque error —
86
+ // exactly the bug this classification exists to remove. Parsing is
87
+ // best-effort: an unparseable or plain-text 402 still classifies, just
88
+ // without the budget/spent figures.
89
+ if (isBudgetRejection(response.status, codeOf(parsed))) {
90
+ throw budgetExhaustedFrom(vendorLabel, parsed, errText || undefined);
91
+ }
92
+ if (retryableStatuses.includes(response.status) && attempt < MAX_RETRIES) {
93
+ logger.warn(`${vendorLabel}Transport: retryable status ${response.status}, retrying (attempt ${attempt + 1}/${MAX_RETRIES})`);
94
+ clearTimeout(timeoutId); // this attempt is over — don't let its timer tick through the backoff
95
+ yield backoff(attempt);
96
+ continue;
97
+ }
98
+ throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
68
99
  }
69
100
  // The proxy streams NDJSON heartbeat frames when it supports them; an old
70
101
  // proxy ignores our Accept header and returns plain JSON, so branch on
@@ -83,6 +114,15 @@ export function postWithRetry(options) {
83
114
  // local stall, so the driver's TimeoutError branch handles both.
84
115
  throw new DOMException(`${vendorLabel} request stalled: ${(_c = frame.error) !== null && _c !== void 0 ? _c : 'no upstream data'}`, 'TimeoutError');
85
116
  }
117
+ // The budget wall, framed (GENC-1464) — the in-band twin of the 402 above,
118
+ // through the same predicate, keyed on status OR code. Classified before
119
+ // the retryable ladder for the same reason UPSTREAM_STALLED is: the
120
+ // classification must win over any status-based retry. That ordering is
121
+ // no longer unique to this branch — the legacy branch above now applies
122
+ // the identical rule, so the two read as one policy rather than two.
123
+ if (isBudgetRejection(frame.status, frame.code)) {
124
+ throw budgetExhaustedFrom(vendorLabel, frame);
125
+ }
86
126
  // An in-band retryable status joins the same attempt/backoff ladder as
87
127
  // its HTTP-status equivalent (shared attempt counter).
88
128
  if (retryableStatuses.includes(frame.status) && attempt < MAX_RETRIES) {
@@ -1,4 +1,5 @@
1
1
  import { __awaiter } from "tslib";
2
+ import { budgetExhaustedFrom, codeOf, isBudgetRejection, parseJsonOrUndefined, VENDOR_LABELS, } from './budget-exhausted-error';
2
3
  const AI_SERVER_PATH = '/gwf/ai-service/chat-completions';
3
4
  const DEFAULT_MODEL = 'gpt-4o-mini';
4
5
  const DEFAULT_TIMEOUT = 30000;
@@ -56,6 +57,24 @@ export class ServerOpenAITransport {
56
57
  clearTimeout(timeoutId);
57
58
  if (!response.ok) {
58
59
  const err = yield response.text();
60
+ const parsed = parseJsonOrUndefined(err);
61
+ // This transport does its own bare `fetch` rather than going through
62
+ // `postWithRetry` (it has no retry ladder, no stall timer and no framed
63
+ // mode), so it would otherwise be the one proxy call where a 402 stays an
64
+ // opaque `AI proxy error 402`. Classify it through the same shared
65
+ // predicate the envelope uses, so the typed error is uniform across
66
+ // transports even though the envelope is not.
67
+ //
68
+ // Scope, stated plainly: this transport serves only `sendStructuredPrompt`
69
+ // — it has no `chat()` — and its single caller
70
+ // (`OpenAIProvider.interpretCriteria`) catches everything and returns
71
+ // null. So a 402 here degrades smart-search silently and can never latch
72
+ // the assistant's `blocked` state, before or after this change. The typed
73
+ // throw improves the log line and any host that catches it directly. See
74
+ // `docs/migration-GENC-1464.md` §"Transport coverage".
75
+ if (isBudgetRejection(response.status, codeOf(parsed))) {
76
+ throw budgetExhaustedFrom(VENDOR_LABELS.openai, parsed, err || undefined);
77
+ }
59
78
  throw new Error(`AI proxy error ${response.status}: ${err}`);
60
79
  }
61
80
  const data = (yield response.json());