@mupt-ai/dari-gateway 0.3.1 → 0.3.3
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 +18 -11
- package/dari-gateway.example.yaml +3 -2
- package/dist/context/interaction_fingerprint.js +10 -1
- 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 +7 -3
- 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 +9 -0
- package/dist/runtime/harness_identity/conversation.js +6 -0
- package/dist/runtime/harness_identity/extract.d.ts +2 -0
- package/dist/runtime/harness_identity/extract.js +20 -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 +3 -3
- package/dist/self_host/main.d.ts +2 -1
- package/dist/self_host/main.js +26 -6
- 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/context/interaction_fingerprint.ts +15 -1
- 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 +8 -5
- 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 +16 -0
- package/src/runtime/harness_identity/extract.ts +24 -5
- package/src/runtime/harness_identity/index.ts +3 -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 +7 -7
- package/src/self_host/main.ts +29 -6
- 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
|
@@ -58,11 +58,8 @@ import {
|
|
|
58
58
|
export type ProviderChoice = {
|
|
59
59
|
decision: RoutingDecision;
|
|
60
60
|
execution: ProviderExecution;
|
|
61
|
-
role: "primary" | "fallback";
|
|
62
61
|
};
|
|
63
62
|
|
|
64
|
-
export type ResolveFallbackChoice = () => Promise<ProviderChoice | null>;
|
|
65
|
-
|
|
66
63
|
export type PreparedStreamExecution = {
|
|
67
64
|
choice: ProviderChoice;
|
|
68
65
|
started: StartedStream;
|
|
@@ -86,7 +83,6 @@ export async function executeNonStreaming(args: {
|
|
|
86
83
|
interaction: InteractionRequest;
|
|
87
84
|
primary: ProviderChoice;
|
|
88
85
|
primaryRetries: number;
|
|
89
|
-
resolveFallback: ResolveFallbackChoice;
|
|
90
86
|
reporter: RoutedRuntimeReporter;
|
|
91
87
|
protocol: RoutedProtocol;
|
|
92
88
|
startedCompletion?: StartedCompletion;
|
|
@@ -111,7 +107,6 @@ export async function completeNonStreaming(args: {
|
|
|
111
107
|
interaction: InteractionRequest;
|
|
112
108
|
primary: ProviderChoice;
|
|
113
109
|
primaryRetries: number;
|
|
114
|
-
resolveFallback: ResolveFallbackChoice;
|
|
115
110
|
reporter: RoutedRuntimeReporter;
|
|
116
111
|
protocol: RoutedProtocol;
|
|
117
112
|
startedCompletion?: StartedCompletion;
|
|
@@ -157,7 +152,7 @@ export async function completeNonStreaming(args: {
|
|
|
157
152
|
output,
|
|
158
153
|
modelCost: args.reporter.modelCost({
|
|
159
154
|
model: choice.decision.selectedModel, reasoning_effort: choice.decision.reasoningEffort,
|
|
160
|
-
|
|
155
|
+
attempt: result.attempt, outcome: "completed", billingOutcome: "completed",
|
|
161
156
|
execution: reportedExecution(choice.execution), message: completion.message,
|
|
162
157
|
}),
|
|
163
158
|
request: args.interaction,
|
|
@@ -231,7 +226,6 @@ async function completeWithRetries(args: {
|
|
|
231
226
|
interaction: InteractionRequest;
|
|
232
227
|
primary: ProviderChoice;
|
|
233
228
|
primaryRetries: number;
|
|
234
|
-
resolveFallback: ResolveFallbackChoice;
|
|
235
229
|
reporter: RoutedRuntimeReporter;
|
|
236
230
|
startedCompletion?: StartedCompletion;
|
|
237
231
|
clientSignal?: AbortSignal;
|
|
@@ -239,7 +233,6 @@ async function completeWithRetries(args: {
|
|
|
239
233
|
return runProviderAttemptPlan({
|
|
240
234
|
primary: args.primary,
|
|
241
235
|
retries: args.primaryRetries,
|
|
242
|
-
resolveFallback: args.resolveFallback,
|
|
243
236
|
primaryInitial: args.startedCompletion,
|
|
244
237
|
isCustomerCanceled: () => customerAbortRequested(args.clientSignal),
|
|
245
238
|
canceledFailure: customerCanceledProviderMessage,
|
|
@@ -459,7 +452,6 @@ export async function prepareStreamingExecution(args: {
|
|
|
459
452
|
interaction: InteractionRequest;
|
|
460
453
|
primary: ProviderChoice;
|
|
461
454
|
primaryRetries: number;
|
|
462
|
-
resolveFallback: ResolveFallbackChoice;
|
|
463
455
|
reporter: RoutedRuntimeReporter;
|
|
464
456
|
startedStream?: StartedStream;
|
|
465
457
|
initialError?: unknown;
|
|
@@ -475,7 +467,6 @@ export async function prepareStreamingExecution(args: {
|
|
|
475
467
|
const result = await runProviderAttemptPlan({
|
|
476
468
|
primary: args.primary,
|
|
477
469
|
retries: args.primaryRetries,
|
|
478
|
-
resolveFallback: args.resolveFallback,
|
|
479
470
|
primaryInitial,
|
|
480
471
|
isCustomerCanceled: () => customerAbortRequested(args.clientSignal),
|
|
481
472
|
canceledFailure: customerCanceledProviderMessage,
|
|
@@ -537,7 +528,7 @@ async function runStreamSetupAttempt(args: {
|
|
|
537
528
|
clientSignal: args.clientSignal,
|
|
538
529
|
});
|
|
539
530
|
// Prime before committing so connection failures and prompt rejections
|
|
540
|
-
// stay inside the one shared retry
|
|
531
|
+
// stay inside the one shared retry state machine.
|
|
541
532
|
const primed = await primeProviderStream(
|
|
542
533
|
started.providerStream.events,
|
|
543
534
|
started.customerAbort,
|
|
@@ -611,7 +602,6 @@ export async function executeStreaming(args: {
|
|
|
611
602
|
protocol: args.protocol,
|
|
612
603
|
providerLatencyMs: () =>
|
|
613
604
|
args.failedSetupLatencyMs + Date.now() - started.providerStartedMs,
|
|
614
|
-
role: args.choice.role,
|
|
615
605
|
attempt: args.attempt,
|
|
616
606
|
reportBarrier: args.reportBarrier,
|
|
617
607
|
delivered: observed.delivered,
|
|
@@ -630,7 +620,7 @@ export async function executeStreaming(args: {
|
|
|
630
620
|
clientSignal: args.clientSignal,
|
|
631
621
|
modelCost: (message, failed) => args.reporter.modelCost({
|
|
632
622
|
model: args.choice.decision.selectedModel, reasoning_effort: args.choice.decision.reasoningEffort,
|
|
633
|
-
|
|
623
|
+
attempt: args.attempt,
|
|
634
624
|
outcome: failed ? "terminal_error" : "completed",
|
|
635
625
|
billingOutcome: failed ? "provider_failed" : "completed",
|
|
636
626
|
execution: reportedExecution(execution), message,
|
|
@@ -673,7 +663,6 @@ function recordProviderAttempt(
|
|
|
673
663
|
reporter.recordProviderAttempt({
|
|
674
664
|
model: choice.decision.selectedModel,
|
|
675
665
|
reasoning_effort: choice.decision.reasoningEffort,
|
|
676
|
-
role: choice.role,
|
|
677
666
|
attempt,
|
|
678
667
|
outcome,
|
|
679
668
|
execution: reportedExecution(choice.execution),
|
|
@@ -720,7 +709,8 @@ async function providerFailedAttempt(
|
|
|
720
709
|
}
|
|
721
710
|
// Retrying an exhausted plan cannot succeed. The platform learns about it
|
|
722
711
|
// before this request returns so the next resolve already serves the
|
|
723
|
-
// user's next subscription, or the
|
|
712
|
+
// user's next subscription, or the model's API credential when the router
|
|
713
|
+
// allows subscription API fallback for it.
|
|
724
714
|
const switched = execution.subscriptionUserId !== undefined
|
|
725
715
|
&& await holdSubscription({
|
|
726
716
|
userId: execution.subscriptionUserId,
|
|
@@ -411,7 +411,7 @@ function reasoningText(value: unknown, type: "summary_text" | "reasoning_text"):
|
|
|
411
411
|
);
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
function textSignature(value: unknown): { id?: string; phase?: "commentary" | "final_answer" } | null {
|
|
414
|
+
export function textSignature(value: unknown): { id?: string; phase?: "commentary" | "final_answer" } | null {
|
|
415
415
|
if (typeof value !== "string" || !value) return null;
|
|
416
416
|
try {
|
|
417
417
|
const parsed: unknown = JSON.parse(value);
|
|
@@ -31,7 +31,6 @@ export type ProviderAttemptPlanResult<TChoice, TValue> = {
|
|
|
31
31
|
type RunProviderAttemptPlanArgs<TChoice, TValue, TInitial> = {
|
|
32
32
|
primary: TChoice;
|
|
33
33
|
retries: number;
|
|
34
|
-
resolveFallback: () => Promise<TChoice | null>;
|
|
35
34
|
primaryInitial?: TInitial;
|
|
36
35
|
isCustomerCanceled: (choice: TChoice) => boolean;
|
|
37
36
|
canceledFailure: () => Record<string, unknown>;
|
|
@@ -51,8 +50,8 @@ type ChoiceResult<TChoice, TValue> = ProviderAttemptPlanResult<TChoice, TValue>;
|
|
|
51
50
|
|
|
52
51
|
/**
|
|
53
52
|
* Owns the order-sensitive provider plan shared by completion and stream setup:
|
|
54
|
-
* retries within
|
|
55
|
-
*
|
|
53
|
+
* retries within the choice, cancellation fences between attempts, and one
|
|
54
|
+
* contiguous attempt record for every failed dispatch.
|
|
56
55
|
*/
|
|
57
56
|
export async function runProviderAttemptPlan<TChoice, TValue, TInitial>(
|
|
58
57
|
args: RunProviderAttemptPlanArgs<TChoice, TValue, TInitial>,
|
|
@@ -62,29 +61,19 @@ export async function runProviderAttemptPlan<TChoice, TValue, TInitial>(
|
|
|
62
61
|
}
|
|
63
62
|
const primary = await runChoice(args, args.primary, args.primaryInitial);
|
|
64
63
|
if (primary.kind !== "provider_failed") return primary;
|
|
65
|
-
// The user's next subscription serves
|
|
66
|
-
//
|
|
64
|
+
// The user's next subscription serves the client's retry of this choice, so
|
|
65
|
+
// the failure stands even if the client has already gone.
|
|
67
66
|
if (subscriptionUsageLimitFromFailure(primary.failure)?.switched === true) return primary;
|
|
68
|
-
// A
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
syntheticCancellation(args, {
|
|
67
|
+
// A client that disconnected while the plan settled is a cancellation,
|
|
68
|
+
// attributed to the only choice that was dispatched.
|
|
69
|
+
if (args.isCustomerCanceled(args.primary)) {
|
|
70
|
+
return syntheticCancellation(args, {
|
|
73
71
|
choice: args.primary,
|
|
74
72
|
attempt: primary.attempt + 1,
|
|
75
73
|
latencyMs: primary.latencyMs,
|
|
76
74
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const fallback = await args.resolveFallback();
|
|
80
|
-
if (args.isCustomerCanceled(args.primary)) return canceledAfterPrimary();
|
|
81
|
-
if (fallback === null) return primary;
|
|
82
|
-
|
|
83
|
-
const fallbackResult = await runChoice(args, fallback, undefined);
|
|
84
|
-
return {
|
|
85
|
-
...fallbackResult,
|
|
86
|
-
latencyMs: primary.latencyMs + fallbackResult.latencyMs,
|
|
87
|
-
};
|
|
75
|
+
}
|
|
76
|
+
return primary;
|
|
88
77
|
}
|
|
89
78
|
|
|
90
79
|
async function runChoice<TChoice, TValue, TInitial>(
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
ProviderKeySource,
|
|
34
34
|
ResolvedRouter,
|
|
35
35
|
ResolvedRouterModelConfig,
|
|
36
|
+
ServedDecision,
|
|
36
37
|
} from "../types.js";
|
|
37
38
|
import { awsRoleCredentials } from "./aws_assume_role.js";
|
|
38
39
|
import {
|
|
@@ -97,7 +98,7 @@ export type StartedStream = {
|
|
|
97
98
|
};
|
|
98
99
|
|
|
99
100
|
export function prepareProviderExecution(args: {
|
|
100
|
-
decision:
|
|
101
|
+
decision: ServedDecision;
|
|
101
102
|
credentials: ResolvedRouter;
|
|
102
103
|
billable: boolean;
|
|
103
104
|
routerId: string;
|
|
@@ -174,6 +175,7 @@ export function prepareProviderExecution(args: {
|
|
|
174
175
|
...(args.decision.leaseTurnsRemaining === undefined
|
|
175
176
|
? {}
|
|
176
177
|
: { "X-Dari-Lease-Turns-Remaining": String(args.decision.leaseTurnsRemaining) }),
|
|
178
|
+
...(args.decision.sessionPinned === true ? { "X-Dari-Session-Pinned": "true" } : {}),
|
|
177
179
|
},
|
|
178
180
|
};
|
|
179
181
|
}
|
|
@@ -50,7 +50,6 @@ type CreateStreamFinalizerArgs = {
|
|
|
50
50
|
reporter: RoutedRuntimeReporter;
|
|
51
51
|
protocol: RoutedProtocol;
|
|
52
52
|
providerLatencyMs: () => number;
|
|
53
|
-
role: "primary" | "fallback";
|
|
54
53
|
attempt: number;
|
|
55
54
|
reportBarrier?: Promise<unknown>;
|
|
56
55
|
delivered: () => DeliveredOutput;
|
|
@@ -171,7 +170,6 @@ function buildStreamTerminalReport(
|
|
|
171
170
|
const attemptBase = {
|
|
172
171
|
model: args.decision.selectedModel,
|
|
173
172
|
reasoning_effort: args.decision.reasoningEffort,
|
|
174
|
-
role: args.role,
|
|
175
173
|
attempt: args.attempt,
|
|
176
174
|
execution,
|
|
177
175
|
} as const;
|
|
@@ -9,7 +9,6 @@ import { isBackgroundRequest } from "../../interaction/types.js";
|
|
|
9
9
|
import { HttpError } from "../../http/errors.js";
|
|
10
10
|
import { assertCreditAvailableBeforeRouting } from "../../telemetry/usage.js";
|
|
11
11
|
import type { PreparedRoutedCompletion } from "../prepare_routed_completion.js";
|
|
12
|
-
import { leaseFallbackDecision } from "../routing_lease.js";
|
|
13
12
|
|
|
14
13
|
// Explicit background classes bypass the selector and every lease path.
|
|
15
14
|
// Keep the existing deterministic cheapest-input-cost serving policy.
|
|
@@ -30,13 +29,7 @@ export function auxiliaryDecision(
|
|
|
30
29
|
reasoningEffort: cheapest.reasoning_effort,
|
|
31
30
|
reason: `${requestClass} request served by the cheapest routable model without selection.`,
|
|
32
31
|
};
|
|
33
|
-
|
|
34
|
-
candidates: prepared.routing.candidateResolution.candidates,
|
|
35
|
-
decision,
|
|
36
|
-
credentials: prepared.credentials,
|
|
37
|
-
});
|
|
38
|
-
if (fallbackDecision) prepared.reporter.attachFallbackDecision(fallbackDecision);
|
|
39
|
-
return fallbackDecision ? { decision, fallbackDecision } : { decision };
|
|
32
|
+
return { decision };
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
/* Ranked on estimated input cost: these requests carry a whole transcript in
|
|
@@ -6,7 +6,7 @@ import type { RoutedDecision } from "../strategies/route_decision.js";
|
|
|
6
6
|
import type { PreparedRoutedCompletion } from "./prepare_routed_completion.js";
|
|
7
7
|
|
|
8
8
|
/* A request for `dari/model/<id>` is served by exactly that enabled model,
|
|
9
|
-
with no selector
|
|
9
|
+
with no selector or lease: the user chose it, like picking a
|
|
10
10
|
model in their agent's /model menu. The client's thinking level is kept
|
|
11
11
|
when the model supports it; otherwise the model's default level is used. */
|
|
12
12
|
export function explicitModelDecision(
|
|
@@ -19,6 +19,11 @@ export type HarnessConversationIdentity =
|
|
|
19
19
|
kind: "openai_chat_session";
|
|
20
20
|
value: string;
|
|
21
21
|
qualifiers?: string[];
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
kind: "muse_code_thread";
|
|
25
|
+
value: string;
|
|
26
|
+
qualifiers?: string[];
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
export type HarnessConversationScope = {
|
|
@@ -46,6 +51,17 @@ export function scopedHarnessSessionId(args: {
|
|
|
46
51
|
return scopedHarnessId({ ...args, identity });
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
// One agent of a session: the main agent on the bare session, each subagent
|
|
55
|
+
// on its own qualifier or thread. Unlike the conversation id, it survives
|
|
56
|
+
// compaction.
|
|
57
|
+
export function scopedHarnessAgentId(args: {
|
|
58
|
+
identity: HarnessConversationIdentity;
|
|
59
|
+
sourceProtocol: SourceProtocol;
|
|
60
|
+
scope: HarnessConversationScope;
|
|
61
|
+
}): string {
|
|
62
|
+
return scopedHarnessId(args);
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export function scopedHarnessConversationId(args: {
|
|
50
66
|
identity: HarnessConversationIdentity;
|
|
51
67
|
sourceProtocol: SourceProtocol;
|
|
@@ -14,11 +14,10 @@ export function chatCompletionsConversationIdentity(
|
|
|
14
14
|
headers: Headers,
|
|
15
15
|
promptCacheKey?: string,
|
|
16
16
|
): HarnessConversationIdentity | undefined {
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
const sessionId = boundedIdentity(
|
|
17
|
+
// Pi summaries use fresh cache/affinity IDs. Its explicit session header
|
|
18
|
+
// groups them with the main turns without changing provider cache affinity.
|
|
19
|
+
// Other callers use x-session-affinity, falling back to prompt_cache_key.
|
|
20
|
+
const sessionId = boundedIdentity(headers.get("x-pi-session-id"), "x-pi-session-id") ?? boundedIdentity(
|
|
22
21
|
headers.get("x-session-affinity"),
|
|
23
22
|
"x-session-affinity",
|
|
24
23
|
) ?? boundedIdentity(promptCacheKey, "prompt_cache_key");
|
|
@@ -34,6 +33,16 @@ export function chatCompletionsConversationIdentity(
|
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
export function piRequestContext(headers: Headers): AgentRequestContext | undefined {
|
|
37
|
+
if (boundedIdentity(headers.get("x-pi-session-id"), "x-pi-session-id") === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const kind = headers.get("x-pi-request-kind")?.trim();
|
|
41
|
+
return {
|
|
42
|
+
requestClass: kind === "turn" ? "main" : kind === "compaction" ? "compaction" : "unknown",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
// OpenCode names the session behind every request, and a subagent's parent
|
|
38
47
|
// session too. The `dari opencode` plugin adds the request kind and agent,
|
|
39
48
|
// which OpenCode does not send itself. Like Claude Code's headers, these are
|
|
@@ -174,6 +183,16 @@ export function codexRequestContext(
|
|
|
174
183
|
};
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
// Muse Code sends its thread on every Responses request. The main agent's
|
|
187
|
+
// thread is its session, and each subagent has its own thread, so each keeps
|
|
188
|
+
// its own conversation and lease.
|
|
189
|
+
export function museCodeConversationIdentity(
|
|
190
|
+
headers: Headers,
|
|
191
|
+
): HarnessConversationIdentity | undefined {
|
|
192
|
+
const threadId = boundedIdentity(headers.get("x-tbh-thread-id"), "x-tbh-thread-id");
|
|
193
|
+
return threadId === undefined ? undefined : { kind: "muse_code_thread", value: threadId };
|
|
194
|
+
}
|
|
195
|
+
|
|
177
196
|
export function claudeCodeConversationIdentity(
|
|
178
197
|
headers: Headers,
|
|
179
198
|
metadataSessionId?: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export {
|
|
2
|
+
scopedHarnessAgentId,
|
|
2
3
|
scopedHarnessConversationId,
|
|
3
4
|
scopedHarnessSessionId,
|
|
4
5
|
type HarnessConversationIdentity,
|
|
@@ -10,5 +11,7 @@ export {
|
|
|
10
11
|
codexConversationIdentity,
|
|
11
12
|
codexRequestPurpose,
|
|
12
13
|
codexRequestContext,
|
|
14
|
+
museCodeConversationIdentity,
|
|
13
15
|
openCodeRequestContext,
|
|
16
|
+
piRequestContext,
|
|
14
17
|
} from "./extract.js";
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
import { isBackgroundRequest, type InteractionRequest } from "../interaction/types.js";
|
|
11
11
|
import { executionProviderForModel } from "../models/execution_provider.js";
|
|
12
12
|
import { codexModelConfig, subscriptionServes } from "../models/subscriptions.js";
|
|
13
|
-
import {
|
|
13
|
+
import { explicitModelId } from "../protocol/input/router_model.js";
|
|
14
|
+
import { gatewayPlatform, type RequestTags, type SessionPin } from "../platform/platform.js";
|
|
14
15
|
import { resolveRouter } from "../strategies/routing.js";
|
|
15
16
|
import { RequestReporter, type ReportedCandidatePruning } from "../telemetry/report.js";
|
|
16
17
|
import { RoutingBillingFinalization } from "../telemetry/billing_finalization.js";
|
|
@@ -20,6 +21,7 @@ import { strictProviderAccess } from "./strict_provider_access.js";
|
|
|
20
21
|
import type { RoutedRuntimeReporter } from "./runtime_reporter.js";
|
|
21
22
|
import type { RoutedProtocol } from "./routed_protocol.js";
|
|
22
23
|
import {
|
|
24
|
+
scopedHarnessAgentId,
|
|
23
25
|
scopedHarnessConversationId,
|
|
24
26
|
scopedHarnessSessionId,
|
|
25
27
|
type HarnessConversationIdentity,
|
|
@@ -36,6 +38,9 @@ export type PreparedRoutedCompletion = {
|
|
|
36
38
|
tags: RequestTags;
|
|
37
39
|
harnessConversationIdentity?: HarnessConversationIdentity;
|
|
38
40
|
routing: RoutingContext;
|
|
41
|
+
// Present when this request is served by session pinning: the session's
|
|
42
|
+
// pin key and the pair it pinned, null before its first routed turn.
|
|
43
|
+
sessionPin?: { key: string; pin: SessionPin | null };
|
|
39
44
|
reporter: RoutedRuntimeReporter;
|
|
40
45
|
};
|
|
41
46
|
|
|
@@ -140,6 +145,30 @@ export async function prepareRoutedCompletion(args: {
|
|
|
140
145
|
receivedAt: args.receivedAt,
|
|
141
146
|
});
|
|
142
147
|
const candidateResolution = routing.candidateResolution;
|
|
148
|
+
let sessionPin: PreparedRoutedCompletion["sessionPin"];
|
|
149
|
+
if (
|
|
150
|
+
credentials.session_pinning === true &&
|
|
151
|
+
credentials.strict !== true &&
|
|
152
|
+
!background &&
|
|
153
|
+
explicitModelId(args.interaction.model) === null
|
|
154
|
+
) {
|
|
155
|
+
// Harness identity keys the main agent and each subagent separately and
|
|
156
|
+
// survives compaction. Claude Code names its subagents only with gateway
|
|
157
|
+
// hint headers; without them, and for callers with no harness identity,
|
|
158
|
+
// each conversation is pinned on its own.
|
|
159
|
+
const identity = args.harnessConversationIdentity;
|
|
160
|
+
const unclassifiedClaudeCode = identity?.kind === "claude_code_session"
|
|
161
|
+
&& args.interaction.agentContext?.requestClass === "unknown";
|
|
162
|
+
const key = identity === undefined || unclassifiedClaudeCode
|
|
163
|
+
? routing.conversationId
|
|
164
|
+
: scopedHarnessAgentId({
|
|
165
|
+
identity,
|
|
166
|
+
sourceProtocol: args.interaction.sourceProtocol,
|
|
167
|
+
scope: harnessScope,
|
|
168
|
+
});
|
|
169
|
+
const pin = await gatewayPlatform().readSessionPin({ routerId: canonicalRouterId, pinKey: key });
|
|
170
|
+
sessionPin = { key, pin };
|
|
171
|
+
}
|
|
143
172
|
const reporter = new RequestReporter({
|
|
144
173
|
requestId: args.requestId,
|
|
145
174
|
clientRequestId: args.clientRequestId,
|
|
@@ -184,12 +213,13 @@ export async function prepareRoutedCompletion(args: {
|
|
|
184
213
|
tags,
|
|
185
214
|
harnessConversationIdentity: args.harnessConversationIdentity,
|
|
186
215
|
routing,
|
|
216
|
+
...(sessionPin === undefined ? {} : { sessionPin }),
|
|
187
217
|
reporter,
|
|
188
218
|
};
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
// Ask the platform for each enabled model's credential up front, so provider
|
|
192
|
-
// execution
|
|
222
|
+
// execution stays synchronous.
|
|
193
223
|
async function withProviderCredentials(
|
|
194
224
|
credentials: ResolvedRouter,
|
|
195
225
|
apiKey: string,
|
|
@@ -39,7 +39,6 @@ import { auxiliaryDecision } from "./claude_code/auxiliary_routing.js";
|
|
|
39
39
|
import { explicitModelDecision, strictModelDecision } from "./explicit_model.js";
|
|
40
40
|
import {
|
|
41
41
|
clearPendingLease,
|
|
42
|
-
committedLeaseDecision,
|
|
43
42
|
leaseHoldDecision,
|
|
44
43
|
leasePrefetchDue,
|
|
45
44
|
leasedDecision,
|
|
@@ -47,6 +46,7 @@ import {
|
|
|
47
46
|
prefetchNextLease,
|
|
48
47
|
selectorNegotiatesLeases,
|
|
49
48
|
} from "./routing_lease.js";
|
|
49
|
+
import { pinSession, sessionPinHoldDecision, sessionPinnedSelection } from "./session_pin.js";
|
|
50
50
|
import type { RoutedProtocol } from "./routed_protocol.js";
|
|
51
51
|
import {
|
|
52
52
|
createExperimentRoutedRuntimeReporter,
|
|
@@ -69,7 +69,6 @@ type SpeculativeExecution = {
|
|
|
69
69
|
type RuntimeExecutionPlan = {
|
|
70
70
|
primary: ProviderChoice;
|
|
71
71
|
primaryRetries: number;
|
|
72
|
-
resolveFallback: () => Promise<ProviderChoice | null>;
|
|
73
72
|
reportBarrier?: Promise<unknown>;
|
|
74
73
|
startedCompletion?: StartedCompletion;
|
|
75
74
|
startedStream?: StartedStream;
|
|
@@ -137,7 +136,6 @@ export async function runPreparedRoutedCompletion(args: {
|
|
|
137
136
|
interaction: prepared.interaction,
|
|
138
137
|
primary: plan.primary,
|
|
139
138
|
primaryRetries: plan.primaryRetries,
|
|
140
|
-
resolveFallback: plan.resolveFallback,
|
|
141
139
|
reporter: prepared.reporter,
|
|
142
140
|
protocol: prepared.protocol,
|
|
143
141
|
startedCompletion: plan.startedCompletion,
|
|
@@ -150,7 +148,6 @@ export async function runPreparedRoutedCompletion(args: {
|
|
|
150
148
|
interaction: prepared.interaction,
|
|
151
149
|
primary: plan.primary,
|
|
152
150
|
primaryRetries: plan.primaryRetries,
|
|
153
|
-
resolveFallback: plan.resolveFallback,
|
|
154
151
|
reporter: prepared.reporter,
|
|
155
152
|
startedStream: plan.startedStream,
|
|
156
153
|
initialError: plan.initialStreamError,
|
|
@@ -191,7 +188,6 @@ export async function runPreparedRoutedInteraction(args: {
|
|
|
191
188
|
interaction: args.prepared.interaction,
|
|
192
189
|
primary: plan.primary,
|
|
193
190
|
primaryRetries: plan.primaryRetries,
|
|
194
|
-
resolveFallback: plan.resolveFallback,
|
|
195
191
|
reporter: args.prepared.reporter,
|
|
196
192
|
protocol: args.prepared.protocol,
|
|
197
193
|
startedCompletion: plan.startedCompletion,
|
|
@@ -253,8 +249,7 @@ async function prepareRuntimeExecution(args: {
|
|
|
253
249
|
}): Promise<RuntimeExecutionPlan> {
|
|
254
250
|
const { prepared } = args;
|
|
255
251
|
|
|
256
|
-
// A strict router never selects
|
|
257
|
-
// an error.
|
|
252
|
+
// A strict router never selects or leases: the pinned pair or an error.
|
|
258
253
|
if (prepared.credentials.strict === true) {
|
|
259
254
|
const pinned = strictModelDecision(prepared);
|
|
260
255
|
prepared.reporter.attachServingDecision(pinned.decision);
|
|
@@ -298,14 +293,20 @@ async function prepareRuntimeExecution(args: {
|
|
|
298
293
|
return selectedExecutionPlan(prepared, auxiliary);
|
|
299
294
|
}
|
|
300
295
|
|
|
296
|
+
// A session-pinning router serves the session's pin, or selects once and
|
|
297
|
+
// pins the result. Leases and speculation never apply to a pinned session.
|
|
298
|
+
if (prepared.sessionPin !== undefined && !args.requireSelectorDecision) {
|
|
299
|
+
return selectedExecutionPlan(
|
|
300
|
+
prepared,
|
|
301
|
+
await sessionPinnedDecision(prepared, args.selectorBackend),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
301
305
|
// Persisted policy leases (negotiated {action, turns}) take precedence.
|
|
302
306
|
// Explicit in-process runs are selector experiments: they must observe the
|
|
303
307
|
// selector and must not consume or prefetch live conversations' leases.
|
|
304
308
|
const held = args.requireSelectorDecision ? null : leaseHoldDecision(prepared);
|
|
305
309
|
if (held) {
|
|
306
|
-
// Serving-only attach: nextDecision stays unset so if a fallback serves
|
|
307
|
-
// this turn, execution's attachServingDecision moves both the prefix's
|
|
308
|
-
// next_model and inherited countdown onto the healthy serving pair.
|
|
309
310
|
prepared.reporter.attachServingDecision(held);
|
|
310
311
|
prepared.reporter.attachDecisionDetails({
|
|
311
312
|
decision_source: "lease_hold",
|
|
@@ -324,7 +325,7 @@ async function prepareRuntimeExecution(args: {
|
|
|
324
325
|
if (leasePrefetchDue(prepared)) {
|
|
325
326
|
trackBackgroundTelemetry(prefetchNextLease(prepared, args.selectorBackend));
|
|
326
327
|
}
|
|
327
|
-
return selectedExecutionPlan(prepared,
|
|
328
|
+
return selectedExecutionPlan(prepared, { decision: held });
|
|
328
329
|
}
|
|
329
330
|
const adopted = args.requireSelectorDecision ? null : pendingLeaseDecision(prepared);
|
|
330
331
|
if (adopted) {
|
|
@@ -349,7 +350,7 @@ async function prepareRuntimeExecution(args: {
|
|
|
349
350
|
// wait elapses, the next lease's own prefetch overwrites the stale rows
|
|
350
351
|
// before its expiry (the menu's shortest lease exceeds the lag).
|
|
351
352
|
await clearPendingLease(prepared);
|
|
352
|
-
return selectedExecutionPlan(prepared,
|
|
353
|
+
return selectedExecutionPlan(prepared, { decision: adopted });
|
|
353
354
|
}
|
|
354
355
|
|
|
355
356
|
// The stateless ten-turn hold covers per-turn policies. A lease-negotiating
|
|
@@ -380,7 +381,6 @@ async function prepareRuntimeExecution(args: {
|
|
|
380
381
|
const primary: ProviderChoice = {
|
|
381
382
|
decision: speculative.decision,
|
|
382
383
|
execution: speculative.execution,
|
|
383
|
-
role: "primary",
|
|
384
384
|
};
|
|
385
385
|
const selectionPromise = selectDecision(
|
|
386
386
|
prepared,
|
|
@@ -390,23 +390,13 @@ async function prepareRuntimeExecution(args: {
|
|
|
390
390
|
);
|
|
391
391
|
// The execution layer awaits selection as its report barrier only after the
|
|
392
392
|
// provider settles, which can be long after a rejection. Mark the promise
|
|
393
|
-
// handled now;
|
|
393
|
+
// handled now; the report barrier still observes the original rejection.
|
|
394
394
|
selectionPromise.catch(() => undefined);
|
|
395
|
-
let fallbackPromise: Promise<ProviderChoice | null> | null = null;
|
|
396
|
-
const resolveFallback = (): Promise<ProviderChoice | null> => {
|
|
397
|
-
fallbackPromise ??= selectionPromise.then((selected) => {
|
|
398
|
-
const decision = fallbackForPrimary(prepared, primary.decision, selected);
|
|
399
|
-
if (decision) prepared.reporter.attachFallbackDecision(decision);
|
|
400
|
-
return decision ? providerChoice(prepared, decision, "fallback") : null;
|
|
401
|
-
});
|
|
402
|
-
return fallbackPromise;
|
|
403
|
-
};
|
|
404
395
|
return {
|
|
405
396
|
primary,
|
|
406
397
|
primaryRetries: configuredPrimaryRetries(
|
|
407
398
|
prepared.credentials.primary_retries,
|
|
408
399
|
),
|
|
409
|
-
resolveFallback,
|
|
410
400
|
reportBarrier: selectionPromise,
|
|
411
401
|
startedCompletion:
|
|
412
402
|
speculative.transport === "completion" ? speculative.started : undefined,
|
|
@@ -424,20 +414,10 @@ function selectedExecutionPlan(
|
|
|
424
414
|
selected: RoutedDecision,
|
|
425
415
|
): RuntimeExecutionPlan {
|
|
426
416
|
return {
|
|
427
|
-
primary: providerChoice(prepared, selected.decision
|
|
417
|
+
primary: providerChoice(prepared, selected.decision),
|
|
428
418
|
primaryRetries: configuredPrimaryRetries(
|
|
429
419
|
prepared.credentials.primary_retries,
|
|
430
420
|
),
|
|
431
|
-
resolveFallback: async () => {
|
|
432
|
-
if (!selected.fallbackDecision) return null;
|
|
433
|
-
const fallbackDecision = selected.decision.leaseTurnsRemaining === undefined
|
|
434
|
-
? selected.fallbackDecision
|
|
435
|
-
: {
|
|
436
|
-
...selected.fallbackDecision,
|
|
437
|
-
leaseTurnsRemaining: selected.decision.leaseTurnsRemaining,
|
|
438
|
-
};
|
|
439
|
-
return providerChoice(prepared, fallbackDecision, "fallback");
|
|
440
|
-
},
|
|
441
421
|
};
|
|
442
422
|
}
|
|
443
423
|
|
|
@@ -503,6 +483,28 @@ async function startSpeculativeExecution(
|
|
|
503
483
|
}
|
|
504
484
|
}
|
|
505
485
|
|
|
486
|
+
async function sessionPinnedDecision(
|
|
487
|
+
prepared: PreparedRoutedCompletion,
|
|
488
|
+
selectorBackend: SelectorBackend,
|
|
489
|
+
): Promise<RoutedDecision> {
|
|
490
|
+
const held = sessionPinHoldDecision(prepared);
|
|
491
|
+
if (held) {
|
|
492
|
+
prepared.reporter.attachDecision(held);
|
|
493
|
+
prepared.reporter.attachDecisionDetails({
|
|
494
|
+
decision_source: "session_pin",
|
|
495
|
+
session_pin: { model: held.selectedModel, reasoning_effort: held.reasoningEffort },
|
|
496
|
+
});
|
|
497
|
+
return { decision: held };
|
|
498
|
+
}
|
|
499
|
+
const selected = await selectDecision(prepared, null, selectorBackend);
|
|
500
|
+
prepared.reporter.attachDecisionDetails({
|
|
501
|
+
decision_source: "selector",
|
|
502
|
+
session_pin_established: true,
|
|
503
|
+
});
|
|
504
|
+
await pinSession(prepared, selected.decision);
|
|
505
|
+
return selected;
|
|
506
|
+
}
|
|
507
|
+
|
|
506
508
|
async function selectDecision(
|
|
507
509
|
prepared: PreparedRoutedCompletion,
|
|
508
510
|
servingDecision: RoutingDecision | null,
|
|
@@ -511,7 +513,7 @@ async function selectDecision(
|
|
|
511
513
|
): Promise<RoutedDecision> {
|
|
512
514
|
const { credentials, reporter, routing } = prepared;
|
|
513
515
|
try {
|
|
514
|
-
const
|
|
516
|
+
const selected = await routeDecision({
|
|
515
517
|
selectorMessages: routing.selectorMessages,
|
|
516
518
|
selectorImages: routing.selectorImages,
|
|
517
519
|
resolution: routing.candidateResolution,
|
|
@@ -521,6 +523,7 @@ async function selectDecision(
|
|
|
521
523
|
selectorBackend,
|
|
522
524
|
leaseHistory: routing.leaseHistory,
|
|
523
525
|
});
|
|
526
|
+
const routed = prepared.sessionPin === undefined ? selected : sessionPinnedSelection(selected);
|
|
524
527
|
if (routed.details) reporter.attachDecisionDetails(routed.details);
|
|
525
528
|
if (servingDecision) {
|
|
526
529
|
if (!routed.run) {
|
|
@@ -544,7 +547,6 @@ async function selectDecision(
|
|
|
544
547
|
reporter.attachSelectorRun(routed.run);
|
|
545
548
|
} else {
|
|
546
549
|
reporter.attachDecision(routed.decision);
|
|
547
|
-
reporter.attachFallbackDecision(routed.fallbackDecision);
|
|
548
550
|
}
|
|
549
551
|
return routed;
|
|
550
552
|
} catch (error) {
|
|
@@ -592,35 +594,13 @@ function configuredPrimaryRetries(value: number | undefined): number {
|
|
|
592
594
|
return Number.isInteger(retries) && retries >= 0 && retries <= 5 ? retries : 1;
|
|
593
595
|
}
|
|
594
596
|
|
|
595
|
-
function fallbackForPrimary(
|
|
596
|
-
prepared: PreparedRoutedCompletion,
|
|
597
|
-
primary: RoutingDecision,
|
|
598
|
-
selected: RoutedDecision,
|
|
599
|
-
): RoutingDecision | undefined {
|
|
600
|
-
if (prepared.credentials.model_fallback_enabled === false) return undefined;
|
|
601
|
-
const fallback = [selected.fallbackDecision, selected.decision].find(
|
|
602
|
-
(candidate): candidate is RoutingDecision =>
|
|
603
|
-
candidate !== undefined &&
|
|
604
|
-
candidate.selectedModel !== primary.selectedModel,
|
|
605
|
-
);
|
|
606
|
-
if (!fallback || selected.decision.leaseTurnsRemaining === undefined) {
|
|
607
|
-
return fallback;
|
|
608
|
-
}
|
|
609
|
-
return {
|
|
610
|
-
...fallback,
|
|
611
|
-
leaseTurnsRemaining: selected.decision.leaseTurnsRemaining,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
597
|
function providerChoice(
|
|
616
598
|
prepared: PreparedRoutedCompletion,
|
|
617
599
|
decision: RoutingDecision,
|
|
618
|
-
role: ProviderChoice["role"],
|
|
619
600
|
): ProviderChoice {
|
|
620
601
|
return {
|
|
621
602
|
decision,
|
|
622
603
|
execution: providerExecution(prepared, decision),
|
|
623
|
-
role,
|
|
624
604
|
};
|
|
625
605
|
}
|
|
626
606
|
|