@mupt-ai/dari-gateway 0.3.0 → 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/provider_reported_cost.js +17 -2
- 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/provider_reported_cost.ts +16 -2
- 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/dist/telemetry/report.js
CHANGED
|
@@ -16,7 +16,7 @@ import { routingBillingProviderAttemptFacts, } from "./billing_finalization.js";
|
|
|
16
16
|
import { estimateBenchmarkReplay } from "./benchmark_replay.js";
|
|
17
17
|
import { extractInteractionObservability } from "./requests/interaction_observability.js";
|
|
18
18
|
import { costString, finiteNumber, isRecord, tokenCount, } from "./ingest_values.js";
|
|
19
|
-
import { buildCompletedPrefixHit, leaseCountdownForPrefix, prefixEntryForIngest,
|
|
19
|
+
import { buildCompletedPrefixHit, leaseCountdownForPrefix, prefixEntryForIngest, reportPrefixState, } from "./prefix_state.js";
|
|
20
20
|
import { subscriptionUsageLimitFromFailure } from "../providers/subscription_limits.js";
|
|
21
21
|
import { scrubSecrets } from "./scrub.js";
|
|
22
22
|
import { encodeRetainedInteractionRequest } from "./requests/retained_interaction_request.js";
|
|
@@ -50,7 +50,6 @@ export class RequestReporter {
|
|
|
50
50
|
selectorOutputAccepted = false;
|
|
51
51
|
decision = null;
|
|
52
52
|
nextDecision = null;
|
|
53
|
-
fallbackDecision = null;
|
|
54
53
|
providerAttempts = [];
|
|
55
54
|
billingProviderAttempts = [];
|
|
56
55
|
decisionDetails = {};
|
|
@@ -66,7 +65,6 @@ export class RequestReporter {
|
|
|
66
65
|
this.selectorOutputAccepted = true;
|
|
67
66
|
this.decision = run.decision;
|
|
68
67
|
this.nextDecision = run.decision;
|
|
69
|
-
this.fallbackDecision = run.fallbackDecision ?? null;
|
|
70
68
|
}
|
|
71
69
|
attachSpeculativeSelectorRun(run, servingDecision) {
|
|
72
70
|
this.selectorRun = run;
|
|
@@ -74,7 +72,6 @@ export class RequestReporter {
|
|
|
74
72
|
this.selectorOutputAccepted = true;
|
|
75
73
|
this.decision = servingDecision;
|
|
76
74
|
this.nextDecision = run.decision;
|
|
77
|
-
this.fallbackDecision = run.fallbackDecision ?? null;
|
|
78
75
|
}
|
|
79
76
|
// Speculative execution can begin before a selector run; its serving
|
|
80
77
|
// decision drives selected-model attribution, prefix entries, and errors.
|
|
@@ -82,9 +79,6 @@ export class RequestReporter {
|
|
|
82
79
|
this.decision = decision;
|
|
83
80
|
this.nextDecision = decision;
|
|
84
81
|
}
|
|
85
|
-
attachFallbackDecision(decision) {
|
|
86
|
-
this.fallbackDecision = decision ?? null;
|
|
87
|
-
}
|
|
88
82
|
attachServingDecision(decision) {
|
|
89
83
|
this.decision = decision;
|
|
90
84
|
}
|
|
@@ -96,10 +90,10 @@ export class RequestReporter {
|
|
|
96
90
|
modelCost(attempt) {
|
|
97
91
|
return modelCost(attempt ? [...this.billingProviderAttempts, this.billingFacts(attempt)] : this.billingProviderAttempts);
|
|
98
92
|
}
|
|
99
|
-
billingFacts({ execution, message,
|
|
93
|
+
billingFacts({ execution, message, attempt, billingOutcome }) {
|
|
100
94
|
return routingBillingProviderAttemptFacts({
|
|
101
95
|
provider: execution.provider, model: execution.model,
|
|
102
|
-
providerKeySource: execution.providerKeySource,
|
|
96
|
+
providerKeySource: execution.providerKeySource, attempt, outcome: billingOutcome,
|
|
103
97
|
providerMessage: message, usage: isRecord(message.usage) ? message.usage : null,
|
|
104
98
|
providerPricingKnown: this.pricingKnown(execution.model),
|
|
105
99
|
});
|
|
@@ -224,16 +218,8 @@ export class RequestReporter {
|
|
|
224
218
|
const decision = this.decision;
|
|
225
219
|
const selectedModel = outcome.providerExecution?.model ?? decision?.selectedModel ?? null;
|
|
226
220
|
const reasoningBucket = decision?.reasoningEffort ?? null;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// pair that actually served instead of re-pinning the failed one.
|
|
230
|
-
const fallbackServed = this.providerAttempts.some((attempt) => attempt.role === "fallback" && attempt.outcome === "completed");
|
|
231
|
-
const nextModel = fallbackServed
|
|
232
|
-
? selectedModel
|
|
233
|
-
: this.nextDecision?.selectedModel ?? selectedModel;
|
|
234
|
-
const nextReasoningEffort = fallbackServed
|
|
235
|
-
? reasoningBucket
|
|
236
|
-
: this.nextDecision?.reasoningEffort ?? reasoningBucket;
|
|
221
|
+
const nextModel = this.nextDecision?.selectedModel ?? selectedModel;
|
|
222
|
+
const nextReasoningEffort = this.nextDecision?.reasoningEffort ?? reasoningBucket;
|
|
237
223
|
const selectedProvider = outcome.providerExecution?.provider ?? (selectedModel
|
|
238
224
|
? executionProviderForModel(a.credentials, selectedModel)
|
|
239
225
|
: null);
|
|
@@ -292,7 +278,6 @@ export class RequestReporter {
|
|
|
292
278
|
leaseTurnsRemaining: leaseCountdownForPrefix({
|
|
293
279
|
served: decision ?? null,
|
|
294
280
|
next: this.nextDecision,
|
|
295
|
-
fallbackServed,
|
|
296
281
|
}),
|
|
297
282
|
reason: this.nextDecision?.reason ?? decision?.reason ?? null,
|
|
298
283
|
chain: selectedChain,
|
|
@@ -324,19 +309,6 @@ export class RequestReporter {
|
|
|
324
309
|
// resolves this turn's conversation id and next-model prediction.
|
|
325
310
|
// The full report below re-applies the same state idempotently, so a
|
|
326
311
|
// lost or timed-out fast write only degrades to the old ordering.
|
|
327
|
-
// An ordinary fallback breaks any lease commitment, prefetched ones
|
|
328
|
-
// included. A lease-aware fallback transfers the active countdown to
|
|
329
|
-
// the serving pair, so only clear pending state when no countdown was
|
|
330
|
-
// transferred. The depth fence orders the clear safely against
|
|
331
|
-
// in-flight prefetch writes.
|
|
332
|
-
if (!backgroundRequest && fallbackServed && decision?.leaseTurnsRemaining === undefined) {
|
|
333
|
-
trackBackgroundTelemetry(reportPendingLease({
|
|
334
|
-
routerId: a.routerId,
|
|
335
|
-
conversationId: a.conversationId,
|
|
336
|
-
decidedAtDepth: a.leaseDepth,
|
|
337
|
-
lease: null,
|
|
338
|
-
}));
|
|
339
|
-
}
|
|
340
312
|
if (prefixEntry !== null || touchedHash !== null) {
|
|
341
313
|
this.prefixStatePromise = reportPrefixState({
|
|
342
314
|
organization_id: a.credentials.organization_id,
|
|
@@ -404,12 +376,6 @@ export class RequestReporter {
|
|
|
404
376
|
strategy: a.strategy,
|
|
405
377
|
selected_reasoning_effort: reasoningBucket,
|
|
406
378
|
next_reasoning_effort: nextReasoningEffort,
|
|
407
|
-
...(this.fallbackDecision
|
|
408
|
-
? {
|
|
409
|
-
fallback_model: this.fallbackDecision.selectedModel,
|
|
410
|
-
fallback_reasoning_effort: this.fallbackDecision.reasoningEffort,
|
|
411
|
-
}
|
|
412
|
-
: {}),
|
|
413
379
|
...(this.providerAttempts.length > 0
|
|
414
380
|
? { provider_attempts: this.providerAttempts }
|
|
415
381
|
: {}),
|
|
@@ -466,8 +432,6 @@ export class RequestReporter {
|
|
|
466
432
|
selected_reasoning_effort: reasoningBucket,
|
|
467
433
|
next_model: nextModel,
|
|
468
434
|
next_reasoning_effort: nextReasoningEffort,
|
|
469
|
-
fallback_model: this.fallbackDecision?.selectedModel ?? null,
|
|
470
|
-
fallback_reasoning_effort: this.fallbackDecision?.reasoningEffort ?? null,
|
|
471
435
|
provider_key_source: keySource,
|
|
472
436
|
message_count: observability.messageCount,
|
|
473
437
|
tool_count: observability.metadata.declared_tool_counts.total,
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CustomRouterConfig, Provider, ReasoningEffort, RouterEval, RouterEvalScore, RouterModelPrice, RouterPrefixHit, RoutingStrategy } from "@mupt-ai/dari-router/policy-engine";
|
|
1
|
+
import type { CustomRouterConfig, Provider, ReasoningEffort, RouterEval, RouterEvalScore, RouterModelPrice, RouterPrefixHit, RoutingDecision, RoutingStrategy } from "@mupt-ai/dari-router/policy-engine";
|
|
2
2
|
export type ProviderKeySource = "user" | "managed" | "subscription";
|
|
3
3
|
export type BearerProviderAuth = {
|
|
4
4
|
type: "bearer";
|
|
@@ -41,6 +41,9 @@ export type PublicEvalCatalogCard = Omit<RouterEval, "scores"> & {
|
|
|
41
41
|
updated_at: string | null;
|
|
42
42
|
scores: PublicEvalCatalogScore[];
|
|
43
43
|
};
|
|
44
|
+
export type ServedDecision = RoutingDecision & {
|
|
45
|
+
sessionPinned?: boolean;
|
|
46
|
+
};
|
|
44
47
|
export type ClientHarness = "claude_code" | "muse_code";
|
|
45
48
|
export type ResolvedRouterModelConfig = {
|
|
46
49
|
provider: string;
|
|
@@ -74,11 +77,10 @@ export type ResolvedRouter = {
|
|
|
74
77
|
zdr?: boolean;
|
|
75
78
|
allow_long_context?: boolean;
|
|
76
79
|
speculative_routing?: boolean;
|
|
80
|
+
session_pinning?: boolean;
|
|
77
81
|
primary_retries?: number;
|
|
78
82
|
strict?: boolean;
|
|
79
83
|
strict_providers?: string[];
|
|
80
|
-
model_fallback_enabled?: boolean;
|
|
81
|
-
fallback_requires_different_provider?: boolean;
|
|
82
84
|
evals?: RouterEval[];
|
|
83
85
|
eval_score_imputation?: boolean;
|
|
84
86
|
image_capability_filtering?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mupt-ai/dari-gateway",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Self-hostable LLM gateway for coding agents and APIs: one endpoint for Anthropic Messages, OpenAI Chat Completions, and OpenAI Responses, with subscription pooling, failover, and prompt-cache-aware routing.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@aws-sdk/client-sts": "3.1063.0",
|
|
52
52
|
"@mupt-ai/dari-router": "0.2.5",
|
|
53
|
-
"@mupt-ai/pi-ai": "1.4.0-mupt.
|
|
53
|
+
"@mupt-ai/pi-ai": "1.4.0-mupt.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/bun": "^1.3.4",
|
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);
|