@genesislcap/foundation-ai 15.6.2 → 15.7.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.
@@ -407,6 +407,102 @@ declare interface AnthropicTransportConfig {
407
407
  maxTokens?: number;
408
408
  }
409
409
 
410
+ /**
411
+ * The vendors whose spend the ai-service proxy meters — i.e. the only vendors
412
+ * that can raise a {@link BudgetExhaustedError}, and therefore the only ones a
413
+ * 402's `otherVendorAvailable: false` is a statement about.
414
+ *
415
+ * Deliberately narrower than the keys of {@link VENDOR_LABELS}, and it must stay
416
+ * exactly the proxy's own list — `VENDORS` in ai-service's `utils/aiVendor.js`.
417
+ * A vendor listed here that the proxy does not meter gets walled by the
418
+ * `otherVendorAvailable: false` sweep on a verdict that says nothing about it,
419
+ * and if it is reachable, `blocked` then derives true and locks a composer with
420
+ * headroom left.
421
+ *
422
+ * Two vendors are excluded for that reason:
423
+ * - `chrome` runs on-device — no pot to exhaust, and free.
424
+ * - `openai` is **not budgeted by the proxy**. ai-service rejects it up front
425
+ * with `400 UNSUPPORTED_PROVIDER` (`SUPPORTED_PROVIDERS` in `aiVendor.js`), no
426
+ * `MODEL_PRICING` row names it, and `otherVendorAvailable` is computed over
427
+ * Anthropic and Gemini alone. So it can never raise a 402, and a 402 is never
428
+ * a statement about it.
429
+ *
430
+ * @beta
431
+ */
432
+ export declare const BUDGETED_VENDORS: readonly AIProviderType[];
433
+
434
+ /**
435
+ * Thrown when the AI-spend budget for the caller (user, tenant, or project) is
436
+ * exhausted and the ai-service proxy refuses the request outright — HTTP `402`
437
+ * with `code: 'BUDGET_EXCEEDED'`, in either the legacy JSON or the NDJSON framed
438
+ * mode.
439
+ *
440
+ * This is a **terminal, non-transient** condition, in the same family as
441
+ * `ResponseTruncatedError`: no amount of retrying clears it, because nothing
442
+ * about the request is wrong. The budget has to be raised out-of-band before any
443
+ * further call can succeed, so both the transport retry ladder and the driver's
444
+ * transient-retry catch step aside for it and the failure surfaces immediately
445
+ * as the `'budget-exhausted'` `TurnFailureReason`.
446
+ *
447
+ * `budgetUsd`/`spentUsd` are populated from the rejection body when the proxy
448
+ * supplies them (the workstream-C contract does); they are optional because a
449
+ * plain-text or truncated 402 from an older proxy still has to classify.
450
+ *
451
+ * @beta
452
+ */
453
+ export declare class BudgetExhaustedError extends Error {
454
+ /** Vendor label of the transport that was refused (e.g. `'Anthropic'`). */
455
+ readonly vendorLabel: string;
456
+ /** The configured spend cap in USD, when the proxy reported one. */
457
+ readonly budgetUsd?: number;
458
+ /** Spend already booked against that cap in USD, when the proxy reported it. */
459
+ readonly spentUsd?: number;
460
+ /** The proxy's human-readable rejection message, when present. */
461
+ readonly detail?: string;
462
+ /**
463
+ * The proxy's own verdict on whether **any other** vendor it meters still has
464
+ * headroom, from the 402's `otherVendorAvailable`.
465
+ *
466
+ * The one field on this error a client cannot re-derive. A client can only
467
+ * infer "another vendor is free" from registry membership — which knows
468
+ * nothing about that vendor's remaining spend — so a `false` here is the only
469
+ * thing standing between the user and "Switch to Gemini to keep going" for a
470
+ * Gemini pot that is already empty. `undefined` means the proxy did not say
471
+ * (an older build), and the client is back to inferring.
472
+ */
473
+ readonly otherVendorAvailable?: boolean;
474
+ /**
475
+ * The vendor the **proxy** says it refused, verbatim from the 402's `vendor`.
476
+ *
477
+ * A second opinion, not the first: {@link BudgetExhaustedError.vendorLabel}
478
+ * comes from the transport that was actually called and wins wherever it
479
+ * resolves. This one rescues attribution when the transport's label does not —
480
+ * a white-labelled or multiplexing gateway whose single transport fronts more
481
+ * than one upstream stamps one static label for all of them, and only the
482
+ * proxy knows which pot the request was actually booked against.
483
+ */
484
+ readonly serverVendor?: string;
485
+ constructor(
486
+ /** Vendor label of the transport that was refused (e.g. `'Anthropic'`). */
487
+ vendorLabel: string,
488
+ /** The configured spend cap in USD, when the proxy reported one. */
489
+ budgetUsd?: number,
490
+ /** Spend already booked against that cap in USD, when the proxy reported it. */
491
+ spentUsd?: number,
492
+ /** The proxy's human-readable rejection message, when present. */
493
+ detail?: string,
494
+ /**
495
+ * The fields the proxy reports about the wider budget picture. An object
496
+ * rather than two more positional arguments: the four above are already the
497
+ * limit of what reads at a call site, and these two are set together or not
498
+ * at all.
499
+ */
500
+ extra?: {
501
+ otherVendorAvailable?: boolean;
502
+ serverVendor?: string;
503
+ });
504
+ }
505
+
410
506
  /**
411
507
  * Prompt-cache policy for a chat request, resolved per turn. **Provider-neutral intent,
412
508
  * provider-specific effect** — read this carefully, the two providers differ sharply:
@@ -619,6 +715,65 @@ export declare type ChatDriverResult = {
619
715
  * successful turn, a user cancel, and a driver dispose (none are failures).
620
716
  */
