@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.
- package/AGENTS.md +206 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +73 -2
- package/agents/fusions.md +105 -0
- package/agents/hosted.md +161 -0
- package/agents/resolver.md +65 -0
- package/agents/settings.md +107 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js +423 -0
- package/dist/chunks/InferenceOverrideCollection-Dat4TrK5.js.map +1 -0
- package/dist/chunks/provider-config-BSpIQRVM.js +1873 -0
- package/dist/chunks/provider-config-BSpIQRVM.js.map +1 -0
- package/dist/classify.d.ts +32 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/executor.d.ts +45 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/fusion-store.d.ts +16 -0
- package/dist/fusion-store.d.ts.map +1 -0
- package/dist/fusion.d.ts +155 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/gate.d.ts +143 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +94 -0
- package/dist/gate.js.map +1 -0
- package/dist/hosted.d.ts +461 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +453 -0
- package/dist/models/InferenceOverride.d.ts +38 -0
- package/dist/models/InferenceOverride.d.ts.map +1 -0
- package/dist/models/InferenceOverrideCollection.d.ts +35 -0
- package/dist/models/InferenceOverrideCollection.d.ts.map +1 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +2 -0
- package/dist/node.d.ts +110 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +1414 -0
- package/dist/node.js.map +1 -0
- package/dist/provider-config.d.ts +113 -0
- package/dist/provider-config.d.ts.map +1 -0
- package/dist/register.d.ts +30 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/resolver.d.ts +166 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +291 -0
- package/dist/resolver.js.map +1 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1138 -0
- package/dist/server.js.map +1 -0
- package/dist/settings/cache.d.ts +5 -0
- package/dist/settings/cache.d.ts.map +1 -0
- package/dist/settings/keys.d.ts +70 -0
- package/dist/settings/keys.d.ts.map +1 -0
- package/dist/settings/layers.d.ts +68 -0
- package/dist/settings/layers.d.ts.map +1 -0
- package/dist/settings/override-service.d.ts +119 -0
- package/dist/settings/override-service.d.ts.map +1 -0
- package/dist/settings/preferences.d.ts +137 -0
- package/dist/settings/preferences.d.ts.map +1 -0
- package/dist/settings/resolver.d.ts +41 -0
- package/dist/settings/resolver.d.ts.map +1 -0
- package/dist/settings/settings-service.d.ts +355 -0
- package/dist/settings/settings-service.d.ts.map +1 -0
- package/dist/settings/targets.d.ts +112 -0
- package/dist/settings/targets.d.ts.map +1 -0
- package/dist/settings/transcripts.d.ts +33 -0
- package/dist/settings/transcripts.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +341 -0
- package/dist/smrt-knowledge.okf.md +80 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte +210 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts +13 -0
- package/dist/svelte/components/InferenceAliasEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceDefaultView.svelte +330 -0
- package/dist/svelte/components/InferenceDefaultView.svelte.d.ts +15 -0
- package/dist/svelte/components/InferenceDefaultView.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceFusionList.svelte +141 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts +16 -0
- package/dist/svelte/components/InferenceFusionList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte +392 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +84 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceTierRow.svelte +199 -0
- package/dist/svelte/components/InferenceTierRow.svelte.d.ts +14 -0
- package/dist/svelte/components/InferenceTierRow.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte +165 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts +18 -0
- package/dist/svelte/components/InferenceTranscriptSettings.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InferenceUsageTable.svelte +177 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +21 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +14 -0
- package/dist/svelte/types.d.ts +262 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +124 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- 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"}
|
package/dist/fusion.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EACL,KAAK,2BAA2B,EAChC,KAAK,aAAa,EAElB,KAAK,oBAAoB,EAG1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AAEvB;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,6BAA6B,CAAC;AAEvE,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;CAClC;AAED,wEAAwE;AACxE,MAAM,WAAW,qBAAqB;IACpC,IAAI,CACF,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GACpC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAClC,GAAG,CAAC,CACF,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GACtD,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACnC;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACvB;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACzC;AAED,yEAAyE;AACzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjC,UAAU,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,sDAAsD;AACtD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,EAAE,oBAAoB,CAAC;CAC/B;AAED,qDAAqD;AACrD,MAAM,WAAW,0BAA0B;IACzC,0EAA0E;IAC1E,UAAU,EAAE,qBAAqB,CAAC;IAClC,iFAAiF;IACjF,KAAK,EAAE,oBAAoB,CAAC;IAC5B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,kBAAkB,GAAG,SAAS,CAAC;IACjD;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yEAAyE;IACzE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,0BAA0B,KAAK,IAAI,CAAC;CAC3D;AAED,6DAA6D;AAC7D,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,6DAA6D;IAC7D,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAyBD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,mBAAmB,CAgErB;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,mBAAmB,GAAG,0BAA0B,GAC9D,mBAAmB,CAYrB;AAED,4EAA4E;AAC5E,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACzC,OAAO,CAAC,qBAAqB,CAAC,CAKhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,qBAAqB,EACjC,OAAO,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACtD,OAAO,CAAC,qBAAqB,CAAC,CAQhC"}
|
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
|
package/dist/gate.js.map
ADDED
|
@@ -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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"}
|