@cloudpeers-jkl/model-router 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,28 @@ In mcp, don't construct deps yourself — import the prewired composition from
59
59
  - **Uniform metering:** every call reports through the injected meter with a
60
60
  `router` metadata block — provider, tier, taskClass, gate.
61
61
 
62
+ ## Embed contract (0.4.0)
63
+
64
+ A separate path from generate — `routeMessages` and `BackendAdapter` are
65
+ unchanged. `routeEmbed({ texts, cloudpeers: { sovereignty_class } }, ctx,
66
+ { adapters: EmbedAdapter[], meter })` walks the adapters in order and returns
67
+ `{ vectors, dims, model, provider, surface, gate, decision }`.
68
+
69
+ - **Gate:** the shared `gateRule` (the same rule as `evaluateGate`), always
70
+ enforced (no monitor stage): `local_only` reaches only `edge` / `self_hosted`
71
+ surfaces, or a cloud provider listed in `MODEL_ROUTER_BAA_PROVIDERS`. No
72
+ permitted adapter → `RouterBlockedError` 412.
73
+ - **Terminal errors:** no available adapter → `RouterError` 503
74
+ `no_embed_backend`. Every permitted adapter failed → 503
75
+ `embed_backends_failed`, with the last adapter error as `cause`. Exceptions:
76
+ when every failure was a 4xx `RouterError`, the last one is rethrown
77
+ unchanged; an aborted `ctx.signal` throws an `AbortError`.
78
+ - **Edge:** `edgeEmbedDescriptor()` is an honest stub (`available()` false,
79
+ `embed()` throws). Edge embedding runs in the caller's browser
80
+ (cloudpeers-github `lib/edge/embeddings.ts`); the server never invokes it.
81
+ - **Metering:** one `deps.meter` call per embed, `metadata.router` =
82
+ `{ op: 'embed', provider, surface, gate, dims, count, decision }`.
83
+
62
84
  ## Env (deploy-time control plane)
63
85
 
64
86
  `MODEL_ROUTER_TIER0_MODEL` / `MODEL_ROUTER_TIER0_URL` (Tier 0 self-hosted),
@@ -40,7 +40,7 @@ export function authMode(env = process.env) {
40
40
  return 'none';
41
41
  }
