@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/README.md
CHANGED
|
@@ -46,10 +46,10 @@ bun install
|
|
|
46
46
|
bun run start
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Or with Docker, from the repository root:
|
|
49
|
+
Or with Docker, from this directory (in the monorepo, run `docker build -t dari-gateway dari-gateway` from the repository root):
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
docker build -
|
|
52
|
+
docker build -t dari-gateway .
|
|
53
53
|
docker run -p 8080:8080 \
|
|
54
54
|
-e DARI_GATEWAY_KEYS -e ANTHROPIC_API_KEY \
|
|
55
55
|
-v "$PWD/dari-gateway.yaml:/config/dari-gateway.yaml" \
|
|
@@ -64,6 +64,7 @@ docker run -p 8080:8080 \
|
|
|
64
64
|
| `DARI_GATEWAY_DB` | SQLite database for subscriptions and conversation state. Default `dari-gateway.db`. It stores subscription tokens in plaintext; protect it like a secret. |
|
|
65
65
|
| `DARI_AUTO_ROUTING_API_KEY` | Optional. Select among a router's models with Dari Auto Routing. |
|
|
66
66
|
| `PORT` | Listen port. Default `8080`. |
|
|
67
|
+
| `DARI_GATEWAY_HOST` | Listen address. Default `127.0.0.1`, so only this machine can connect. The container image sets `0.0.0.0`; set it yourself to expose a process on your network. |
|
|
67
68
|
| Provider keys | Each provider's standard variable, such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `META_API_KEY`, or `OPENROUTER_API_KEY`. |
|
|
68
69
|
|
|
69
70
|
## Configure
|
|
@@ -75,8 +76,8 @@ routers:
|
|
|
75
76
|
- anthropic/claude-sonnet-5-5
|
|
76
77
|
- id: openai/gpt-5.5
|
|
77
78
|
thinking: [medium, high]
|
|
78
|
-
fallback: true # retry the next model when a provider fails
|
|
79
79
|
subscriptions: true # prefer connected subscriptions over API keys
|
|
80
|
+
sessionPinning: false # route each session and subagent once, then keep its model
|
|
80
81
|
local: # serves /local/v1/...
|
|
81
82
|
models: [local/qwen3-coder]
|
|
82
83
|
|
|
@@ -106,7 +107,7 @@ In gateway mode the CLI makes no calls to Dari's hosted API. `dari credentials s
|
|
|
106
107
|
|
|
107
108
|
## Embed
|
|
108
109
|
|
|
109
|
-
Deployments with their own storage and accounting compose the runtime with a `GatewayPlatform` (router resolution, conversation state, subscription holds, request records, error reporting) and a selector backend:
|
|
110
|
+
Deployments with their own storage and accounting compose the runtime with a `GatewayPlatform` (router resolution, conversation state, session pins, subscription holds, request records, error reporting) and a selector backend:
|
|
110
111
|
|
|
111
112
|
```ts
|
|
112
113
|
import { createPrioritySelectorBackend, createRequestHandler } from "@mupt-ai/dari-gateway";
|
|
@@ -120,19 +121,26 @@ The root entry needs no Bun APIs, so any runtime with web-standard `fetch`, `Req
|
|
|
120
121
|
Platforms can also:
|
|
121
122
|
|
|
122
123
|
- implement `resolveProviderCredential({ provider, model, routerId, organizationId, apiKey, clientHarness? })` to supply a per-request `{ apiKey, baseUrl?, headers?, subscription? }`, such as a tenant's own key, in place of the router's resolved key. With `subscription` (`"openai_codex"`, `"anthropic_claude_code"`, or `"meta_muse"`), `apiKey` is that plan's access token: Codex's `auth.json` `tokens.access_token`, the token `claude setup-token` prints, or the Model API key a Meta sign-in mints. The platform keeps it fresh. Strict routers serve these supplied plans, but never subscriptions the gateway resolved itself. A plan serves only its own provider's models; a Claude Code plan serves only Claude Code clients, and a Muse Code plan only Muse Code clients (recognized by their `x-client-id`);
|
|
123
|
-
- resolve routers with `strict: true` to serve only the requested `dari/model/<id>` at the requested thinking level, with no selector, lease,
|
|
124
|
+
- resolve routers with `strict: true` to serve only the requested `dari/model/<id>` at the requested thinking level, with no selector, lease, or personal subscription. Set `strict_providers: ["openai"]` on the resolved router to permit any bundled-catalog model from that provider without enumerating `enabled_models`. Use `dari/model/openai/gpt-5.5`; bare IDs and unknown models remain refused. This grant is ignored on non-strict routers and does not discover uncatalogued models;
|
|
125
|
+
- resolve routers with `session_pinning: true` to route each session once and serve that pair for the rest of the session. Each subagent (Claude Code agent id, Codex subagent thread, OpenCode child session) is routed once on its own. Pins are kept through `readSessionPin` and `writeSessionPin`, keyed by router and an opaque pin key; background requests and `dari/model/<id>` requests never use them;
|
|
124
126
|
- read exact usage from `recordRequest`: the summary carries input, output, cache-read, and cache-write tokens and `total_cost_usd`, preferring the provider's billed cost (`cost_source: "provider"`, from OpenRouter and Vercel AI Gateway). Failed and aborted requests are recorded too;
|
|
125
127
|
- attribute cost with `X-Dari-Tags: run=r1,trial=3`, which is parsed into `RequestRecord.tags` and the `recordUsage` payload's `tags`.
|
|
126
128
|
|
|
127
129
|
## Development
|
|
128
130
|
|
|
129
131
|
```bash
|
|
130
|
-
(cd ../dari-router && bun install --frozen-lockfile && bun run build)
|
|
131
132
|
bun install --frozen-lockfile
|
|
132
133
|
bun run typecheck
|
|
133
134
|
bun test
|
|
134
135
|
```
|
|
135
136
|
|
|
137
|
+
This package depends on the published `@mupt-ai/dari-router`. To test against a local router checkout inside the monorepo, build it and link it after installing:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
(cd ../dari-router && bun install --frozen-lockfile && bun run build)
|
|
141
|
+
../scripts/link-gateway-router.sh
|
|
142
|
+
```
|
|
143
|
+
|
|
136
144
|
Tests make no provider or network calls.
|
|
137
145
|
|
|
138
146
|
## License
|
|
@@ -9,11 +9,12 @@ routers:
|
|
|
9
9
|
- anthropic/claude-sonnet-5-5
|
|
10
10
|
- id: openai/gpt-5.5
|
|
11
11
|
thinking: [medium, high]
|
|
12
|
-
# Retry the next model when a provider fails (default true).
|
|
13
|
-
fallback: true
|
|
14
12
|
# Serve with connected Claude Code / ChatGPT subscriptions before API
|
|
15
13
|
# keys (default true).
|
|
16
14
|
subscriptions: true
|
|
15
|
+
# Route each session's first turn, then keep that model for the whole
|
|
16
|
+
# session; each subagent is routed once on its own (default false).
|
|
17
|
+
sessionPinning: false
|
|
17
18
|
|
|
18
19
|
local:
|
|
19
20
|
models:
|
package/dist/http/auth.js
CHANGED
|
@@ -4,7 +4,7 @@ export function bearerToken(headers) {
|
|
|
4
4
|
const value = headers.get("authorization") ?? "";
|
|
5
5
|
const [scheme, token] = value.split(/\s+/, 2);
|
|
6
6
|
if (scheme?.toLowerCase() !== "bearer" || !token) {
|
|
7
|
-
throw new HttpError(401, "Missing or invalid
|
|
7
|
+
throw new HttpError(401, "Missing or invalid API key.", "authentication_error");
|
|
8
8
|
}
|
|
9
9
|
return publicRoutingToken(token);
|
|
10
10
|
}
|
|
@@ -14,7 +14,7 @@ export function anthropicToken(headers) {
|
|
|
14
14
|
return bearerToken(headers);
|
|
15
15
|
const apiKey = headers.get("x-api-key");
|
|
16
16
|
if (!apiKey) {
|
|
17
|
-
throw new HttpError(401, "Missing or invalid
|
|
17
|
+
throw new HttpError(401, "Missing or invalid API key.", "authentication_error");
|
|
18
18
|
}
|
|
19
19
|
return publicRoutingToken(apiKey);
|
|
20
20
|
}
|
|
@@ -3,4 +3,4 @@ export type Http2RoutingServer = {
|
|
|
3
3
|
readonly port: number;
|
|
4
4
|
stop(force?: boolean): Promise<void>;
|
|
5
5
|
};
|
|
6
|
-
export declare function startHttp2Server(port: number, handler: Http2RequestHandler): Http2RoutingServer;
|
|
6
|
+
export declare function startHttp2Server(port: number, handler: Http2RequestHandler, hostname?: string): Http2RoutingServer;
|
|
@@ -8,7 +8,7 @@ const HTTP2_FORBIDDEN_RESPONSE_HEADERS = new Set([
|
|
|
8
8
|
"transfer-encoding",
|
|
9
9
|
"upgrade",
|
|
10
10
|
]);
|
|
11
|
-
export function startHttp2Server(port, handler) {
|
|
11
|
+
export function startHttp2Server(port, handler, hostname = "0.0.0.0") {
|
|
12
12
|
const sessions = new Set();
|
|
13
13
|
let closePromise;
|
|
14
14
|
const server = createServer((request, response) => {
|
|
@@ -18,10 +18,10 @@ export function startHttp2Server(port, handler) {
|
|
|
18
18
|
sessions.add(session);
|
|
19
19
|
session.once("close", () => sessions.delete(session));
|
|
20
20
|
});
|
|
21
|
-
server.listen(port,
|
|
21
|
+
server.listen(port, hostname);
|
|
22
22
|
const address = server.address();
|
|
23
23
|
if (address === null || typeof address === "string") {
|
|
24
|
-
throw new Error("HTTP/2
|
|
24
|
+
throw new Error("HTTP/2 gateway server did not bind to a TCP port.");
|
|
25
25
|
}
|
|
26
26
|
return {
|
|
27
27
|
port: address.port,
|
package/dist/http/serve.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ type StoppableServer = {
|
|
|
3
3
|
};
|
|
4
4
|
export declare function startServer(listenPort: number, handler: (request: Request) => Promise<Response>, options?: {
|
|
5
5
|
http2?: boolean;
|
|
6
|
+
hostname?: string;
|
|
6
7
|
}): import("./http2_server.js").Http2RoutingServer | Bun.Server<undefined>;
|
|
7
8
|
/**
|
|
8
9
|
* On SIGTERM/SIGINT: stop accepting work, let active requests finish, drain
|
package/dist/http/serve.js
CHANGED
|
@@ -9,13 +9,14 @@ const FORCE_STOP_SETTLE_TIMEOUT_MS = 250;
|
|
|
9
9
|
const SHUTDOWN_WORK_TIMEOUT_MS = 7_500;
|
|
10
10
|
export function startServer(listenPort, handler, options = {}) {
|
|
11
11
|
if (options.http2) {
|
|
12
|
-
return startHttp2Server(listenPort, handler);
|
|
12
|
+
return startHttp2Server(listenPort, handler, options.hostname);
|
|
13
13
|
}
|
|
14
14
|
// The selector runs before the first response byte, and routed models can
|
|
15
15
|
// remain silent while reasoning. Disable Bun's default 10s idle timeout;
|
|
16
16
|
// clients and infrastructure still bound total request lifetime.
|
|
17
17
|
return Bun.serve({
|
|
18
18
|
port: listenPort,
|
|
19
|
+
hostname: options.hostname,
|
|
19
20
|
fetch: handler,
|
|
20
21
|
idleTimeout: 0,
|
|
21
22
|
});
|
|
@@ -31,14 +32,14 @@ export function installShutdownHandlers(server, closeClients) {
|
|
|
31
32
|
return;
|
|
32
33
|
shuttingDown = true;
|
|
33
34
|
const workDeadlineAtMs = performance.now() + SHUTDOWN_WORK_TIMEOUT_MS;
|
|
34
|
-
console.info("
|
|
35
|
+
console.info("gateway server shutting down", JSON.stringify({ signal }));
|
|
35
36
|
try {
|
|
36
37
|
await stopServerForShutdown(server);
|
|
37
38
|
}
|
|
38
39
|
finally {
|
|
39
40
|
const remaining = await drainBackgroundTelemetryFor(Math.max(0, workDeadlineAtMs - performance.now()));
|
|
40
41
|
if (remaining > 0) {
|
|
41
|
-
console.warn("
|
|
42
|
+
console.warn("gateway server background drain timed out", JSON.stringify({ remaining_task_count: remaining }));
|
|
42
43
|
captureOperationalError(new Error(`Routing shutdown abandoned ${remaining} background task(s).`), { code: "background_drain_timeout" });
|
|
43
44
|
}
|
|
44
45
|
await closeClients();
|
|
@@ -73,6 +74,6 @@ function stopWithin(server, force, timeoutMs) {
|
|
|
73
74
|
]);
|
|
74
75
|
}
|
|
75
76
|
function reportServerStopFailure(error) {
|
|
76
|
-
console.warn("
|
|
77
|
+
console.warn("gateway server shutdown failed", error instanceof Error ? error.name : "UnknownError");
|
|
77
78
|
captureOperationalError(error, { code: "server_force_stop_failed" });
|
|
78
79
|
}
|
|
@@ -4,7 +4,7 @@ import { captureRequestError } from "../observability/errors.js";
|
|
|
4
4
|
import { chatCompletionResponse, chatCompletionsSseStream } from "../protocol/output/chat_completions.js";
|
|
5
5
|
import { chatCompletionsToInteraction } from "../protocol/input/chat_completions.js";
|
|
6
6
|
import { handleRoutedCompletion } from "../runtime/routed_completion.js";
|
|
7
|
-
import { chatCompletionsConversationIdentity, openCodeRequestContext, } from "../runtime/harness_identity/index.js";
|
|
7
|
+
import { chatCompletionsConversationIdentity, openCodeRequestContext, piRequestContext, } from "../runtime/harness_identity/index.js";
|
|
8
8
|
import { parseJsonRequest } from "./json.js";
|
|
9
9
|
import { museCodeClientId } from "./muse_code.js";
|
|
10
10
|
export async function handleChatCompletions(request, routerId, selectorBackend) {
|
|
@@ -12,7 +12,7 @@ export async function handleChatCompletions(request, routerId, selectorBackend)
|
|
|
12
12
|
const apiKey = bearerToken(request.headers);
|
|
13
13
|
const payload = await parseJsonRequest(request);
|
|
14
14
|
const interaction = chatCompletionsToInteraction({ ...payload, source_protocol: "openai_chat" });
|
|
15
|
-
const agentContext = openCodeRequestContext(request.headers);
|
|
15
|
+
const agentContext = piRequestContext(request.headers) ?? openCodeRequestContext(request.headers);
|
|
16
16
|
const museClientId = museCodeClientId(request.headers);
|
|
17
17
|
return await handleRoutedCompletion({
|
|
18
18
|
request,
|
package/dist/ingress/codex.js
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";
|
|
@@ -27,8 +28,9 @@ export async function handleResponses(request, routerId, selectorBackend) {
|
|
|
27
28
|
// providers. That preference describes a direct model, not a heterogeneous
|
|
28
29
|
// router, so let Dari select the reasoning effort for verified Codex turns.
|
|
29
30
|
// Parsing above still validates malformed Responses reasoning controls.
|
|
30
|
-
if (harnessConversationIdentity !== undefined)
|
|
31
|
+
if (harnessConversationIdentity !== undefined && explicitModelId(payload.model) === null) {
|
|
31
32
|
payload.reasoning = undefined;
|
|
33
|
+
}
|
|
32
34
|
return await handleRoutedCompletion({
|
|
33
35
|
request,
|
|
34
36
|
routerId,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ResolvedRouter } from "../types.js";
|
|
2
|
-
export declare function codexModelsResponse(router: Pick<ResolvedRouter, "enabled_models" | "model_configs" | "allow_long_context">, imageInputFor: (model: string) => boolean): Record<string, unknown>;
|
|
2
|
+
export declare function codexModelsResponse(router: Pick<ResolvedRouter, "enabled_models" | "model_configs" | "allow_long_context" | "autorouting_enabled">, imageInputFor: (model: string) => boolean): Record<string, unknown>;
|
|
@@ -22,8 +22,11 @@ function codexCatalogEntry(args) {
|
|
|
22
22
|
slug: args.slug,
|
|
23
23
|
display_name: args.displayName,
|
|
24
24
|
description: args.description,
|
|
25
|
-
default_reasoning_level: null,
|
|
26
|
-
supported_reasoning_levels: []
|
|
25
|
+
default_reasoning_level: args.defaultReasoningLevel ?? null,
|
|
26
|
+
supported_reasoning_levels: (args.reasoningLevels ?? []).map((effort) => ({
|
|
27
|
+
effort,
|
|
28
|
+
description: `${effort} thinking`,
|
|
29
|
+
})),
|
|
27
30
|
shell_type: "shell_command",
|
|
28
31
|
visibility: "list",
|
|
29
32
|
supported_in_api: true,
|
|
@@ -61,20 +64,22 @@ function codexCatalogEntry(args) {
|
|
|
61
64
|
multi_agent_version: null,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
|
-
/* `dari/routing`
|
|
65
|
-
model follows as `dari/model/<id>` so
|
|
67
|
+
/* `dari/routing` comes first while the router may autoroute (the Codex
|
|
68
|
+
launcher patches it); each enabled model follows as `dari/model/<id>` so
|
|
69
|
+
Codex's /model menu can switch to it. The `dari` launchers build every
|
|
70
|
+
agent's model menu from this list. */
|
|
66
71
|
export function codexModelsResponse(router, imageInputFor) {
|
|
67
72
|
return {
|
|
68
73
|
models: [
|
|
69
|
-
codexCatalogEntry({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
...(router.autorouting_enabled === false ? [] : [codexCatalogEntry({
|
|
75
|
+
slug: CANONICAL_ROUTER_MODEL,
|
|
76
|
+
displayName: "Dari Routing",
|
|
77
|
+
description: "Routes Codex HTTP Responses requests through the configured Dari model router.",
|
|
78
|
+
priority: 0,
|
|
79
|
+
contextWindow: codexRouterContextWindow(router),
|
|
80
|
+
hostedWebSearch: codexRouterSupportsHostedWebSearch(router),
|
|
81
|
+
imageInput: router.enabled_models.some(imageInputFor),
|
|
82
|
+
})]),
|
|
78
83
|
...router.enabled_models.map((model, index) => codexCatalogEntry({
|
|
79
84
|
slug: explicitModelSlug(model),
|
|
80
85
|
displayName: model,
|
|
@@ -83,6 +88,10 @@ export function codexModelsResponse(router, imageInputFor) {
|
|
|
83
88
|
contextWindow: codexRouterContextWindow({ ...router, enabled_models: [model] }),
|
|
84
89
|
hostedWebSearch: codexRouterSupportsHostedWebSearch({ ...router, enabled_models: [model] }),
|
|
85
90
|
imageInput: imageInputFor(model),
|
|
91
|
+
reasoningLevels: router.model_configs?.[model]?.supported_thinking_levels
|
|
92
|
+
?.map((effort) => effort === "off" ? "none" : effort),
|
|
93
|
+
defaultReasoningLevel: router.model_configs?.[model]?.default_thinking_level === "off"
|
|
94
|
+
? "none" : router.model_configs?.[model]?.default_thinking_level,
|
|
86
95
|
})),
|
|
87
96
|
],
|
|
88
97
|
};
|
|
@@ -33,6 +33,10 @@ export type GatewayPlatform = {
|
|
|
33
33
|
writePrefixState(write: PrefixStateWrite): Promise<void>;
|
|
34
34
|
/** Set or clear a conversation's prefetched routing lease. */
|
|
35
35
|
writePendingLease(write: PendingLeaseWrite): Promise<void>;
|
|
36
|
+
/** The pair a session pinned on a session-pinning router, or null. */
|
|
37
|
+
readSessionPin(request: SessionPinKey): Promise<SessionPin | null>;
|
|
38
|
+
/** Pin a session to a pair, replacing any earlier pin. */
|
|
39
|
+
writeSessionPin(write: SessionPinKey & SessionPin): Promise<void>;
|
|
36
40
|
/**
|
|
37
41
|
* Hold an exhausted personal subscription until it resets. Resolves whether
|
|
38
42
|
* another connected subscription for the same provider can serve a retry.
|
|
@@ -84,6 +88,14 @@ export type PendingLeaseWrite = {
|
|
|
84
88
|
output_text: string | null;
|
|
85
89
|
} | null;
|
|
86
90
|
};
|
|
91
|
+
export type SessionPinKey = {
|
|
92
|
+
routerId: string;
|
|
93
|
+
pinKey: string;
|
|
94
|
+
};
|
|
95
|
+
export type SessionPin = {
|
|
96
|
+
model: string;
|
|
97
|
+
reasoningEffort: ReasoningEffort;
|
|
98
|
+
};
|
|
87
99
|
export type SubscriptionHold = {
|
|
88
100
|
userId: string;
|
|
89
101
|
credentialId: string | undefined;
|
|
@@ -117,8 +129,6 @@ export type RequestSummary = {
|
|
|
117
129
|
selected_reasoning_effort: ReasoningEffort | null;
|
|
118
130
|
next_model: string | null;
|
|
119
131
|
next_reasoning_effort: ReasoningEffort | null;
|
|
120
|
-
fallback_model: string | null;
|
|
121
|
-
fallback_reasoning_effort: ReasoningEffort | null;
|
|
122
132
|
provider_key_source: ProviderKeySource | null;
|
|
123
133
|
message_count: number;
|
|
124
134
|
tool_count: number;
|
|
@@ -145,7 +155,7 @@ export type RequestErrorContext = {
|
|
|
145
155
|
sourceProtocol: SourceProtocol | "openai_models" | "anthropic_models";
|
|
146
156
|
status: number;
|
|
147
157
|
};
|
|
148
|
-
export type OperationalErrorCode = "background_drain_timeout" | "billing_usage_report_failed" | "routing_report_assembly_failed" | "routing_report_barrier_failed" | "server_force_stop_failed" | "stream_report_finalization_failed" | "subscription_unavailable_ingest_failed";
|
|
158
|
+
export type OperationalErrorCode = "background_drain_timeout" | "billing_usage_report_failed" | "routing_report_assembly_failed" | "routing_report_barrier_failed" | "server_force_stop_failed" | "session_pin_write_failed" | "stream_report_finalization_failed" | "subscription_unavailable_ingest_failed";
|
|
149
159
|
export type OperationalErrorContext = {
|
|
150
160
|
code: OperationalErrorCode;
|
|
151
161
|
requestId?: string;
|
|
@@ -499,10 +499,8 @@ function reasoningControls(value) {
|
|
|
499
499
|
if (!isRecord(value))
|
|
500
500
|
throw new HttpError(400, "reasoning must be an object.", "invalid_request_error", "reasoning");
|
|
501
501
|
const normalizedEffort = normalizedReasoningEffort(value.effort, "reasoning.effort");
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
throw new HttpError(400, "reasoning.summary is not supported by this router MVP.", "invalid_request_error", "reasoning.summary");
|
|
505
|
-
}
|
|
502
|
+
// Summaries are display-only: clients such as Muse Code ask for them by
|
|
503
|
+
// default, and the router serves the turn without them.
|
|
506
504
|
return normalizedEffort === undefined ? undefined : { effort: normalizedEffort };
|
|
507
505
|
}
|
|
508
506
|
function normalizedReasoningEffort(value, param) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InteractionOutput } from "../../interaction/types.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ServedDecision } from "../../types.js";
|
|
3
3
|
export declare function anthropicMessageId(requestId: string): string;
|
|
4
4
|
export declare function anthropicContent(output: InteractionOutput): Array<Record<string, unknown>>;
|
|
5
5
|
export declare function anthropicReasoningBlock(item: Extract<InteractionOutput["items"][number], {
|
|
@@ -7,4 +7,4 @@ export declare function anthropicReasoningBlock(item: Extract<InteractionOutput[
|
|
|
7
7
|
}>): Record<string, unknown>;
|
|
8
8
|
export declare function anthropicStopReason(output: Pick<InteractionOutput, "stopReason">): string;
|
|
9
9
|
export declare function anthropicUsage(output: Pick<InteractionOutput, "usage">): Record<string, unknown>;
|
|
10
|
-
export declare function anthropicRouting(decision:
|
|
10
|
+
export declare function anthropicRouting(decision: ServedDecision, requestId: string, conversationId?: string): Record<string, unknown>;
|
|
@@ -103,6 +103,7 @@ export function anthropicRouting(decision, requestId, conversationId) {
|
|
|
103
103
|
...(decision.leaseTurnsRemaining === undefined
|
|
104
104
|
? {}
|
|
105
105
|
: { lease_turns_remaining: decision.leaseTurnsRemaining }),
|
|
106
|
+
...(decision.sessionPinned === true ? { session_pinned: true } : {}),
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
function toolInput(kind, value) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { openAIResponsesOutputItem, openAIResponsesOutputItems, openAIResponsesResponseId, openAIResponsesRouting, openAIResponsesTerminal, openAIResponsesUsage, } from "./openai_responses_output.js";
|
|
2
|
-
import { piMessageToInteractionOutput } from "../../providers/pi_output.js";
|
|
2
|
+
import { piMessageToInteractionOutput, textSignature } from "../../providers/pi_output.js";
|
|
3
3
|
import { isProviderFailureMessage, providerFailureFromEvent, providerFailureMessage, } from "../../runtime/provider_stream_outcome.js";
|
|
4
4
|
import { sseStream } from "./sse_stream.js";
|
|
5
5
|
export function openAIResponsesSseStream(args) {
|
|
@@ -78,6 +78,7 @@ class ResponsesAccumulator {
|
|
|
78
78
|
addedIndexes = new Set();
|
|
79
79
|
completedIndexes = new Set();
|
|
80
80
|
messageIds = new Map();
|
|
81
|
+
messagePhases = new Map();
|
|
81
82
|
reasoningIds = new Map();
|
|
82
83
|
messagePartStarted = new Set();
|
|
83
84
|
reasoningPartStarted = new Set();
|
|
@@ -102,6 +103,8 @@ class ResponsesAccumulator {
|
|
|
102
103
|
this.activate(index, send);
|
|
103
104
|
if (type === "thinking_start")
|
|
104
105
|
this.ensureReasoning(index, send);
|
|
106
|
+
if (type === "text_start")
|
|
107
|
+
this.rememberMessageSignature(event, index);
|
|
105
108
|
return;
|
|
106
109
|
}
|
|
107
110
|
if (type === "text_delta" && typeof event.delta === "string" && event.delta) {
|
|
@@ -273,18 +276,31 @@ class ResponsesAccumulator {
|
|
|
273
276
|
currentUsage() {
|
|
274
277
|
return openAIResponsesUsage(this.finalOutput ?? this.baseOutput());
|
|
275
278
|
}
|
|
279
|
+
// Pi signs a Responses text block with its message id and phase at
|
|
280
|
+
// text_start, so the item is announced with the phase it ends with.
|
|
281
|
+
rememberMessageSignature(event, index) {
|
|
282
|
+
const content = isRecord(event.partial) ? event.partial.content : undefined;
|
|
283
|
+
const block = Array.isArray(content) ? content[event.contentIndex] : undefined;
|
|
284
|
+
const signature = isRecord(block) ? textSignature(block.textSignature) : null;
|
|
285
|
+
if (signature?.id && !this.messageIds.has(index))
|
|
286
|
+
this.messageIds.set(index, signature.id);
|
|
287
|
+
if (signature?.phase)
|
|
288
|
+
this.messagePhases.set(index, signature.phase);
|
|
289
|
+
}
|
|
276
290
|
ensureMessage(index, send) {
|
|
277
291
|
const existing = this.items.get(index);
|
|
278
292
|
if (existing?.type === "message")
|
|
279
293
|
return existing;
|
|
280
294
|
const id = this.messageIds.get(index) ?? streamedItemId("msg", this.responseId, index);
|
|
281
295
|
this.messageIds.set(index, id);
|
|
296
|
+
const phase = this.messagePhases.get(index);
|
|
282
297
|
const message = {
|
|
283
298
|
type: "message",
|
|
284
299
|
id,
|
|
285
300
|
role: "assistant",
|
|
286
301
|
content: [{ type: "text", text: "" }],
|
|
287
302
|
contentShape: "parts",
|
|
303
|
+
...(phase ? { phase } : {}),
|
|
288
304
|
source: this.args.source,
|
|
289
305
|
};
|
|
290
306
|
this.items.set(index, message);
|
|
@@ -297,6 +313,7 @@ class ResponsesAccumulator {
|
|
|
297
313
|
id,
|
|
298
314
|
status: "in_progress",
|
|
299
315
|
content: [],
|
|
316
|
+
...(phase ? { phase } : {}),
|
|
300
317
|
},
|
|
301
318
|
});
|
|
302
319
|
this.sendMessagePartAdded(index, id, send);
|
|
@@ -363,6 +380,7 @@ class ResponsesAccumulator {
|
|
|
363
380
|
id: serialized.id,
|
|
364
381
|
status: "in_progress",
|
|
365
382
|
content: [],
|
|
383
|
+
...(item.phase ? { phase: item.phase } : {}),
|
|
366
384
|
},
|
|
367
385
|
});
|
|
368
386
|
}
|
|
@@ -7,9 +7,7 @@ import { type ProviderExecution, type StartedCompletion, type StartedStream } fr
|
|
|
7
7
|
export type ProviderChoice = {
|
|
8
8
|
decision: RoutingDecision;
|
|
9
9
|
execution: ProviderExecution;
|
|
10
|
-
role: "primary" | "fallback";
|
|
11
10
|
};
|
|
12
|
-
export type ResolveFallbackChoice = () => Promise<ProviderChoice | null>;
|
|
13
11
|
export type PreparedStreamExecution = {
|
|
14
12
|
choice: ProviderChoice;
|
|
15
13
|
started: StartedStream;
|
|
@@ -27,7 +25,6 @@ export declare function executeNonStreaming(args: {
|
|
|
27
25
|
interaction: InteractionRequest;
|
|
28
26
|
primary: ProviderChoice;
|
|
29
27
|
primaryRetries: number;
|
|
30
|
-
resolveFallback: ResolveFallbackChoice;
|
|
31
28
|
reporter: RoutedRuntimeReporter;
|
|
32
29
|
protocol: RoutedProtocol;
|
|
33
30
|
startedCompletion?: StartedCompletion;
|
|
@@ -39,7 +36,6 @@ export declare function completeNonStreaming(args: {
|
|
|
39
36
|
interaction: InteractionRequest;
|
|
40
37
|
primary: ProviderChoice;
|
|
41
38
|
primaryRetries: number;
|
|
42
|
-
resolveFallback: ResolveFallbackChoice;
|
|
43
39
|
reporter: RoutedRuntimeReporter;
|
|
44
40
|
protocol: RoutedProtocol;
|
|
45
41
|
startedCompletion?: StartedCompletion;
|
|
@@ -51,7 +47,6 @@ export declare function prepareStreamingExecution(args: {
|
|
|
51
47
|
interaction: InteractionRequest;
|
|
52
48
|
primary: ProviderChoice;
|
|
53
49
|
primaryRetries: number;
|
|
54
|
-
resolveFallback: ResolveFallbackChoice;
|
|
55
50
|
reporter: RoutedRuntimeReporter;
|
|
56
51
|
startedStream?: StartedStream;
|
|
57
52
|
initialError?: unknown;
|
|
@@ -58,7 +58,7 @@ export async function completeNonStreaming(args) {
|
|
|
58
58
|
output,
|
|
59
59
|
modelCost: args.reporter.modelCost({
|
|
60
60
|
model: choice.decision.selectedModel, reasoning_effort: choice.decision.reasoningEffort,
|
|
61
|
-
|
|
61
|
+
attempt: result.attempt, outcome: "completed", billingOutcome: "completed",
|
|
62
62
|
execution: reportedExecution(choice.execution), message: completion.message,
|
|
63
63
|
}),
|
|
64
64
|
request: args.interaction,
|
|
@@ -121,7 +121,6 @@ async function completeWithRetries(args) {
|
|
|
121
121
|
return runProviderAttemptPlan({
|
|
122
122
|
primary: args.primary,
|
|
123
123
|
retries: args.primaryRetries,
|
|
124
|
-
resolveFallback: args.resolveFallback,
|
|
125
124
|
primaryInitial: args.startedCompletion,
|
|
126
125
|
isCustomerCanceled: () => customerAbortRequested(args.clientSignal),
|
|
127
126
|
canceledFailure: customerCanceledProviderMessage,
|
|
@@ -255,7 +254,6 @@ export async function prepareStreamingExecution(args) {
|
|
|
255
254
|
const result = await runProviderAttemptPlan({
|
|
256
255
|
primary: args.primary,
|
|
257
256
|
retries: args.primaryRetries,
|
|
258
|
-
resolveFallback: args.resolveFallback,
|
|
259
257
|
primaryInitial,
|
|
260
258
|
isCustomerCanceled: () => customerAbortRequested(args.clientSignal),
|
|
261
259
|
canceledFailure: customerCanceledProviderMessage,
|
|
@@ -307,7 +305,7 @@ async function runStreamSetupAttempt(args) {
|
|
|
307
305
|
clientSignal: args.clientSignal,
|
|
308
306
|
});
|
|
309
307
|
// Prime before committing so connection failures and prompt rejections
|
|
310
|
-
// stay inside the one shared retry
|
|
308
|
+
// stay inside the one shared retry state machine.
|
|
311
309
|
const primed = await primeProviderStream(started.providerStream.events, started.customerAbort);
|
|
312
310
|
if (primed.outcome === "error") {
|
|
313
311
|
started.customerAbort.dispose();
|
|
@@ -349,7 +347,6 @@ export async function executeStreaming(args) {
|
|
|
349
347
|
reporter: args.reporter,
|
|
350
348
|
protocol: args.protocol,
|
|
351
349
|
providerLatencyMs: () => args.failedSetupLatencyMs + Date.now() - started.providerStartedMs,
|
|
352
|
-
role: args.choice.role,
|
|
353
350
|
attempt: args.attempt,
|
|
354
351
|
reportBarrier: args.reportBarrier,
|
|
355
352
|
delivered: observed.delivered,
|
|
@@ -367,7 +364,7 @@ export async function executeStreaming(args) {
|
|
|
367
364
|
clientSignal: args.clientSignal,
|
|
368
365
|
modelCost: (message, failed) => args.reporter.modelCost({
|
|
369
366
|
model: args.choice.decision.selectedModel, reasoning_effort: args.choice.decision.reasoningEffort,
|
|
370
|
-
|
|
367
|
+
attempt: args.attempt,
|
|
371
368
|
outcome: failed ? "terminal_error" : "completed",
|
|
372
369
|
billingOutcome: failed ? "provider_failed" : "completed",
|
|
373
370
|
execution: reportedExecution(execution), message,
|
|
@@ -396,7 +393,6 @@ function recordProviderAttempt(reporter, choice, attempt, outcome, billingOutcom
|
|
|
396
393
|
reporter.recordProviderAttempt({
|
|
397
394
|
model: choice.decision.selectedModel,
|
|
398
395
|
reasoning_effort: choice.decision.reasoningEffort,
|
|
399
|
-
role: choice.role,
|
|
400
396
|
attempt,
|
|
401
397
|
outcome,
|
|
402
398
|
execution: reportedExecution(choice.execution),
|
|
@@ -424,7 +420,8 @@ async function providerFailedAttempt(message, latencyMs, execution, responseCapt
|
|
|
424
420
|
}
|
|
425
421
|
// Retrying an exhausted plan cannot succeed. The platform learns about it
|
|
426
422
|
// before this request returns so the next resolve already serves the
|
|
427
|
-
// user's next subscription, or the
|
|
423
|
+
// user's next subscription, or the model's API credential when the router
|
|
424
|
+
// allows subscription API fallback for it.
|
|
428
425
|
const switched = execution.subscriptionUserId !== undefined
|
|
429
426
|
&& await holdSubscription({
|
|
430
427
|
userId: execution.subscriptionUserId,
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { InteractionOutput, InteractionRequest, ProviderIdentity } from "../interaction/types.js";
|
|
2
2
|
export declare function piMessageToInteractionOutput(message: Record<string, unknown>, source: ProviderIdentity, request: Pick<InteractionRequest, "tools">): InteractionOutput;
|
|
3
|
+
export declare function textSignature(value: unknown): {
|
|
4
|
+
id?: string;
|
|
5
|
+
phase?: "commentary" | "final_answer";
|
|
6
|
+
} | null;
|
|
@@ -364,7 +364,7 @@ function reasoningText(value, type) {
|
|
|
364
364
|
return [];
|
|
365
365
|
return value.flatMap((entry) => isRecord(entry) && entry.type === type && typeof entry.text === "string" ? [entry.text] : []);
|
|
366
366
|
}
|
|
367
|
-
function textSignature(value) {
|
|
367
|
+
export function textSignature(value) {
|
|
368
368
|
if (typeof value !== "string" || !value)
|
|
369
369
|
return null;
|
|
370
370
|
try {
|
|
@@ -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>;
|
|
@@ -48,8 +47,8 @@ type RunProviderAttemptPlanArgs<TChoice, TValue, TInitial> = {
|
|
|
48
47
|
};
|
|
49
48
|
/**
|
|
50
49
|
* Owns the order-sensitive provider plan shared by completion and stream setup:
|
|
51
|
-
* retries within
|
|
52
|
-
*
|
|
50
|
+
* retries within the choice, cancellation fences between attempts, and one
|
|
51
|
+
* contiguous attempt record for every failed dispatch.
|
|
53
52
|
*/
|
|
54
53
|
export declare function runProviderAttemptPlan<TChoice, TValue, TInitial>(args: RunProviderAttemptPlanArgs<TChoice, TValue, TInitial>): Promise<ProviderAttemptPlanResult<TChoice, TValue>>;
|
|
55
54
|
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { subscriptionUsageLimitFromFailure } from "./subscription_limits.js";
|
|
2
2
|
/**
|
|
3
3
|
* Owns the order-sensitive provider plan shared by completion and stream setup:
|
|
4
|
-
* retries within
|
|
5
|
-
*
|
|
4
|
+
* retries within the choice, cancellation fences between attempts, and one
|
|
5
|
+
* contiguous attempt record for every failed dispatch.
|
|
6
6
|
*/
|
|
7
7
|
export async function runProviderAttemptPlan(args) {
|
|
8
8
|
if (!Number.isSafeInteger(args.retries) || args.retries < 0) {
|
|
@@ -11,30 +11,20 @@ export async function runProviderAttemptPlan(args) {
|
|
|
11
11
|
const primary = await runChoice(args, args.primary, args.primaryInitial);
|
|
12
12
|
if (primary.kind !== "provider_failed")
|
|
13
13
|
return primary;
|
|
14
|
-
// The user's next subscription serves
|
|
15
|
-
//
|
|
14
|
+
// The user's next subscription serves the client's retry of this choice, so
|
|
15
|
+
// the failure stands even if the client has already gone.
|
|
16
16
|
if (subscriptionUsageLimitFromFailure(primary.failure)?.switched === true)
|
|
17
17
|
return primary;
|
|
18
|
-
// A
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const fallback = await args.resolveFallback();
|
|
29
|
-
if (args.isCustomerCanceled(args.primary))
|
|
30
|
-
return canceledAfterPrimary();
|
|
31
|
-
if (fallback === null)
|
|
32
|
-
return primary;
|
|
33
|
-
const fallbackResult = await runChoice(args, fallback, undefined);
|
|
34
|
-
return {
|
|
35
|
-
...fallbackResult,
|
|
36
|
-
latencyMs: primary.latencyMs + fallbackResult.latencyMs,
|
|
37
|
-
};
|
|
18
|
+
// A client that disconnected while the plan settled is a cancellation,
|
|
19
|
+
// attributed to the only choice that was dispatched.
|
|
20
|
+
if (args.isCustomerCanceled(args.primary)) {
|
|
21
|
+
return syntheticCancellation(args, {
|
|
22
|
+
choice: args.primary,
|
|
23
|
+
attempt: primary.attempt + 1,
|
|
24
|
+
latencyMs: primary.latencyMs,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return primary;
|
|
38
28
|
}
|
|
39
29
|
async function runChoice(args, choice, initial) {
|
|
40
30
|
let latencyMs = 0;
|