@mupt-ai/dari-router 0.1.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/LICENSE +202 -0
- package/README.md +100 -0
- package/dist/anonymous_actions.d.ts +55 -0
- package/dist/anonymous_actions.js +277 -0
- package/dist/anonymous_actions.js.map +1 -0
- package/dist/auto_router.d.ts +10 -0
- package/dist/auto_router.js +56 -0
- package/dist/auto_router.js.map +1 -0
- package/dist/cache_behavior.d.ts +14 -0
- package/dist/cache_behavior.js +112 -0
- package/dist/cache_behavior.js.map +1 -0
- package/dist/cache_scope.d.ts +5 -0
- package/dist/cache_scope.js +49 -0
- package/dist/cache_scope.js.map +1 -0
- package/dist/cache_switch_decision.d.ts +24 -0
- package/dist/cache_switch_decision.js +21 -0
- package/dist/cache_switch_decision.js.map +1 -0
- package/dist/candidate_pruning.d.ts +45 -0
- package/dist/candidate_pruning.js +154 -0
- package/dist/candidate_pruning.js.map +1 -0
- package/dist/candidate_resolution.d.ts +41 -0
- package/dist/candidate_resolution.js +111 -0
- package/dist/candidate_resolution.js.map +1 -0
- package/dist/compatibility.d.ts +22 -0
- package/dist/compatibility.js +76 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/continuation_state.d.ts +36 -0
- package/dist/continuation_state.js +249 -0
- package/dist/continuation_state.js.map +1 -0
- package/dist/cost.d.ts +31 -0
- package/dist/cost.js +314 -0
- package/dist/cost.js.map +1 -0
- package/dist/dari_policy.d.ts +34 -0
- package/dist/dari_policy.js +267 -0
- package/dist/dari_policy.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/fingerprint.d.ts +13 -0
- package/dist/fingerprint.js +130 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/fixed_turn_cost_config.d.ts +9 -0
- package/dist/fixed_turn_cost_config.js +22 -0
- package/dist/fixed_turn_cost_config.js.map +1 -0
- package/dist/framework_error.d.ts +8 -0
- package/dist/framework_error.js +24 -0
- package/dist/framework_error.js.map +1 -0
- package/dist/framework_lifecycle.d.ts +11 -0
- package/dist/framework_lifecycle.js +57 -0
- package/dist/framework_lifecycle.js.map +1 -0
- package/dist/framework_models.d.ts +3 -0
- package/dist/framework_models.js +120 -0
- package/dist/framework_models.js.map +1 -0
- package/dist/framework_router.d.ts +2 -0
- package/dist/framework_router.js +384 -0
- package/dist/framework_router.js.map +1 -0
- package/dist/framework_streaming.d.ts +26 -0
- package/dist/framework_streaming.js +486 -0
- package/dist/framework_streaming.js.map +1 -0
- package/dist/framework_types.d.ts +272 -0
- package/dist/framework_types.js +5 -0
- package/dist/framework_types.js.map +1 -0
- package/dist/framework_validation.d.ts +7 -0
- package/dist/framework_validation.js +169 -0
- package/dist/framework_validation.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +10 -0
- package/dist/json.js +29 -0
- package/dist/json.js.map +1 -0
- package/dist/model_ids.d.ts +4 -0
- package/dist/model_ids.js +26 -0
- package/dist/model_ids.js.map +1 -0
- package/dist/pi_context.d.ts +4 -0
- package/dist/pi_context.js +206 -0
- package/dist/pi_context.js.map +1 -0
- package/dist/pi_options.d.ts +3 -0
- package/dist/pi_options.js +240 -0
- package/dist/pi_options.js.map +1 -0
- package/dist/pi_output.d.ts +4 -0
- package/dist/pi_output.js +328 -0
- package/dist/pi_output.js.map +1 -0
- package/dist/pi_reasoning_signature.d.ts +10 -0
- package/dist/pi_reasoning_signature.js +33 -0
- package/dist/pi_reasoning_signature.js.map +1 -0
- package/dist/pi_runtime.d.ts +3 -0
- package/dist/pi_runtime.js +155 -0
- package/dist/pi_runtime.js.map +1 -0
- package/dist/pi_types.d.ts +35 -0
- package/dist/pi_types.js +2 -0
- package/dist/pi_types.js.map +1 -0
- package/dist/portable_reasoning_state.d.ts +7 -0
- package/dist/portable_reasoning_state.js +81 -0
- package/dist/portable_reasoning_state.js.map +1 -0
- package/dist/previous_decision.d.ts +13 -0
- package/dist/previous_decision.js +82 -0
- package/dist/previous_decision.js.map +1 -0
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +65 -0
- package/dist/prompts.js.map +1 -0
- package/dist/protocol_anthropic.d.ts +18 -0
- package/dist/protocol_anthropic.js +443 -0
- package/dist/protocol_anthropic.js.map +1 -0
- package/dist/protocol_openai_chat.d.ts +25 -0
- package/dist/protocol_openai_chat.js +503 -0
- package/dist/protocol_openai_chat.js.map +1 -0
- package/dist/protocol_validation.d.ts +23 -0
- package/dist/protocol_validation.js +134 -0
- package/dist/protocol_validation.js.map +1 -0
- package/dist/route.d.ts +105 -0
- package/dist/route.js +226 -0
- package/dist/route.js.map +1 -0
- package/dist/routing_history.d.ts +7 -0
- package/dist/routing_history.js +132 -0
- package/dist/routing_history.js.map +1 -0
- package/dist/selector_input.d.ts +42 -0
- package/dist/selector_input.js +153 -0
- package/dist/selector_input.js.map +1 -0
- package/dist/selector_parse.d.ts +13 -0
- package/dist/selector_parse.js +95 -0
- package/dist/selector_parse.js.map +1 -0
- package/dist/selector_request.d.ts +26 -0
- package/dist/selector_request.js +85 -0
- package/dist/selector_request.js.map +1 -0
- package/dist/selector_truncation.d.ts +2 -0
- package/dist/selector_truncation.js +48 -0
- package/dist/selector_truncation.js.map +1 -0
- package/dist/selector_wire.d.ts +10 -0
- package/dist/selector_wire.js +113 -0
- package/dist/selector_wire.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/anonymous_actions.ts +434 -0
- package/src/auto_router.ts +95 -0
- package/src/cache_behavior.ts +130 -0
- package/src/cache_scope.ts +58 -0
- package/src/cache_switch_decision.ts +53 -0
- package/src/candidate_pruning.ts +257 -0
- package/src/candidate_resolution.ts +208 -0
- package/src/compatibility.ts +162 -0
- package/src/continuation_state.ts +368 -0
- package/src/cost.ts +455 -0
- package/src/dari_policy.ts +360 -0
- package/src/errors.ts +27 -0
- package/src/fingerprint.ts +148 -0
- package/src/fixed_turn_cost_config.ts +24 -0
- package/src/framework_error.ts +38 -0
- package/src/framework_lifecycle.ts +74 -0
- package/src/framework_models.ts +186 -0
- package/src/framework_router.ts +490 -0
- package/src/framework_streaming.ts +613 -0
- package/src/framework_types.ts +372 -0
- package/src/framework_validation.ts +235 -0
- package/src/index.ts +37 -0
- package/src/json.ts +43 -0
- package/src/model_ids.ts +29 -0
- package/src/pi_context.ts +251 -0
- package/src/pi_options.ts +295 -0
- package/src/pi_output.ts +387 -0
- package/src/pi_reasoning_signature.ts +41 -0
- package/src/pi_runtime.ts +238 -0
- package/src/pi_types.ts +71 -0
- package/src/portable_reasoning_state.ts +129 -0
- package/src/previous_decision.ts +109 -0
- package/src/prompts.ts +69 -0
- package/src/protocol_anthropic.ts +571 -0
- package/src/protocol_openai_chat.ts +645 -0
- package/src/protocol_validation.ts +202 -0
- package/src/route.ts +490 -0
- package/src/routing_history.ts +157 -0
- package/src/selector_input.ts +246 -0
- package/src/selector_parse.ts +138 -0
- package/src/selector_request.ts +130 -0
- package/src/selector_truncation.ts +48 -0
- package/src/selector_wire.ts +167 -0
- package/src/types.ts +231 -0
package/src/pi_types.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Api,
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
AssistantMessageEvent,
|
|
5
|
+
Context,
|
|
6
|
+
Model,
|
|
7
|
+
SimpleStreamOptions,
|
|
8
|
+
} from "@mupt-ai/pi-ai";
|
|
9
|
+
|
|
10
|
+
import type { ChatCompletionRequest, ReasoningEffort } from "./types.js";
|
|
11
|
+
import type {
|
|
12
|
+
RouterCandidate,
|
|
13
|
+
RouterExecutorInput,
|
|
14
|
+
RouterExecutorResult,
|
|
15
|
+
RouterModel,
|
|
16
|
+
RouterRequest,
|
|
17
|
+
} from "./framework_types.js";
|
|
18
|
+
|
|
19
|
+
export type PiModel = Model<Api>;
|
|
20
|
+
|
|
21
|
+
export type PiCredentialInput = {
|
|
22
|
+
provider: string;
|
|
23
|
+
model: string;
|
|
24
|
+
api: string;
|
|
25
|
+
purpose: "execution" | "selector";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PiApiKey = string | ((input: PiCredentialInput) => string | Promise<string>);
|
|
29
|
+
|
|
30
|
+
export type PiModelRegistry = {
|
|
31
|
+
getModel(provider: string, id: string): PiModel | undefined;
|
|
32
|
+
completeSimple(
|
|
33
|
+
model: PiModel,
|
|
34
|
+
context: Context,
|
|
35
|
+
options?: SimpleStreamOptions,
|
|
36
|
+
): Promise<AssistantMessage>;
|
|
37
|
+
streamSimple(
|
|
38
|
+
model: PiModel,
|
|
39
|
+
context: Context,
|
|
40
|
+
options?: SimpleStreamOptions,
|
|
41
|
+
): AsyncIterable<AssistantMessageEvent>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type CreatePiRuntimeOptions = {
|
|
45
|
+
apiKey: PiApiKey;
|
|
46
|
+
registry?: PiModelRegistry;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
maxRetries?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type PiRouterModelOptions<Metadata = unknown> = Omit<
|
|
52
|
+
RouterModel<Metadata>,
|
|
53
|
+
"id" | "executor" | "provider" | "api"
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
export type PiRuntime = {
|
|
57
|
+
model<Metadata = unknown>(
|
|
58
|
+
id: string,
|
|
59
|
+
options?: Partial<PiRouterModelOptions<Metadata>>,
|
|
60
|
+
): RouterModel<Metadata>;
|
|
61
|
+
execute<Metadata>(input: RouterExecutorInput<Metadata>): Promise<RouterExecutorResult>;
|
|
62
|
+
select(request: ChatCompletionRequest, signal?: AbortSignal): Promise<string>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type PiExecution = {
|
|
66
|
+
request: RouterRequest;
|
|
67
|
+
candidate: Pick<RouterCandidate, "id" | "provider" | "api">;
|
|
68
|
+
reasoningEffort: ReasoningEffort;
|
|
69
|
+
signal: AbortSignal;
|
|
70
|
+
purpose: PiCredentialInput["purpose"];
|
|
71
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { RouterFrameworkError } from "./framework_error.js";
|
|
2
|
+
import type { ProviderIdentity } from "./continuation_state.js";
|
|
3
|
+
import { isRecord } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const PREFIX = "dari-ir-v1.";
|
|
6
|
+
const FAMILY_PREFIX = "dari-ir-";
|
|
7
|
+
const MAX_JSON_BYTES = 4 * 1024;
|
|
8
|
+
const MAX_ENCODED_CHARS = Math.ceil(MAX_JSON_BYTES / 3) * 4;
|
|
9
|
+
|
|
10
|
+
export type PortableReasoningState = {
|
|
11
|
+
source?: ProviderIdentity;
|
|
12
|
+
itemId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function encodePortableReasoningState(state: PortableReasoningState): string {
|
|
16
|
+
const source = state.source ? providerIdentity(state.source, "configuration") : undefined;
|
|
17
|
+
const itemId = state.itemId ? boundedString(state.itemId, 512, "configuration", "item ID") : undefined;
|
|
18
|
+
const payload = {
|
|
19
|
+
v: 1,
|
|
20
|
+
...(source ? { source } : {}),
|
|
21
|
+
...(itemId ? { id: itemId } : {}),
|
|
22
|
+
};
|
|
23
|
+
const json = JSON.stringify(payload);
|
|
24
|
+
if (Buffer.byteLength(json, "utf8") > MAX_JSON_BYTES) {
|
|
25
|
+
throw invalidPortableState(
|
|
26
|
+
"configuration",
|
|
27
|
+
"Portable reasoning state exceeds the encoded size limit.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return `${PREFIX}${Buffer.from(json, "utf8").toString("base64url")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decodeUntrustedPortableReasoningState(value: string): PortableReasoningState | null {
|
|
34
|
+
if (!value.startsWith(FAMILY_PREFIX)) return null;
|
|
35
|
+
if (!value.startsWith(PREFIX)) {
|
|
36
|
+
throw invalidPortableState(
|
|
37
|
+
"invalid_request",
|
|
38
|
+
"Portable reasoning state version is not supported.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const encoded = value.slice(PREFIX.length);
|
|
43
|
+
if (!encoded || encoded.length > MAX_ENCODED_CHARS || !/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
44
|
+
throw invalidPortableState(
|
|
45
|
+
"invalid_request",
|
|
46
|
+
"Portable reasoning state encoding is invalid.",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const bytes = Buffer.from(encoded, "base64url");
|
|
50
|
+
if (bytes.length > MAX_JSON_BYTES || bytes.toString("base64url") !== encoded) {
|
|
51
|
+
throw invalidPortableState(
|
|
52
|
+
"invalid_request",
|
|
53
|
+
"Portable reasoning state encoding is invalid.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let decoded: unknown;
|
|
58
|
+
try {
|
|
59
|
+
decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
60
|
+
} catch {
|
|
61
|
+
throw invalidPortableState(
|
|
62
|
+
"invalid_request",
|
|
63
|
+
"Portable reasoning state payload is invalid.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(decoded) || decoded.v !== 1) {
|
|
67
|
+
throw invalidPortableState(
|
|
68
|
+
"invalid_request",
|
|
69
|
+
"Portable reasoning state schema is invalid.",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const keys = Object.keys(decoded);
|
|
73
|
+
if (!keys.every((key) => key === "v" || key === "source" || key === "id")) {
|
|
74
|
+
throw invalidPortableState(
|
|
75
|
+
"invalid_request",
|
|
76
|
+
"Portable reasoning state schema is invalid.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
...(decoded.source !== undefined
|
|
81
|
+
? { source: providerIdentity(decoded.source, "invalid_request") }
|
|
82
|
+
: {}),
|
|
83
|
+
...(decoded.id !== undefined
|
|
84
|
+
? { itemId: boundedString(decoded.id, 512, "invalid_request", "item ID") }
|
|
85
|
+
: {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function providerIdentity(
|
|
90
|
+
value: unknown,
|
|
91
|
+
kind: "configuration" | "invalid_request",
|
|
92
|
+
): ProviderIdentity {
|
|
93
|
+
if (!isRecord(value) || !hasExactKeys(value, ["provider", "api", "model"])) {
|
|
94
|
+
throw invalidPortableState(kind, "Portable reasoning state source is invalid.");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
provider: boundedString(value.provider, 128, kind, "provider"),
|
|
98
|
+
api: boundedString(value.api, 128, kind, "API"),
|
|
99
|
+
model: boundedString(value.model, 512, kind, "model"),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function boundedString(
|
|
104
|
+
value: unknown,
|
|
105
|
+
maxLength: number,
|
|
106
|
+
kind: "configuration" | "invalid_request",
|
|
107
|
+
field: string,
|
|
108
|
+
): string {
|
|
109
|
+
if (typeof value !== "string" || !value || value.length > maxLength) {
|
|
110
|
+
throw invalidPortableState(kind, `Portable reasoning state ${field} is invalid.`);
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasExactKeys(value: Record<string, unknown>, keys: string[]): boolean {
|
|
116
|
+
const actual = Object.keys(value);
|
|
117
|
+
return actual.length === keys.length && keys.every((key) => actual.includes(key));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function invalidPortableState(
|
|
121
|
+
kind: "configuration" | "invalid_request",
|
|
122
|
+
message: string,
|
|
123
|
+
): RouterFrameworkError {
|
|
124
|
+
return new RouterFrameworkError(
|
|
125
|
+
kind,
|
|
126
|
+
message,
|
|
127
|
+
"invalid_provider_continuation_state",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isReasoningEffort,
|
|
3
|
+
routingCandidateKey,
|
|
4
|
+
type PreviousDecision,
|
|
5
|
+
type RouterPrefixHit,
|
|
6
|
+
type RoutingCandidate,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
// A previous decision only carries over when its exact model/thinking-level
|
|
10
|
+
// pair is still a candidate for this request.
|
|
11
|
+
export function compatiblePreviousDecision(
|
|
12
|
+
previousDecision: PreviousDecision | undefined,
|
|
13
|
+
candidates: RoutingCandidate[],
|
|
14
|
+
): PreviousDecision | undefined {
|
|
15
|
+
if (previousDecision === undefined) return undefined;
|
|
16
|
+
const previousKey = routingCandidateKey(previousDecision);
|
|
17
|
+
return candidates.some((candidate) => routingCandidateKey(candidate) === previousKey)
|
|
18
|
+
? previousDecision
|
|
19
|
+
: undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ActiveLease = RoutingCandidate & { turnsRemaining: number };
|
|
23
|
+
|
|
24
|
+
// An unexpired lease from the deepest identity hit. The lease names the stored
|
|
25
|
+
// next-turn target exactly; if that pair is no longer a candidate for this
|
|
26
|
+
// request, the lease dissolves and routing decides fresh.
|
|
27
|
+
export function activeLeaseFromHit(
|
|
28
|
+
entry: RouterPrefixHit,
|
|
29
|
+
candidates: RoutingCandidate[],
|
|
30
|
+
): ActiveLease | undefined {
|
|
31
|
+
const remaining = entry.lease_turns_remaining;
|
|
32
|
+
if (typeof remaining !== "number" || !Number.isInteger(remaining) || remaining <= 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const nextEffort = entry.next_reasoning_effort;
|
|
36
|
+
if (!entry.next_model || !isReasoningEffort(nextEffort)) return undefined;
|
|
37
|
+
const target = { model: entry.next_model, reasoningEffort: nextEffort };
|
|
38
|
+
const targetKey = routingCandidateKey(target);
|
|
39
|
+
return candidates.some((candidate) => routingCandidateKey(candidate) === targetKey)
|
|
40
|
+
? { ...target, turnsRemaining: remaining }
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type PendingLease = RoutingCandidate & {
|
|
45
|
+
turns: number;
|
|
46
|
+
reason: string | null;
|
|
47
|
+
outputText: string | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// The next lease decided ahead of the active one's expiry. Like the active
|
|
51
|
+
// lease, it only survives while its exact pair is still a candidate.
|
|
52
|
+
export function pendingLeaseFromHit(
|
|
53
|
+
entry: RouterPrefixHit,
|
|
54
|
+
candidates: RoutingCandidate[],
|
|
55
|
+
): PendingLease | undefined {
|
|
56
|
+
const turns = entry.pending_lease_turns;
|
|
57
|
+
if (typeof turns !== "number" || !Number.isInteger(turns) || turns <= 0) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const effort = entry.pending_lease_reasoning_effort;
|
|
61
|
+
if (!entry.pending_lease_model || !isReasoningEffort(effort)) return undefined;
|
|
62
|
+
const target = { model: entry.pending_lease_model, reasoningEffort: effort };
|
|
63
|
+
const targetKey = routingCandidateKey(target);
|
|
64
|
+
return candidates.some((candidate) => routingCandidateKey(candidate) === targetKey)
|
|
65
|
+
? {
|
|
66
|
+
...target,
|
|
67
|
+
turns,
|
|
68
|
+
reason: entry.pending_lease_reason ?? null,
|
|
69
|
+
outputText: entry.pending_lease_output_text ?? null,
|
|
70
|
+
}
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Reconstructs the previous decision from the deepest warm-prefix entry: the
|
|
75
|
+
// stored next-turn recommendation when it is still a candidate, otherwise the
|
|
76
|
+
// serving pair.
|
|
77
|
+
export function previousDecisionFromHit(
|
|
78
|
+
entry: RouterPrefixHit,
|
|
79
|
+
candidates: RoutingCandidate[],
|
|
80
|
+
): PreviousDecision | undefined {
|
|
81
|
+
const candidateKeys = new Set(candidates.map(routingCandidateKey));
|
|
82
|
+
const uniqueCandidateForModel = (model: string): RoutingCandidate | null => {
|
|
83
|
+
const matching = candidates.filter((candidate) => candidate.model === model);
|
|
84
|
+
return matching.length === 1 ? matching[0]! : null;
|
|
85
|
+
};
|
|
86
|
+
const nextEffort = entry.next_reasoning_effort;
|
|
87
|
+
const storedNext = entry.next_model
|
|
88
|
+
? isReasoningEffort(nextEffort)
|
|
89
|
+
? { model: entry.next_model, reasoningEffort: nextEffort }
|
|
90
|
+
: uniqueCandidateForModel(entry.next_model)
|
|
91
|
+
: null;
|
|
92
|
+
const serving = isReasoningEffort(entry.reasoning_bucket)
|
|
93
|
+
? { model: entry.model, reasoningEffort: entry.reasoning_bucket }
|
|
94
|
+
: uniqueCandidateForModel(entry.model);
|
|
95
|
+
const storedNextIsCandidate = Boolean(
|
|
96
|
+
storedNext && candidateKeys.has(routingCandidateKey(storedNext)),
|
|
97
|
+
);
|
|
98
|
+
const selected = storedNext && storedNextIsCandidate
|
|
99
|
+
? storedNext
|
|
100
|
+
: serving && candidateKeys.has(routingCandidateKey(serving))
|
|
101
|
+
? serving
|
|
102
|
+
: null;
|
|
103
|
+
if (!selected) return undefined;
|
|
104
|
+
const storedNextWasRejected = Boolean(entry.next_model) && !storedNextIsCandidate;
|
|
105
|
+
return {
|
|
106
|
+
...selected,
|
|
107
|
+
reason: storedNextWasRejected ? "" : (entry.reason ?? ""),
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Selector system prompts, standalone so prompt review never requires
|
|
2
|
+
// reading request-assembly code. Per-request candidate data rides in the
|
|
3
|
+
// user message, keeping these byte-stable for provider prompt caching.
|
|
4
|
+
|
|
5
|
+
import { FIXED_TURN_COST_PROJECTED_TURNS } from "./fixed_turn_cost_config.js";
|
|
6
|
+
|
|
7
|
+
// Interpolated where a prompt names the projection horizons so the wording
|
|
8
|
+
// cannot drift from the config that produces the data.
|
|
9
|
+
const PROJECTED_TURNS_PHRASE = FIXED_TURN_COST_PROJECTED_TURNS.join(", ");
|
|
10
|
+
|
|
11
|
+
export const SELECTOR_SYSTEM_PROMPT = `You are Dari's model router. Pick the best candidate model/thinking-level pair for the user's request.
|
|
12
|
+
|
|
13
|
+
# Selection procedure
|
|
14
|
+
|
|
15
|
+
1. Capability first: make sure the selected pair can reasonably handle the task. Consider both model capability and how much thinking the task needs; capability is never traded away for cost.
|
|
16
|
+
2. Use imported eval scorecards as benchmark evidence when relevant.
|
|
17
|
+
3. A missing benchmark score means that candidate pair was not evaluated on that benchmark; do not treat it as zero, failure, or negative evidence.
|
|
18
|
+
4. When several pairs are viable on capability, use cost_estimates as the deciding factor between them.
|
|
19
|
+
5. Prefer the previous_decision pair when the task hasn't changed.
|
|
20
|
+
|
|
21
|
+
# Cost comparison rules
|
|
22
|
+
|
|
23
|
+
- fixed_turn_cost_estimate.projections is the primary cost metric whenever it is present. Each entry estimates the whole agent loop from this decision point out to projected_turns requests, using current cache warmth, provider cache rules, cache hit probabilities, and output tokens folded into later requests. Compare candidates at the horizon the task looks likely to need.
|
|
24
|
+
- Candidates with fixed_turn_cost_estimate are the preferred cost-comparison pool when capability is otherwise comparable.
|
|
25
|
+
- Do not rank candidates by output_cost_per_mtok or est_input_cost_usd ahead of fixed_turn_cost_estimate.projections. Do not discard those estimates just because some candidates lack them.
|
|
26
|
+
- For candidates without fixed_turn_cost_estimate, use est_input_cost_usd and output_cost_per_mtok as fallback cost evidence, and select such a candidate only when its capability and fallback pricing make it a better fit than candidates with fixed-turn estimates.
|
|
27
|
+
|
|
28
|
+
# Output
|
|
29
|
+
|
|
30
|
+
Return JSON matching the provided schema. selected_model and reasoning_effort must exactly match one candidate pair.
|
|
31
|
+
The runtime chooses provider fallbacks separately; return only the primary decision.`;
|
|
32
|
+
|
|
33
|
+
export const CUSTOM_SELECTOR_SYSTEM_PROMPT = `You are Dari's model router. Pick the best candidate model/thinking-level pair for the user's request.
|
|
34
|
+
This router is configured with user-authored rules. Each custom_rules entry pairs a natural-language when condition with a model and an optional thinking_level. A null thinking_level means Auto: choose the most appropriate candidate thinking level for that rule's model. A non-null thinking_level pins the exact pair.
|
|
35
|
+
Decide which rule's when condition best describes the current request — judge the latest phase or intent of the conversation (for example planning versus implementation), not just the first message — and select that rule's model and either its pinned thinking_level or an appropriate enabled level when it is Auto.
|
|
36
|
+
custom_rules is ordered by model price, most expensive first; the order is presentation, not priority.
|
|
37
|
+
When several rules match, pick the one whose when condition is most specific to the current request.
|
|
38
|
+
When no rule matches, select default_target if it is set; its null thinking_level also means Auto. Otherwise prefer previous_decision when it is a candidate; otherwise pick the most capable candidate pair.
|
|
39
|
+
Prefer previous_decision while the matched rule (or task phase) hasn't changed.
|
|
40
|
+
Use imported eval scorecards as benchmark evidence when relevant. A missing benchmark score means that candidate pair was not evaluated on that benchmark; do not treat it as zero, failure, or negative evidence.
|
|
41
|
+
Use cost_estimates only to break ties between rule-matched candidates or to choose among an Auto target's viable thinking levels; a rule match is never traded away for cost.
|
|
42
|
+
When using cost_estimates, fixed_turn_cost_estimate.projections is the primary cost metric whenever present because it includes current cache warmth and projected loop cost at ${PROJECTED_TURNS_PHRASE} turns. For candidates without fixed_turn_cost_estimate, use est_input_cost_usd and output_cost_per_mtok as fallback cost evidence. Do not rank by output_cost_per_mtok ahead of fixed_turn_cost_estimate.projections.
|
|
43
|
+
Return JSON matching the provided schema.
|
|
44
|
+
selected_model and reasoning_effort must exactly match one candidate pair.
|
|
45
|
+
The runtime chooses provider fallbacks separately; return only the primary decision.`;
|
|
46
|
+
|
|
47
|
+
// System prompt for the anonymous candidate-action protocol (see
|
|
48
|
+
// anonymous_actions.ts). It carries every invariant instruction, including how
|
|
49
|
+
// to read the per-action blocks, so the user message stays pure data.
|
|
50
|
+
export const ANONYMOUS_ACTION_SYSTEM_PROMPT = [
|
|
51
|
+
"You are Dari's model router for a coding agent.",
|
|
52
|
+
"Choose exactly one anonymous candidate action for the agent's next turns, and how many turns to commit to it.",
|
|
53
|
+
"Your choice is a lease: the chosen action serves that many consecutive turns before you are consulted again. A longer lease amortizes the cold start paid on its first turn across warm cache hits on the turns that follow; the lease ends early only if the task finishes or the provider fails.",
|
|
54
|
+
"Each action's block lists its benchmark standing and its projected cost at each lease length you may choose.",
|
|
55
|
+
"Rank is the action's position among every model scored on that benchmark, 1 being best. Z is how many standard deviations that model sits above or below the mean of those models.",
|
|
56
|
+
"Cost is the projected spend for the whole agent loop over exactly the turns of each lease option, from current cache warmth. Compare actions at the lease you intend to pick.",
|
|
57
|
+
"A missing benchmark score means that action was not evaluated on that benchmark; do not treat it as zero, failure, or negative evidence.",
|
|
58
|
+
"Prefer a cheaper action only when it is sufficiently likely to complete the task successfully.",
|
|
59
|
+
// Scaffolding for the thinking budget: the decision decomposes into judging
|
|
60
|
+
// how hard the task is and then finding the cheapest action-and-lease that
|
|
61
|
+
// clears that bar. Stating the decomposition costs a few prompt tokens and
|
|
62
|
+
// saves the policy from having to discover it from reward alone.
|
|
63
|
+
"Think in exactly three short steps, then stop:",
|
|
64
|
+
"1. Phase: one sentence — what stage the task is at based on the latest turn (exploring, editing, testing, wrapping up), roughly how many turns remain, and how hard the next step looks.",
|
|
65
|
+
"2. Evidence: one sentence — the one or two benchmarks most like this work, and which actions stand out on them.",
|
|
66
|
+
"3. Pick: name the cheapest action whose standing clears the difficulty, and the longest lease the remaining work justifies; go pricier or shorter only when the task demands it. Then end thinking immediately and answer.",
|
|
67
|
+
"Do not walk through every action, restate the scorecards, or retell the conversation.",
|
|
68
|
+
"Return only JSON matching the provided schema.",
|
|
69
|
+
].join("\n");
|