@happyvertical/smrt-inference 0.0.0-stage → 0.60.16

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 (112) hide show
  1. package/AGENTS.md +206 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +73 -2
  5. package/agents/fusions.md +105 -0
  6. package/agents/hosted.md +161 -0
  7. package/agents/resolver.md +65 -0
  8. package/agents/settings.md +107 -0
  9. package/dist/__smrt-register__.d.ts +2 -0
  10. package/dist/__smrt-register__.d.ts.map +1 -0
  11. package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js +423 -0
  12. package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js.map +1 -0
  13. package/dist/chunks/provider-config-BSpIQRVM.js +1873 -0
  14. package/dist/chunks/provider-config-BSpIQRVM.js.map +1 -0
  15. package/dist/classify.d.ts +32 -0
  16. package/dist/classify.d.ts.map +1 -0
  17. package/dist/config.d.ts +89 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/errors.d.ts +51 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/executor.d.ts +45 -0
  22. package/dist/executor.d.ts.map +1 -0
  23. package/dist/fusion-store.d.ts +16 -0
  24. package/dist/fusion-store.d.ts.map +1 -0
  25. package/dist/fusion.d.ts +155 -0
  26. package/dist/fusion.d.ts.map +1 -0
  27. package/dist/gate.d.ts +143 -0
  28. package/dist/gate.d.ts.map +1 -0
  29. package/dist/gate.js +94 -0
  30. package/dist/gate.js.map +1 -0
  31. package/dist/hosted.d.ts +461 -0
  32. package/dist/hosted.d.ts.map +1 -0
  33. package/dist/index.d.ts +9 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +8 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/manifest.json +453 -0
  38. package/dist/models/InferenceOverride.d.ts +38 -0
  39. package/dist/models/InferenceOverride.d.ts.map +1 -0
  40. package/dist/models/InferenceOverrideCollection.d.ts +35 -0
  41. package/dist/models/InferenceOverrideCollection.d.ts.map +1 -0
  42. package/dist/models.d.ts +11 -0
  43. package/dist/models.d.ts.map +1 -0
  44. package/dist/models.js +2 -0
  45. package/dist/node.d.ts +110 -0
  46. package/dist/node.d.ts.map +1 -0
  47. package/dist/node.js +1414 -0
  48. package/dist/node.js.map +1 -0
  49. package/dist/provider-config.d.ts +113 -0
  50. package/dist/provider-config.d.ts.map +1 -0
  51. package/dist/register.d.ts +30 -0
  52. package/dist/register.d.ts.map +1 -0
  53. package/dist/resolver.d.ts +166 -0
  54. package/dist/resolver.d.ts.map +1 -0
  55. package/dist/resolver.js +291 -0
  56. package/dist/resolver.js.map +1 -0
  57. package/dist/server.d.ts +25 -0
  58. package/dist/server.d.ts.map +1 -0
  59. package/dist/server.js +1138 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/settings/cache.d.ts +5 -0
  62. package/dist/settings/cache.d.ts.map +1 -0
  63. package/dist/settings/keys.d.ts +70 -0
  64. package/dist/settings/keys.d.ts.map +1 -0
  65. package/dist/settings/layers.d.ts +68 -0
  66. package/dist/settings/layers.d.ts.map +1 -0
  67. package/dist/settings/override-service.d.ts +119 -0
  68. package/dist/settings/override-service.d.ts.map +1 -0
  69. package/dist/settings/preferences.d.ts +137 -0
  70. package/dist/settings/preferences.d.ts.map +1 -0
  71. package/dist/settings/resolver.d.ts +41 -0
  72. package/dist/settings/resolver.d.ts.map +1 -0
  73. package/dist/settings/settings-service.d.ts +355 -0
  74. package/dist/settings/settings-service.d.ts.map +1 -0
  75. package/dist/settings/targets.d.ts +112 -0
  76. package/dist/settings/targets.d.ts.map +1 -0
  77. package/dist/settings/transcripts.d.ts +33 -0
  78. package/dist/settings/transcripts.d.ts.map +1 -0
  79. package/dist/smrt-knowledge.json +341 -0
  80. package/dist/smrt-knowledge.okf.md +80 -0
  81. package/dist/svelte/components/InferenceAliasEditor.svelte +210 -0
  82. package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts +13 -0
  83. package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts.map +1 -0
  84. package/dist/svelte/components/InferenceDefaultView.svelte +330 -0
  85. package/dist/svelte/components/InferenceDefaultView.svelte.d.ts +15 -0
  86. package/dist/svelte/components/InferenceDefaultView.svelte.d.ts.map +1 -0
  87. package/dist/svelte/components/InferenceFusionList.svelte +141 -0
  88. package/dist/svelte/components/InferenceFusionList.svelte.d.ts +16 -0
  89. package/dist/svelte/components/InferenceFusionList.svelte.d.ts.map +1 -0
  90. package/dist/svelte/components/InferenceSettingsPanel.svelte +392 -0
  91. package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +84 -0
  92. package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts.map +1 -0
  93. package/dist/svelte/components/InferenceTierRow.svelte +199 -0
  94. package/dist/svelte/components/InferenceTierRow.svelte.d.ts +14 -0
  95. package/dist/svelte/components/InferenceTierRow.svelte.d.ts.map +1 -0
  96. package/dist/svelte/components/InferenceTranscriptSettings.svelte +165 -0
  97. package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts +18 -0
  98. package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts.map +1 -0
  99. package/dist/svelte/components/InferenceUsageTable.svelte +177 -0
  100. package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +21 -0
  101. package/dist/svelte/components/InferenceUsageTable.svelte.d.ts.map +1 -0
  102. package/dist/svelte/index.d.ts +17 -0
  103. package/dist/svelte/index.d.ts.map +1 -0
  104. package/dist/svelte/index.js +14 -0
  105. package/dist/svelte/types.d.ts +262 -0
  106. package/dist/svelte/types.d.ts.map +1 -0
  107. package/dist/svelte/types.js +124 -0
  108. package/dist/types.d.ts +47 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +13 -0
  111. package/dist/types.js.map +1 -0
  112. package/package.json +105 -3
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The chain executor (#4752): runs `alias` and `candidates` tier targets.
3
+ *
4
+ * Per call it expands the resolved target into ordered candidates and tries
5
+ * them in turn. A failure is classified (`classify.ts`); `transient` retries
6
+ * the same candidate once after a jittered delay, every class then advances.
7
+ * When a tier's chain is spent and `inference.degrade !== false`, the walk
8
+ * moves down the ladder (never up), stopping at `minimumTier`. Each move is
9
+ * a substitution `{ from, to, reason }` carried in the answering call's
10
+ * `substitutions` usage tag with `tierResolved` set to the tier that
11
+ * answered; a call that never answered records one zero-token row through
12
+ * core's usage pipeline and throws {@link InferenceExhaustedError}.
13
+ *
14
+ * Text already streamed to the caller is never extended by another
15
+ * candidate: a candidate that emitted text and then failed ends the walk
16
+ * with a `partial` substitution (the caller gets the error unless
17
+ * `allowPartial`).
18
+ *
19
+ * Before calling a candidate the walk checks its capabilities (#4758,
20
+ * `context.capabilitiesFor()`) against the request: JSON mode for a
21
+ * `responseFormat`, tool calling for `tools`. A mismatch the caller permits
22
+ * a fallback for (`capabilityFallback`) runs the candidate without the
23
+ * feature, tagged `capabilityFallback`; any other mismatch is a
24
+ * `missing-capability` substitution and the walk advances without a call.
25
+ *
26
+ * The executor imports no SDK: it drives the clients core builds through
27
+ * `context.clientFor()`, so tests run it with fake clients.
28
+ */
29
+ import { type InferenceExecutionContext, type InferenceModelClient } from '@happyvertical/smrt-core';
30
+ /** Test seams for the executor; production uses the defaults. */
31
+ export interface ChainExecutorOptions {
32
+ /** Delay before the one transient retry. Default 200–500 ms, jittered. */
33
+ retryDelayMs?: () => number;
34
+ /** Sleep implementation. */
35
+ sleep?: (ms: number) => Promise<void>;
36
+ /** Clock for the failure row's duration. */
37
+ now?: () => number;
38
+ }
39
+ /**
40
+ * A client whose calls walk the chain in `context.resolution`. `message`,
41
+ * `chat`, `complete` and `stream` walk; `embed` uses the first candidate
42
+ * that can run.
43
+ */
44
+ export declare function createChainClient(context: InferenceExecutionContext, options?: ChainExecutorOptions): InferenceModelClient;
45
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAKL,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EAI1B,MAAM,0BAA0B,CAAC;AAelC,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,4BAA4B;IAC5B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAggBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,yBAAyB,EAClC,OAAO,GAAE,oBAAyB,GACjC,oBAAoB,CAmFtB"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Process registry for code-defined fusions (#4753), on `globalThis` so it
3
+ * survives HMR and duplicate package copies like core's registries. Storage
4
+ * only: `defineFusion()` (fusion.ts) validates before it stores.
5
+ */
6
+ import type { InferenceConfig, InferenceFusionDefinition } from '@happyvertical/smrt-core';
7
+ /** The registry map (created on first use). */
8
+ export declare function fusionStore(): Map<string, InferenceFusionDefinition>;
9
+ /**
10
+ * The definition a fusion key runs: `inference.fusions.<key>` from config
11
+ * when present (the app's layer wins), else the `defineFusion()` one.
12
+ */
13
+ export declare function lookupFusion(key: string, config: InferenceConfig | undefined): InferenceFusionDefinition | undefined;
14
+ /** True when the smrt-prompts process registry holds `key`. */
15
+ export declare function isPromptRegistered(key: string): boolean;
16
+ //# sourceMappingURL=fusion-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fusion-store.d.ts","sourceRoot":"","sources":["../src/fusion-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EACV,eAAe,EACf,yBAAyB,EAC1B,MAAM,0BAA0B,CAAC;AAQlC,+CAA+C;AAC/C,wBAAgB,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAIpE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,GAAG,SAAS,GAClC,yBAAyB,GAAG,SAAS,CAIvC;AAED,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIvD"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Consumer-defined fusions (#4753): the interpreter for `{ fusion: '<key>' }`
3
+ * tier targets. SMRT ships the interpreter and the vocabulary, never a
4
+ * fusion: modes are code, fusions are config.
5
+ *
6
+ * Each seat is a registered prompt key (its resolved template is the seat's
7
+ * instructions) run on a target through the chain executor
8
+ * ({@link createChainClient}), so alias chains, classified fallback and the
9
+ * ladder apply per seat, and every seat's usage row comes from core's usage
10
+ * pipeline tagged `runId`, `fusionKey` and `seat`. The run itself is one
11
+ * `_smrt_ai_runs` row through `context.recordRun()`.
12
+ *
13
+ * Model calls per mode are fixed: `single` 1, `pair` 3, `handoff` 2. A gate
14
+ * adds one typed-decision call and can only remove the remaining stage
15
+ * (recorded `skipped`); a seat's fallback candidates are substitutions of
16
+ * that seat, recorded in its tags, not extra seats.
17
+ *
18
+ * Like the executor, this module imports no SDK: tests drive it with fake
19
+ * clients through `context.clientFor()`.
20
+ */
21
+ import { type InferenceCallOptions, type InferenceExecutionContext, type InferenceExecutor, type InferenceFusionDefinition, type InferenceModelClient, type InferenceRunStatus } from '@happyvertical/smrt-core';
22
+ import { type ChainExecutorOptions } from './executor.js';
23
+ import type { InferenceSubstitution } from './types.js';
24
+ /** Input to {@link defineFusion}: a definition plus its key. */
25
+ export interface DefineFusionInput extends InferenceFusionDefinition {
26
+ key: string;
27
+ }
28
+ /**
29
+ * Register a fusion in the process registry. Throws
30
+ * {@link InferenceConfigError} naming every offender by path
31
+ * (`fusions.<key>...`), including a seat whose prompt key no
32
+ * `definePrompt()` registered (define prompts first). Alias references are
33
+ * checked when the fusion runs, against the config of that call. Same
34
+ * definition again is a no-op; a different one under the same key throws.
35
+ * `inference.fusions.<key>` in app config wins over a registered fusion.
36
+ */
37
+ export declare function defineFusion(input: DefineFusionInput): InferenceFusionDefinition;
38
+ /** The registered (code-defined) fusion for `key`, if any. */
39
+ export declare function getFusion(key: string): InferenceFusionDefinition | undefined;
40
+ /** Registered fusion keys. */
41
+ export declare function listFusions(): string[];
42
+ /** Remove every registered fusion (tests). */
43
+ export declare function clearFusions(): void;
44
+ /** A seat's resolved instructions. */
45
+ export interface FusionSeatPrompt {
46
+ /** The rendered template, sent as the seat's system message. */
47
+ instructions: string;
48
+ /** `promptKey`, `promptLayer`, `templateHash` (#4751). */
49
+ usageTags?: Record<string, string>;
50
+ /** Prompt-level defaults (`temperature`, `maxTokens`); the seat wins. */
51
+ params?: Record<string, unknown>;
52
+ }
53
+ /** Resolves a seat's prompt key; the default uses smrt-prompts. */
54
+ export type FusionPromptResolver = (key: string, options: {
55
+ tenantId?: string | null;
56
+ db?: unknown;
57
+ }) => Promise<FusionSeatPrompt>;
58
+ /**
59
+ * Replace the seat prompt resolver (tests, or hosts that bundle without
60
+ * `@happyvertical/smrt-prompts` resolvable at runtime). `undefined` restores
61
+ * the default.
62
+ */
63
+ export declare function setFusionPromptResolver(resolver: FusionPromptResolver | undefined): void;
64
+ /** Token totals observed for one seat. */
65
+ export interface FusionSeatUsage {
66
+ promptTokens: number;
67
+ completionTokens: number;
68
+ totalTokens: number;
69
+ /** Sum of recorded estimates; absent when no event carried one. */
70
+ estimatedCost?: number;
71
+ }
72
+ /** How one seat went. */
73
+ export type FusionSeatStatus = 'ok' | 'failed' | 'skipped' | 'not-run';
74
+ /** One seat of a {@link FusionRunResult}. */
75
+ export interface FusionSeatResult {
76
+ /** Persona: the seat name (default the prompt key). */
77
+ name: string;
78
+ prompt: string;
79
+ status: FusionSeatStatus;
80
+ /** Target label: `tier:<tier>`, `alias:<name>`, `candidates`, `provider/model`. */
81
+ target: string;
82
+ /** The candidate that answered (`provider/model`, `default`, ...). */
83
+ model?: string;
84
+ /** The seat's text (text seats, and JSON seats that parsed). */
85
+ text?: string;
86
+ /** A JSON seat's parsed value. */
87
+ json?: unknown;
88
+ /**
89
+ * A JSON seat whose text did not parse keeps the text here (never
90
+ * dropped), with the parse error in `parseError`.
91
+ */
92
+ raw?: string;
93
+ parseError?: string;
94
+ /** Why a seat died: failure class (or `unclassified`) and message. */
95
+ failure?: {
96
+ reason: string;
97
+ message: string;
98
+ code?: string;
99
+ };
100
+ substitutions?: InferenceSubstitution[];
101
+ usage?: FusionSeatUsage;
102
+ durationMs?: number;
103
+ }
104
+ /** The decision gate's verdict. */
105
+ export interface FusionGateResult {
106
+ /**
107
+ * `sufficient` skipped the remaining stage; `insufficient` ran it;
108
+ * `unavailable` (no decision client) and `error` ran it too; `not-run`
109
+ * when no output existed to judge.
110
+ */
111
+ status: 'sufficient' | 'insufficient' | 'unavailable' | 'error' | 'not-run';
112
+ choice?: string;
113
+ confidence?: number;
114
+ /** Decision provider and model. */
115
+ model?: string;
116
+ message?: string;
117
+ }
118
+ /** A finished (or failed) fusion run. */
119
+ export interface FusionRunResult {
120
+ runId: string;
121
+ fusionKey: string;
122
+ mode: InferenceFusionDefinition['mode'];
123
+ status: InferenceRunStatus;
124
+ /** The answer: the final seat's text (see the mode rules). */
125
+ text: string;
126
+ seats: FusionSeatResult[];
127
+ gate?: FusionGateResult;
128
+ durationMs: number;
129
+ }
130
+ /**
131
+ * Per-call options a fusion client reads (and strips) besides the usual
132
+ * `ai.message()` options, which reach the final seat.
133
+ */
134
+ export interface FusionCallOptions {
135
+ /** Receives the structured result (per-seat outputs, `raw` JSON text). */
136
+ onFusionResult?: (result: FusionRunResult) => void;
137
+ }
138
+ /**
139
+ * Run a fusion for one input and return the structured result. Throws
140
+ * {@link InferenceFusionError} (`INFERENCE_FUSION_FAILED`) when every seat
141
+ * died, with the run recorded as `failed`.
142
+ */
143
+ export declare function runFusion(context: InferenceExecutionContext, input: string, callerOptions?: InferenceCallOptions, options?: ChainExecutorOptions): Promise<FusionRunResult>;
144
+ /**
145
+ * The client core receives for a `{ fusion }` target: `message()` runs the
146
+ * fusion and resolves the final seat's text; `onFusionResult` in the call
147
+ * options receives the structured result.
148
+ */
149
+ export declare function createFusionClient(context: InferenceExecutionContext, options?: ChainExecutorOptions): InferenceModelClient;
150
+ /**
151
+ * The executor registered into core's seam: `alias` / `candidates` chains
152
+ * and `fusion` targets (#4753), one `kinds` table.
153
+ */
154
+ export declare function createChainExecutor(options?: ChainExecutorOptions): InferenceExecutor;
155
+ //# sourceMappingURL=fusion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fusion.d.ts","sourceRoot":"","sources":["../src/fusion.ts"],"names":[],"mappings":"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"}
package/dist/gate.d.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The hosted inference gate (#4760, epic #4667): Node-only, so it stays off
3
+ * the browser-safe root entry.
4
+ *
5
+ * Core asks the registered `InferenceGate` before every `source: hosted`
6
+ * call (`byo`, `local` and `app` targets never reach it). This gate, in
7
+ * order:
8
+ *
9
+ * 1. **Kill switch.** `inference.hosted.enabled`
10
+ * ({@link HOSTED_INFERENCE_KILL_SWITCH}): a global DISABLE, then the
11
+ * tenant's own DISABLE, blocks every hosted call at once, whatever the
12
+ * plan or spend. Read uncached on every call, without any model call.
13
+ * This is the incident lever
14
+ * (`smrt inference:hosted disable [--tenant <id>]`).
15
+ * 2. **Tenant.** A hosted call with no tenant to bill is blocked.
16
+ * 3. **Entitlement.** The tenant's entitled plan thresholds through
17
+ * smrt-subscriptions' `SubscriptionResolver` (memoized per tenant by the
18
+ * evaluator: once per request burst). No entitled subscription blocks
19
+ * unless `requireSubscription` is false.
20
+ * 4. **Spend.** The plan's `ai.hosted.spend_micros` (tenant) and
21
+ * `ai.hosted.user_spend_micros` (per user) caps against metered hosted
22
+ * spend in each cap's window, read fresh from `_smrt_ai_usage`: block,
23
+ * warn (`onWarning`), or allow.
24
+ *
25
+ * Any error (a database or resolver outage, missing tables, a malformed
26
+ * cap) propagates, and core turns it into a fail-closed block (policy
27
+ * `gate-error`) that counts as no spend.
28
+ *
29
+ * The kill-switch store and spend evaluator are structural: smrt-features'
30
+ * `KillSwitchService` and smrt-subscriptions' `HostedAiSpendEvaluator`
31
+ * satisfy them. Both packages build with the CLI, which depends on this
32
+ * one, so this package must not depend on them.
33
+ */
34
+ import { type InferenceEntitlementDetails, type InferenceGate, type InferenceGateRequest } from '@happyvertical/smrt-core';
35
+ import { type InferencePrincipal } from './resolver.js';
36
+ /**
37
+ * The hosted-inference kill switch key, stored as smrt-features override
38
+ * rows and read with kill-switch semantics (smrt-features
39
+ * `readKillSwitch()`): a global DISABLE, then the tenant's own DISABLE.
40
+ * Persisted identifier: never rename.
41
+ */
42
+ export declare const HOSTED_INFERENCE_KILL_SWITCH = "inference.hosted.enabled";
43
+ /** A kill switch's state (smrt-features `KillSwitchState`, structurally). */
44
+ export interface HostedKillSwitchState {
45
+ key: string;
46
+ enabled: boolean;
47
+ disabledBy?: 'global' | 'tenant';
48
+ }
49
+ /** Reads (and optionally writes) kill switches: `KillSwitchService`. */
50
+ export interface HostedKillSwitchStore {
51
+ read(key: string, options: {
52
+ tenantId?: string | null;
53
+ }): Promise<HostedKillSwitchState>;
54
+ set?(key: string, options: {
55
+ enabled: boolean;
56
+ tenantId?: string | null;
57
+ }): Promise<HostedKillSwitchState>;
58
+ }
59
+ /** One cap evaluation, as smrt-subscriptions' `HostedAiSpendEvaluation`. */
60
+ export interface HostedSpendEvaluation {
61
+ policy: string;
62
+ /** Integer micro-dollars. */
63
+ cap: number;
64
+ /** Integer micro-dollars. */
65
+ spend: number;
66
+ resetsAt: Date | null;
67
+ }
68
+ /** The spend decision, as smrt-subscriptions' `HostedAiSpendDecision`. */
69
+ export interface HostedSpendDecision {
70
+ state: 'ok' | 'warn' | 'blocked';
71
+ entitled: boolean;
72
+ decidedBy: HostedSpendEvaluation | null;
73
+ }
74
+ /** Evaluates hosted spend against the plan: `HostedAiSpendEvaluator`. */
75
+ export interface HostedSpendEvaluator {
76
+ evaluate(input: {
77
+ tenantId: string;
78
+ userId?: string;
79
+ }): Promise<HostedSpendDecision>;
80
+ invalidate?(tenantId?: string): void;
81
+ }
82
+ /** A hosted call allowed past a warning threshold. */
83
+ export interface HostedInferenceGateWarning {
84
+ tenantId: string;
85
+ userId?: string;
86
+ details: InferenceEntitlementDetails;
87
+ request: InferenceGateRequest;
88
+ }
89
+ /** Options for {@link createHostedInferenceGate}. */
90
+ export interface HostedInferenceGateOptions {
91
+ /** Kill-switch store: `new KillSwitchService({ db })` (smrt-features). */
92
+ killSwitch: HostedKillSwitchStore;
93
+ /** Spend: `await HostedAiSpendEvaluator.create({ db })` (smrt-subscriptions). */
94
+ spend: HostedSpendEvaluator;
95
+ /**
96
+ * The acting principal; default the ambient one (tenancy context, then
97
+ * the users session), the same read the per-user resolver makes.
98
+ */
99
+ principal?: () => InferencePrincipal | undefined;
100
+ /**
101
+ * Block hosted calls for a tenant with no entitled subscription (default
102
+ * `true`). Set `false` for apps that give hosted AI to every tenant and
103
+ * use plans only for caps.
104
+ */
105
+ requireSubscription?: boolean;
106
+ /** Called for a call allowed past a `warn` threshold. Must not throw. */
107
+ onWarning?: (warning: HostedInferenceGateWarning) => void;
108
+ }
109
+ /** The gate, plus a way to drop memoized plan thresholds. */
110
+ export interface HostedInferenceGate extends InferenceGate {
111
+ /** Forget memoized plan thresholds (all tenants, or one). */
112
+ invalidate(tenantId?: string): void;
113
+ }
114
+ /**
115
+ * Create the hosted inference gate. Register it with
116
+ * {@link registerHostedInferenceGate}.
117
+ */
118
+ export declare function createHostedInferenceGate(options: HostedInferenceGateOptions): HostedInferenceGate;
119
+ /**
120
+ * Register a hosted gate (or build one from options) with core's seam, and
121
+ * the principal's user as the `userId` usage tag that per-user caps meter.
122
+ * Returns the gate. To remove it, call core's `setInferenceGate(undefined)`
123
+ * and `setAiUsageUserResolver(undefined)`.
124
+ */
125
+ export declare function registerHostedInferenceGate(gateOrOptions: HostedInferenceGate | HostedInferenceGateOptions): HostedInferenceGate;
126
+ /** Read the hosted kill switch (global, then `tenantId`). No model call. */
127
+ export declare function getHostedInferenceSwitch(killSwitch: HostedKillSwitchStore, options?: {
128
+ tenantId?: string | null;
129
+ }): Promise<HostedKillSwitchState>;
130
+ /**
131
+ * Turn hosted inference off (`enabled: false`) or back on, globally or for
132
+ * one tenant: the incident lever. Takes effect on the next hosted call in
133
+ * every process (the gate reads it uncached). Enabling a tenant does not
134
+ * lift a global disable. The caller is the authorization: smrt-features'
135
+ * `KillSwitchService.set()` writes the override rows directly (the CLI
136
+ * holds database credentials); an admin UI writes through
137
+ * `FeatureOverrideService` with its authorizer instead.
138
+ */
139
+ export declare function setHostedInferenceEnabled(killSwitch: HostedKillSwitchStore, options: {
140
+ enabled: boolean;
141
+ tenantId?: string | null;
142
+ }): Promise<HostedKillSwitchState>;
143
+ //# sourceMappingURL=gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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"}
package/dist/gate.js ADDED
@@ -0,0 +1,94 @@
1
+ import { getAmbientInferencePrincipal } from "./resolver.js";
2
+ import { setAiUsageUserResolver, setInferenceGate } from "@happyvertical/smrt-core";
3
+ //#region src/gate.ts
4
+ var HOSTED_INFERENCE_KILL_SWITCH = "inference.hosted.enabled";
5
+ function text(value) {
6
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
7
+ }
8
+ function block(details) {
9
+ return {
10
+ action: "block",
11
+ details
12
+ };
13
+ }
14
+ function spendDetails(decision) {
15
+ const by = decision.decidedBy;
16
+ if (!by) return null;
17
+ return {
18
+ policy: by.policy,
19
+ cap: by.cap,
20
+ spend: by.spend,
21
+ ...by.resetsAt ? { resetsAt: by.resetsAt.toISOString() } : {}
22
+ };
23
+ }
24
+ function createHostedInferenceGate(options) {
25
+ const principalOf = options.principal ?? getAmbientInferencePrincipal;
26
+ const requireSubscription = options.requireSubscription !== false;
27
+ const { killSwitch, spend } = options;
28
+ return {
29
+ async check(request) {
30
+ const principal = principalOf();
31
+ const tenantId = text(request.tenantId) ?? text(principal?.tenantId);
32
+ const state = await killSwitch.read(HOSTED_INFERENCE_KILL_SWITCH, { ...tenantId ? { tenantId } : {} });
33
+ if (state.enabled !== true) return block({
34
+ policy: "kill-switch",
35
+ ...state.disabledBy ? { disabledBy: state.disabledBy } : {}
36
+ });
37
+ if (!tenantId) return block({ policy: "no-tenant" });
38
+ const userId = request.userId === null || text(principal?.tenantId)?.toLowerCase() !== tenantId.toLowerCase() ? void 0 : text(principal?.userId);
39
+ const decision = await spend.evaluate({
40
+ tenantId,
41
+ ...userId ? { userId } : {}
42
+ });
43
+ if (!decision.entitled && requireSubscription) return block({ policy: "subscription" });
44
+ if (decision.state === "blocked") {
45
+ const details = spendDetails(decision);
46
+ if (!details) throw new Error("Hosted spend blocked without a cap.");
47
+ return block(details);
48
+ }
49
+ if (decision.state === "warn") {
50
+ const details = spendDetails(decision);
51
+ if (details) {
52
+ try {
53
+ options.onWarning?.({
54
+ tenantId,
55
+ ...userId ? { userId } : {},
56
+ details,
57
+ request
58
+ });
59
+ } catch {}
60
+ return {
61
+ action: "warn",
62
+ details
63
+ };
64
+ }
65
+ }
66
+ return { action: "allow" };
67
+ },
68
+ invalidate(tenantId) {
69
+ spend.invalidate?.(tenantId);
70
+ }
71
+ };
72
+ }
73
+ function registerHostedInferenceGate(gateOrOptions) {
74
+ const isGate = typeof gateOrOptions.check === "function";
75
+ const gate = isGate ? gateOrOptions : createHostedInferenceGate(gateOrOptions);
76
+ const principalOf = !isGate && gateOrOptions.principal || getAmbientInferencePrincipal;
77
+ setInferenceGate(gate);
78
+ setAiUsageUserResolver(() => text(principalOf()?.userId));
79
+ return gate;
80
+ }
81
+ async function getHostedInferenceSwitch(killSwitch, options = {}) {
82
+ return killSwitch.read(HOSTED_INFERENCE_KILL_SWITCH, options.tenantId ? { tenantId: options.tenantId } : {});
83
+ }
84
+ async function setHostedInferenceEnabled(killSwitch, options) {
85
+ if (!killSwitch.set) throw new Error("This kill-switch store is read-only.");
86
+ return killSwitch.set(HOSTED_INFERENCE_KILL_SWITCH, {
87
+ enabled: options.enabled,
88
+ ...options.tenantId !== void 0 ? { tenantId: options.tenantId } : {}
89
+ });
90
+ }
91
+ //#endregion
92
+ export { HOSTED_INFERENCE_KILL_SWITCH, createHostedInferenceGate, getHostedInferenceSwitch, registerHostedInferenceGate, setHostedInferenceEnabled };
93
+
94
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","names":[],"sources":["../src/gate.ts"],"sourcesContent":["/**\n * The hosted inference gate (#4760, epic #4667): Node-only, so it stays off\n * the browser-safe root entry.\n *\n * Core asks the registered `InferenceGate` before every `source: hosted`\n * call (`byo`, `local` and `app` targets never reach it). This gate, in\n * order:\n *\n * 1. **Kill switch.** `inference.hosted.enabled`\n * ({@link HOSTED_INFERENCE_KILL_SWITCH}): a global DISABLE, then the\n * tenant's own DISABLE, blocks every hosted call at once, whatever the\n * plan or spend. Read uncached on every call, without any model call.\n * This is the incident lever\n * (`smrt inference:hosted disable [--tenant <id>]`).\n * 2. **Tenant.** A hosted call with no tenant to bill is blocked.\n * 3. **Entitlement.** The tenant's entitled plan thresholds through\n * smrt-subscriptions' `SubscriptionResolver` (memoized per tenant by the\n * evaluator: once per request burst). No entitled subscription blocks\n * unless `requireSubscription` is false.\n * 4. **Spend.** The plan's `ai.hosted.spend_micros` (tenant) and\n * `ai.hosted.user_spend_micros` (per user) caps against metered hosted\n * spend in each cap's window, read fresh from `_smrt_ai_usage`: block,\n * warn (`onWarning`), or allow.\n *\n * Any error (a database or resolver outage, missing tables, a malformed\n * cap) propagates, and core turns it into a fail-closed block (policy\n * `gate-error`) that counts as no spend.\n *\n * The kill-switch store and spend evaluator are structural: smrt-features'\n * `KillSwitchService` and smrt-subscriptions' `HostedAiSpendEvaluator`\n * satisfy them. Both packages build with the CLI, which depends on this\n * one, so this package must not depend on them.\n */\n\nimport {\n type InferenceEntitlementDetails,\n type InferenceGate,\n type InferenceGateDecision,\n type InferenceGateRequest,\n setAiUsageUserResolver,\n setInferenceGate,\n} from '@happyvertical/smrt-core';\nimport {\n getAmbientInferencePrincipal,\n type InferencePrincipal,\n} from './resolver.js';\n\n/**\n * The hosted-inference kill switch key, stored as smrt-features override\n * rows and read with kill-switch semantics (smrt-features\n * `readKillSwitch()`): a global DISABLE, then the tenant's own DISABLE.\n * Persisted identifier: never rename.\n */\nexport const HOSTED_INFERENCE_KILL_SWITCH = 'inference.hosted.enabled';\n\n/** A kill switch's state (smrt-features `KillSwitchState`, structurally). */\nexport interface HostedKillSwitchState {\n key: string;\n enabled: boolean;\n disabledBy?: 'global' | 'tenant';\n}\n\n/** Reads (and optionally writes) kill switches: `KillSwitchService`. */\nexport interface HostedKillSwitchStore {\n read(\n key: string,\n options: { tenantId?: string | null },\n ): Promise<HostedKillSwitchState>;\n set?(\n key: string,\n options: { enabled: boolean; tenantId?: string | null },\n ): Promise<HostedKillSwitchState>;\n}\n\n/** One cap evaluation, as smrt-subscriptions' `HostedAiSpendEvaluation`. */\nexport interface HostedSpendEvaluation {\n policy: string;\n /** Integer micro-dollars. */\n cap: number;\n /** Integer micro-dollars. */\n spend: number;\n resetsAt: Date | null;\n}\n\n/** The spend decision, as smrt-subscriptions' `HostedAiSpendDecision`. */\nexport interface HostedSpendDecision {\n state: 'ok' | 'warn' | 'blocked';\n entitled: boolean;\n decidedBy: HostedSpendEvaluation | null;\n}\n\n/** Evaluates hosted spend against the plan: `HostedAiSpendEvaluator`. */\nexport interface HostedSpendEvaluator {\n evaluate(input: {\n tenantId: string;\n userId?: string;\n }): Promise<HostedSpendDecision>;\n invalidate?(tenantId?: string): void;\n}\n\n/** A hosted call allowed past a warning threshold. */\nexport interface HostedInferenceGateWarning {\n tenantId: string;\n userId?: string;\n details: InferenceEntitlementDetails;\n request: InferenceGateRequest;\n}\n\n/** Options for {@link createHostedInferenceGate}. */\nexport interface HostedInferenceGateOptions {\n /** Kill-switch store: `new KillSwitchService({ db })` (smrt-features). */\n killSwitch: HostedKillSwitchStore;\n /** Spend: `await HostedAiSpendEvaluator.create({ db })` (smrt-subscriptions). */\n spend: HostedSpendEvaluator;\n /**\n * The acting principal; default the ambient one (tenancy context, then\n * the users session), the same read the per-user resolver makes.\n */\n principal?: () => InferencePrincipal | undefined;\n /**\n * Block hosted calls for a tenant with no entitled subscription (default\n * `true`). Set `false` for apps that give hosted AI to every tenant and\n * use plans only for caps.\n */\n requireSubscription?: boolean;\n /** Called for a call allowed past a `warn` threshold. Must not throw. */\n onWarning?: (warning: HostedInferenceGateWarning) => void;\n}\n\n/** The gate, plus a way to drop memoized plan thresholds. */\nexport interface HostedInferenceGate extends InferenceGate {\n /** Forget memoized plan thresholds (all tenants, or one). */\n invalidate(tenantId?: string): void;\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() !== ''\n ? value.trim()\n : undefined;\n}\n\nfunction block(details: InferenceEntitlementDetails): InferenceGateDecision {\n return { action: 'block', details };\n}\n\nfunction spendDetails(\n decision: HostedSpendDecision,\n): InferenceEntitlementDetails | null {\n const by = decision.decidedBy;\n if (!by) return null;\n return {\n policy: by.policy,\n cap: by.cap,\n spend: by.spend,\n ...(by.resetsAt ? { resetsAt: by.resetsAt.toISOString() } : {}),\n };\n}\n\n/**\n * Create the hosted inference gate. Register it with\n * {@link registerHostedInferenceGate}.\n */\nexport function createHostedInferenceGate(\n options: HostedInferenceGateOptions,\n): HostedInferenceGate {\n const principalOf = options.principal ?? getAmbientInferencePrincipal;\n const requireSubscription = options.requireSubscription !== false;\n const { killSwitch, spend } = options;\n\n return {\n async check(request) {\n const principal = principalOf();\n const tenantId = text(request.tenantId) ?? text(principal?.tenantId);\n\n const state = await killSwitch.read(HOSTED_INFERENCE_KILL_SWITCH, {\n ...(tenantId ? { tenantId } : {}),\n });\n if (state.enabled !== true) {\n return block({\n policy: 'kill-switch',\n ...(state.disabledBy ? { disabledBy: state.disabledBy } : {}),\n });\n }\n if (!tenantId) return block({ policy: 'no-tenant' });\n\n // The per-user cap applies to the principal's own user, in its own\n // tenant; a shared client (`userId: null`, agents) has none.\n const userId =\n request.userId === null ||\n text(principal?.tenantId)?.toLowerCase() !== tenantId.toLowerCase()\n ? undefined\n : text(principal?.userId);\n\n const decision = await spend.evaluate({\n tenantId,\n ...(userId ? { userId } : {}),\n });\n if (!decision.entitled && requireSubscription) {\n return block({ policy: 'subscription' });\n }\n if (decision.state === 'blocked') {\n const details = spendDetails(decision);\n // A blocked decision always names its cap; refuse to guess if not.\n if (!details) throw new Error('Hosted spend blocked without a cap.');\n return block(details);\n }\n if (decision.state === 'warn') {\n const details = spendDetails(decision);\n if (details) {\n try {\n options.onWarning?.({\n tenantId,\n ...(userId ? { userId } : {}),\n details,\n request,\n });\n } catch {\n // A warning sink never blocks or fails the call.\n }\n return { action: 'warn', details };\n }\n }\n return { action: 'allow' };\n },\n invalidate(tenantId) {\n spend.invalidate?.(tenantId);\n },\n };\n}\n\n/**\n * Register a hosted gate (or build one from options) with core's seam, and\n * the principal's user as the `userId` usage tag that per-user caps meter.\n * Returns the gate. To remove it, call core's `setInferenceGate(undefined)`\n * and `setAiUsageUserResolver(undefined)`.\n */\nexport function registerHostedInferenceGate(\n gateOrOptions: HostedInferenceGate | HostedInferenceGateOptions,\n): HostedInferenceGate {\n const isGate =\n typeof (gateOrOptions as HostedInferenceGate).check === 'function';\n const gate = isGate\n ? (gateOrOptions as HostedInferenceGate)\n : createHostedInferenceGate(gateOrOptions as HostedInferenceGateOptions);\n const principalOf =\n (!isGate && (gateOrOptions as HostedInferenceGateOptions).principal) ||\n getAmbientInferencePrincipal;\n setInferenceGate(gate);\n setAiUsageUserResolver(() => text(principalOf()?.userId));\n return gate;\n}\n\n/** Read the hosted kill switch (global, then `tenantId`). No model call. */\nexport async function getHostedInferenceSwitch(\n killSwitch: HostedKillSwitchStore,\n options: { tenantId?: string | null } = {},\n): Promise<HostedKillSwitchState> {\n return killSwitch.read(\n HOSTED_INFERENCE_KILL_SWITCH,\n options.tenantId ? { tenantId: options.tenantId } : {},\n );\n}\n\n/**\n * Turn hosted inference off (`enabled: false`) or back on, globally or for\n * one tenant: the incident lever. Takes effect on the next hosted call in\n * every process (the gate reads it uncached). Enabling a tenant does not\n * lift a global disable. The caller is the authorization: smrt-features'\n * `KillSwitchService.set()` writes the override rows directly (the CLI\n * holds database credentials); an admin UI writes through\n * `FeatureOverrideService` with its authorizer instead.\n */\nexport async function setHostedInferenceEnabled(\n killSwitch: HostedKillSwitchStore,\n options: { enabled: boolean; tenantId?: string | null },\n): Promise<HostedKillSwitchState> {\n if (!killSwitch.set) {\n throw new Error('This kill-switch store is read-only.');\n }\n return killSwitch.set(HOSTED_INFERENCE_KILL_SWITCH, {\n enabled: options.enabled,\n ...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),\n });\n}\n"],"mappings":"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"}