621
717
  failureReason?: TurnFailureReason;
718
+ /**
719
+ * Budget figures reported by the proxy, present **only** when
720
+ * `failureReason === 'budget-exhausted'` and the rejection carried them.
721
+ * Lifted off `BudgetExhaustedError` so a host can render the real numbers
722
+ * ("$25.40 of $25") without catching the transport error itself — before
723
+ * this the figures reached the debug log but not the caller, so the
724
+ * assistant's blocked banner could only ever show generic copy.
725
+ *
726
+ * Present whenever the wall carried anything worth reporting — figures, or
727
+ * an attributable vendor. Omitted entirely (not set to `undefined`) when
728
+ * it carried neither, and absent from every non-budget result — a clean
729
+ * turn's shape stays byte-identical to the historical `{ reason: 'done' }`.
730
+ */
731
+ budget?: {
732
+ /** The configured spend cap in USD, when the proxy reported one. */
733
+ budgetUsd?: number;
734
+ /** Spend already booked against that cap in USD, when reported. */
735
+ spentUsd?: number;
736
+ /**
737
+ * Vendor label of the transport that was refused (e.g. `'Anthropic'`).
738
+ * Always present when `budget` is: it is the one field that keeps a
739
+ * per-vendor budget model open without a breaking change later.
740
+ */
741
+ vendorLabel: string;
742
+ /**
743
+ * The refusing vendor as a typed {@link AIProviderType}: the normalised
744
+ * key of `vendorLabel` where that label is one a vendor claims, otherwise
745
+ * the normalised key of the proxy's own `vendor` field on the 402.
746
+ *
747
+ * So it is **not** always a normalisation of `vendorLabel`, and the two
748
+ * fields can legitimately disagree — a white-labelled or multiplexing
749
+ * gateway stamps one static transport label in front of several upstreams,
750
+ * and only the proxy can say which of them refused. The fallback is
751
+ * deliberate: it is what keeps attribution working for those deployments
752
+ * instead of degrading them all to "unattributable".
753
+ *
754
+ * Optional because a label no vendor claims and a 402 that names no vendor
755
+ * must degrade to "unattributable" rather than to a wrong attribution.
756
+ * Derived from the label first, rather than from the driver's
757
+ * last-resolved provider, **on purpose**: the label originates at the
758
+ * transport that was actually refused, whereas the last-resolved provider
759
+ * is stale on the classification seam (an orchestrated turn classifies
760
+ * against the registry default, which the chat driver may never have
761
+ * resolved).
762
+ */
763
+ vendor?: AIProviderType;
764
+ /**
765
+ * The proxy's verdict on whether any OTHER vendor it meters still has
766
+ * headroom (the 402's `otherVendorAvailable`).
767
+ *
768
+ * The only field here a client cannot re-derive, and the reason it is
769
+ * carried: a client can infer "another vendor exists" from registry
770
+ * membership, but never "another vendor has budget left". A `false` is
771
+ * therefore server-known truth that must beat that inference —
772
+ * `FoundationAiAssistant` uses it to stop advising a switch to a vendor
773
+ * whose pot is already empty. `undefined` means the proxy did not say.
774
+ */
775
+ otherVendorAvailable?: boolean;
776
+ };
622
777
  } | {
623
778
  reason: 'agent-handoff';
624
779
  summary: string;
@@ -1540,6 +1695,21 @@ export declare interface CriteriaInterpretContext {
1540
1695
  fields?: FieldLike[];
1541
1696
  }
1542
1697
 
1698
+ /**
1699
+ * The single copy shown to a user whose AI budget is gone — the transcript bubble
1700
+ * the chat driver appends, and the default text of the assistant's blocked banner.
1701
+ *
1702
+ * Deliberately **vendor-neutral**: the same bundle ships to white-labelled
1703
+ * deployments where "contact Genesis" is simply wrong. A host that wants
1704
+ * branded wording overrides both surfaces — the banner via
1705
+ * `FoundationAiAssistant.setBlocked(true, reason)`, the transcript bubble via
1706
+ * `ChatDriverConfig.budgetExhaustedMessage` — so neither is stuck with this
1707
+ * default while the other is customised.
1708
+ *
1709
+ * @beta
1710
+ */
1711
+ export declare const DEFAULT_BUDGET_EXHAUSTED_MESSAGE = "You've reached your AI usage limit. Contact your administrator to raise it.";
1712
+
1543
1713
  /**
1544
1714
  * A field descriptor accepted by criteria interpretation utilities.
1545
1715
  * Can be a plain string name or an object with common field metadata properties.
@@ -1838,6 +2008,20 @@ export declare type InteractionResult<T = unknown> = {
1838
2008
  /** Returns true when AI features (beta) are enabled via ?feature.ai URL param or GENX_ENABLE_AI=true env var. */
1839
2009
  export declare const isAIFeatureEnabled: () => boolean;
1840
2010
 
2011
+ /**
2012
+ * Whether this vendor's spend is metered by the proxy — see
2013
+ * {@link BUDGETED_VENDORS}.
2014
+ *
2015
+ * A HOST-FACING helper with, deliberately, no internal caller: the library's
2016
+ * own consumers iterate {@link BUDGETED_VENDORS} directly (structural, cannot
2017
+ * drift), while a host writing its own budget pre-flight or status surface
2018
+ * needs the membership test in predicate form. Kept exported for that use —
2019
+ * genesis-create's pre-flight is the shape of consumer it exists for.
2020
+ *
2021
+ * @beta
2022
+ */
2023
+ export declare const isBudgetedVendor: (vendor: AIProviderType | undefined) => boolean;
2024
+
1841
2025
  /**
1842
2026
  * Type guard that narrows a `ChatToolCall` to `ChatToolCallUnknown`.
1843
2027
  *
@@ -2213,10 +2397,20 @@ declare interface StructuredPromptOptions {
2213
2397
  * an incomplete tool call; deterministic, so it is not retried.
2214
2398
  * - `refusal` — a safety-classifier decline (e.g. Fable 5 `stop_reason: 'refusal'`),
2215
2399
  * which returns empty content; deterministic, so it is not retried.
2400
+ * - `budget_exhausted` — the AI-spend budget is used up and the proxy refused the
2401
+ * request (HTTP 402). **Terminal for the parent too, and enforced**: the parent
2402
+ * driver ends its own turn as soon as this outcome comes back, instead of
2403
+ * appending it as a tool result and calling the model again into the same wall.
2404
+ * So a handler that receives it will not be called a second time this turn, and
2405
+ * the parent turn reports `'budget-exhausted'` in its own right.
2406
+ *
2407
+ * Recovery guidance differs sharply by reason — see `docs/sub_agent.md`. Only
2408
+ * `budget_exhausted` and `refusal` are terminal; retrying either just reproduces
2409
+ * it. The rest are worth a retry or a question to the user.
2216
2410
  *
2217
2411
  * @beta
2218
2412
  */
2219
- export declare type SubAgentFailureReason = 'max_iterations' | 'malformed_tool_call' | 'empty_response' | 'unknown_tool_limit' | 'timeout' | 'response_truncated' | 'refusal';
2413
+ export declare type SubAgentFailureReason = 'max_iterations' | 'malformed_tool_call' | 'empty_response' | 'unknown_tool_limit' | 'timeout' | 'response_truncated' | 'refusal' | 'budget_exhausted';
2220
2414
 
2221
2415
  /**
2222
2416
  * Options passed to `requestSubAgent` at call time.
@@ -2274,9 +2468,60 @@ export declare const SUPPORTED_GEMINI_MODEL_IDS: readonly GeminiModelId[];
2274
2468
  * incomplete tool call; deterministic, so it bails without retry.
2275
2469
  * - `refusal` — a safety-classifier decline (e.g. Fable 5 `stop_reason: 'refusal'`),
2276
2470
  * which returns empty content; deterministic, so it is not retried.
2471
+ * - `budget-exhausted` — the AI-spend budget is used up and the proxy refused the request
2472
+ * (`BudgetExhaustedError`, HTTP 402). Terminal, not retried: the
2473
+ * budget must be raised out-of-band before any call can succeed.
2474
+ *
2475
+ * @beta
2476
+ */
2477
+ export declare type TurnFailureReason = 'exception' | 'malformed-function-call' | 'empty-response' | 'unknown-tool-limit' | 'max-iterations' | 'response-truncated' | 'refusal' | 'budget-exhausted';
2478
+
2479
+ /**
2480
+ * Display name for each concrete AI vendor: the **single source** every
2481
+ * transport's `vendorLabel` is read from, and the only place a vendor's name is
2482
+ * spelled for a user.
2483
+ *
2484
+ * This map is the hinge of the per-vendor budget model (GENC-1464). A wall is
2485
+ * attributed to a vendor by reverse-looking-up the label the refused transport
2486
+ * stamped on its {@link BudgetExhaustedError} — so a label that drifts from this
2487
+ * map (a literal typo, a casing change, a new vendor added with a hardcoded
2488
+ * string) silently degrades every wall from that transport to "unattributable"
2489
+ * and re-locks the whole session instead of just that vendor. It fails safe, but
2490
+ * the feature quietly stops working, which is why the transports read their label
2491
+ * from here rather than restating it.
2492
+ *
2493
+ * The map has **two kinds of entry**, and the difference matters:
2494
+ *
2495
+ * - The {@link BUDGETED_VENDORS} — `anthropic` and `gemini` — reach the model
2496
+ * through the ai-service proxy, are metered by it, and stamp their label on a
2497
+ * {@link BudgetExhaustedError}. For these the label is both an attribution key
2498
+ * and display copy.
2499
+ * - `chrome` and `openai` are **display-only**. `ChromeProvider` talks to the
2500
+ * on-device Prompt API and the proxy refuses `openai` outright, so neither can
2501
+ * be refused for budget or stamp a label — but both report a `provider` from
2502
+ * `getStatus()`, which puts them in the assistant's reachable set, which is
2503
+ * what the blocked banner names when it tells a walled user where they can
2504
+ * still go ("Switch to Chrome to keep going."). Dropping either entry would
2505
+ * print the raw type there.
2506
+ *
2507
+ * `'none'` is excluded deliberately: it is the "no provider configured" sentinel,
2508
+ * not a vendor, and nothing can be refused by it — nor switched to.
2509
+ *
2510
+ * @beta
2511
+ */
2512
+ export declare const VENDOR_LABELS: Readonly<Record<Exclude<AIProviderType, 'none'>, string>>;
2513
+
2514
+ /**
2515
+ * The {@link AIProviderType} behind a vendor label, or `undefined` for a label
2516
+ * no vendor claims.
2517
+ *
2518
+ * Case-insensitive and whitespace-tolerant, because the label travels as free
2519
+ * text on the wire (the driver contract carries `vendorLabel`, not the type) and
2520
+ * an unrecognised label must degrade to "unattributable" rather than to a wrong
2521
+ * attribution — blocking the wrong vendor is worse than blocking none.
2277
2522
  *
2278
2523
  * @beta
2279
2524
  */
2280
- export declare type TurnFailureReason = 'exception' | 'malformed-function-call' | 'empty-response' | 'unknown-tool-limit' | 'max-iterations' | 'response-truncated' | 'refusal';
2525
+ export declare function vendorTypeOfLabel(label?: string): AIProviderType | undefined;
2281
2526
 
2282
2527
  export { }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/foundation-ai",
3
3
  "description": "Genesis Foundation AI - Provider-agnostic AI configuration and shared utilities",
4
- "version": "15.6.2",
4
+ "version": "15.7.0",
5
5
  "sideEffects": false,
6
6
  "license": "SEE LICENSE IN license.txt",
7
7
  "main": "dist/esm/index.js",
@@ -52,17 +52,17 @@
52
52
  }
