@mupt-ai/dari-gateway 0.3.1 → 0.3.2
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 +14 -6
- package/dari-gateway.example.yaml +3 -2
- package/dist/http/auth.js +2 -2
- package/dist/http/http2_server.d.ts +1 -1
- package/dist/http/http2_server.js +3 -3
- package/dist/http/serve.d.ts +1 -0
- package/dist/http/serve.js +5 -4
- package/dist/ingress/chat_completions.js +2 -2
- package/dist/ingress/codex.js +3 -1
- package/dist/ingress/codex_models.d.ts +1 -1
- package/dist/ingress/codex_models.js +22 -13
- package/dist/platform/platform.d.ts +13 -3
- package/dist/protocol/input/openai_responses.js +2 -4
- package/dist/protocol/output/anthropic_messages_output.d.ts +2 -2
- package/dist/protocol/output/anthropic_messages_output.js +1 -0
- package/dist/protocol/output/openai_responses_stream.js +19 -1
- package/dist/providers/execution.d.ts +0 -5
- package/dist/providers/execution.js +5 -8
- package/dist/providers/pi_output.d.ts +4 -0
- package/dist/providers/pi_output.js +1 -1
- package/dist/providers/provider_attempt_runner.d.ts +2 -3
- package/dist/providers/provider_attempt_runner.js +14 -24
- package/dist/providers/provider_call.d.ts +3 -3
- package/dist/providers/provider_call.js +1 -0
- package/dist/providers/stream_finalization.d.ts +0 -1
- package/dist/providers/stream_finalization.js +0 -1
- package/dist/runtime/claude_code/auxiliary_routing.js +1 -9
- package/dist/runtime/explicit_model.js +1 -1
- package/dist/runtime/harness_identity/conversation.d.ts +5 -0
- package/dist/runtime/harness_identity/conversation.js +6 -0
- package/dist/runtime/harness_identity/extract.d.ts +1 -0
- package/dist/runtime/harness_identity/extract.js +13 -5
- package/dist/runtime/harness_identity/index.d.ts +2 -2
- package/dist/runtime/harness_identity/index.js +2 -2
- package/dist/runtime/prepare_routed_completion.d.ts +5 -1
- package/dist/runtime/prepare_routed_completion.js +26 -2
- package/dist/runtime/routed_runtime.js +33 -53
- package/dist/runtime/routing_lease.d.ts +4 -6
- package/dist/runtime/routing_lease.js +2 -47
- package/dist/runtime/runtime_reporter.d.ts +0 -2
- package/dist/runtime/runtime_reporter.js +0 -1
- package/dist/runtime/session_pin.d.ts +7 -0
- package/dist/runtime/session_pin.js +51 -0
- package/dist/self_host/config.d.ts +2 -3
- package/dist/self_host/config.js +1 -2
- package/dist/self_host/main.d.ts +2 -1
- package/dist/self_host/main.js +20 -4
- package/dist/self_host/platform.d.ts +4 -2
- package/dist/self_host/platform.js +6 -2
- package/dist/self_host/resolve.d.ts +8 -1
- package/dist/self_host/resolve.js +31 -8
- package/dist/self_host/session_pin_store.d.ts +14 -0
- package/dist/self_host/session_pin_store.js +44 -0
- package/dist/strategies/priority_selector.d.ts +1 -2
- package/dist/strategies/priority_selector.js +1 -2
- package/dist/strategies/route_decision.d.ts +0 -1
- package/dist/strategies/route_decision.js +0 -3
- package/dist/strategies/routing.d.ts +0 -3
- package/dist/strategies/routing.js +1 -14
- package/dist/telemetry/billing_finalization.d.ts +1 -2
- package/dist/telemetry/billing_finalization.js +1 -1
- package/dist/telemetry/prefix_state.d.ts +0 -1
- package/dist/telemetry/prefix_state.js +1 -3
- package/dist/telemetry/report.d.ts +0 -2
- package/dist/telemetry/report.js +5 -41
- package/dist/types.d.ts +5 -3
- package/package.json +2 -2
- package/src/http/auth.ts +2 -2
- package/src/http/http2_server.ts +7 -3
- package/src/http/serve.ts +6 -5
- package/src/ingress/chat_completions.ts +2 -1
- package/src/ingress/codex.ts +4 -1
- package/src/ingress/codex_models.ts +18 -7
- package/src/platform/platform.ts +16 -2
- package/src/protocol/input/openai_responses.ts +2 -9
- package/src/protocol/output/anthropic_messages_output.ts +3 -2
- package/src/protocol/output/openai_responses_stream.ts +17 -1
- package/src/providers/execution.ts +5 -15
- package/src/providers/pi_output.ts +1 -1
- package/src/providers/provider_attempt_runner.ts +10 -21
- package/src/providers/provider_call.ts +3 -1
- package/src/providers/stream_finalization.ts +0 -2
- package/src/providers/subscription_limits.ts +1 -1
- package/src/runtime/claude_code/auxiliary_routing.ts +1 -8
- package/src/runtime/explicit_model.ts +1 -1
- package/src/runtime/harness_identity/conversation.ts +11 -0
- package/src/runtime/harness_identity/extract.ts +14 -5
- package/src/runtime/harness_identity/index.ts +2 -0
- package/src/runtime/prepare_routed_completion.ts +32 -2
- package/src/runtime/routed_runtime.ts +39 -59
- package/src/runtime/routing_lease.ts +2 -56
- package/src/runtime/runtime_reporter.ts +0 -3
- package/src/runtime/session_pin.ts +61 -0
- package/src/self_host/config.ts +3 -6
- package/src/self_host/main.ts +23 -4
- package/src/self_host/platform.ts +8 -2
- package/src/self_host/resolve.ts +37 -7
- package/src/self_host/session_pin_store.ts +58 -0
- package/src/strategies/priority_selector.ts +1 -2
- package/src/strategies/route_decision.ts +0 -5
- package/src/strategies/routing.ts +1 -17
- package/src/telemetry/billing_finalization.ts +4 -3
- package/src/telemetry/prefix_state.ts +1 -6
- package/src/telemetry/report.ts +4 -48
- package/src/types.ts +12 -6
- package/dist/strategies/fallback_decision.d.ts +0 -8
- package/dist/strategies/fallback_decision.js +0 -17
- package/src/strategies/fallback_decision.ts +0 -30
package/src/self_host/resolve.ts
CHANGED
|
@@ -58,7 +58,7 @@ export async function resolveSelfHostedRouter(
|
|
|
58
58
|
): Promise<ResolvedRouter> {
|
|
59
59
|
const keyIndex = gatewayKeyIndex(deps.gatewayKeys, request.apiKey);
|
|
60
60
|
if (keyIndex === null) {
|
|
61
|
-
throw new HttpError(401, "Missing or invalid
|
|
61
|
+
throw new HttpError(401, "Missing or invalid gateway key.", "authentication_error");
|
|
62
62
|
}
|
|
63
63
|
const router = deps.config.routers[request.routerId];
|
|
64
64
|
if (router === undefined) {
|
|
@@ -91,7 +91,10 @@ export async function resolveSelfHostedRouter(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const models = router.models.filter((model) => credentials.get(providerForModel(model.id)) != null);
|
|
94
|
-
if (models.length === 0)
|
|
94
|
+
if (models.length === 0) {
|
|
95
|
+
const missing = [...credentials].filter(([, credential]) => credential == null).map(([provider]) => provider);
|
|
96
|
+
throw unavailableRouterError(request.routerId, exhausted, missing);
|
|
97
|
+
}
|
|
95
98
|
|
|
96
99
|
const resolved: ResolvedRouter = {
|
|
97
100
|
router_id: request.routerId,
|
|
@@ -104,9 +107,8 @@ export async function resolveSelfHostedRouter(
|
|
|
104
107
|
subscription_credential_ids: {},
|
|
105
108
|
zdr: router.zdr,
|
|
106
109
|
speculative_routing: false,
|
|
110
|
+
session_pinning: router.sessionPinning,
|
|
107
111
|
primary_retries: router.retries,
|
|
108
|
-
model_fallback_enabled: router.fallback,
|
|
109
|
-
fallback_requires_different_provider: router.fallbackRequiresDifferentProvider,
|
|
110
112
|
lease_context_enabled: false,
|
|
111
113
|
routing_strategy: "dari",
|
|
112
114
|
custom_config: null,
|
|
@@ -152,7 +154,10 @@ export async function resolveSelfHostedRouter(
|
|
|
152
154
|
|
|
153
155
|
// Configured endpoints read their own key variable, or send a placeholder
|
|
154
156
|
// when they take none; known providers use their standard variable.
|
|
155
|
-
async function providerApiKey(
|
|
157
|
+
async function providerApiKey(
|
|
158
|
+
deps: Pick<SelfHostedResolverDeps, "config" | "providerApiKey" | "env">,
|
|
159
|
+
provider: string,
|
|
160
|
+
): Promise<string | undefined> {
|
|
156
161
|
const endpoint = deps.config.providers[provider];
|
|
157
162
|
if (endpoint === undefined) return (await deps.providerApiKey(provider))?.trim() || undefined;
|
|
158
163
|
if (endpoint.apiKeyEnv === undefined) return "no-key";
|
|
@@ -204,23 +209,48 @@ const SUBSCRIPTION_PLAN_NAMES: Record<SubscriptionProvider, string> = {
|
|
|
204
209
|
meta_muse: "Muse Code",
|
|
205
210
|
};
|
|
206
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Providers a router lists that no credential can serve: no API key and no
|
|
214
|
+
* connected subscription the router allows. Startup uses this, so it counts a
|
|
215
|
+
* held subscription as connected (the request-time 424 reports holds) and
|
|
216
|
+
* checks both harnesses, since startup does not know the client.
|
|
217
|
+
*/
|
|
218
|
+
export async function providersWithoutCredentials(
|
|
219
|
+
deps: Pick<SelfHostedResolverDeps, "subscriptions" | "providerApiKey" | "config" | "env">,
|
|
220
|
+
router: RouterConfig,
|
|
221
|
+
): Promise<string[]> {
|
|
222
|
+
const missing: string[] = [];
|
|
223
|
+
for (const provider of new Set(router.models.map((model) => providerForModel(model.id)))) {
|
|
224
|
+
const plans = router.subscriptions
|
|
225
|
+
? [subscriptionProviderFor(provider, "claude_code"), subscriptionProviderFor(provider, "muse_code")]
|
|
226
|
+
: [];
|
|
227
|
+
const connected = plans.some((plan) => plan !== null && deps.subscriptions.list(plan).length > 0);
|
|
228
|
+
if (connected) continue;
|
|
229
|
+
const apiKey = await providerApiKey(deps, provider);
|
|
230
|
+
if (apiKey === undefined) missing.push(provider);
|
|
231
|
+
}
|
|
232
|
+
return missing;
|
|
233
|
+
}
|
|
234
|
+
|
|
207
235
|
function unavailableRouterError(
|
|
208
236
|
routerId: string,
|
|
209
237
|
exhausted: ReadonlySet<SubscriptionProvider>,
|
|
238
|
+
missing: readonly string[],
|
|
210
239
|
): HttpError {
|
|
240
|
+
const missingNote = missing.length > 0 ? ` Missing providers: ${missing.join(", ")}.` : "";
|
|
211
241
|
if (exhausted.size > 0) {
|
|
212
242
|
const plans = [...exhausted]
|
|
213
243
|
.map((provider) => SUBSCRIPTION_PLAN_NAMES[provider])
|
|
214
244
|
.join(" and ");
|
|
215
245
|
return new HttpError(
|
|
216
246
|
424,
|
|
217
|
-
`Every connected ${plans} subscription is at its usage limit, and no API key can serve router ${routerId} instead
|
|
247
|
+
`Every connected ${plans} subscription is at its usage limit, and no API key can serve router ${routerId} instead.${missingNote}`,
|
|
218
248
|
"personal_subscription_unavailable",
|
|
219
249
|
);
|
|
220
250
|
}
|
|
221
251
|
return new HttpError(
|
|
222
252
|
424,
|
|
223
|
-
`No credentials can serve router ${routerId}
|
|
253
|
+
`No credentials can serve router ${routerId}.${missingNote} Set each provider's API key or connect a subscription that can serve it.`,
|
|
224
254
|
"provider_credentials_missing",
|
|
225
255
|
);
|
|
226
256
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite storage for session pins: the model and thinking level each session
|
|
3
|
+
* (and each subagent) of a session-pinning router was routed to on its first
|
|
4
|
+
* turn. Serving a pin refreshes it at most daily; pins unused for longer than
|
|
5
|
+
* the retention window are no longer served and are deleted.
|
|
6
|
+
*/
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
import type { SessionPin, SessionPinKey } from "../platform/platform.js";
|
|
10
|
+
|
|
11
|
+
const RETENTION_MS = 30 * 24 * 60 * 60_000;
|
|
12
|
+
const REFRESH_MS = 24 * 60 * 60_000;
|
|
13
|
+
|
|
14
|
+
type PinRow = { model: string; reasoning_effort: SessionPin["reasoningEffort"]; updated_at: number };
|
|
15
|
+
|
|
16
|
+
export class SessionPinStore {
|
|
17
|
+
constructor(private readonly db: Database) {
|
|
18
|
+
db.run(`CREATE TABLE IF NOT EXISTS routing_session_pins (
|
|
19
|
+
router_id TEXT NOT NULL,
|
|
20
|
+
pin_key TEXT NOT NULL,
|
|
21
|
+
model TEXT NOT NULL,
|
|
22
|
+
reasoning_effort TEXT NOT NULL,
|
|
23
|
+
updated_at INTEGER NOT NULL,
|
|
24
|
+
PRIMARY KEY (router_id, pin_key)
|
|
25
|
+
)`);
|
|
26
|
+
db.run("CREATE INDEX IF NOT EXISTS ix_routing_session_pins_updated_at ON routing_session_pins (updated_at)");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
read(key: SessionPinKey, now: Date = new Date()): SessionPin | null {
|
|
30
|
+
const row = this.db
|
|
31
|
+
.query<PinRow, [string, string, number]>(
|
|
32
|
+
`SELECT model, reasoning_effort, updated_at FROM routing_session_pins
|
|
33
|
+
WHERE router_id = ? AND pin_key = ? AND updated_at >= ?`,
|
|
34
|
+
)
|
|
35
|
+
.get(key.routerId, key.pinKey, now.getTime() - RETENTION_MS);
|
|
36
|
+
if (row === null) return null;
|
|
37
|
+
if (row.updated_at < now.getTime() - REFRESH_MS) {
|
|
38
|
+
this.db
|
|
39
|
+
.query("UPDATE routing_session_pins SET updated_at = ? WHERE router_id = ? AND pin_key = ?")
|
|
40
|
+
.run(now.getTime(), key.routerId, key.pinKey);
|
|
41
|
+
}
|
|
42
|
+
return { model: row.model, reasoningEffort: row.reasoning_effort };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
write(write: SessionPinKey & SessionPin, now: Date = new Date()): void {
|
|
46
|
+
this.db.transaction(() => {
|
|
47
|
+
this.db
|
|
48
|
+
.query(`INSERT INTO routing_session_pins (router_id, pin_key, model, reasoning_effort, updated_at)
|
|
49
|
+
VALUES (?, ?, ?, ?, ?)
|
|
50
|
+
ON CONFLICT (router_id, pin_key) DO UPDATE SET
|
|
51
|
+
model = excluded.model,
|
|
52
|
+
reasoning_effort = excluded.reasoning_effort,
|
|
53
|
+
updated_at = excluded.updated_at`)
|
|
54
|
+
.run(write.routerId, write.pinKey, write.model, write.reasoningEffort, now.getTime());
|
|
55
|
+
this.db.query("DELETE FROM routing_session_pins WHERE updated_at < ?").run(now.getTime() - RETENTION_MS);
|
|
56
|
+
})();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -7,8 +7,7 @@ const PRIORITY_CONTEXT_WINDOW_CHARS = 1_000_000;
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Serve the router's first eligible model/thinking-level pair, in the order
|
|
10
|
-
* the router lists them.
|
|
11
|
-
* when the router enables model fallback.
|
|
10
|
+
* the router lists them.
|
|
12
11
|
*/
|
|
13
12
|
export function createPrioritySelectorBackend(): SelectorBackend {
|
|
14
13
|
return {
|
|
@@ -15,7 +15,6 @@ import type { ResolvedRouter } from "../types.js";
|
|
|
15
15
|
|
|
16
16
|
export type RoutedDecision = {
|
|
17
17
|
decision: RoutingDecision;
|
|
18
|
-
fallbackDecision?: RoutingDecision;
|
|
19
18
|
run?: SelectorRun;
|
|
20
19
|
details?: Record<string, unknown>;
|
|
21
20
|
};
|
|
@@ -81,9 +80,6 @@ export async function routeDecision(
|
|
|
81
80
|
? { customRules: custom.rules, defaultTarget: custom.defaultTarget }
|
|
82
81
|
: {}),
|
|
83
82
|
selectorBackend: args.selectorBackend,
|
|
84
|
-
modelFallbackEnabled: credentials.model_fallback_enabled !== false,
|
|
85
|
-
fallbackRequiresDifferentProvider:
|
|
86
|
-
credentials.fallback_requires_different_provider !== false,
|
|
87
83
|
executionProviderForModel: (model) =>
|
|
88
84
|
executionProviderForModel(credentials, model),
|
|
89
85
|
imputeEvalScores: credentials.eval_score_imputation === true,
|
|
@@ -99,7 +95,6 @@ export async function routeDecision(
|
|
|
99
95
|
};
|
|
100
96
|
return {
|
|
101
97
|
decision: run.decision,
|
|
102
|
-
fallbackDecision: run.fallbackDecision,
|
|
103
98
|
run,
|
|
104
99
|
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
105
100
|
};
|
|
@@ -34,7 +34,6 @@ import type {
|
|
|
34
34
|
ProviderAuth,
|
|
35
35
|
ResolvedRouter,
|
|
36
36
|
} from "../types.js";
|
|
37
|
-
import { deriveFallbackDecision } from "./fallback_decision.js";
|
|
38
37
|
import type { SelectorImage } from "./selector_projection.js";
|
|
39
38
|
|
|
40
39
|
export function resolveRouter(args: ResolveRouterRequest): Promise<ResolvedRouter> {
|
|
@@ -54,7 +53,6 @@ export type SelectorTrace = {
|
|
|
54
53
|
|
|
55
54
|
export type SelectorRun = SelectorTrace & {
|
|
56
55
|
decision: RoutingDecision;
|
|
57
|
-
fallbackDecision?: RoutingDecision;
|
|
58
56
|
};
|
|
59
57
|
|
|
60
58
|
export type SelectModelArgs = {
|
|
@@ -70,8 +68,6 @@ export type SelectModelArgs = {
|
|
|
70
68
|
customRules?: CustomRouterRule[];
|
|
71
69
|
defaultTarget?: CustomDefaultTarget | null;
|
|
72
70
|
sessionId?: string;
|
|
73
|
-
modelFallbackEnabled?: boolean;
|
|
74
|
-
fallbackRequiresDifferentProvider?: boolean;
|
|
75
71
|
executionProviderForModel?: (model: string) => string;
|
|
76
72
|
imputeEvalScores?: boolean;
|
|
77
73
|
imputationReferenceEvals?: RouterEval[];
|
|
@@ -171,8 +167,6 @@ export async function selectModel(
|
|
|
171
167
|
contextWindowChars,
|
|
172
168
|
messages: selectorBackend.prepareMessages?.(safeMessages) ?? safeMessages,
|
|
173
169
|
...(leaseContextEnabled ? { leaseHistory: args.leaseHistory ?? [] } : {}),
|
|
174
|
-
modelFallbackEnabled: args.modelFallbackEnabled,
|
|
175
|
-
fallbackRequiresDifferentProvider: args.fallbackRequiresDifferentProvider,
|
|
176
170
|
imputeEvalScores: args.imputeEvalScores,
|
|
177
171
|
imputationReferenceEvals: args.imputationReferenceEvals,
|
|
178
172
|
});
|
|
@@ -285,17 +279,7 @@ function validateSelectorRun(run: SelectorRun, args: SelectModelArgs): SelectorR
|
|
|
285
279
|
run,
|
|
286
280
|
);
|
|
287
281
|
}
|
|
288
|
-
|
|
289
|
-
const fallbackDecision = deriveFallbackDecision({
|
|
290
|
-
candidates: args.candidates,
|
|
291
|
-
selected,
|
|
292
|
-
enabled: args.modelFallbackEnabled,
|
|
293
|
-
requiresDifferentProvider: args.fallbackRequiresDifferentProvider,
|
|
294
|
-
providerForModel: args.executionProviderForModel,
|
|
295
|
-
});
|
|
296
|
-
return fallbackDecision
|
|
297
|
-
? { ...primaryRun, fallbackDecision }
|
|
298
|
-
: primaryRun;
|
|
282
|
+
return run;
|
|
299
283
|
}
|
|
300
284
|
|
|
301
285
|
function requiredSelectorConfig(value: string, label: string): string {
|
|
@@ -14,7 +14,9 @@ export type RoutingBillingProviderAttemptFacts = {
|
|
|
14
14
|
provider: string;
|
|
15
15
|
model: string;
|
|
16
16
|
provider_key_source: ProviderKeySource;
|
|
17
|
-
|
|
17
|
+
// The control-plane billing contract numbers attempts within a role; every
|
|
18
|
+
// attempt retries the routed choice, so the role is always primary.
|
|
19
|
+
role: "primary";
|
|
18
20
|
attempt: number;
|
|
19
21
|
outcome: "completed" | "provider_failed" | "customer_canceled";
|
|
20
22
|
provider_request_id: string | null;
|
|
@@ -94,7 +96,6 @@ export function routingBillingProviderAttemptFacts(args: {
|
|
|
94
96
|
provider: string;
|
|
95
97
|
model: string;
|
|
96
98
|
providerKeySource: ProviderKeySource;
|
|
97
|
-
role: "primary" | "fallback";
|
|
98
99
|
attempt: number;
|
|
99
100
|
outcome: "completed" | "provider_failed" | "customer_canceled";
|
|
100
101
|
providerMessage: Record<string, unknown> | null;
|
|
@@ -129,7 +130,7 @@ export function routingBillingProviderAttemptFacts(args: {
|
|
|
129
130
|
provider: args.provider,
|
|
130
131
|
model: args.model,
|
|
131
132
|
provider_key_source: args.providerKeySource,
|
|
132
|
-
role:
|
|
133
|
+
role: "primary",
|
|
133
134
|
attempt: args.attempt,
|
|
134
135
|
outcome: args.outcome,
|
|
135
136
|
provider_request_id: providerRequestId(args.providerMessage),
|
|
@@ -80,15 +80,10 @@ export function buildCompletedPrefixHit(args: {
|
|
|
80
80
|
export function leaseCountdownForPrefix(args: {
|
|
81
81
|
served: RoutingDecision | null;
|
|
82
82
|
next: RoutingDecision | null;
|
|
83
|
-
// True when a completed fallback attempt served this turn. A lease-aware
|
|
84
|
-
// fallback carries the remaining countdown on the serving decision; an
|
|
85
|
-
// ordinary fallback has no countdown and ends any primary commitment.
|
|
86
|
-
fallbackServed?: boolean;
|
|
87
83
|
}): number | null {
|
|
88
|
-
const granted = args.
|
|
84
|
+
const granted = args.next ?? args.served;
|
|
89
85
|
const remaining = granted?.leaseTurnsRemaining;
|
|
90
86
|
if (remaining === undefined || remaining === null) return null;
|
|
91
|
-
if (args.fallbackServed === true) return remaining;
|
|
92
87
|
const servedGrantedPair =
|
|
93
88
|
args.next === null ||
|
|
94
89
|
(args.served !== null &&
|
package/src/telemetry/report.ts
CHANGED
|
@@ -62,7 +62,6 @@ import {
|
|
|
62
62
|
buildCompletedPrefixHit,
|
|
63
63
|
leaseCountdownForPrefix,
|
|
64
64
|
prefixEntryForIngest,
|
|
65
|
-
reportPendingLease,
|
|
66
65
|
reportPrefixState,
|
|
67
66
|
} from "./prefix_state.js";
|
|
68
67
|
import { subscriptionUsageLimitFromFailure } from "../providers/subscription_limits.js";
|
|
@@ -149,7 +148,6 @@ export class RequestReporter {
|
|
|
149
148
|
private selectorOutputAccepted = false;
|
|
150
149
|
private decision: RoutingDecision | null = null;
|
|
151
150
|
private nextDecision: RoutingDecision | null = null;
|
|
152
|
-
private fallbackDecision: RoutingDecision | null = null;
|
|
153
151
|
private providerAttempts: ProviderAttemptReport[] = [];
|
|
154
152
|
private billingProviderAttempts: RoutingBillingProviderAttemptFacts[] = [];
|
|
155
153
|
private decisionDetails: Record<string, unknown> = {};
|
|
@@ -202,7 +200,6 @@ export class RequestReporter {
|
|
|
202
200
|
this.selectorOutputAccepted = true;
|
|
203
201
|
this.decision = run.decision;
|
|
204
202
|
this.nextDecision = run.decision;
|
|
205
|
-
this.fallbackDecision = run.fallbackDecision ?? null;
|
|
206
203
|
}
|
|
207
204
|
|
|
208
205
|
attachSpeculativeSelectorRun(run: SelectorRun, servingDecision: RoutingDecision): void {
|
|
@@ -211,7 +208,6 @@ export class RequestReporter {
|
|
|
211
208
|
this.selectorOutputAccepted = true;
|
|
212
209
|
this.decision = servingDecision;
|
|
213
210
|
this.nextDecision = run.decision;
|
|
214
|
-
this.fallbackDecision = run.fallbackDecision ?? null;
|
|
215
211
|
}
|
|
216
212
|
|
|
217
213
|
// Speculative execution can begin before a selector run; its serving
|
|
@@ -221,10 +217,6 @@ export class RequestReporter {
|
|
|
221
217
|
this.nextDecision = decision;
|
|
222
218
|
}
|
|
223
219
|
|
|
224
|
-
attachFallbackDecision(decision: RoutingDecision | undefined): void {
|
|
225
|
-
this.fallbackDecision = decision ?? null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
220
|
attachServingDecision(decision: RoutingDecision): void {
|
|
229
221
|
this.decision = decision;
|
|
230
222
|
}
|
|
@@ -239,10 +231,10 @@ export class RequestReporter {
|
|
|
239
231
|
return modelCost(attempt ? [...this.billingProviderAttempts, this.billingFacts(attempt)] : this.billingProviderAttempts);
|
|
240
232
|
}
|
|
241
233
|
|
|
242
|
-
private billingFacts({ execution, message,
|
|
234
|
+
private billingFacts({ execution, message, attempt, billingOutcome }: ProviderAttemptRecord): RoutingBillingProviderAttemptFacts {
|
|
243
235
|
return routingBillingProviderAttemptFacts({
|
|
244
236
|
provider: execution.provider, model: execution.model,
|
|
245
|
-
providerKeySource: execution.providerKeySource,
|
|
237
|
+
providerKeySource: execution.providerKeySource, attempt, outcome: billingOutcome,
|
|
246
238
|
providerMessage: message, usage: isRecord(message.usage) ? message.usage : null,
|
|
247
239
|
providerPricingKnown: this.pricingKnown(execution.model),
|
|
248
240
|
});
|
|
@@ -388,18 +380,8 @@ export class RequestReporter {
|
|
|
388
380
|
const decision = this.decision;
|
|
389
381
|
const selectedModel = outcome.providerExecution?.model ?? decision?.selectedModel ?? null;
|
|
390
382
|
const reasoningBucket = decision?.reasoningEffort ?? null;
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// pair that actually served instead of re-pinning the failed one.
|
|
394
|
-
const fallbackServed = this.providerAttempts.some(
|
|
395
|
-
(attempt) => attempt.role === "fallback" && attempt.outcome === "completed",
|
|
396
|
-
);
|
|
397
|
-
const nextModel = fallbackServed
|
|
398
|
-
? selectedModel
|
|
399
|
-
: this.nextDecision?.selectedModel ?? selectedModel;
|
|
400
|
-
const nextReasoningEffort = fallbackServed
|
|
401
|
-
? reasoningBucket
|
|
402
|
-
: this.nextDecision?.reasoningEffort ?? reasoningBucket;
|
|
383
|
+
const nextModel = this.nextDecision?.selectedModel ?? selectedModel;
|
|
384
|
+
const nextReasoningEffort = this.nextDecision?.reasoningEffort ?? reasoningBucket;
|
|
403
385
|
const selectedProvider = outcome.providerExecution?.provider ?? (selectedModel
|
|
404
386
|
? executionProviderForModel(a.credentials, selectedModel)
|
|
405
387
|
: null);
|
|
@@ -470,7 +452,6 @@ export class RequestReporter {
|
|
|
470
452
|
leaseTurnsRemaining: leaseCountdownForPrefix({
|
|
471
453
|
served: decision ?? null,
|
|
472
454
|
next: this.nextDecision,
|
|
473
|
-
fallbackServed,
|
|
474
455
|
}),
|
|
475
456
|
reason: this.nextDecision?.reason ?? decision?.reason ?? null,
|
|
476
457
|
chain: selectedChain,
|
|
@@ -506,21 +487,6 @@ export class RequestReporter {
|
|
|
506
487
|
// resolves this turn's conversation id and next-model prediction.
|
|
507
488
|
// The full report below re-applies the same state idempotently, so a
|
|
508
489
|
// lost or timed-out fast write only degrades to the old ordering.
|
|
509
|
-
// An ordinary fallback breaks any lease commitment, prefetched ones
|
|
510
|
-
// included. A lease-aware fallback transfers the active countdown to
|
|
511
|
-
// the serving pair, so only clear pending state when no countdown was
|
|
512
|
-
// transferred. The depth fence orders the clear safely against
|
|
513
|
-
// in-flight prefetch writes.
|
|
514
|
-
if (!backgroundRequest && fallbackServed && decision?.leaseTurnsRemaining === undefined) {
|
|
515
|
-
trackBackgroundTelemetry(
|
|
516
|
-
reportPendingLease({
|
|
517
|
-
routerId: a.routerId,
|
|
518
|
-
conversationId: a.conversationId,
|
|
519
|
-
decidedAtDepth: a.leaseDepth,
|
|
520
|
-
lease: null,
|
|
521
|
-
}),
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
490
|
if (prefixEntry !== null || touchedHash !== null) {
|
|
525
491
|
this.prefixStatePromise = reportPrefixState({
|
|
526
492
|
organization_id: a.credentials.organization_id,
|
|
@@ -592,13 +558,6 @@ export class RequestReporter {
|
|
|
592
558
|
strategy: a.strategy,
|
|
593
559
|
selected_reasoning_effort: reasoningBucket,
|
|
594
560
|
next_reasoning_effort: nextReasoningEffort,
|
|
595
|
-
...(this.fallbackDecision
|
|
596
|
-
? {
|
|
597
|
-
fallback_model: this.fallbackDecision.selectedModel,
|
|
598
|
-
fallback_reasoning_effort:
|
|
599
|
-
this.fallbackDecision.reasoningEffort,
|
|
600
|
-
}
|
|
601
|
-
: {}),
|
|
602
561
|
...(this.providerAttempts.length > 0
|
|
603
562
|
? { provider_attempts: this.providerAttempts }
|
|
604
563
|
: {}),
|
|
@@ -662,9 +621,6 @@ export class RequestReporter {
|
|
|
662
621
|
selected_reasoning_effort: reasoningBucket,
|
|
663
622
|
next_model: nextModel,
|
|
664
623
|
next_reasoning_effort: nextReasoningEffort,
|
|
665
|
-
fallback_model: this.fallbackDecision?.selectedModel ?? null,
|
|
666
|
-
fallback_reasoning_effort:
|
|
667
|
-
this.fallbackDecision?.reasoningEffort ?? null,
|
|
668
624
|
provider_key_source: keySource,
|
|
669
625
|
message_count: observability.messageCount,
|
|
670
626
|
tool_count: observability.metadata.declared_tool_counts.total,
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
RouterEvalScore,
|
|
7
7
|
RouterModelPrice,
|
|
8
8
|
RouterPrefixHit,
|
|
9
|
+
RoutingDecision,
|
|
9
10
|
RoutingStrategy,
|
|
10
11
|
} from "@mupt-ai/dari-router/policy-engine";
|
|
11
12
|
// The router resolution contract between the gateway runtime and its
|
|
@@ -83,6 +84,10 @@ export type PublicEvalCatalogCard = Omit<RouterEval, "scores"> & {
|
|
|
83
84
|
scores: PublicEvalCatalogScore[];
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
// A decision as served: session-pinned decisions say so in response headers
|
|
88
|
+
// and Anthropic routing metadata.
|
|
89
|
+
export type ServedDecision = RoutingDecision & { sessionPinned?: boolean };
|
|
90
|
+
|
|
86
91
|
export type ClientHarness = "claude_code" | "muse_code";
|
|
87
92
|
|
|
88
93
|
export type ResolvedRouterModelConfig = {
|
|
@@ -122,17 +127,18 @@ export type ResolvedRouter = {
|
|
|
122
127
|
zdr?: boolean;
|
|
123
128
|
allow_long_context?: boolean;
|
|
124
129
|
speculative_routing?: boolean;
|
|
130
|
+
// Route each session's (and each subagent's) first turn, then serve that
|
|
131
|
+
// pair for the rest of the session instead of selecting or leasing.
|
|
132
|
+
session_pinning?: boolean;
|
|
125
133
|
primary_retries?: number;
|
|
126
134
|
// Serve only the requested `dari/model/<id>` at the requested thinking
|
|
127
|
-
// level: no selector, lease,
|
|
128
|
-
//
|
|
129
|
-
//
|
|
135
|
+
// level: no selector, lease, or personal subscription the platform did not
|
|
136
|
+
// supply through resolveProviderCredential, and an error for anything
|
|
137
|
+
// else, whatever the client sends.
|
|
130
138
|
strict?: boolean;
|
|
131
139
|
// On strict routers only, allow requested catalog models of these providers.
|
|
132
|
-
// Requires dari/model/<provider>/<id
|
|
140
|
+
// Requires dari/model/<provider>/<id>.
|
|
133
141
|
strict_providers?: string[];
|
|
134
|
-
model_fallback_enabled?: boolean;
|
|
135
|
-
fallback_requires_different_provider?: boolean;
|
|
136
142
|
evals?: RouterEval[];
|
|
137
143
|
// When true, fill missing eval scores using thinking-level ratios or an
|
|
138
144
|
// explicitly mapped predecessor model.
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { type RoutingCandidate, type RoutingDecision } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
-
export declare function deriveFallbackDecision(args: {
|
|
3
|
-
candidates: RoutingCandidate[];
|
|
4
|
-
selected: RoutingCandidate;
|
|
5
|
-
enabled?: boolean;
|
|
6
|
-
requiresDifferentProvider?: boolean;
|
|
7
|
-
providerForModel?: (model: string) => string;
|
|
8
|
-
}): RoutingDecision | undefined;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { providerForModel, } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
-
export function deriveFallbackDecision(args) {
|
|
3
|
-
if (!args.enabled)
|
|
4
|
-
return undefined;
|
|
5
|
-
const resolveProvider = args.providerForModel ?? providerForModel;
|
|
6
|
-
const selectedProvider = resolveProvider(args.selected.model);
|
|
7
|
-
const fallback = args.candidates.find((candidate) => candidate.model !== args.selected.model &&
|
|
8
|
-
(!args.requiresDifferentProvider ||
|
|
9
|
-
resolveProvider(candidate.model) !== selectedProvider));
|
|
10
|
-
return fallback
|
|
11
|
-
? {
|
|
12
|
-
selectedModel: fallback.model,
|
|
13
|
-
reasoningEffort: fallback.reasoningEffort,
|
|
14
|
-
reason: "Runtime-selected fallback for the routed primary decision.",
|
|
15
|
-
}
|
|
16
|
-
: undefined;
|
|
17
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
providerForModel,
|
|
3
|
-
type RoutingCandidate,
|
|
4
|
-
type RoutingDecision,
|
|
5
|
-
} from "@mupt-ai/dari-router/policy-engine";
|
|
6
|
-
|
|
7
|
-
export function deriveFallbackDecision(args: {
|
|
8
|
-
candidates: RoutingCandidate[];
|
|
9
|
-
selected: RoutingCandidate;
|
|
10
|
-
enabled?: boolean;
|
|
11
|
-
requiresDifferentProvider?: boolean;
|
|
12
|
-
providerForModel?: (model: string) => string;
|
|
13
|
-
}): RoutingDecision | undefined {
|
|
14
|
-
if (!args.enabled) return undefined;
|
|
15
|
-
const resolveProvider = args.providerForModel ?? providerForModel;
|
|
16
|
-
const selectedProvider = resolveProvider(args.selected.model);
|
|
17
|
-
const fallback = args.candidates.find(
|
|
18
|
-
(candidate) =>
|
|
19
|
-
candidate.model !== args.selected.model &&
|
|
20
|
-
(!args.requiresDifferentProvider ||
|
|
21
|
-
resolveProvider(candidate.model) !== selectedProvider),
|
|
22
|
-
);
|
|
23
|
-
return fallback
|
|
24
|
-
? {
|
|
25
|
-
selectedModel: fallback.model,
|
|
26
|
-
reasoningEffort: fallback.reasoningEffort,
|
|
27
|
-
reason: "Runtime-selected fallback for the routed primary decision.",
|
|
28
|
-
}
|
|
29
|
-
: undefined;
|
|
30
|
-
}
|