@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.
- package/dist/dts/ai-provider.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/interactions/criteria-interpretation.d.ts +40 -40
- package/dist/dts/interactions/criteria-interpretation.d.ts.map +1 -1
- package/dist/dts/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/dts/providers/chrome-provider.d.ts.map +1 -1
- package/dist/dts/providers/default-provider.d.ts.map +1 -1
- package/dist/dts/providers/gemini-provider.d.ts.map +1 -1
- package/dist/dts/providers/openai-provider.d.ts.map +1 -1
- package/dist/dts/transports/anthropic-transport.d.ts.map +1 -1
- package/dist/dts/transports/budget-exhausted-error.d.ts +254 -0
- package/dist/dts/transports/budget-exhausted-error.d.ts.map +1 -0
- package/dist/dts/transports/chrome-prompt-transport.d.ts.map +1 -1
- package/dist/dts/transports/default-transport.d.ts.map +1 -1
- package/dist/dts/transports/gemini-transport.d.ts.map +1 -1
- package/dist/dts/transports/post-with-retry.d.ts +6 -1
- package/dist/dts/transports/post-with-retry.d.ts.map +1 -1
- package/dist/dts/transports/server-openai-transport.d.ts.map +1 -1
- package/dist/dts/types/chat.types.d.ts +74 -2
- package/dist/dts/types/chat.types.d.ts.map +1 -1
- package/dist/dts/utils/temperature.d.ts.map +1 -1
- package/dist/esm/ai-provider.js +14 -14
- package/dist/esm/index.js +1 -0
- package/dist/esm/interactions/criteria-interpretation.js +3 -6
- package/dist/esm/transports/anthropic-transport.js +6 -4
- package/dist/esm/transports/budget-exhausted-error.js +277 -0
- package/dist/esm/transports/chrome-prompt-transport.js +3 -2
- package/dist/esm/transports/gemini-transport.js +18 -13
- package/dist/esm/transports/post-with-retry.js +49 -9
- package/dist/esm/transports/server-openai-transport.js +19 -0
- package/dist/foundation-ai.api.json +533 -6
- package/dist/foundation-ai.d.ts +247 -2
- 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
|
|
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
|
|
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 = (
|
|
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 ((
|
|
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' && ((
|
|
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
|
|
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 = (
|
|
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: (
|
|
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 = (
|
|
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
|
|
554
|
-
|
|
555
|
-
const
|
|
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:
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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());
|