@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 +22 -0
- package/dist/adapters/anthropic-credentials.js +1 -1
- package/dist/adapters/edge.d.ts +26 -0
- package/dist/adapters/edge.js +16 -0
- package/dist/capabilities.d.ts +3 -3
- package/dist/embed.d.ts +66 -0
- package/dist/embed.js +176 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js +6 -2
- package/dist/schema.d.ts +8 -8
- package/dist/types.d.ts +55 -8
- package/package.json +1 -1
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: (
|
|
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
|
+
}
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -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,
|
|
7
|
+
import type { BackendAdapter, BackendSurface, GenerateProvider, ToolUseCapability } from './types.js';
|
|
8
8
|
export interface BackendCapabilities {
|
|
9
|
-
id:
|
|
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<
|
|
18
|
+
export declare function capabilityMatrix(adapters: Partial<Record<GenerateProvider, BackendAdapter>>): BackendCapabilities[];
|
package/dist/embed.d.ts
ADDED
|
@@ -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,
|
|
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<
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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?:
|
|
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:
|
|
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:
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
"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",
|