42
42
  export class AnthropicCredentials {
43
- constructor(env = process.env, deps = { fetch: (...a) => globalThis.fetch(...a), now: () => Date.now() }) {
43
+ constructor(env = process.env, deps = { fetch: (input, init) => globalThis.fetch(input, init), now: () => Date.now() }) {
44
44
  this.env = env;
45
45
  this.deps = deps;
46
46
  this.cached = null;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Edge (on-device) embed descriptor — surface: edge. Honest stub.
3
+ *
4
+ * The real edge execution runs in the visitor's browser (cloudpeers-github
5
+ * `lib/edge/embeddings.ts`: ONNX Runtime Web, all-MiniLM-L6-v2 q8, 384 dims,
6
+ * weights served same-origin). The text never reaches a server, so the
7
+ * server never invokes this adapter: `available()` is false and `embed()`
8
+ * throws. It exists so the capability surface and the gate can name the edge
9
+ * surface truthfully — `cloud:false`, therefore permitted for local_only —
10
+ * and so browser-side metering rows can carry a provider the router knows.
11
+ *
12
+ * A client-side embedder (browser/app) may construct its own EmbedAdapter
13
+ * with id 'edge' and surface 'edge' that does run locally, and pass it to
14
+ * routeEmbed in that runtime.
15
+ */
16
+ import type { EmbedAdapter } from '../types.js';
17
+ /**
18
+ * The model the browser edge embedder ships today.
19
+ * Keep in sync with cloudpeers-github `lib/edge/embeddings.ts` EDGE_MODEL (id, dims).
20
+ */
21
+ export declare const EDGE_EMBED_MODEL: {
22
+ readonly id: "Xenova/all-MiniLM-L6-v2";
23
+ readonly dims: 384;
24
+ readonly license: "Apache-2.0";
25
+ };
26
+ export declare function edgeEmbedDescriptor(): EmbedAdapter;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The model the browser edge embedder ships today.
3
+ * Keep in sync with cloudpeers-github `lib/edge/embeddings.ts` EDGE_MODEL (id, dims).
4
+ */
5
+ export const EDGE_EMBED_MODEL = { id: 'Xenova/all-MiniLM-L6-v2', dims: 384, license: 'Apache-2.0' };
6
+ export function edgeEmbedDescriptor() {
7
+ return {
8
+ id: 'edge',
9
+ display_name: 'on-device embedder (runs in the caller runtime, never on a server)',
10
+ surface: 'edge',
11
+ available: () => false,
12
+ async embed(_texts, _ctx) {
13
+ throw new Error('edge embedding executes on the caller device; the server never invokes it');
14
+ },
15
+ };
16
+ }
@@ -4,9 +4,9 @@
4
4
  * reflects truth"). Embedded-library v1 exposes this as a function; the
5
5
  * mcp server surfaces it on its discovery endpoints.
6
6
  */
7
- import type { BackendAdapter, BackendSurface, Provider, ToolUseCapability } from './types.js';
7
+ import type { BackendAdapter, BackendSurface, GenerateProvider, ToolUseCapability } from './types.js';
8
8
  export interface BackendCapabilities {
9
- id: Provider;
9
+ id: GenerateProvider;
10
10
  display_name: string;
11
11
  surface: BackendSurface;
12
12
  available: boolean;
@@ -15,4 +15,4 @@ export interface BackendCapabilities {
15
15
  supports_prompt_caching: boolean;
16
16
  tool_use: ToolUseCapability;
17
17
  }
18
- export declare function capabilityMatrix(adapters: Partial<Record<Provider, BackendAdapter>>): BackendCapabilities[];
18
+ export declare function capabilityMatrix(adapters: Partial<Record<GenerateProvider, BackendAdapter>>): BackendCapabilities[];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Embed contract — `routeEmbed` (edge-inference Phase 1, 2026-09-24).
3
+ *
4
+ * Deliberately separate from the generate path (routeMessages / BackendAdapter
5
+ * / tier chain), which is untouched. Flow: validate (400) → order adapters
6
+ * (advisory backend_hint) → per adapter: availability → privacy gate →
7
+ * embed → shape check (a malformed result escalates to the next adapter) →
8
+ * uniform metering through the injected meter.
9
+ *
10
+ * Gate: the same rule as `evaluateGate` — for local_only, cloud surfaces are
11
+ * permitted only through a MODEL_ROUTER_BAA_PROVIDERS provider; edge and
12
+ * self_hosted (cloud:false) are always permitted — but always in ENFORCE
13
+ * mode. There is no monitor stage for embed: this path has no legacy callers
14
+ * to keep working, so it fails closed from its first release. A local_only
15
+ * embed with no permitted adapter throws RouterBlockedError (412).
16
+ */
17
+ import { z } from 'zod';
18
+ import type { EmbedAdapter, EmbedDeps, EmbedRequest, EmbedRouteResult, GateOutcome, SovereigntyClass } from './types.js';
19
+ export declare const MAX_EMBED_TEXTS = 256;
20
+ export declare const MAX_EMBED_TEXT_CHARS = 8192;
21
+ export declare const embedRequestSchema: z.ZodObject<{
22
+ texts: z.ZodArray<z.ZodString, "many">;
23
+ cloudpeers: z.ZodObject<{
24
+ v: z.ZodOptional<z.ZodLiteral<1>>;
25
+ sovereignty_class: z.ZodEnum<["local_only", "aggregate_only", "externalizable"]>;
26
+ backend_hint: z.ZodOptional<z.ZodEnum<["selfhosted", "gemini", "anthropic", "edge"]>>;
27
+ }, "strict", z.ZodTypeAny, {
28
+ sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
29
+ v?: 1 | undefined;
30
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | "edge" | undefined;
31
+ }, {
32
+ sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
33
+ v?: 1 | undefined;
34
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | "edge" | undefined;
35
+ }>;
36
+ }, "strict", z.ZodTypeAny, {
37
+ texts: string[];
38
+ cloudpeers: {
39
+ sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
40
+ v?: 1 | undefined;
41
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | "edge" | undefined;
42
+ };
43
+ }, {
44
+ texts: string[];
45
+ cloudpeers: {
46
+ sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
47
+ v?: 1 | undefined;
48
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | "edge" | undefined;
49
+ };
50
+ }>;
51
+ /** Parse + narrow. Throws RouterValidationError (statusCode 400) on shape errors. */
52
+ export declare function parseEmbedRequest(input: unknown): EmbedRequest;
53
+ export interface EmbedContext {
54
+ userId: string;
55
+ serviceId: string;
56
+ /** Metering label (token-tracker `operation`). */
57
+ operation: string;
58
+ metadata?: Record<string, unknown>;
59
+ signal?: AbortSignal;
60
+ }
61
+ /**
62
+ * The embed gate: evaluateGate's rule with the monitor stage removed.
63
+ * `would_block` (monitor) is promoted to `blocked`.
64
+ */
65
+ export declare function evaluateEmbedGate(sovereigntyClass: SovereigntyClass, adapter: EmbedAdapter): GateOutcome;
66
+ export declare function routeEmbed(input: unknown, ctx: EmbedContext, deps: EmbedDeps): Promise<EmbedRouteResult>;
package/dist/embed.js ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Embed contract — `routeEmbed` (edge-inference Phase 1, 2026-09-24).
3
+ *
4
+ * Deliberately separate from the generate path (routeMessages / BackendAdapter
5
+ * / tier chain), which is untouched. Flow: validate (400) → order adapters
6
+ * (advisory backend_hint) → per adapter: availability → privacy gate →
7
+ * embed → shape check (a malformed result escalates to the next adapter) →
8
+ * uniform metering through the injected meter.
9
+ *
10
+ * Gate: the same rule as `evaluateGate` — for local_only, cloud surfaces are
11
+ * permitted only through a MODEL_ROUTER_BAA_PROVIDERS provider; edge and
12
+ * self_hosted (cloud:false) are always permitted — but always in ENFORCE
13
+ * mode. There is no monitor stage for embed: this path has no legacy callers
14
+ * to keep working, so it fails closed from its first release. A local_only
15
+ * embed with no permitted adapter throws RouterBlockedError (412).
16
+ */
17
+ import { z } from 'zod';
18
+ import { RouterBlockedError, RouterError, RouterValidationError } from './errors.js';
19
+ import { gateRule } from './policy.js';
20
+ export const MAX_EMBED_TEXTS = 256;
21
+ export const MAX_EMBED_TEXT_CHARS = 8192;
22
+ const PROVIDERS = ['selfhosted', 'gemini', 'anthropic', 'edge'];
23
+ export const embedRequestSchema = z
24
+ .object({
25
+ texts: z.array(z.string().min(1).max(MAX_EMBED_TEXT_CHARS)).min(1).max(MAX_EMBED_TEXTS),
26
+ cloudpeers: z
27
+ .object({
28
+ v: z.literal(1).optional(),
29
+ sovereignty_class: z.enum(['local_only', 'aggregate_only', 'externalizable']),
30
+ backend_hint: z.enum(PROVIDERS).optional(),
31
+ })
32
+ .strict(),
33
+ })
34
+ .strict();
35
+ /** Parse + narrow. Throws RouterValidationError (statusCode 400) on shape errors. */
36
+ export function parseEmbedRequest(input) {
37
+ const parsed = embedRequestSchema.safeParse(input);
38
+ if (!parsed.success) {
39
+ const detail = parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`).join('; ');
40
+ throw new RouterValidationError(`Invalid embed request: ${detail}`);
41
+ }
42
+ return parsed.data;
43
+ }
44
+ /**
45
+ * The embed gate: evaluateGate's rule with the monitor stage removed.
46
+ * `would_block` (monitor) is promoted to `blocked`.
47
+ */
48
+ export function evaluateEmbedGate(sovereigntyClass, adapter) {
49
+ const outcome = gateRule(sovereigntyClass, adapter.surface === 'cloud', adapter.id);
50
+ return outcome === 'would_block' ? 'blocked' : outcome;
51
+ }
52
+ function key(a) {
53
+ return `embed/${a.id}`;
54
+ }
55
+ function ordered(adapters, hint) {
56
+ if (!hint)
57
+ return adapters;
58
+ return [...adapters.filter((a) => a.id === hint), ...adapters.filter((a) => a.id !== hint)];
59
+ }
60
+ function wellFormed(out, n) {
61
+ return (Array.isArray(out.vectors) &&
62
+ out.vectors.length === n &&
63
+ Number.isInteger(out.dims) &&
64
+ out.dims > 0 &&
65
+ out.vectors.every((v) => v.length === out.dims));
66
+ }
67
+ function abortError() {
68
+ const aborted = new Error('embed request aborted');
69
+ aborted.name = 'AbortError';
70
+ return aborted;
71
+ }
72
+ export async function routeEmbed(input, ctx, deps) {
73
+ const req = parseEmbedRequest(input);
74
+ const sov = req.cloudpeers.sovereignty_class;
75
+ const walked = [];
76
+ const blockedCloud = [];
77
+ let escalations = 0;
78
+ let lastError = null;
79
+ /** True while every adapter failure so far has been a caller-side (4xx) RouterError. */
80
+ let allClientErrors = true;
81
+ const decisionFor = (chosen) => {
82
+ const d = {
83
+ sovereignty_class: sov,
84
+ eligible_set: [...walked],
85
+ chosen_backend: chosen ? key(chosen) : null,
86
+ };
87
+ if (sov === 'local_only') {
88
+ d.no_egress_proof = {
89
+ gate_enforced: true,
90
+ cloud_tiers_blocked: [...blockedCloud],
91
+ chosen_backend_cloud: chosen ? chosen.surface === 'cloud' : false,
92
+ };
93
+ }
94
+ return d;
95
+ };
96
+ for (const adapter of ordered(deps.adapters, req.cloudpeers.backend_hint)) {
97
+ if (ctx.signal?.aborted)
98
+ throw abortError();
99
+ if (!adapter.available())
100
+ continue;
101
+ const gate = evaluateEmbedGate(sov, adapter);
102
+ if (gate === 'blocked') {
103
+ blockedCloud.push(key(adapter));
104
+ console.warn(`[privacy-gate] BLOCKED local_only → ${key(adapter)} (op=${ctx.operation} svc=${ctx.serviceId})`);
105
+ continue;
106
+ }
107
+ walked.push(key(adapter));
108
+ const started = performance.now();
109
+ try {
110
+ const out = await adapter.embed(req.texts, {
111
+ userId: ctx.userId,
112
+ serviceId: ctx.serviceId,
113
+ operation: ctx.operation,
114
+ signal: ctx.signal,
115
+ });
116
+ if (!wellFormed(out, req.texts.length))
117
+ throw new Error('malformed embed result (vector count or dims)');
118
+ const latency_ms = Math.round(performance.now() - started);
119
+ const decision = decisionFor(adapter);
120
+ await deps
121
+ .meter({
122
+ userId: ctx.userId,
123
+ serviceId: ctx.serviceId,
124
+ model: out.model,
125
+ operation: ctx.operation,
126
+ promptTokens: out.tokens_in ?? 0,
127
+ completionTokens: 0,
128
+ metadata: {
129
+ ...ctx.metadata,
130
+ router: {
131
+ op: 'embed',
132
+ provider: adapter.id,
133
+ surface: adapter.surface,
134
+ gate,
135
+ dims: out.dims,
136
+ count: out.vectors.length,
137
+ decision,
138
+ },
139
+ },
140
+ })
141
+ .catch((err) => console.error('[model-router] embed metering failed (non-blocking):', err?.message));
142
+ return {
143
+ ...out,
144
+ provider: adapter.id,
145
+ surface: adapter.surface,
146
+ gate,
147
+ latency_ms,
148
+ escalations,
149
+ decision,
150
+ };
151
+ }
152
+ catch (err) {
153
+ lastError = err instanceof Error ? err : new Error(String(err));
154
+ if (!(lastError instanceof RouterError && lastError.statusCode >= 400 && lastError.statusCode < 500)) {
155
+ allClientErrors = false;
156
+ }
157
+ escalations++;
158
+ console.warn(`[model-router] ${key(adapter)} failed (op=${ctx.operation}): ${lastError.message}`);
159
+ }
160
+ }
161
+ console.warn(`[stage1-decision] ${JSON.stringify(decisionFor(null))}`);
162
+ if (sov === 'local_only' && walked.length === 0) {
163
+ throw new RouterBlockedError('No permitted embed backend for local_only (edge/self_hosted unavailable, no BAA provider)', 412, 'routing_blocked');
164
+ }
165
+ if (ctx.signal?.aborted)
166
+ throw abortError();
167
+ if (lastError) {
168
+ // Every failure was the caller's (4xx RouterError): surface it unchanged, not as an outage.
169
+ if (allClientErrors)
170
+ throw lastError;
171
+ const unavailable = new RouterError(`Every permitted embed backend failed; last: ${lastError.message}`, 503, 'embed_backends_failed');
172
+ unavailable.cause = lastError;
173
+ throw unavailable;
174
+ }
175
+ throw new RouterError('No available embed backend', 503, 'no_embed_backend');
176
+ }
package/dist/index.d.ts CHANGED
@@ -9,12 +9,15 @@ export { RouterError, RouterValidationError, RouterBlockedError } from './errors
9
9
  export { parseRouterRequest, routerMessagesRequestSchema, cloudpeersEnvelopeSchema } from './schema.js';
10
10
  export { TIERS, TASK_ROUTES, evaluateGate, gateEnforced, resolveChain, routingPolicyArtifact } from './policy.js';
11
11
  export { routeModel, routeMessages, routeMessagesStream } from './router.js';
12
+ export { routeEmbed, parseEmbedRequest, embedRequestSchema, evaluateEmbedGate } from './embed.js';
13
+ export type { EmbedContext } from './embed.js';
14
+ export { edgeEmbedDescriptor, EDGE_EMBED_MODEL } from './adapters/edge.js';
12
15
  export type { RouteContext, MessagesStreamResult } from './router.js';
13
16
  export { capabilityMatrix } from './capabilities.js';
14
17
  export type { BackendCapabilities } from './capabilities.js';
15
18
  export { anthropicAdapter } from './adapters/anthropic.js';
16
19
  export { geminiAdapter, anthropicToGeminiContents, geminiResponseToAnthropic, geminiChunksToSSE, geminiFinishToStopReason, } from './adapters/gemini.js';
17
20
  export { selfhostedAdapter, messagesToPrompt } from './adapters/selfhosted.js';
18
- import type { BackendAdapter, Provider } from './types.js';
21
+ import type { BackendAdapter, GenerateProvider } from './types.js';
19
22
  /** The approved backend set (A1.2): selfhosted, gemini, anthropic. No OpenAI. */
20
- export declare function defaultBackendAdapters(): Record<Provider, BackendAdapter>;
23
+ export declare function defaultBackendAdapters(): Record<GenerateProvider, BackendAdapter>;
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ export { RouterError, RouterValidationError, RouterBlockedError } from './errors
9
9
  export { parseRouterRequest, routerMessagesRequestSchema, cloudpeersEnvelopeSchema } from './schema.js';
10
10
  export { TIERS, TASK_ROUTES, evaluateGate, gateEnforced, resolveChain, routingPolicyArtifact } from './policy.js';
11
11
  export { routeModel, routeMessages, routeMessagesStream } from './router.js';
12
+ export { routeEmbed, parseEmbedRequest, embedRequestSchema, evaluateEmbedGate } from './embed.js';
13
+ export { edgeEmbedDescriptor, EDGE_EMBED_MODEL } from './adapters/edge.js';
12
14
  export { capabilityMatrix } from './capabilities.js';
13
15
  export { anthropicAdapter } from './adapters/anthropic.js';
14
16
  export { geminiAdapter, anthropicToGeminiContents, geminiResponseToAnthropic, geminiChunksToSSE, geminiFinishToStopReason, } from './adapters/gemini.js';
package/dist/policy.d.ts CHANGED
@@ -10,6 +10,8 @@ export declare function gateEnforced(): boolean;
10
10
  * Never triggers for aggregate_only/externalizable (spec scope: local_only).
11
11
  */
12
12
  export declare function evaluateGate(sovereigntyClass: SovereigntyClass, tier: TierDef): GateOutcome;
13
+ /** The gate rule itself, shared by evaluateGate (tiers) and evaluateEmbedGate (embed adapters). */
14
+ export declare function gateRule(sovereigntyClass: SovereigntyClass, cloud: boolean, provider: Provider): GateOutcome;
13
15
  /**
14
16
  * §4 — the caller's `model` is honored when it belongs to the selected
15
17
  * tier's provider family (a gemini caller may pick flash vs pro); otherwise
package/dist/policy.js CHANGED
@@ -99,9 +99,13 @@ export function gateEnforced() {
99
99
  * Never triggers for aggregate_only/externalizable (spec scope: local_only).
100
100
  */
101
101
  export function evaluateGate(sovereigntyClass, tier) {
102
- if (sovereigntyClass !== 'local_only' || !tier.cloud)
102
+ return gateRule(sovereigntyClass, tier.cloud, tier.provider);
103
+ }
104
+ /** The gate rule itself, shared by evaluateGate (tiers) and evaluateEmbedGate (embed adapters). */
105
+ export function gateRule(sovereigntyClass, cloud, provider) {
106
+ if (sovereigntyClass !== 'local_only' || !cloud)
103
107
  return 'allowed';
104
- if (baaProviders().has(tier.provider))
108
+ if (baaProviders().has(provider))
105
109
  return 'allowed'; // Stage 2 governed egress
106
110
  return gateEnforced() ? 'blocked' : 'would_block'; // Stage 3 : Stage 1
107
111
  }
package/dist/schema.d.ts CHANGED
@@ -35,11 +35,11 @@ export declare const cloudpeersEnvelopeSchema: z.ZodObject<{
35
35
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
36
36
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
37
37
  v?: 1 | undefined;
38
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
38
39
  mandate_id?: string | undefined;
39
40
  policy_id?: string | undefined;
40
41
  attribution_chain?: string[] | undefined;
41
42
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
42
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
43
43
  tool_use_strict?: boolean | undefined;
44
44
  byok?: {
45
45
  provider: string;
@@ -49,11 +49,11 @@ export declare const cloudpeersEnvelopeSchema: z.ZodObject<{
49
49
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
50
50
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
51
51
  v?: 1 | undefined;
52
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
52
53
  mandate_id?: string | undefined;
53
54
  policy_id?: string | undefined;
54
55
  attribution_chain?: string[] | undefined;
55
56
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
56
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
57
57
  tool_use_strict?: boolean | undefined;
58
58
  byok?: {
59
59
  provider: string;
@@ -125,11 +125,11 @@ export declare const routerMessagesRequestSchema: z.ZodObject<{
125
125
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
126
126
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
127
127
  v?: 1 | undefined;
128
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
128
129
  mandate_id?: string | undefined;
129
130
  policy_id?: string | undefined;
130
131
  attribution_chain?: string[] | undefined;
131
132
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
132
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
133
133
  tool_use_strict?: boolean | undefined;
134
134
  byok?: {
135
135
  provider: string;
@@ -139,11 +139,11 @@ export declare const routerMessagesRequestSchema: z.ZodObject<{
139
139
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
140
140
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
141
141
  v?: 1 | undefined;
142
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
142
143
  mandate_id?: string | undefined;
143
144
  policy_id?: string | undefined;
144
145
  attribution_chain?: string[] | undefined;
145
146
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
146
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
147
147
  tool_use_strict?: boolean | undefined;
148
148
  byok?: {
149
149
  provider: string;
@@ -215,11 +215,11 @@ export declare const routerMessagesRequestSchema: z.ZodObject<{
215
215
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
216
216
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
217
217
  v?: 1 | undefined;
218
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
218
219
  mandate_id?: string | undefined;
219
220
  policy_id?: string | undefined;
220
221
  attribution_chain?: string[] | undefined;
221
222
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
222
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
223
223
  tool_use_strict?: boolean | undefined;
224
224
  byok?: {
225
225
  provider: string;
@@ -229,11 +229,11 @@ export declare const routerMessagesRequestSchema: z.ZodObject<{
229
229
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
230
230
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
231
231
  v?: 1 | undefined;
232
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
232
233
  mandate_id?: string | undefined;
233
234
  policy_id?: string | undefined;
234
235
  attribution_chain?: string[] | undefined;
235
236
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
236
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
237
237
  tool_use_strict?: boolean | undefined;
238
238
  byok?: {
239
239
  provider: string;
@@ -305,11 +305,11 @@ export declare const routerMessagesRequestSchema: z.ZodObject<{
305
305
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
306
306
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
307
307
  v?: 1 | undefined;
308
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
308
309
  mandate_id?: string | undefined;
309
310
  policy_id?: string | undefined;
310
311
  attribution_chain?: string[] | undefined;
311
312
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
312
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
313
313
  tool_use_strict?: boolean | undefined;
314
314
  byok?: {
315
315
  provider: string;
@@ -319,11 +319,11 @@ export declare const routerMessagesRequestSchema: z.ZodObject<{
319
319
  sovereignty_class: "local_only" | "aggregate_only" | "externalizable";
320
320
  task_class: "extraction" | "classification" | "coaching" | "reasoning" | "synthesis";
321
321
  v?: 1 | undefined;
322
+ backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
322
323
  mandate_id?: string | undefined;
323
324
  policy_id?: string | undefined;
324
325
  attribution_chain?: string[] | undefined;
325
326
  trust_level?: "L0" | "L1" | "L2" | "L3" | "L4" | "L5" | undefined;
326
- backend_hint?: "selfhosted" | "gemini" | "anthropic" | undefined;
327
327
  tool_use_strict?: boolean | undefined;
328
328
  byok?: {
329
329
  provider: string;
package/dist/types.d.ts CHANGED
@@ -16,7 +16,15 @@
16
16
  export type SovereigntyClass = 'local_only' | 'aggregate_only' | 'externalizable';
17
17
  /** Static task classes (confidence v1 — no learned complexity classifier). */
18
18
  export type TaskClass = 'extraction' | 'classification' | 'coaching' | 'reasoning' | 'synthesis';
19
- export type Provider = 'selfhosted' | 'gemini' | 'anthropic';
19
+ /**
20
+ * `edge` = on-device execution in the caller's browser/app (LiteRT / ONNX
21
+ * WASM). It appears only on the embed contract (`EmbedAdapter`); the
22
+ * generate path (tier table, routing-policy.json, backend_hint) never
23
+ * resolves to it.
24
+ */
25
+ export type Provider = 'selfhosted' | 'gemini' | 'anthropic' | 'edge';
26
+ /** Providers the generate path (tier chain / BackendAdapter) can resolve to. */
27
+ export type GenerateProvider = Exclude<Provider, 'edge'>;
20
28
  export type BackendSurface = 'cloud' | 'self_hosted' | 'edge';
21
29
  export type GateOutcome = 'allowed' | 'would_block' | 'blocked';
22
30
  export interface TextBlock {
@@ -60,7 +68,7 @@ export interface CloudpeersEnvelope {
60
68
  /** Lab Zero delegation grant. */
61
69
  trust_level?: 'L0' | 'L1' | 'L2' | 'L3' | 'L4' | 'L5';
62
70
  /** Advisory backend selector — reorders the tier chain, never bypasses the gate. */
63
- backend_hint?: Provider;
71
+ backend_hint?: GenerateProvider;
64
72
  /** §8.2 — reject 422 instead of degrading when tool-use support is missing. */
65
73
  tool_use_strict?: boolean;
66
74
  /** §10.2 — accepted by the schema, REJECTED by the router (deferred per A1.2). */
@@ -138,7 +146,7 @@ export interface InvocationContext {
138
146
  signal?: AbortSignal;
139
147
  }
140
148
  export interface BackendAdapter {
141
- id: Provider;
149
+ id: GenerateProvider;
142
150
  display_name: string;
143
151
  surface: BackendSurface;
144
152
  supports_streaming: boolean;
@@ -189,7 +197,7 @@ export interface PolicyVerdict {
189
197
  export interface RouterHooks {
190
198
  verifyMandate?: (mandateId: string) => Promise<MandateVerdict>;
191
199
  applyPolicy?: (policyId: string, call: {
192
- backend_id: Provider;
200
+ backend_id: GenerateProvider;
193
201
  model: string;
194
202
  tier: number;
195
203
  }) => Promise<PolicyVerdict>;
@@ -201,7 +209,7 @@ export interface RouterHooks {
201
209
  writeAttribution?: (chain: string[], usage: UsageEvent) => Promise<void>;
202
210
  }
203
211
  export interface RouterDeps {
204
- adapters: Partial<Record<Provider, BackendAdapter>>;
212
+ adapters: Partial<Record<GenerateProvider, BackendAdapter>>;
205
213
  meter: Meter;
206
214
  quota: QuotaFn;
207
215
  hooks?: RouterHooks;
@@ -225,7 +233,7 @@ export interface RouterRequest {
225
233
  }
226
234
  export interface RouterResult {
227
235
  text: string;
228
- provider: Provider;
236
+ provider: GenerateProvider;
229
237
  model: string;
230
238
  tier: number;
231
239
  escalations: number;
@@ -260,7 +268,7 @@ export interface StageOneDecision {
260
268
  }
261
269
  export interface MessagesRouteResult {
262
270
  response: AnthropicMessagesResponse;
263
- provider: Provider;
271
+ provider: GenerateProvider;
264
272
  model: string;
265
273
  tier: number;
266
274
  escalations: number;
@@ -271,8 +279,47 @@ export interface MessagesRouteResult {
271
279
  }
272
280
  export interface TierDef {
273
281
  tier: number;
274
- provider: Provider;
282
+ provider: GenerateProvider;
275
283
  model: string;
276
284
  /** Cloud tiers are subject to the local_only privacy gate. */
277
285
  cloud: boolean;
278
286
  }
287
+ export interface EmbedOutput {
288
+ vectors: number[][];
289
+ dims: number;
290
+ model: string;
291
+ /** Provider-reported input tokens, when the backend reports them. */
292
+ tokens_in?: number;
293
+ }
294
+ export interface EmbedAdapter {
295
+ id: Provider;
296
+ display_name: string;
297
+ surface: BackendSurface;
298
+ /** Deploy-time availability (key/endpoint configured / executable here). */
299
+ available(): boolean;
300
+ embed(texts: string[], ctx: InvocationContext): Promise<EmbedOutput>;
301
+ }
302
+ export interface EmbedEnvelope {
303
+ v?: 1;
304
+ sovereignty_class: SovereigntyClass;
305
+ /** Advisory: moves this provider's adapter to the front; never bypasses the gate. */
306
+ backend_hint?: Provider;
307
+ }
308
+ export interface EmbedRequest {
309
+ texts: string[];
310
+ cloudpeers: EmbedEnvelope;
311
+ }
312
+ export interface EmbedDeps {
313
+ /** Candidate adapters in preference order. */
314
+ adapters: EmbedAdapter[];
315
+ meter: Meter;
316
+ }
317
+ export interface EmbedRouteResult extends EmbedOutput {
318
+ provider: Provider;
319
+ surface: BackendSurface;
320
+ gate: GateOutcome;
321
+ latency_ms: number;
322
+ /** Adapters that failed before this one served. */
323
+ escalations: number;
324
+ decision: StageOneDecision;
325
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudpeers-jkl/model-router",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "cloudpeers inference router — embedded library (no central data plane). Sovereignty privacy gate, static task-class routing, Anthropic Messages lingua franca, uniform metering.",
5
5
  "type": "module",
6
6
  "license": "MIT",