@genesislcap/foundation-ai 15.40.0 → 15.41.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.
- package/README.md +21 -0
- package/dist/dts/index.d.ts +2 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/transports/ai-service-error.d.ts +146 -0
- package/dist/dts/transports/ai-service-error.d.ts.map +1 -0
- package/dist/dts/transports/post-with-retry.d.ts.map +1 -1
- package/dist/dts/types/chat.types.d.ts +72 -11
- package/dist/dts/types/chat.types.d.ts.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/transports/ai-service-error.js +183 -0
- package/dist/esm/transports/post-with-retry.js +43 -5
- package/dist/foundation-ai.api.json +756 -37
- package/dist/foundation-ai.d.ts +198 -11
- package/package.json +10 -10
|
@@ -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 (((
|
|
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: ${(
|
|
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, (
|
|
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
|
-
|
|
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
|
}
|