@mupt-ai/dari-router 0.1.0 → 0.2.1
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 +89 -60
- package/dist/anonymous_actions.js +32 -10
- package/dist/anonymous_actions.js.map +1 -1
- package/dist/auto_router.d.ts +4 -1
- package/dist/auto_router.js +3 -3
- package/dist/auto_router.js.map +1 -1
- package/dist/cache_behavior.d.ts +8 -6
- package/dist/cache_behavior.js +60 -30
- package/dist/cache_behavior.js.map +1 -1
- package/dist/cache_scope.d.ts +2 -2
- package/dist/cache_scope.js +23 -18
- package/dist/cache_scope.js.map +1 -1
- package/dist/cost.d.ts +3 -2
- package/dist/cost.js +32 -23
- package/dist/cost.js.map +1 -1
- package/dist/dari_policy.d.ts +8 -2
- package/dist/dari_policy.js +39 -1
- package/dist/dari_policy.js.map +1 -1
- package/dist/eval_score_imputation.d.ts +26 -0
- package/dist/eval_score_imputation.js +121 -0
- package/dist/eval_score_imputation.js.map +1 -0
- package/dist/framework_lifecycle.d.ts +1 -1
- package/dist/framework_lifecycle.js +4 -1
- package/dist/framework_lifecycle.js.map +1 -1
- package/dist/framework_models.js +6 -0
- package/dist/framework_models.js.map +1 -1
- package/dist/framework_router.js +22 -11
- package/dist/framework_router.js.map +1 -1
- package/dist/framework_types.d.ts +9 -5
- package/dist/index.d.ts +9 -35
- package/dist/index.js +6 -37
- package/dist/index.js.map +1 -1
- package/dist/model_ids.js +4 -0
- package/dist/model_ids.js.map +1 -1
- package/dist/pi_runtime.js +19 -11
- package/dist/pi_runtime.js.map +1 -1
- package/dist/pi_types.d.ts +2 -2
- package/{src/index.ts → dist/policy-engine.d.ts} +2 -14
- package/dist/policy-engine.js +25 -0
- package/dist/policy-engine.js.map +1 -0
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +3 -3
- package/dist/prompts.js.map +1 -1
- package/dist/protocols.d.ts +5 -0
- package/dist/protocols.js +6 -0
- package/dist/protocols.js.map +1 -0
- package/dist/route.d.ts +3 -4
- package/dist/route.js +17 -10
- package/dist/route.js.map +1 -1
- package/dist/selector_input.d.ts +1 -0
- package/dist/selector_input.js +66 -78
- package/dist/selector_input.js.map +1 -1
- package/dist/selector_parse.d.ts +1 -0
- package/dist/selector_parse.js +2 -1
- package/dist/selector_parse.js.map +1 -1
- package/dist/selector_request.d.ts +1 -0
- package/dist/selector_request.js +1 -0
- package/dist/selector_request.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/examples/auto_router.ts +40 -0
- package/examples/basic_route.ts +130 -0
- package/examples/create_router.ts +114 -0
- package/examples/dari_policy.ts +86 -0
- package/examples/lease_stores.ts +159 -0
- package/examples/managed/README.md +50 -0
- package/examples/managed/byok-router/README.md +13 -0
- package/examples/managed/byok-router/router.yml +13 -0
- package/examples/managed/custom-rules-router/README.md +12 -0
- package/examples/managed/custom-rules-router/router.yml +24 -0
- package/examples/managed/eval-router/README.md +18 -0
- package/examples/managed/eval-router/router.yml +11 -0
- package/examples/managed/slm-router/README.md +12 -0
- package/examples/managed/slm-router/router.yml +8 -0
- package/examples/pi_runtime.ts +55 -0
- package/package.json +26 -7
- package/src/anonymous_actions.ts +0 -434
- package/src/auto_router.ts +0 -95
- package/src/cache_behavior.ts +0 -130
- package/src/cache_scope.ts +0 -58
- package/src/cache_switch_decision.ts +0 -53
- package/src/candidate_pruning.ts +0 -257
- package/src/candidate_resolution.ts +0 -208
- package/src/compatibility.ts +0 -162
- package/src/continuation_state.ts +0 -368
- package/src/cost.ts +0 -455
- package/src/dari_policy.ts +0 -360
- package/src/errors.ts +0 -27
- package/src/fingerprint.ts +0 -148
- package/src/fixed_turn_cost_config.ts +0 -24
- package/src/framework_error.ts +0 -38
- package/src/framework_lifecycle.ts +0 -74
- package/src/framework_models.ts +0 -186
- package/src/framework_router.ts +0 -490
- package/src/framework_streaming.ts +0 -613
- package/src/framework_types.ts +0 -372
- package/src/framework_validation.ts +0 -235
- package/src/json.ts +0 -43
- package/src/model_ids.ts +0 -29
- package/src/pi_context.ts +0 -251
- package/src/pi_options.ts +0 -295
- package/src/pi_output.ts +0 -387
- package/src/pi_reasoning_signature.ts +0 -41
- package/src/pi_runtime.ts +0 -238
- package/src/pi_types.ts +0 -71
- package/src/portable_reasoning_state.ts +0 -129
- package/src/previous_decision.ts +0 -109
- package/src/prompts.ts +0 -69
- package/src/protocol_anthropic.ts +0 -571
- package/src/protocol_openai_chat.ts +0 -645
- package/src/protocol_validation.ts +0 -202
- package/src/route.ts +0 -490
- package/src/routing_history.ts +0 -157
- package/src/selector_input.ts +0 -246
- package/src/selector_parse.ts +0 -138
- package/src/selector_request.ts +0 -130
- package/src/selector_truncation.ts +0 -48
- package/src/selector_wire.ts +0 -167
- package/src/types.ts +0 -231
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// A complete router with a custom policy and a local executor. Both protocol
|
|
2
|
+
// examples run in-process, so this file needs no API key or network access.
|
|
3
|
+
//
|
|
4
|
+
// Run with: bun run example
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createRouter,
|
|
8
|
+
type RouterExecutor,
|
|
9
|
+
type RoutingPolicy,
|
|
10
|
+
} from "../src/index.js";
|
|
11
|
+
|
|
12
|
+
const FAST = "demo/fast";
|
|
13
|
+
const STRONG = "demo/strong";
|
|
14
|
+
|
|
15
|
+
const policy: RoutingPolicy = ({ request, candidates }) => {
|
|
16
|
+
const promptLength = request.items.reduce((length, item) =>
|
|
17
|
+
item.type === "message"
|
|
18
|
+
? length + item.content.reduce(
|
|
19
|
+
(total, part) => total + (part.type === "text" ? part.text.length : 0),
|
|
20
|
+
0,
|
|
21
|
+
)
|
|
22
|
+
: length,
|
|
23
|
+
0);
|
|
24
|
+
const preferred = promptLength > 80 ? STRONG : FAST;
|
|
25
|
+
const model = candidates.find((candidate) => candidate.id === preferred)
|
|
26
|
+
?? candidates[0]!;
|
|
27
|
+
return {
|
|
28
|
+
model: model.id,
|
|
29
|
+
reason: promptLength > 80
|
|
30
|
+
? "The long request uses the stronger model."
|
|
31
|
+
: "The short request uses the fast model.",
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const localExecutor: RouterExecutor = {
|
|
36
|
+
execute({ request, model, decision }) {
|
|
37
|
+
const text = `${model.id} handled ${request.protocol}: ${decision.reason}`;
|
|
38
|
+
if (!request.stream) {
|
|
39
|
+
return {
|
|
40
|
+
type: "complete",
|
|
41
|
+
output: {
|
|
42
|
+
content: [{ type: "text", text }],
|
|
43
|
+
finishReason: "stop",
|
|
44
|
+
usage: { inputTokens: 12, outputTokens: 8 },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
type: "stream",
|
|
50
|
+
events: {
|
|
51
|
+
async *[Symbol.asyncIterator]() {
|
|
52
|
+
yield { type: "text_delta", index: 0, delta: text } as const;
|
|
53
|
+
yield {
|
|
54
|
+
type: "finish",
|
|
55
|
+
finishReason: "stop",
|
|
56
|
+
usage: { inputTokens: 12, outputTokens: 8 },
|
|
57
|
+
} as const;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const router = createRouter({
|
|
65
|
+
models: [
|
|
66
|
+
{
|
|
67
|
+
id: FAST,
|
|
68
|
+
executor: "local",
|
|
69
|
+
reasoningEfforts: ["off"],
|
|
70
|
+
capabilities: { streaming: true },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: STRONG,
|
|
74
|
+
executor: "local",
|
|
75
|
+
reasoningEfforts: ["off", "high"],
|
|
76
|
+
capabilities: {
|
|
77
|
+
imageInput: true,
|
|
78
|
+
toolUse: true,
|
|
79
|
+
structuredOutput: true,
|
|
80
|
+
streaming: true,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
policy,
|
|
85
|
+
executors: { local: localExecutor },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const openAIResponse = await router.fetch(new Request(
|
|
89
|
+
"http://router.local/v1/chat/completions",
|
|
90
|
+
{
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
model: "my-router",
|
|
95
|
+
messages: [{ role: "user", content: "Say hello." }],
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
));
|
|
99
|
+
console.log("OpenAI Chat Completions:\n", await openAIResponse.json());
|
|
100
|
+
|
|
101
|
+
const anthropicResponse = await router.fetch(new Request(
|
|
102
|
+
"http://router.local/v1/messages",
|
|
103
|
+
{
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
model: "my-router",
|
|
108
|
+
max_tokens: 128,
|
|
109
|
+
messages: [{ role: "user", content: "Stream a short hello." }],
|
|
110
|
+
stream: true,
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
));
|
|
114
|
+
console.log("\nAnthropic Messages stream:\n", await anthropicResponse.text());
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The framework's optional Dari policy runs the deterministic routing pipeline
|
|
2
|
+
// with an injected selector. This example uses a scripted selector and local
|
|
3
|
+
// executor, so it needs no API key or network access.
|
|
4
|
+
//
|
|
5
|
+
// Run with: bun run example:dari-policy
|
|
6
|
+
|
|
7
|
+
import { createRouter } from "../src/index.js";
|
|
8
|
+
import {
|
|
9
|
+
createDariRoutingPolicy,
|
|
10
|
+
type SelectorInput,
|
|
11
|
+
} from "../src/policy-engine.js";
|
|
12
|
+
|
|
13
|
+
const FAST = "demo/fast";
|
|
14
|
+
const STRONG = "demo/strong";
|
|
15
|
+
|
|
16
|
+
const policy = createDariRoutingPolicy({
|
|
17
|
+
selector: async (request) => {
|
|
18
|
+
const input = JSON.parse(
|
|
19
|
+
String(request.messages?.[1]?.content),
|
|
20
|
+
) as SelectorInput;
|
|
21
|
+
const selected = input.candidate_pairs.find(
|
|
22
|
+
(candidate) => candidate.model === STRONG && candidate.thinking_level === "high",
|
|
23
|
+
) ?? input.candidate_pairs[0]!;
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
selected_model: selected.model,
|
|
26
|
+
reasoning_effort: selected.thinking_level,
|
|
27
|
+
reason: "The scripted selector chose the stronger candidate.",
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
selectorModel: "demo/selector",
|
|
31
|
+
selectorContextWindowChars: 100_000,
|
|
32
|
+
pricing: (model) => model === FAST
|
|
33
|
+
? { input: 0.2, output: 0.8, cacheRead: 0.05, cacheWrite: 0.2 }
|
|
34
|
+
: { input: 1, output: 4, cacheRead: 0.1, cacheWrite: 1 },
|
|
35
|
+
averageOutputTokensByModel: {
|
|
36
|
+
[FAST]: { off: 300 },
|
|
37
|
+
[STRONG]: { off: 800, high: 1_600 },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const router = createRouter({
|
|
42
|
+
models: [
|
|
43
|
+
{
|
|
44
|
+
id: FAST,
|
|
45
|
+
executor: "local",
|
|
46
|
+
api: "openai-completions",
|
|
47
|
+
reasoningEfforts: ["off"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: STRONG,
|
|
51
|
+
executor: "local",
|
|
52
|
+
api: "openai-completions",
|
|
53
|
+
reasoningEfforts: ["off", "high"],
|
|
54
|
+
defaultReasoningEffort: "high",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
policy,
|
|
58
|
+
executors: {
|
|
59
|
+
local: {
|
|
60
|
+
execute: ({ model, decision }) => ({
|
|
61
|
+
type: "complete",
|
|
62
|
+
output: {
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `${model.id}/${decision.reasoningEffort}: ${decision.reason}`,
|
|
66
|
+
}],
|
|
67
|
+
finishReason: "stop",
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const response = await router.fetch(new Request(
|
|
75
|
+
"http://router.local/v1/chat/completions",
|
|
76
|
+
{
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
model: "my-router",
|
|
81
|
+
messages: [{ role: "user", content: "Plan a database migration." }],
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
console.log(await response.json());
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Shared lease stores for ephemeral or replicated deployments: redis- and
|
|
2
|
+
// postgres-backed LeaseStore implementations against minimal structural client
|
|
3
|
+
// interfaces. The demo runs on in-file fakes (no external services) to show
|
|
4
|
+
// leases surviving a router "restart".
|
|
5
|
+
//
|
|
6
|
+
// Run with: bun run example:lease-stores
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createRouter,
|
|
10
|
+
type LeaseStore,
|
|
11
|
+
type RouterExecutor,
|
|
12
|
+
type RouterLease,
|
|
13
|
+
type RoutingPolicy,
|
|
14
|
+
} from "../src/index.js";
|
|
15
|
+
|
|
16
|
+
// --- Redis-backed store -----------------------------------------------------
|
|
17
|
+
// Expiry is native (PX), so pruneExpired is a no-op.
|
|
18
|
+
|
|
19
|
+
type RedisLike = {
|
|
20
|
+
get(key: string): Promise<string | null>;
|
|
21
|
+
set(key: string, value: string, options: { PX: number }): Promise<unknown>;
|
|
22
|
+
del(key: string): Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function redisLeaseStore(redis: RedisLike, keyPrefix = "router-lease:"): LeaseStore {
|
|
26
|
+
return {
|
|
27
|
+
async get(cacheKey) {
|
|
28
|
+
const raw = await redis.get(`${keyPrefix}${cacheKey}`);
|
|
29
|
+
if (raw === null) return undefined;
|
|
30
|
+
return JSON.parse(raw) as RouterLease;
|
|
31
|
+
},
|
|
32
|
+
async set(cacheKey, lease) {
|
|
33
|
+
const ttlMs = lease.expiresAt - Date.now();
|
|
34
|
+
if (ttlMs <= 0) return;
|
|
35
|
+
await redis.set(`${keyPrefix}${cacheKey}`, JSON.stringify(lease), { PX: ttlMs });
|
|
36
|
+
},
|
|
37
|
+
async delete(cacheKey) {
|
|
38
|
+
await redis.del(`${keyPrefix}${cacheKey}`);
|
|
39
|
+
},
|
|
40
|
+
pruneExpired() {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Postgres-backed store --------------------------------------------------
|
|
45
|
+
// Table:
|
|
46
|
+
// create table router_leases (
|
|
47
|
+
// cache_key text primary key,
|
|
48
|
+
// model text not null,
|
|
49
|
+
// reasoning_effort text not null,
|
|
50
|
+
// turns_remaining int not null,
|
|
51
|
+
// expires_at bigint not null
|
|
52
|
+
// );
|
|
53
|
+
//
|
|
54
|
+
// The router re-checks expiresAt itself before honoring a lease, so a lazy
|
|
55
|
+
// pruneExpired (or a cron) is safe. If concurrent same-cacheKey requests
|
|
56
|
+
// matter at your scale, do the turn decrement in SQL behind this interface.
|
|
57
|
+
|
|
58
|
+
type SqlLike = {
|
|
59
|
+
query(text: string, params: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function postgresLeaseStore(sql: SqlLike): LeaseStore {
|
|
63
|
+
return {
|
|
64
|
+
async get(cacheKey) {
|
|
65
|
+
const { rows } = await sql.query(
|
|
66
|
+
"select model, reasoning_effort, turns_remaining, expires_at from router_leases where cache_key = $1",
|
|
67
|
+
[cacheKey],
|
|
68
|
+
);
|
|
69
|
+
const row = rows[0];
|
|
70
|
+
if (row === undefined) return undefined;
|
|
71
|
+
return {
|
|
72
|
+
model: String(row.model),
|
|
73
|
+
reasoningEffort: row.reasoning_effort as RouterLease["reasoningEffort"],
|
|
74
|
+
turnsRemaining: Number(row.turns_remaining),
|
|
75
|
+
expiresAt: Number(row.expires_at),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
async set(cacheKey, lease) {
|
|
79
|
+
await sql.query(
|
|
80
|
+
`insert into router_leases (cache_key, model, reasoning_effort, turns_remaining, expires_at)
|
|
81
|
+
values ($1, $2, $3, $4, $5)
|
|
82
|
+
on conflict (cache_key) do update set
|
|
83
|
+
model = excluded.model,
|
|
84
|
+
reasoning_effort = excluded.reasoning_effort,
|
|
85
|
+
turns_remaining = excluded.turns_remaining,
|
|
86
|
+
expires_at = excluded.expires_at`,
|
|
87
|
+
[cacheKey, lease.model, lease.reasoningEffort, lease.turnsRemaining, lease.expiresAt],
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
async delete(cacheKey) {
|
|
91
|
+
await sql.query("delete from router_leases where cache_key = $1", [cacheKey]);
|
|
92
|
+
},
|
|
93
|
+
async pruneExpired(nowMs) {
|
|
94
|
+
await sql.query("delete from router_leases where expires_at <= $1", [nowMs]);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Demo: leases survive a router restart ----------------------------------
|
|
100
|
+
|
|
101
|
+
function fakeRedis(): RedisLike {
|
|
102
|
+
const data = new Map<string, { value: string; expiresAt: number }>();
|
|
103
|
+
return {
|
|
104
|
+
async get(key) {
|
|
105
|
+
const entry = data.get(key);
|
|
106
|
+
if (entry === undefined || entry.expiresAt <= Date.now()) return null;
|
|
107
|
+
return entry.value;
|
|
108
|
+
},
|
|
109
|
+
async set(key, value, options) {
|
|
110
|
+
data.set(key, { value, expiresAt: Date.now() + options.PX });
|
|
111
|
+
},
|
|
112
|
+
async del(key) {
|
|
113
|
+
data.delete(key);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const policy: RoutingPolicy = ({ candidates }) => ({
|
|
119
|
+
model: candidates[0]!.id,
|
|
120
|
+
reason: "Fresh selection by the policy.",
|
|
121
|
+
leaseTurnsRemaining: 4,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const executor: RouterExecutor = {
|
|
125
|
+
execute: ({ decision }) => ({
|
|
126
|
+
type: "complete",
|
|
127
|
+
output: {
|
|
128
|
+
content: [{ type: "text", text: decision.reason }],
|
|
129
|
+
finishReason: "stop",
|
|
130
|
+
usage: { inputTokens: 4, outputTokens: 4 },
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const models = [{ id: "demo/model", executor: "demo", reasoningEfforts: ["off" as const] }];
|
|
136
|
+
const sharedStore = redisLeaseStore(fakeRedis());
|
|
137
|
+
const routerOptions = { models, policy, executors: { demo: executor }, leaseStore: sharedStore };
|
|
138
|
+
|
|
139
|
+
const ask = (router: { fetch: (request: Request) => Promise<Response> }) =>
|
|
140
|
+
router.fetch(
|
|
141
|
+
new Request("https://example.test/v1/chat/completions", {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
model: "dari/routing",
|
|
146
|
+
prompt_cache_key: "conversation-1",
|
|
147
|
+
messages: [{ role: "user", content: "hello" }],
|
|
148
|
+
}),
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const first = createRouter(routerOptions);
|
|
153
|
+
const turn1 = await (await ask(first)).json() as { dari_routing: { reason: string } };
|
|
154
|
+
console.log("turn 1 (fresh process): ", turn1.dari_routing.reason);
|
|
155
|
+
|
|
156
|
+
// "Restart": a brand-new router instance sharing only the external store.
|
|
157
|
+
const second = createRouter(routerOptions);
|
|
158
|
+
const turn2 = await (await ask(second)).json() as { dari_routing: { reason: string } };
|
|
159
|
+
console.log("turn 2 (new process): ", turn2.dari_routing.reason);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Managed Router Examples
|
|
2
|
+
|
|
3
|
+
Runnable `router.yml` manifests for Dari's managed router path. TypeScript framework examples remain in the parent [`examples/`](../) directory.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Install the [Dari CLI](https://github.com/mupt-ai/dari-cli), sign in, and inspect the current model catalog:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl -fsSL https://raw.githubusercontent.com/mupt-ai/dari-cli/main/install.sh | bash
|
|
11
|
+
dari auth login
|
|
12
|
+
dari router models
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Your organization needs access to the providers and models used by an example. For BYOK examples, export the provider key named in that example's README.
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
- [`slm-router/`](slm-router) — automatic selection with managed provider credentials.
|
|
20
|
+
- [`byok-router/`](byok-router) — automatic selection with a provider key from your environment.
|
|
21
|
+
- [`custom-rules-router/`](custom-rules-router) — natural-language rules with explicit model and reasoning-level constraints.
|
|
22
|
+
- [`eval-router/`](eval-router) — automatic selection informed by an organization eval scorecard.
|
|
23
|
+
|
|
24
|
+
## Create And Call A Router
|
|
25
|
+
|
|
26
|
+
From an example directory:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd examples/managed/slm-router
|
|
30
|
+
dari router create ./router.yml
|
|
31
|
+
dari router get <router_id>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Create a routing key and call the endpoint returned by `dari router get`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dari api-keys create --name example-client --type routing
|
|
38
|
+
export DARI_ROUTING_API_KEY="dari_..."
|
|
39
|
+
|
|
40
|
+
curl https://routing.dari.dev/rtr_.../chat/completions \
|
|
41
|
+
-H "Authorization: Bearer $DARI_ROUTING_API_KEY" \
|
|
42
|
+
-H "Content-Type: application/json" \
|
|
43
|
+
-d '{"model":"dari/routing","messages":[{"role":"user","content":"Explain prompt caching."}]}'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run `dari router models` before editing a manifest. Model IDs and reasoning levels must match the current catalog.
|
|
47
|
+
|
|
48
|
+
## Adding An Example
|
|
49
|
+
|
|
50
|
+
Each example is a self-contained directory containing a `README.md` and `router.yml`. Keep one managed-router concept per example. Use `provider_key_envs` for BYOK examples, never commit provider credentials or organization-specific eval IDs, and validate changes against a test organization with `dari router create <example>/router.yml`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# BYOK Router
|
|
2
|
+
|
|
3
|
+
This managed router uses a Dari-managed OpenAI key and your own Fireworks key. The manifest reads the Fireworks credential from `FIREWORKS_API_KEY`; no key is stored in YAML.
|
|
4
|
+
|
|
5
|
+
## Create
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export FIREWORKS_API_KEY=fw_...
|
|
9
|
+
dari auth login
|
|
10
|
+
dari router create ./router.yml
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The CLI sends the key only while creating or replacing the router configuration. Run `dari router models` first to confirm the selected models and provider support.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: Example BYOK Router
|
|
2
|
+
enabled_models:
|
|
3
|
+
- openai/gpt-5.6-sol
|
|
4
|
+
- zai-org/GLM-5.2
|
|
5
|
+
model_providers:
|
|
6
|
+
openai/gpt-5.6-sol: openai
|
|
7
|
+
zai-org/GLM-5.2: fireworks
|
|
8
|
+
provider_key_sources:
|
|
9
|
+
openai: managed
|
|
10
|
+
fireworks: user
|
|
11
|
+
provider_key_envs:
|
|
12
|
+
fireworks: FIREWORKS_API_KEY
|
|
13
|
+
routing_strategy: slm
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Custom Rules Router
|
|
2
|
+
|
|
3
|
+
A managed **Describe Your Router** configuration. Dari's selector applies the natural-language `when` rules to each request, then uses the configured default when no rule matches.
|
|
4
|
+
|
|
5
|
+
## Create
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
dari auth login
|
|
9
|
+
dari router create ./router.yml
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The manifest explicitly restricts the reasoning levels available for each model. `null` or an omitted `thinking_level` means the selector can choose an enabled level automatically; this example pins each rule for clarity.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Example Custom Rules Router
|
|
2
|
+
enabled_models:
|
|
3
|
+
- openai/gpt-5.6-sol
|
|
4
|
+
- deepseek-ai/DeepSeek-V4-Flash-0731
|
|
5
|
+
model_providers:
|
|
6
|
+
openai/gpt-5.6-sol: openai
|
|
7
|
+
deepseek-ai/DeepSeek-V4-Flash-0731: fireworks
|
|
8
|
+
model_thinking_levels:
|
|
9
|
+
openai/gpt-5.6-sol: [low, medium, high]
|
|
10
|
+
deepseek-ai/DeepSeek-V4-Flash-0731: [off]
|
|
11
|
+
provider_key_sources:
|
|
12
|
+
openai: managed
|
|
13
|
+
fireworks: managed
|
|
14
|
+
routing_strategy: custom
|
|
15
|
+
custom_config:
|
|
16
|
+
rules:
|
|
17
|
+
- when: planning, architecture, or difficult debugging
|
|
18
|
+
use: openai/gpt-5.6-sol
|
|
19
|
+
thinking_level: high
|
|
20
|
+
- when: short questions and straightforward implementation
|
|
21
|
+
use: deepseek-ai/DeepSeek-V4-Flash-0731
|
|
22
|
+
thinking_level: off
|
|
23
|
+
default: deepseek-ai/DeepSeek-V4-Flash-0731
|
|
24
|
+
default_thinking_level: off
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Eval Router
|
|
2
|
+
|
|
3
|
+
An automatic managed router that imports an eval scorecard into its selection context.
|
|
4
|
+
|
|
5
|
+
The eval ID is organization-specific, so replace `evl_your_scorecard` in `router.yml` with an ID returned by:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
dari auth login
|
|
9
|
+
dari eval list
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then create the router:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
dari router create ./router.yml
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The scorecard must contain scores for the enabled model and reasoning-level pairs you want the selector to consider. Keep organization-specific IDs and scores out of public example files.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Replace evl_your_scorecard with an eval ID from `dari eval list` before creating.
|
|
2
|
+
name: Example Eval Router
|
|
3
|
+
enabled_models:
|
|
4
|
+
- openai/gpt-5.6-sol
|
|
5
|
+
- anthropic/claude-sonnet-5
|
|
6
|
+
provider_key_sources:
|
|
7
|
+
openai: managed
|
|
8
|
+
anthropic: managed
|
|
9
|
+
routing_strategy: slm
|
|
10
|
+
eval_ids:
|
|
11
|
+
- evl_your_scorecard
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SLM Router
|
|
2
|
+
|
|
3
|
+
The smallest managed-router example. Dari's selector automatically chooses between the enabled models, using Dari-managed provider keys.
|
|
4
|
+
|
|
5
|
+
## Create
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
dari auth login
|
|
9
|
+
dari router create ./router.yml
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Your organization must have managed access to the OpenAI and Anthropic providers. Run `dari router models` to confirm the model IDs are available before creating the router.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// A real end-to-end router using the built-in Pi AI runtime.
|
|
2
|
+
//
|
|
3
|
+
// Run with:
|
|
4
|
+
// OPENAI_API_KEY=... bun run example:pi
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createPiRuntime,
|
|
8
|
+
createRouter,
|
|
9
|
+
type RoutingPolicy,
|
|
10
|
+
} from "../src/index.js";
|
|
11
|
+
|
|
12
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
13
|
+
if (!apiKey) throw new Error("OPENAI_API_KEY is required.");
|
|
14
|
+
|
|
15
|
+
const pi = await createPiRuntime({ apiKey });
|
|
16
|
+
const models = [
|
|
17
|
+
pi.model("openai/gpt-5.4-mini"),
|
|
18
|
+
pi.model("openai/gpt-5.4"),
|
|
19
|
+
];
|
|
20
|
+
const policy: RoutingPolicy = ({ request, candidates }) => {
|
|
21
|
+
const promptLength = request.items.reduce((length, item) =>
|
|
22
|
+
item.type === "message"
|
|
23
|
+
? length + item.content.reduce(
|
|
24
|
+
(total, part) => total + (part.type === "text" ? part.text.length : 0),
|
|
25
|
+
0,
|
|
26
|
+
)
|
|
27
|
+
: length,
|
|
28
|
+
0);
|
|
29
|
+
const preferred = promptLength > 500
|
|
30
|
+
? "openai/gpt-5.4"
|
|
31
|
+
: "openai/gpt-5.4-mini";
|
|
32
|
+
return {
|
|
33
|
+
model: candidates.find((candidate) => candidate.id === preferred)?.id
|
|
34
|
+
?? candidates[0]!.id,
|
|
35
|
+
reason: promptLength > 500
|
|
36
|
+
? "Use the stronger model for the longer prompt."
|
|
37
|
+
: "Use the smaller model for the short prompt.",
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const router = createRouter({ models, policy, executor: pi });
|
|
41
|
+
|
|
42
|
+
const response = await router.fetch(new Request(
|
|
43
|
+
"http://router.local/v1/chat/completions",
|
|
44
|
+
{
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
model: "my-router",
|
|
49
|
+
messages: [{ role: "user", content: "Explain LLM routing in one sentence." }],
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
));
|
|
53
|
+
|
|
54
|
+
if (!response.ok) throw new Error(await response.text());
|
|
55
|
+
console.log(await response.json());
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mupt-ai/dari-router",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "End-to-end TypeScript framework for routing across LLMs with built-in Pi AI execution and pluggable policies and executors.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/mupt-ai/dari-router
|
|
8
|
+
"url": "git+https://github.com/mupt-ai/dari-router.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://docs.dari.dev/framework/overview",
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/mupt-ai/dari-router
|
|
12
|
+
"url": "https://github.com/mupt-ai/dari-router/issues"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"llm",
|
|
@@ -34,17 +34,30 @@
|
|
|
34
34
|
".": {
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
36
|
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./policy-engine": {
|
|
39
|
+
"types": "./dist/policy-engine.d.ts",
|
|
40
|
+
"default": "./dist/policy-engine.js"
|
|
41
|
+
},
|
|
42
|
+
"./eval-score-imputation": {
|
|
43
|
+
"types": "./dist/eval_score_imputation.d.ts",
|
|
44
|
+
"default": "./dist/eval_score_imputation.js"
|
|
45
|
+
},
|
|
46
|
+
"./protocols": {
|
|
47
|
+
"types": "./dist/protocols.d.ts",
|
|
48
|
+
"default": "./dist/protocols.js"
|
|
37
49
|
}
|
|
38
50
|
},
|
|
39
51
|
"files": [
|
|
40
52
|
"dist",
|
|
41
|
-
"
|
|
53
|
+
"examples",
|
|
42
54
|
"README.md",
|
|
43
55
|
"LICENSE"
|
|
44
56
|
],
|
|
45
57
|
"scripts": {
|
|
46
58
|
"build": "tsc -p tsconfig.build.json",
|
|
47
|
-
"
|
|
59
|
+
"test:package": "bash scripts/test-package.sh",
|
|
60
|
+
"prepublishOnly": "bun run typecheck && bun test && bun run test:package",
|
|
48
61
|
"typecheck": "tsc --noEmit",
|
|
49
62
|
"test": "bun test",
|
|
50
63
|
"example": "bun run examples/create_router.ts",
|
|
@@ -53,10 +66,16 @@
|
|
|
53
66
|
"example:decision-pipeline": "bun run examples/basic_route.ts",
|
|
54
67
|
"example:lease-stores": "bun run examples/lease_stores.ts"
|
|
55
68
|
},
|
|
56
|
-
"
|
|
57
|
-
"@mupt-ai/pi-ai": "0.80.6-mupt.2"
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@mupt-ai/pi-ai": ">=0.80.6-mupt.2 <0.81.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"@mupt-ai/pi-ai": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
58
76
|
},
|
|
59
77
|
"devDependencies": {
|
|
78
|
+
"@mupt-ai/pi-ai": "0.80.6-mupt.2",
|
|
60
79
|
"@types/bun": "^1.3.4",
|
|
61
80
|
"@types/node": "^24.10.2",
|
|
62
81
|
"typescript": "^5.6.3"
|