@genesislcap/foundation-ai 15.40.0 → 15.42.0

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.
@@ -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 { AI_SERVICE_ERROR_CODES, AIServiceError, aiServiceAccessRefusalOf, aiServiceDetailOf, aiServiceErrorCodeOf, isUncodedTooLarge, routerRefusalDetailOf, } from './ai-service-error';
6
7
  import { budgetExhaustedFrom, codeOf, isBudgetRejection, parseJsonOrUndefined, } from './budget-exhausted-error';
7
8
  import { ContextOverflowError, contextOverflowOf } from './context-overflow-error';
8
9
  import { readFramedBody } from './ndjson-frames';
@@ -19,11 +20,17 @@ const MAX_RETRIES = 5;
19
20
  */
20
21
  export function postWithRetry(options) {
21
22
  return __awaiter(this, void 0, void 0, function* () {
22
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
23
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
23
24
  const { url, headers, body, credentials, vendorLabel, retryableStatuses, timeout, stallTimeout, backoffBaseMs, signal, } = options;
24
25
  // The vendor behind this transport's label, resolved once — the refusal classifier dispatches on
25
26
  // it, and it is the only vendor knowledge this envelope needs (GENC-1506).
26
27
  const vendor = vendorOfTransportLabel(vendorLabel);
28
+ // The service's own text is logged here, for whoever runs the app; the error's message leaves it
29
+ // out, because that message reaches places that keep it (GENC-1611).
30
+ const serviceRefusal = (code, status, detail) => {
31
+ logger.error(`${vendorLabel}Transport: the AI service refused the request (${status} ${code})${detail ? `: ${detail}` : ''}`);
32
+ return new AIServiceError(vendorLabel, code, status, detail);
33
+ };
27
34
  // Total time this sequence has spent waiting BEYOND its own ladder because a server asked it to.
28
35
  // The per-attempt cap alone is not a bound on the sequence — five capped waits is five minutes, with
29
36
  // no timer running through any of them — so the allowance is tracked here and spent down across
@@ -164,6 +171,26 @@ export function postWithRetry(options) {
164
171
  if (isBudgetRejection(response.status, codeOf(parsed))) {
165
172
  throw budgetExhaustedFrom(vendorLabel, parsed, errText || undefined);
166
173
  }
174
+ // The AI SERVICE wall (GENC-1611): the app's own endpoint naming what is wrong. Keyed on the
175
+ // code, not the status, so a missing key costs one request whether it comes as the app
176
+ // proxy's 424 or as Create's 503. VENDOR_UNREACHABLE keeps the ladder below and is only
177
+ // named once the ladder gives up.
178
+ const serviceCode = aiServiceErrorCodeOf(codeOf(parsed));
179
+ if (serviceCode && serviceCode !== AI_SERVICE_ERROR_CODES.VENDOR_UNREACHABLE) {
180
+ throw serviceRefusal(serviceCode, response.status, aiServiceDetailOf(parsed));
181
+ }
182
+ // The app's router turned the request away before the service ran: no session, or not the
183
+ // right the endpoint demands. Its NACK carries no code and no vendor envelope, which is what
184
+ // tells it apart from a vendor's own 401/403 (the provider wall above, or passed through).
185
+ const accessRefusal = aiServiceAccessRefusalOf(response.status, parsed, errText);
186
+ if (accessRefusal) {
187
+ throw serviceRefusal(accessRefusal, response.status, (_e = routerRefusalDetailOf(parsed)) !== null && _e !== void 0 ? _e : (errText || undefined));
188
+ }
189
+ // A 413 with no code is a size cap in front of the endpoint (the router's, or a proxy's), which
190
+ // answers before the endpoint can. It is never worth sending again.
191
+ if (isUncodedTooLarge(response.status, parsed)) {
192
+ throw serviceRefusal(AI_SERVICE_ERROR_CODES.REQUEST_TOO_LARGE, response.status, errText || undefined);
193
+ }
167
194
  // An explicit `x-should-retry: false` outranks our own retryable-status set (GENC-1506).
168
195
  // The provider knows things the status does not — a 429 it will keep refusing, say — and
169
196
  // Anthropic's own SDK honours this header. Absent means "no opinion", never `false`.
@@ -177,12 +204,15 @@ export function postWithRetry(options) {
177
204
  yield backoff(attempt, retryAfterMs(response.headers) || retryInfoMs(parsed));
178
205
  continue;
179
206
  }
207
+ if (serviceCode) {
208
+ throw serviceRefusal(serviceCode, response.status, aiServiceDetailOf(parsed));
209
+ }
180
210
  throw new Error(`${vendorLabel} request error ${response.status}: ${errText}`);
181
211
  }
182
212
  // The proxy streams NDJSON heartbeat frames when it supports them; an old
183
213
  // proxy ignores our Accept header and returns plain JSON, so branch on
184
214
  // what actually came back.
185
- if (((_f = (_e = response.headers) === null || _e === void 0 ? void 0 : _e.get('content-type')) === null || _f === void 0 ? void 0 : _f.toLowerCase().includes('application/x-ndjson')) &&
215
+ if (((_g = (_f = response.headers) === null || _f === void 0 ? void 0 : _f.get('content-type')) === null || _g === void 0 ? void 0 : _g.toLowerCase().includes('application/x-ndjson')) &&
186
216
  response.body) {
187
217
  rearmStallTimer();
188
218
  const frame = yield readFramedBody(response.body, rearmStallTimer, vendorLabel, stallTimeout);
@@ -194,7 +224,7 @@ export function postWithRetry(options) {
194
224
  if (frame.code === 'UPSTREAM_STALLED') {
195
225
  // The proxy watchdog saw no upstream data — same tagged shape as a
196
226
  // local stall, so the driver's TimeoutError branch handles both.
197
- throw new DOMException(`${vendorLabel} request stalled: ${(_g = frame.error) !== null && _g !== void 0 ? _g : 'no upstream data'}`, 'TimeoutError');
227
+ throw new DOMException(`${vendorLabel} request stalled: ${(_h = frame.error) !== null && _h !== void 0 ? _h : 'no upstream data'}`, 'TimeoutError');
198
228
  }
199
229
  // The budget wall, framed (GENC-1464) — the in-band twin of the 402 above,
200
230
  // through the same predicate, keyed on status OR code. Classified before
@@ -214,11 +244,16 @@ export function postWithRetry(options) {
214
244
  }
215
245
  const framedOverflow = contextOverflowOf(vendor, frame.status, frame);
216
246
  if (framedOverflow) {
217
- throw new ContextOverflowError(vendorLabel, framedOverflow.promptTokens, framedOverflow.limitTokens, (_h = framedOverflow.message) !== null && _h !== void 0 ? _h : frame.error);
247
+ throw new ContextOverflowError(vendorLabel, framedOverflow.promptTokens, framedOverflow.limitTokens, (_j = framedOverflow.message) !== null && _j !== void 0 ? _j : frame.error);
218
248
  }
219
249
  if (isBudgetRejection(frame.status, frame.code)) {
220
250
  throw budgetExhaustedFrom(vendorLabel, frame);
221
251
  }
252
+ // The AI SERVICE wall, framed — the same rule as the legacy branch above.
253
+ const framedServiceCode = aiServiceErrorCodeOf(frame.code);
254
+ if (framedServiceCode && framedServiceCode !== AI_SERVICE_ERROR_CODES.VENDOR_UNREACHABLE) {
255
+ throw serviceRefusal(framedServiceCode, frame.status, aiServiceDetailOf(frame));
256
+ }
222
257
  // An in-band retryable status joins the same attempt/backoff ladder as
223
258
  // its HTTP-status equivalent (shared attempt counter). No `x-should-retry` to consult here —
224
259
  // in framed mode the HTTP response is the proxy's own 200, so its headers say nothing about
@@ -229,7 +264,10 @@ export function postWithRetry(options) {
229
264
  yield backoff(attempt, retryInfoMs(frame));
230
265
  continue;
231
266
  }
232
- throw new Error(`${vendorLabel} request error ${frame.status}: ${(_j = frame.error) !== null && _j !== void 0 ? _j : JSON.stringify(frame.details)}`);
267
+ if (framedServiceCode) {
268
+ throw serviceRefusal(framedServiceCode, frame.status, aiServiceDetailOf(frame));
269
+ }
270
+ throw new Error(`${vendorLabel} request error ${frame.status}: ${(_k = frame.error) !== null && _k !== void 0 ? _k : JSON.stringify(frame.details)}`);
233
271
  }
234
272
  return (yield response.json());
235
273
  }