@mupt-ai/dari-router 0.1.0 → 0.2.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 +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 +56 -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 +83 -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
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Selector prompt sizing. The selector can see a front-trimmed view of the
|
|
2
|
-
// conversation; the eventual provider call still receives the full request.
|
|
3
|
-
|
|
4
|
-
import { isRecord, type ChatMessage } from "./types.js";
|
|
5
|
-
|
|
6
|
-
const SELECTOR_TRUNCATION_MARKER = "<earlier messages truncated for routing>";
|
|
7
|
-
const MARKER_MESSAGE: ChatMessage = { role: "user", content: SELECTOR_TRUNCATION_MARKER };
|
|
8
|
-
|
|
9
|
-
// Drop oldest messages first — in multi-turn conversations the latest
|
|
10
|
-
// messages carry the routing signal.
|
|
11
|
-
export function trimMessagesFromFront(messages: ChatMessage[], dropChars: number): ChatMessage[] {
|
|
12
|
-
const targetLength = Math.max(0, JSON.stringify(messages).length - Math.max(0, dropChars));
|
|
13
|
-
const kept = [...messages];
|
|
14
|
-
while (kept.length > 1 && JSON.stringify([MARKER_MESSAGE, ...kept]).length > targetLength) {
|
|
15
|
-
kept.shift();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const marked = [MARKER_MESSAGE, ...kept];
|
|
19
|
-
if (JSON.stringify(marked).length <= targetLength) return marked;
|
|
20
|
-
|
|
21
|
-
if (kept.length === 1) {
|
|
22
|
-
const first = kept[0]!;
|
|
23
|
-
const trimmableContent = trimSourceContent(first);
|
|
24
|
-
if (trimmableContent === null) return [MARKER_MESSAGE];
|
|
25
|
-
const prefix = `${SELECTOR_TRUNCATION_MARKER}\n`;
|
|
26
|
-
let content = `${prefix}${trimmableContent}`;
|
|
27
|
-
while (content.length > prefix.length) {
|
|
28
|
-
const excess = JSON.stringify([{ ...first, content }]).length - targetLength;
|
|
29
|
-
if (excess <= 0) break;
|
|
30
|
-
content = `${prefix}${content.slice(prefix.length + excess)}`;
|
|
31
|
-
}
|
|
32
|
-
return [{ ...first, content }];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return [MARKER_MESSAGE];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function trimSourceContent(message: ChatMessage): string | null {
|
|
39
|
-
if (typeof message.content === "string") return message.content;
|
|
40
|
-
if (!Array.isArray(message.content)) return null;
|
|
41
|
-
return message.content.map(formatContentPartForTrim).join("\n");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function formatContentPartForTrim(part: Record<string, unknown>): string {
|
|
45
|
-
if (isRecord(part) && part.type === "text" && typeof part.text === "string") return part.text;
|
|
46
|
-
if (isRecord(part) && part.type === "image_url") return "<image omitted>";
|
|
47
|
-
return JSON.stringify(part);
|
|
48
|
-
}
|
package/src/selector_wire.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
// Inverse codec for the selector-request wire format buildSelectorRequest
|
|
2
|
-
// produces: the selector input travels as a JSON-stringified user message
|
|
3
|
-
// inside a chat-completion request.
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
anonymizeSelectorInput,
|
|
7
|
-
assignAnonymousActions,
|
|
8
|
-
type Rng,
|
|
9
|
-
} from "./anonymous_actions.js";
|
|
10
|
-
import { RouterFrameworkError } from "./framework_error.js";
|
|
11
|
-
import type { JsonObject } from "./json.js";
|
|
12
|
-
import type { SelectorInput } from "./selector_input.js";
|
|
13
|
-
import {
|
|
14
|
-
isReasoningEffort,
|
|
15
|
-
isRecord,
|
|
16
|
-
routingCandidateKey,
|
|
17
|
-
type PreviousDecision,
|
|
18
|
-
type RoutingCandidate,
|
|
19
|
-
} from "./types.js";
|
|
20
|
-
|
|
21
|
-
export type DecodedSelectorRequest = {
|
|
22
|
-
candidates: RoutingCandidate[];
|
|
23
|
-
previousDecision?: PreviousDecision;
|
|
24
|
-
selectorInput: SelectorInput;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Decodes an untrusted POSTed selector request body; throws kind
|
|
28
|
-
// "invalid_request" on malformed input.
|
|
29
|
-
export function decodeUntrustedSelectorRequest(payload: unknown): DecodedSelectorRequest {
|
|
30
|
-
const selectorInput = parseSelectorInput(payload);
|
|
31
|
-
const candidates: RoutingCandidate[] = [];
|
|
32
|
-
const seenPairs = new Set<string>();
|
|
33
|
-
for (const [index, pair] of (selectorInput.candidate_pairs as unknown[]).entries()) {
|
|
34
|
-
const param = `candidate_pairs[${index}]`;
|
|
35
|
-
if (!isRecord(pair)) {
|
|
36
|
-
throw invalidSelectorRequest("Selector candidate must be an object.", param);
|
|
37
|
-
}
|
|
38
|
-
const candidate = parseCandidatePair(pair, "candidate", param);
|
|
39
|
-
const key = routingCandidateKey(candidate);
|
|
40
|
-
if (seenPairs.has(key)) {
|
|
41
|
-
throw invalidSelectorRequest(
|
|
42
|
-
`Selector input contains duplicate candidate ${candidate.model}/${candidate.reasoningEffort}.`,
|
|
43
|
-
param,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
seenPairs.add(key);
|
|
47
|
-
candidates.push(candidate);
|
|
48
|
-
}
|
|
49
|
-
const previousDecision = parsePreviousDecision(selectorInput.previous_decision);
|
|
50
|
-
if (candidates.length === 0) {
|
|
51
|
-
throw invalidSelectorRequest("Selector request contains no candidates.");
|
|
52
|
-
}
|
|
53
|
-
return { candidates, ...(previousDecision !== undefined ? { previousDecision } : {}), selectorInput };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// anonymizeSelectorInput's validators throw plain Errors; dry-run it here so
|
|
57
|
-
// malformed input surfaces as invalid_request instead of an internal error.
|
|
58
|
-
export function assertAnonymizableSelectorInput(
|
|
59
|
-
selectorInput: SelectorInput,
|
|
60
|
-
candidates: RoutingCandidate[],
|
|
61
|
-
rng: Rng = Math.random,
|
|
62
|
-
): void {
|
|
63
|
-
if (candidates.length < 2) return;
|
|
64
|
-
try {
|
|
65
|
-
anonymizeSelectorInput(
|
|
66
|
-
selectorInput as unknown as JsonObject,
|
|
67
|
-
assignAnonymousActions(candidates, rng),
|
|
68
|
-
);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
throw invalidSelectorRequest(
|
|
71
|
-
`Selector input is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parsePreviousDecision(value: unknown): PreviousDecision | undefined {
|
|
77
|
-
if (value === null || value === undefined) return undefined;
|
|
78
|
-
if (!isRecord(value)) {
|
|
79
|
-
throw invalidSelectorRequest(
|
|
80
|
-
"Selector previous_decision must be an object or null.",
|
|
81
|
-
"previous_decision",
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
const candidate = parseCandidatePair(value, "previous_decision", "previous_decision");
|
|
85
|
-
if (typeof value.reason !== "string") {
|
|
86
|
-
throw invalidSelectorRequest(
|
|
87
|
-
"Selector previous_decision reason must be a string.",
|
|
88
|
-
"previous_decision.reason",
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return { ...candidate, reason: value.reason };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function parseCandidatePair(
|
|
95
|
-
value: Record<string, unknown>,
|
|
96
|
-
subject: "candidate" | "previous_decision",
|
|
97
|
-
param: string,
|
|
98
|
-
): RoutingCandidate {
|
|
99
|
-
if (typeof value.model !== "string" || value.model === "") {
|
|
100
|
-
throw invalidSelectorRequest(`Selector ${subject} is missing a model name.`, `${param}.model`);
|
|
101
|
-
}
|
|
102
|
-
if (!isReasoningEffort(value.thinking_level)) {
|
|
103
|
-
throw invalidSelectorRequest(
|
|
104
|
-
`Selector ${subject} has invalid thinking_level: ${String(value.thinking_level)}`,
|
|
105
|
-
`${param}.thinking_level`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
return { model: value.model, reasoningEffort: value.thinking_level };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function parseSelectorInput(payload: unknown): SelectorInput {
|
|
112
|
-
if (!isRecord(payload)) {
|
|
113
|
-
throw invalidSelectorRequest("Selector request must be a JSON object.");
|
|
114
|
-
}
|
|
115
|
-
const messages: unknown = payload.messages ?? [];
|
|
116
|
-
if (!Array.isArray(messages)) {
|
|
117
|
-
throw invalidSelectorRequest("Selector request messages must be an array.", "messages");
|
|
118
|
-
}
|
|
119
|
-
const userMessage = messages.find(
|
|
120
|
-
(message): message is Record<string, unknown> => isRecord(message) && message.role === "user",
|
|
121
|
-
);
|
|
122
|
-
if (userMessage === undefined) {
|
|
123
|
-
throw invalidSelectorRequest("Selector request is missing the user message.");
|
|
124
|
-
}
|
|
125
|
-
const content = userMessage.content;
|
|
126
|
-
if (typeof content !== "string") {
|
|
127
|
-
throw invalidSelectorRequest("Selector request user message content must be a string.");
|
|
128
|
-
}
|
|
129
|
-
let parsed: unknown;
|
|
130
|
-
try {
|
|
131
|
-
parsed = JSON.parse(content);
|
|
132
|
-
} catch {
|
|
133
|
-
throw invalidSelectorRequest("Selector request user message is not valid JSON.");
|
|
134
|
-
}
|
|
135
|
-
if (!isRecord(parsed)) {
|
|
136
|
-
throw invalidSelectorRequest("Selector request user message is not a JSON object.");
|
|
137
|
-
}
|
|
138
|
-
if (!Array.isArray(parsed.candidate_pairs)) {
|
|
139
|
-
throw invalidSelectorRequest("Selector input is missing candidate_pairs.");
|
|
140
|
-
}
|
|
141
|
-
// Anonymization would silently drop custom rules rather than apply them.
|
|
142
|
-
if ("custom_rules" in parsed || "default_target" in parsed) {
|
|
143
|
-
throw new RouterFrameworkError(
|
|
144
|
-
"invalid_request",
|
|
145
|
-
"The Dari Auto Router serves only the default routing policy and cannot apply " +
|
|
146
|
-
"custom routing rules. Self-host the selector or use the Dari managed platform " +
|
|
147
|
-
"for custom routing.",
|
|
148
|
-
"custom_rules_not_supported",
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
if (!Array.isArray(parsed.messages)) {
|
|
152
|
-
throw invalidSelectorRequest("Selector input messages must be an array.", "messages");
|
|
153
|
-
}
|
|
154
|
-
for (const [index, message] of (parsed.messages as unknown[]).entries()) {
|
|
155
|
-
if (!isRecord(message) || typeof message.role !== "string") {
|
|
156
|
-
throw invalidSelectorRequest(
|
|
157
|
-
"Selector input messages must be objects with a string role.",
|
|
158
|
-
`messages[${index}]`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return parsed as unknown as SelectorInput;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function invalidSelectorRequest(message: string, param?: string): RouterFrameworkError {
|
|
166
|
-
return new RouterFrameworkError("invalid_request", message, "invalid_request_error", param);
|
|
167
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
// Provider ids are the lowercase prefix of a `provider/native-model-id` model
|
|
6
|
-
// string, e.g. "openai" in "openai/gpt-5.6-luna".
|
|
7
|
-
export type Provider = string;
|
|
8
|
-
|
|
9
|
-
export type ChatMessage = {
|
|
10
|
-
role: string;
|
|
11
|
-
content?: string | Array<Record<string, unknown>> | null;
|
|
12
|
-
tool_call_id?: string;
|
|
13
|
-
tool_calls?: Array<Record<string, unknown>>;
|
|
14
|
-
reasoning_content?: string;
|
|
15
|
-
reasoning?: string;
|
|
16
|
-
reasoning_text?: string;
|
|
17
|
-
reasoning_details?: Array<Record<string, unknown>>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const REASONING_EFFORTS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
21
|
-
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
22
|
-
|
|
23
|
-
export function isReasoningEffort(value: unknown): value is ReasoningEffort {
|
|
24
|
-
return typeof value === "string" && REASONING_EFFORTS.some((effort) => effort === value);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type ResponseFormat =
|
|
28
|
-
| { type: "text" }
|
|
29
|
-
| { type: "json_object" }
|
|
30
|
-
| {
|
|
31
|
-
type: "json_schema";
|
|
32
|
-
json_schema: {
|
|
33
|
-
name: string;
|
|
34
|
-
schema: Record<string, unknown>;
|
|
35
|
-
strict?: boolean;
|
|
36
|
-
description?: string;
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export type ChatCompletionRequest = {
|
|
41
|
-
model?: string;
|
|
42
|
-
messages?: ChatMessage[];
|
|
43
|
-
stream?: boolean;
|
|
44
|
-
source_protocol?: "anthropic_messages" | "openai_chat" | "openai_responses";
|
|
45
|
-
prompt_cache_key?: string;
|
|
46
|
-
temperature?: number;
|
|
47
|
-
top_p?: number;
|
|
48
|
-
max_tokens?: number;
|
|
49
|
-
max_completion_tokens?: number;
|
|
50
|
-
stop?: string | string[];
|
|
51
|
-
tools?: Array<Record<string, unknown>>;
|
|
52
|
-
tool_choice?: string | Record<string, unknown>;
|
|
53
|
-
parallel_tool_calls?: boolean;
|
|
54
|
-
response_format?: ResponseFormat;
|
|
55
|
-
reasoning_effort?: ReasoningEffort;
|
|
56
|
-
metadata?: Record<string, unknown>;
|
|
57
|
-
user?: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export type RoutingCandidate = {
|
|
61
|
-
model: string;
|
|
62
|
-
reasoningEffort: ReasoningEffort;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export function routingCandidateKey(candidate: RoutingCandidate): string {
|
|
66
|
-
return `${candidate.model}\u0000${candidate.reasoningEffort}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function sameRoutingCandidate(
|
|
70
|
-
left: RoutingCandidate,
|
|
71
|
-
right: RoutingCandidate,
|
|
72
|
-
): boolean {
|
|
73
|
-
return left.model === right.model && left.reasoningEffort === right.reasoningEffort;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type RoutingDecision = {
|
|
77
|
-
selectedModel: string;
|
|
78
|
-
reasoningEffort: ReasoningEffort;
|
|
79
|
-
reason: string;
|
|
80
|
-
// Turns still committed to this selection after the current one, when a
|
|
81
|
-
// lease-aware policy made (or is holding) a commitment. The serving layer
|
|
82
|
-
// persists the countdown on the conversation's prefix chain.
|
|
83
|
-
leaseTurnsRemaining?: number;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export type RouterEvalScore = {
|
|
87
|
-
model_id: string;
|
|
88
|
-
score: number;
|
|
89
|
-
notes?: string | null;
|
|
90
|
-
thinking_level?: ReasoningEffort | null;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export type RouterEval = {
|
|
94
|
-
id: string;
|
|
95
|
-
name: string;
|
|
96
|
-
description?: string | null;
|
|
97
|
-
min_score: number;
|
|
98
|
-
max_score: number;
|
|
99
|
-
scores: RouterEvalScore[];
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// One stored prefix entry for a conversation chain, as resolved by the
|
|
103
|
-
// host's prefix-hit storage. The core never persists these; it only consumes
|
|
104
|
-
// them as explicit input to cost estimation and conversation-identity
|
|
105
|
-
// recovery. Hosts may resolve entries past the cache-warmth window; `updated_at`
|
|
106
|
-
// is what separates the warm ones from the identity-only ones.
|
|
107
|
-
export type RouterPrefixHit = {
|
|
108
|
-
hash: string;
|
|
109
|
-
// Client-durable hash tier (no reasoning payloads, item ids, or
|
|
110
|
-
// provider-specific canonicalization). Matches the host's loose chain for
|
|
111
|
-
// conversation identity even when the client mutated its echo. Null on
|
|
112
|
-
// entries stored before the loose tier existed.
|
|
113
|
-
loose_hash?: string | null;
|
|
114
|
-
conversation_id: string;
|
|
115
|
-
model: string;
|
|
116
|
-
// Selector recommendation to use for the next turn. The serving model
|
|
117
|
-
// remains separate because it owns this prefix's provider-cache metadata.
|
|
118
|
-
next_model?: string | null;
|
|
119
|
-
next_reasoning_effort?: ReasoningEffort | null;
|
|
120
|
-
message_depth: number;
|
|
121
|
-
// Legacy Chat-projection block depth, retained for compatibility with
|
|
122
|
-
// existing hosts and rolling deployments.
|
|
123
|
-
block_depth: number;
|
|
124
|
-
// Candidate-provider-visible block depth. Null/absent on entries written
|
|
125
|
-
// before provider-aware prompt accounting was introduced.
|
|
126
|
-
provider_block_depth?: number | null;
|
|
127
|
-
// Depth of the provider input represented by prompt_tokens. The entry hash
|
|
128
|
-
// may extend through provider output for conversation identity.
|
|
129
|
-
prompt_anchor_depth?: number | null;
|
|
130
|
-
prompt_tokens: number;
|
|
131
|
-
// Running maximum prompt depth of the hypothetical sticky benchmark.
|
|
132
|
-
// Hosts persist this with prefix state so future turns can price the
|
|
133
|
-
// benchmark incrementally without replaying request history.
|
|
134
|
-
benchmark_replay_depth?: number | null;
|
|
135
|
-
input_tokens: number;
|
|
136
|
-
cache_read_tokens: number;
|
|
137
|
-
cache_write_tokens: number;
|
|
138
|
-
tool_choice_fp: string;
|
|
139
|
-
response_format_fp: string;
|
|
140
|
-
// Effective reasoning bucket the serving request ran with ("off" groups
|
|
141
|
-
// omitted/off payloads). Null on entries written before buckets existed.
|
|
142
|
-
reasoning_bucket?: string | null;
|
|
143
|
-
// Turns still committed to next_model/next_reasoning_effort by a lease-aware
|
|
144
|
-
// policy. Positive while a lease holds; absent or null when routing decides
|
|
145
|
-
// per turn.
|
|
146
|
-
lease_turns_remaining?: number | null;
|
|
147
|
-
// Next lease decided ahead of expiry (the serving-side analogue of
|
|
148
|
-
// training's n-lag prefetch). Adopted when the active lease runs out, so the
|
|
149
|
-
// expiry turn pays no decision latency.
|
|
150
|
-
pending_lease_model?: string | null;
|
|
151
|
-
pending_lease_reasoning_effort?: ReasoningEffort | null;
|
|
152
|
-
pending_lease_turns?: number | null;
|
|
153
|
-
pending_lease_reason?: string | null;
|
|
154
|
-
pending_lease_output_text?: string | null;
|
|
155
|
-
reason: string | null;
|
|
156
|
-
updated_at: string;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
export type RoutingStrategy = "slm" | "custom";
|
|
160
|
-
|
|
161
|
-
export type CustomRouterRule = {
|
|
162
|
-
// Natural-language condition describing when this rule applies, e.g.
|
|
163
|
-
// "planning and architecture" or "implementation".
|
|
164
|
-
when: string;
|
|
165
|
-
// The enabled model serving matching requests. Null means the router
|
|
166
|
-
// chooses among that model's enabled thinking levels automatically.
|
|
167
|
-
use: string;
|
|
168
|
-
thinking_level?: ReasoningEffort | null;
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
export type CustomRouterConfig = {
|
|
172
|
-
rules: CustomRouterRule[];
|
|
173
|
-
default?: string | null;
|
|
174
|
-
default_thinking_level?: ReasoningEffort | null;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
export type RouterModelPrice = {
|
|
178
|
-
input: number;
|
|
179
|
-
output: number;
|
|
180
|
-
cached_input?: number | null;
|
|
181
|
-
cache_write?: number | null;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
export type PrefixHit = {
|
|
185
|
-
entry: RouterPrefixHit;
|
|
186
|
-
depth: number;
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
export type ChainHits = {
|
|
190
|
-
deepest?: PrefixHit;
|
|
191
|
-
perModel: Map<string, PrefixHit>;
|
|
192
|
-
// Deepest hit per model per reasoning bucket. Effort-keyed provider caches
|
|
193
|
-
// keep one warm partition per effective effort (partitions coexist:
|
|
194
|
-
// A -> B -> A reuses A's still-warm entry), so warm-prefix selection must
|
|
195
|
-
// not let a deeper cross-bucket entry shadow a valid same-bucket one.
|
|
196
|
-
// Entries written before buckets existed live under null.
|
|
197
|
-
perModelBuckets: Map<string, Map<string | null, PrefixHit>>;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export type TurnCostProjection = {
|
|
201
|
-
projected_turns: number;
|
|
202
|
-
total_cost_usd: number;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
export type FixedTurnCostEstimate = {
|
|
206
|
-
output_tokens_per_turn: number;
|
|
207
|
-
assumed_reasoning_effort: ReasoningEffort;
|
|
208
|
-
// Cumulative loop cost at each horizon in FIXED_TURN_COST_PROJECTED_TURNS,
|
|
209
|
-
// ascending by turn count.
|
|
210
|
-
projections: TurnCostProjection[];
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
export type CandidateCostEstimate = {
|
|
214
|
-
model: string;
|
|
215
|
-
reasoning_effort: ReasoningEffort;
|
|
216
|
-
warm_tokens: number;
|
|
217
|
-
est_prompt_tokens: number;
|
|
218
|
-
est_input_cost_usd: number | null;
|
|
219
|
-
output_cost_per_mtok: number | null;
|
|
220
|
-
pricing_known: boolean;
|
|
221
|
-
fixed_turn_cost_estimate: FixedTurnCostEstimate | null;
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
export type PreviousDecision = {
|
|
225
|
-
model: string;
|
|
226
|
-
reasoningEffort: ReasoningEffort;
|
|
227
|
-
reason: string;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Request capabilities that hard-constrain candidate eligibility.
|
|
231
|
-
export type HardCapability = "image_input" | "structured_output" | "openai_hosted_web_search";
|