@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/http/auth.ts
CHANGED
|
@@ -6,7 +6,7 @@ export function bearerToken(headers: Headers): string {
|
|
|
6
6
|
const value = headers.get("authorization") ?? "";
|
|
7
7
|
const [scheme, token] = value.split(/\s+/, 2);
|
|
8
8
|
if (scheme?.toLowerCase() !== "bearer" || !token) {
|
|
9
|
-
throw new HttpError(401, "Missing or invalid
|
|
9
|
+
throw new HttpError(401, "Missing or invalid API key.", "authentication_error");
|
|
10
10
|
}
|
|
11
11
|
return publicRoutingToken(token);
|
|
12
12
|
}
|
|
@@ -16,7 +16,7 @@ export function anthropicToken(headers: Headers): string {
|
|
|
16
16
|
if (authorization) return bearerToken(headers);
|
|
17
17
|
const apiKey = headers.get("x-api-key");
|
|
18
18
|
if (!apiKey) {
|
|
19
|
-
throw new HttpError(401, "Missing or invalid
|
|
19
|
+
throw new HttpError(401, "Missing or invalid API key.", "authentication_error");
|
|
20
20
|
}
|
|
21
21
|
return publicRoutingToken(apiKey);
|
|
22
22
|
}
|
package/src/http/http2_server.ts
CHANGED
|
@@ -23,7 +23,11 @@ const HTTP2_FORBIDDEN_RESPONSE_HEADERS = new Set([
|
|
|
23
23
|
"upgrade",
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
-
export function startHttp2Server(
|
|
26
|
+
export function startHttp2Server(
|
|
27
|
+
port: number,
|
|
28
|
+
handler: Http2RequestHandler,
|
|
29
|
+
hostname = "0.0.0.0",
|
|
30
|
+
): Http2RoutingServer {
|
|
27
31
|
const sessions = new Set<ServerHttp2Session>();
|
|
28
32
|
let closePromise: Promise<void> | undefined;
|
|
29
33
|
const server = createServer((request, response) => {
|
|
@@ -33,10 +37,10 @@ export function startHttp2Server(port: number, handler: Http2RequestHandler): Ht
|
|
|
33
37
|
sessions.add(session);
|
|
34
38
|
session.once("close", () => sessions.delete(session));
|
|
35
39
|
});
|
|
36
|
-
server.listen(port,
|
|
40
|
+
server.listen(port, hostname);
|
|
37
41
|
const address = server.address();
|
|
38
42
|
if (address === null || typeof address === "string") {
|
|
39
|
-
throw new Error("HTTP/2
|
|
43
|
+
throw new Error("HTTP/2 gateway server did not bind to a TCP port.");
|
|
40
44
|
}
|
|
41
45
|
return {
|
|
42
46
|
port: address.port,
|
package/src/http/serve.ts
CHANGED
|
@@ -16,10 +16,10 @@ type StoppableServer = {
|
|
|
16
16
|
export function startServer(
|
|
17
17
|
listenPort: number,
|
|
18
18
|
handler: (request: Request) => Promise<Response>,
|
|
19
|
-
options: { http2?: boolean } = {},
|
|
19
|
+
options: { http2?: boolean; hostname?: string } = {},
|
|
20
20
|
) {
|
|
21
21
|
if (options.http2) {
|
|
22
|
-
return startHttp2Server(listenPort, handler);
|
|
22
|
+
return startHttp2Server(listenPort, handler, options.hostname);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// The selector runs before the first response byte, and routed models can
|
|
@@ -27,6 +27,7 @@ export function startServer(
|
|
|
27
27
|
// clients and infrastructure still bound total request lifetime.
|
|
28
28
|
return Bun.serve({
|
|
29
29
|
port: listenPort,
|
|
30
|
+
hostname: options.hostname,
|
|
30
31
|
fetch: handler,
|
|
31
32
|
idleTimeout: 0,
|
|
32
33
|
});
|
|
@@ -45,7 +46,7 @@ export function installShutdownHandlers(
|
|
|
45
46
|
if (shuttingDown) return;
|
|
46
47
|
shuttingDown = true;
|
|
47
48
|
const workDeadlineAtMs = performance.now() + SHUTDOWN_WORK_TIMEOUT_MS;
|
|
48
|
-
console.info("
|
|
49
|
+
console.info("gateway server shutting down", JSON.stringify({ signal }));
|
|
49
50
|
try {
|
|
50
51
|
await stopServerForShutdown(server);
|
|
51
52
|
} finally {
|
|
@@ -54,7 +55,7 @@ export function installShutdownHandlers(
|
|
|
54
55
|
);
|
|
55
56
|
if (remaining > 0) {
|
|
56
57
|
console.warn(
|
|
57
|
-
"
|
|
58
|
+
"gateway server background drain timed out",
|
|
58
59
|
JSON.stringify({ remaining_task_count: remaining }),
|
|
59
60
|
);
|
|
60
61
|
captureOperationalError(
|
|
@@ -112,7 +113,7 @@ function stopWithin(
|
|
|
112
113
|
|
|
113
114
|
function reportServerStopFailure(error: unknown): void {
|
|
114
115
|
console.warn(
|
|
115
|
-
"
|
|
116
|
+
"gateway server shutdown failed",
|
|
116
117
|
error instanceof Error ? error.name : "UnknownError",
|
|
117
118
|
);
|
|
118
119
|
captureOperationalError(error, { code: "server_force_stop_failed" });
|
|
@@ -9,6 +9,7 @@ import type { SelectorBackend } from "../strategies/routing.js";
|
|
|
9
9
|
import {
|
|
10
10
|
chatCompletionsConversationIdentity,
|
|
11
11
|
openCodeRequestContext,
|
|
12
|
+
piRequestContext,
|
|
12
13
|
} from "../runtime/harness_identity/index.js";
|
|
13
14
|
import { parseJsonRequest } from "./json.js";
|
|
14
15
|
import { museCodeClientId } from "./muse_code.js";
|
|
@@ -22,7 +23,7 @@ export async function handleChatCompletions(
|
|
|
22
23
|
const apiKey = bearerToken(request.headers);
|
|
23
24
|
const payload = await parseJsonRequest<ChatCompletionRequest>(request);
|
|
24
25
|
const interaction = chatCompletionsToInteraction({ ...payload, source_protocol: "openai_chat" });
|
|
25
|
-
const agentContext = openCodeRequestContext(request.headers);
|
|
26
|
+
const agentContext = piRequestContext(request.headers) ?? openCodeRequestContext(request.headers);
|
|
26
27
|
const museClientId = museCodeClientId(request.headers);
|
|
27
28
|
return await handleRoutedCompletion({
|
|
28
29
|
request,
|
package/src/ingress/codex.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { explicitModelId } from "../protocol/input/router_model.js";
|
|
1
2
|
import { bearerToken } from "../http/auth.js";
|
|
2
3
|
import { errorResponse } from "../http/errors.js";
|
|
3
4
|
import { captureRequestError } from "../observability/errors.js";
|
|
@@ -40,7 +41,9 @@ export async function handleResponses(
|
|
|
40
41
|
// providers. That preference describes a direct model, not a heterogeneous
|
|
41
42
|
// router, so let Dari select the reasoning effort for verified Codex turns.
|
|
42
43
|
// Parsing above still validates malformed Responses reasoning controls.
|
|
43
|
-
if (harnessConversationIdentity !== undefined
|
|
44
|
+
if (harnessConversationIdentity !== undefined && explicitModelId(payload.model) === null) {
|
|
45
|
+
payload.reasoning = undefined;
|
|
46
|
+
}
|
|
44
47
|
return await handleRoutedCompletion({
|
|
45
48
|
request,
|
|
46
49
|
routerId,
|
|
@@ -32,6 +32,8 @@ type CodexCatalogEntry = {
|
|
|
32
32
|
contextWindow: number;
|
|
33
33
|
hostedWebSearch: boolean;
|
|
34
34
|
imageInput: boolean;
|
|
35
|
+
reasoningLevels?: string[];
|
|
36
|
+
defaultReasoningLevel?: string | null;
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
function codexCatalogEntry(args: CodexCatalogEntry): Record<string, unknown> {
|
|
@@ -39,8 +41,11 @@ function codexCatalogEntry(args: CodexCatalogEntry): Record<string, unknown> {
|
|
|
39
41
|
slug: args.slug,
|
|
40
42
|
display_name: args.displayName,
|
|
41
43
|
description: args.description,
|
|
42
|
-
default_reasoning_level: null,
|
|
43
|
-
supported_reasoning_levels: []
|
|
44
|
+
default_reasoning_level: args.defaultReasoningLevel ?? null,
|
|
45
|
+
supported_reasoning_levels: (args.reasoningLevels ?? []).map((effort) => ({
|
|
46
|
+
effort,
|
|
47
|
+
description: `${effort} thinking`,
|
|
48
|
+
})),
|
|
44
49
|
shell_type: "shell_command",
|
|
45
50
|
visibility: "list",
|
|
46
51
|
supported_in_api: true,
|
|
@@ -80,15 +85,17 @@ function codexCatalogEntry(args: CodexCatalogEntry): Record<string, unknown> {
|
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
/* `dari/routing`
|
|
84
|
-
model follows as `dari/model/<id>` so
|
|
88
|
+
/* `dari/routing` comes first while the router may autoroute (the Codex
|
|
89
|
+
launcher patches it); each enabled model follows as `dari/model/<id>` so
|
|
90
|
+
Codex's /model menu can switch to it. The `dari` launchers build every
|
|
91
|
+
agent's model menu from this list. */
|
|
85
92
|
export function codexModelsResponse(
|
|
86
|
-
router: Pick<ResolvedRouter, "enabled_models" | "model_configs" | "allow_long_context">,
|
|
93
|
+
router: Pick<ResolvedRouter, "enabled_models" | "model_configs" | "allow_long_context" | "autorouting_enabled">,
|
|
87
94
|
imageInputFor: (model: string) => boolean
|
|
88
95
|
): Record<string, unknown> {
|
|
89
96
|
return {
|
|
90
97
|
models: [
|
|
91
|
-
codexCatalogEntry({
|
|
98
|
+
...(router.autorouting_enabled === false ? [] : [codexCatalogEntry({
|
|
92
99
|
slug: CANONICAL_ROUTER_MODEL,
|
|
93
100
|
displayName: "Dari Routing",
|
|
94
101
|
description: "Routes Codex HTTP Responses requests through the configured Dari model router.",
|
|
@@ -96,7 +103,7 @@ export function codexModelsResponse(
|
|
|
96
103
|
contextWindow: codexRouterContextWindow(router),
|
|
97
104
|
hostedWebSearch: codexRouterSupportsHostedWebSearch(router),
|
|
98
105
|
imageInput: router.enabled_models.some(imageInputFor),
|
|
99
|
-
}),
|
|
106
|
+
})]),
|
|
100
107
|
...router.enabled_models.map((model, index) =>
|
|
101
108
|
codexCatalogEntry({
|
|
102
109
|
slug: explicitModelSlug(model),
|
|
@@ -106,6 +113,10 @@ export function codexModelsResponse(
|
|
|
106
113
|
contextWindow: codexRouterContextWindow({ ...router, enabled_models: [model] }),
|
|
107
114
|
hostedWebSearch: codexRouterSupportsHostedWebSearch({ ...router, enabled_models: [model] }),
|
|
108
115
|
imageInput: imageInputFor(model),
|
|
116
|
+
reasoningLevels: router.model_configs?.[model]?.supported_thinking_levels
|
|
117
|
+
?.map((effort) => effort === "off" ? "none" : effort),
|
|
118
|
+
defaultReasoningLevel: router.model_configs?.[model]?.default_thinking_level === "off"
|
|
119
|
+
? "none" : router.model_configs?.[model]?.default_thinking_level,
|
|
109
120
|
})
|
|
110
121
|
),
|
|
111
122
|
],
|
package/src/platform/platform.ts
CHANGED
|
@@ -45,6 +45,10 @@ export type GatewayPlatform = {
|
|
|
45
45
|
writePrefixState(write: PrefixStateWrite): Promise<void>;
|
|
46
46
|
/** Set or clear a conversation's prefetched routing lease. */
|
|
47
47
|
writePendingLease(write: PendingLeaseWrite): Promise<void>;
|
|
48
|
+
/** The pair a session pinned on a session-pinning router, or null. */
|
|
49
|
+
readSessionPin(request: SessionPinKey): Promise<SessionPin | null>;
|
|
50
|
+
/** Pin a session to a pair, replacing any earlier pin. */
|
|
51
|
+
writeSessionPin(write: SessionPinKey & SessionPin): Promise<void>;
|
|
48
52
|
/**
|
|
49
53
|
* Hold an exhausted personal subscription until it resets. Resolves whether
|
|
50
54
|
* another connected subscription for the same provider can serve a retry.
|
|
@@ -104,6 +108,17 @@ export type PendingLeaseWrite = {
|
|
|
104
108
|
} | null;
|
|
105
109
|
};
|
|
106
110
|
|
|
111
|
+
// A main agent and each of its subagents have their own pin key.
|
|
112
|
+
export type SessionPinKey = {
|
|
113
|
+
routerId: string;
|
|
114
|
+
pinKey: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type SessionPin = {
|
|
118
|
+
model: string;
|
|
119
|
+
reasoningEffort: ReasoningEffort;
|
|
120
|
+
};
|
|
121
|
+
|
|
107
122
|
export type SubscriptionHold = {
|
|
108
123
|
userId: string;
|
|
109
124
|
credentialId: string | undefined;
|
|
@@ -146,8 +161,6 @@ export type RequestSummary = {
|
|
|
146
161
|
selected_reasoning_effort: ReasoningEffort | null;
|
|
147
162
|
next_model: string | null;
|
|
148
163
|
next_reasoning_effort: ReasoningEffort | null;
|
|
149
|
-
fallback_model: string | null;
|
|
150
|
-
fallback_reasoning_effort: ReasoningEffort | null;
|
|
151
164
|
provider_key_source: ProviderKeySource | null;
|
|
152
165
|
message_count: number;
|
|
153
166
|
tool_count: number;
|
|
@@ -184,6 +197,7 @@ export type OperationalErrorCode =
|
|
|
184
197
|
| "routing_report_assembly_failed"
|
|
185
198
|
| "routing_report_barrier_failed"
|
|
186
199
|
| "server_force_stop_failed"
|
|
200
|
+
| "session_pin_write_failed"
|
|
187
201
|
| "stream_report_finalization_failed"
|
|
188
202
|
| "subscription_unavailable_ingest_failed";
|
|
189
203
|
|
|
@@ -572,15 +572,8 @@ function reasoningControls(value: unknown): InteractionRequest["reasoning"] {
|
|
|
572
572
|
if (value === undefined || value === null) return undefined;
|
|
573
573
|
if (!isRecord(value)) throw new HttpError(400, "reasoning must be an object.", "invalid_request_error", "reasoning");
|
|
574
574
|
const normalizedEffort = normalizedReasoningEffort(value.effort, "reasoning.effort");
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
throw new HttpError(
|
|
578
|
-
400,
|
|
579
|
-
"reasoning.summary is not supported by this router MVP.",
|
|
580
|
-
"invalid_request_error",
|
|
581
|
-
"reasoning.summary"
|
|
582
|
-
);
|
|
583
|
-
}
|
|
575
|
+
// Summaries are display-only: clients such as Muse Code ask for them by
|
|
576
|
+
// default, and the router serves the turn without them.
|
|
584
577
|
return normalizedEffort === undefined ? undefined : { effort: normalizedEffort };
|
|
585
578
|
}
|
|
586
579
|
|
|
@@ -2,7 +2,7 @@ import { parseDataImageUrl } from "../../interaction/image.js";
|
|
|
2
2
|
import { encodeProviderContinuationState } from "../../interaction/provider_continuation_state.js";
|
|
3
3
|
import { encodePortableReasoningState } from "../../interaction/portable_reasoning_state.js";
|
|
4
4
|
import type { InteractionOutput } from "../../interaction/types.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ServedDecision } from "../../types.js";
|
|
6
6
|
|
|
7
7
|
export function anthropicMessageId(requestId: string): string {
|
|
8
8
|
return `msg_${requestId.replace(/[^A-Za-z0-9_-]/g, "_")}`.slice(0, 64);
|
|
@@ -99,7 +99,7 @@ export function anthropicUsage(output: Pick<InteractionOutput, "usage">): Record
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export function anthropicRouting(
|
|
102
|
-
decision:
|
|
102
|
+
decision: ServedDecision,
|
|
103
103
|
requestId: string,
|
|
104
104
|
conversationId?: string
|
|
105
105
|
): Record<string, unknown> {
|
|
@@ -113,6 +113,7 @@ export function anthropicRouting(
|
|
|
113
113
|
...(decision.leaseTurnsRemaining === undefined
|
|
114
114
|
? {}
|
|
115
115
|
: { lease_turns_remaining: decision.leaseTurnsRemaining }),
|
|
116
|
+
...(decision.sessionPinned === true ? { session_pinned: true } : {}),
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
openAIResponsesTerminal,
|
|
7
7
|
openAIResponsesUsage,
|
|
8
8
|
} from "./openai_responses_output.js";
|
|
9
|
-
import { piMessageToInteractionOutput } from "../../providers/pi_output.js";
|
|
9
|
+
import { piMessageToInteractionOutput, textSignature } from "../../providers/pi_output.js";
|
|
10
10
|
import type {
|
|
11
11
|
InteractionOutput,
|
|
12
12
|
InteractionOutputItem,
|
|
@@ -119,6 +119,7 @@ class ResponsesAccumulator {
|
|
|
119
119
|
private readonly addedIndexes = new Set<number>();
|
|
120
120
|
private readonly completedIndexes = new Set<number>();
|
|
121
121
|
private readonly messageIds = new Map<number, string>();
|
|
122
|
+
private readonly messagePhases = new Map<number, "commentary" | "final_answer">();
|
|
122
123
|
private readonly reasoningIds = new Map<number, string>();
|
|
123
124
|
private readonly messagePartStarted = new Set<number>();
|
|
124
125
|
private readonly reasoningPartStarted = new Set<number>();
|
|
@@ -144,6 +145,7 @@ class ResponsesAccumulator {
|
|
|
144
145
|
const index = this.indexFor(event);
|
|
145
146
|
this.activate(index, send);
|
|
146
147
|
if (type === "thinking_start") this.ensureReasoning(index, send);
|
|
148
|
+
if (type === "text_start") this.rememberMessageSignature(event, index);
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
149
151
|
if (type === "text_delta" && typeof event.delta === "string" && event.delta) {
|
|
@@ -330,17 +332,29 @@ class ResponsesAccumulator {
|
|
|
330
332
|
return openAIResponsesUsage(this.finalOutput ?? this.baseOutput());
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
// Pi signs a Responses text block with its message id and phase at
|
|
336
|
+
// text_start, so the item is announced with the phase it ends with.
|
|
337
|
+
private rememberMessageSignature(event: Record<string, unknown>, index: number): void {
|
|
338
|
+
const content = isRecord(event.partial) ? event.partial.content : undefined;
|
|
339
|
+
const block = Array.isArray(content) ? content[event.contentIndex as number] : undefined;
|
|
340
|
+
const signature = isRecord(block) ? textSignature(block.textSignature) : null;
|
|
341
|
+
if (signature?.id && !this.messageIds.has(index)) this.messageIds.set(index, signature.id);
|
|
342
|
+
if (signature?.phase) this.messagePhases.set(index, signature.phase);
|
|
343
|
+
}
|
|
344
|
+
|
|
333
345
|
private ensureMessage(index: number, send: Send): Extract<InteractionOutputItem, { type: "message" }> {
|
|
334
346
|
const existing = this.items.get(index);
|
|
335
347
|
if (existing?.type === "message") return existing;
|
|
336
348
|
const id = this.messageIds.get(index) ?? streamedItemId("msg", this.responseId, index);
|
|
337
349
|
this.messageIds.set(index, id);
|
|
350
|
+
const phase = this.messagePhases.get(index);
|
|
338
351
|
const message: Extract<InteractionOutputItem, { type: "message" }> = {
|
|
339
352
|
type: "message",
|
|
340
353
|
id,
|
|
341
354
|
role: "assistant",
|
|
342
355
|
content: [{ type: "text", text: "" }],
|
|
343
356
|
contentShape: "parts",
|
|
357
|
+
...(phase ? { phase } : {}),
|
|
344
358
|
source: this.args.source,
|
|
345
359
|
};
|
|
346
360
|
this.items.set(index, message);
|
|
@@ -353,6 +367,7 @@ class ResponsesAccumulator {
|
|
|
353
367
|
id,
|
|
354
368
|
status: "in_progress",
|
|
355
369
|
content: [],
|
|
370
|
+
...(phase ? { phase } : {}),
|
|
356
371
|
},
|
|
357
372
|
});
|
|
358
373
|
this.sendMessagePartAdded(index, id, send);
|
|
@@ -420,6 +435,7 @@ class ResponsesAccumulator {
|
|
|
420
435
|
id: serialized.id,
|
|
421
436
|
status: "in_progress",
|
|
422
437
|
content: [],
|
|
438
|
+
...(item.phase ? { phase: item.phase } : {}),
|
|
423
439
|
},
|
|
424
440
|
});
|
|
425
441
|
} else {
|
|
@@ -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(
|
|
@@ -46,6 +46,17 @@ export function scopedHarnessSessionId(args: {
|
|
|
46
46
|
return scopedHarnessId({ ...args, identity });
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// One agent of a session: the main agent on the bare session, each subagent
|
|
50
|
+
// on its own qualifier or thread. Unlike the conversation id, it survives
|
|
51
|
+
// compaction.
|
|
52
|
+
export function scopedHarnessAgentId(args: {
|
|
53
|
+
identity: HarnessConversationIdentity;
|
|
54
|
+
sourceProtocol: SourceProtocol;
|
|
55
|
+
scope: HarnessConversationScope;
|
|
56
|
+
}): string {
|
|
57
|
+
return scopedHarnessId(args);
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
export function scopedHarnessConversationId(args: {
|
|
50
61
|
identity: HarnessConversationIdentity;
|
|
51
62
|
sourceProtocol: SourceProtocol;
|