53
53
  },
54
54
  "devDependencies": {
55
- "@genesislcap/foundation-testing": "15.6.2",
56
- "@genesislcap/genx": "15.6.2",
57
- "@genesislcap/rollup-builder": "15.6.2",
58
- "@genesislcap/ts-builder": "15.6.2",
59
- "@genesislcap/uvu-playwright-builder": "15.6.2",
60
- "@genesislcap/vite-builder": "15.6.2",
61
- "@genesislcap/webpack-builder": "15.6.2"
55
+ "@genesislcap/foundation-testing": "15.7.0",
56
+ "@genesislcap/genx": "15.7.0",
57
+ "@genesislcap/rollup-builder": "15.7.0",
58
+ "@genesislcap/ts-builder": "15.7.0",
59
+ "@genesislcap/uvu-playwright-builder": "15.7.0",
60
+ "@genesislcap/vite-builder": "15.7.0",
61
+ "@genesislcap/webpack-builder": "15.7.0"
62
62
  },
63
63
  "dependencies": {
64
- "@genesislcap/foundation-logger": "15.6.2",
65
- "@genesislcap/foundation-utils": "15.6.2",
64
+ "@genesislcap/foundation-logger": "15.7.0",
65
+ "@genesislcap/foundation-utils": "15.7.0",
66
66
  "@microsoft/fast-foundation": "2.50.0"
67
67
  },
68
68
  "repository": {
@@ -73,5 +73,5 @@
73
73
  "publishConfig": {
74
74
  "access": "public"
75
75
  },
76
- "gitHead": "a5781780cb56906755ddbfc4be75e221719ae4b4"
76
+ "gitHead": "485a2cc9d4b8695ea2e57a9cc66cad55effb2184"
77
77
  }