@mupt-ai/dari-gateway 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/context/interaction_fingerprint.js +10 -1
- package/dist/ingress/codex.js +5 -3
- package/dist/runtime/harness_identity/conversation.d.ts +4 -0
- package/dist/runtime/harness_identity/extract.d.ts +1 -0
- package/dist/runtime/harness_identity/extract.js +7 -0
- package/dist/runtime/harness_identity/index.d.ts +1 -1
- package/dist/runtime/harness_identity/index.js +1 -1
- package/dist/self_host/config.js +2 -1
- package/dist/self_host/main.js +6 -2
- package/package.json +1 -1
- package/src/context/interaction_fingerprint.ts +15 -1
- package/src/ingress/codex.ts +5 -5
- package/src/runtime/harness_identity/conversation.ts +5 -0
- package/src/runtime/harness_identity/extract.ts +10 -0
- package/src/runtime/harness_identity/index.ts +1 -0
- package/src/self_host/config.ts +4 -1
- package/src/self_host/main.ts +6 -2
package/README.md
CHANGED
|
@@ -36,27 +36,26 @@ The gateway is the same runtime that serves Dari's hosted routing service. Choos
|
|
|
36
36
|
|
|
37
37
|
## Run
|
|
38
38
|
|
|
39
|
-
Requires [Bun](https://bun.sh) 1.3 or later.
|
|
39
|
+
Requires [Bun](https://bun.sh) 1.3 or later. Write a `dari-gateway.yaml` (start from [`dari-gateway.example.yaml`](dari-gateway.example.yaml)), then:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
cp dari-gateway.example.yaml dari-gateway.yaml # edit routers and models
|
|
43
42
|
export DARI_GATEWAY_KEYS="$(openssl rand -hex 24)" # keys your clients send
|
|
44
43
|
export ANTHROPIC_API_KEY=... OPENAI_API_KEY=... # optional with subscriptions
|
|
45
|
-
|
|
46
|
-
bun run start
|
|
44
|
+
bunx @mupt-ai/dari-gateway
|
|
47
45
|
```
|
|
48
46
|
|
|
49
|
-
Or with Docker
|
|
47
|
+
Or with Docker:
|
|
50
48
|
|
|
51
49
|
```bash
|
|
52
|
-
docker build -t dari-gateway .
|
|
53
50
|
docker run -p 8080:8080 \
|
|
54
51
|
-e DARI_GATEWAY_KEYS -e ANTHROPIC_API_KEY \
|
|
55
52
|
-v "$PWD/dari-gateway.yaml:/config/dari-gateway.yaml" \
|
|
56
53
|
-v dari-gateway-data:/data \
|
|
57
|
-
dari-gateway
|
|
54
|
+
ghcr.io/mupt-ai/dari-gateway
|
|
58
55
|
```
|
|
59
56
|
|
|
57
|
+
From a clone, `bun install && bun run start` or `docker build -t dari-gateway .` run the same thing.
|
|
58
|
+
|
|
60
59
|
| Variable | Meaning |
|
|
61
60
|
| --- | --- |
|
|
62
61
|
| `DARI_GATEWAY_KEYS` | Required. Comma-separated bearer keys clients authenticate with. |
|
|
@@ -59,7 +59,7 @@ export function interactionLooseChain(routerId, request) {
|
|
|
59
59
|
// transcript step. Growing history and tool changes preserve identity, while
|
|
60
60
|
// unrelated work reusing the same harness session gets a new group.
|
|
61
61
|
export function interactionConversationPrefix(request) {
|
|
62
|
-
const firstStep = conversationFingerprintSteps(looseFingerprintSteps(request.sourceProtocol, request.items)).at(0);
|
|
62
|
+
const firstStep = conversationFingerprintSteps(looseFingerprintSteps(request.sourceProtocol, request.items.filter((item) => !isOpenClawRuntimeContext(item)))).at(0);
|
|
63
63
|
return sha256(stableStringify({
|
|
64
64
|
version: CONVERSATION_PREFIX_VERSION,
|
|
65
65
|
firstStep: firstStep === undefined
|
|
@@ -67,6 +67,15 @@ export function interactionConversationPrefix(request) {
|
|
|
67
67
|
: canonicalInteractionFingerprintStep(firstStep),
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
|
+
// OpenClaw appends a user message of runtime context (active exec sessions,
|
|
71
|
+
// subagents) to each request and drops it from later history. On a first
|
|
72
|
+
// turn it merges into the first transcript step, so leaving it in would give
|
|
73
|
+
// the first turn a different conversation than the turns after it.
|
|
74
|
+
const OPENCLAW_RUNTIME_CONTEXT = "<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>";
|
|
75
|
+
function isOpenClawRuntimeContext(item) {
|
|
76
|
+
return item.type === "message" && item.role === "user" && item.content.length > 0 &&
|
|
77
|
+
item.content.every((part) => part.type === "text" && part.text.trimStart().startsWith(OPENCLAW_RUNTIME_CONTEXT));
|
|
78
|
+
}
|
|
70
79
|
export function interactionPrefixContext(routerId, request, targets) {
|
|
71
80
|
const chainByModel = new Map();
|
|
72
81
|
const itemIndexByHashByModel = new Map();
|
package/dist/ingress/codex.js
CHANGED
|
@@ -8,7 +8,7 @@ import { openAIResponsesToInteraction } from "../protocol/input/openai_responses
|
|
|
8
8
|
import { parseJsonRequest } from "./json.js";
|
|
9
9
|
import { museCodeClientId } from "./muse_code.js";
|
|
10
10
|
import { handleRoutedCompletion } from "../runtime/routed_completion.js";
|
|
11
|
-
import { codexConversationIdentity, codexRequestPurpose, codexRequestContext, } from "../runtime/harness_identity/index.js";
|
|
11
|
+
import { codexConversationIdentity, codexRequestPurpose, codexRequestContext, museCodeConversationIdentity, } from "../runtime/harness_identity/index.js";
|
|
12
12
|
export async function handleResponses(request, routerId, selectorBackend) {
|
|
13
13
|
try {
|
|
14
14
|
const apiKey = bearerToken(request.headers);
|
|
@@ -23,14 +23,16 @@ export async function handleResponses(request, routerId, selectorBackend) {
|
|
|
23
23
|
...(agentContext === undefined ? {} : { agentContext }),
|
|
24
24
|
...(museClientId === undefined ? {} : { museCodeClientId: museClientId }),
|
|
25
25
|
};
|
|
26
|
-
const
|
|
26
|
+
const codexIdentity = codexConversationIdentity(request.headers, rawPayload);
|
|
27
27
|
// Codex applies its global model_reasoning_effort setting to custom model
|
|
28
28
|
// providers. That preference describes a direct model, not a heterogeneous
|
|
29
29
|
// router, so let Dari select the reasoning effort for verified Codex turns.
|
|
30
30
|
// Parsing above still validates malformed Responses reasoning controls.
|
|
31
|
-
if (
|
|
31
|
+
if (codexIdentity !== undefined && explicitModelId(payload.model) === null) {
|
|
32
32
|
payload.reasoning = undefined;
|
|
33
33
|
}
|
|
34
|
+
const harnessConversationIdentity = codexIdentity
|
|
35
|
+
?? (museClientId === undefined ? undefined : museCodeConversationIdentity(request.headers));
|
|
34
36
|
return await handleRoutedCompletion({
|
|
35
37
|
request,
|
|
36
38
|
routerId,
|
|
@@ -13,6 +13,10 @@ export type HarnessConversationIdentity = {
|
|
|
13
13
|
kind: "openai_chat_session";
|
|
14
14
|
value: string;
|
|
15
15
|
qualifiers?: string[];
|
|
16
|
+
} | {
|
|
17
|
+
kind: "muse_code_thread";
|
|
18
|
+
value: string;
|
|
19
|
+
qualifiers?: string[];
|
|
16
20
|
};
|
|
17
21
|
export type HarnessConversationScope = {
|
|
18
22
|
organizationId: string;
|
|
@@ -6,4 +6,5 @@ export declare function openCodeRequestContext(headers: Headers): AgentRequestCo
|
|
|
6
6
|
export declare function codexConversationIdentity(headers: Headers, payload: Record<string, unknown>): HarnessConversationIdentity | undefined;
|
|
7
7
|
export declare function codexRequestPurpose(headers: Headers, payload: Record<string, unknown>): InteractionRequestPurpose | undefined;
|
|
8
8
|
export declare function codexRequestContext(headers: Headers, payload: Record<string, unknown>): AgentRequestContext | undefined;
|
|
9
|
+
export declare function museCodeConversationIdentity(headers: Headers): HarnessConversationIdentity | undefined;
|
|
9
10
|
export declare function claudeCodeConversationIdentity(headers: Headers, metadataSessionId?: string): HarnessConversationIdentity | undefined;
|
|
@@ -129,6 +129,13 @@ export function codexRequestContext(headers, payload) {
|
|
|
129
129
|
agentType: boundedIdentity(agentKind, "x-codex-turn-metadata.subagent_kind"),
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
|
+
// Muse Code sends its thread on every Responses request. The main agent's
|
|
133
|
+
// thread is its session, and each subagent has its own thread, so each keeps
|
|
134
|
+
// its own conversation and lease.
|
|
135
|
+
export function museCodeConversationIdentity(headers) {
|
|
136
|
+
const threadId = boundedIdentity(headers.get("x-tbh-thread-id"), "x-tbh-thread-id");
|
|
137
|
+
return threadId === undefined ? undefined : { kind: "muse_code_thread", value: threadId };
|
|
138
|
+
}
|
|
132
139
|
export function claudeCodeConversationIdentity(headers, metadataSessionId) {
|
|
133
140
|
const candidates = [
|
|
134
141
|
uuidIdentity(headers.get("x-claude-code-session-id"), "x-claude-code-session-id"),
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { scopedHarnessAgentId, scopedHarnessConversationId, scopedHarnessSessionId, type HarnessConversationIdentity, type HarnessConversationScope, } from "./conversation.js";
|
|
2
|
-
export { chatCompletionsConversationIdentity, claudeCodeConversationIdentity, codexConversationIdentity, codexRequestPurpose, codexRequestContext, openCodeRequestContext, piRequestContext, } from "./extract.js";
|
|
2
|
+
export { chatCompletionsConversationIdentity, claudeCodeConversationIdentity, codexConversationIdentity, codexRequestPurpose, codexRequestContext, museCodeConversationIdentity, openCodeRequestContext, piRequestContext, } from "./extract.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { scopedHarnessAgentId, scopedHarnessConversationId, scopedHarnessSessionId, } from "./conversation.js";
|
|
2
|
-
export { chatCompletionsConversationIdentity, claudeCodeConversationIdentity, codexConversationIdentity, codexRequestPurpose, codexRequestContext, openCodeRequestContext, piRequestContext, } from "./extract.js";
|
|
2
|
+
export { chatCompletionsConversationIdentity, claudeCodeConversationIdentity, codexConversationIdentity, codexRequestPurpose, codexRequestContext, museCodeConversationIdentity, openCodeRequestContext, piRequestContext, } from "./extract.js";
|
package/dist/self_host/config.js
CHANGED
|
@@ -5,7 +5,8 @@ const MODEL_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*\/\S+$/;
|
|
|
5
5
|
export async function loadGatewayConfig(path) {
|
|
6
6
|
const file = Bun.file(path);
|
|
7
7
|
if (!(await file.exists())) {
|
|
8
|
-
throw new Error(`Gateway config not found at ${path}
|
|
8
|
+
throw new Error(`Gateway config not found at ${path}. Write one from dari-gateway.example.yaml, `
|
|
9
|
+
+ "or set DARI_GATEWAY_CONFIG (in the container image, mount it at /config/dari-gateway.yaml).");
|
|
9
10
|
}
|
|
10
11
|
const text = await file.text();
|
|
11
12
|
const raw = path.endsWith(".json") ? JSON.parse(text) : Bun.YAML.parse(text);
|
package/dist/self_host/main.js
CHANGED
|
@@ -106,6 +106,10 @@ export async function startSelfHostedGateway(env = process.env) {
|
|
|
106
106
|
return { server, close: () => db.close() };
|
|
107
107
|
}
|
|
108
108
|
if (import.meta.main) {
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// Startup errors are configuration mistakes: print the message, not a stack.
|
|
110
|
+
const started = await startSelfHostedGateway().catch((error) => {
|
|
111
|
+
console.error(`dari-gateway: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
114
|
+
installShutdownHandlers(started.server, async () => started.close());
|
|
111
115
|
}
|
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.3",
|
|
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": {
|
|
@@ -105,7 +105,10 @@ export function interactionLooseChain(routerId: string, request: InteractionRequ
|
|
|
105
105
|
// unrelated work reusing the same harness session gets a new group.
|
|
106
106
|
export function interactionConversationPrefix(request: InteractionRequest): string {
|
|
107
107
|
const firstStep = conversationFingerprintSteps(
|
|
108
|
-
looseFingerprintSteps(
|
|
108
|
+
looseFingerprintSteps(
|
|
109
|
+
request.sourceProtocol,
|
|
110
|
+
request.items.filter((item) => !isOpenClawRuntimeContext(item)),
|
|
111
|
+
),
|
|
109
112
|
).at(0);
|
|
110
113
|
return sha256(stableStringify({
|
|
111
114
|
version: CONVERSATION_PREFIX_VERSION,
|
|
@@ -115,6 +118,17 @@ export function interactionConversationPrefix(request: InteractionRequest): stri
|
|
|
115
118
|
}));
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
// OpenClaw appends a user message of runtime context (active exec sessions,
|
|
122
|
+
// subagents) to each request and drops it from later history. On a first
|
|
123
|
+
// turn it merges into the first transcript step, so leaving it in would give
|
|
124
|
+
// the first turn a different conversation than the turns after it.
|
|
125
|
+
const OPENCLAW_RUNTIME_CONTEXT = "<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>";
|
|
126
|
+
|
|
127
|
+
function isOpenClawRuntimeContext(item: InteractionItem): boolean {
|
|
128
|
+
return item.type === "message" && item.role === "user" && item.content.length > 0 &&
|
|
129
|
+
item.content.every((part) => part.type === "text" && part.text.trimStart().startsWith(OPENCLAW_RUNTIME_CONTEXT));
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
export function interactionPrefixContext(
|
|
119
133
|
routerId: string,
|
|
120
134
|
request: InteractionRequest,
|
package/src/ingress/codex.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
codexConversationIdentity,
|
|
14
14
|
codexRequestPurpose,
|
|
15
15
|
codexRequestContext,
|
|
16
|
+
museCodeConversationIdentity,
|
|
16
17
|
} from "../runtime/harness_identity/index.js";
|
|
17
18
|
|
|
18
19
|
export async function handleResponses(
|
|
@@ -33,17 +34,16 @@ export async function handleResponses(
|
|
|
33
34
|
...(agentContext === undefined ? {} : { agentContext }),
|
|
34
35
|
...(museClientId === undefined ? {} : { museCodeClientId: museClientId }),
|
|
35
36
|
};
|
|
36
|
-
const
|
|
37
|
-
request.headers,
|
|
38
|
-
rawPayload,
|
|
39
|
-
);
|
|
37
|
+
const codexIdentity = codexConversationIdentity(request.headers, rawPayload);
|
|
40
38
|
// Codex applies its global model_reasoning_effort setting to custom model
|
|
41
39
|
// providers. That preference describes a direct model, not a heterogeneous
|
|
42
40
|
// router, so let Dari select the reasoning effort for verified Codex turns.
|
|
43
41
|
// Parsing above still validates malformed Responses reasoning controls.
|
|
44
|
-
if (
|
|
42
|
+
if (codexIdentity !== undefined && explicitModelId(payload.model) === null) {
|
|
45
43
|
payload.reasoning = undefined;
|
|
46
44
|
}
|
|
45
|
+
const harnessConversationIdentity = codexIdentity
|
|
46
|
+
?? (museClientId === undefined ? undefined : museCodeConversationIdentity(request.headers));
|
|
47
47
|
return await handleRoutedCompletion({
|
|
48
48
|
request,
|
|
49
49
|
routerId,
|
|
@@ -19,6 +19,11 @@ export type HarnessConversationIdentity =
|
|
|
19
19
|
kind: "openai_chat_session";
|
|
20
20
|
value: string;
|
|
21
21
|
qualifiers?: string[];
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
kind: "muse_code_thread";
|
|
25
|
+
value: string;
|
|
26
|
+
qualifiers?: string[];
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
export type HarnessConversationScope = {
|
|
@@ -183,6 +183,16 @@ export function codexRequestContext(
|
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Muse Code sends its thread on every Responses request. The main agent's
|
|
187
|
+
// thread is its session, and each subagent has its own thread, so each keeps
|
|
188
|
+
// its own conversation and lease.
|
|
189
|
+
export function museCodeConversationIdentity(
|
|
190
|
+
headers: Headers,
|
|
191
|
+
): HarnessConversationIdentity | undefined {
|
|
192
|
+
const threadId = boundedIdentity(headers.get("x-tbh-thread-id"), "x-tbh-thread-id");
|
|
193
|
+
return threadId === undefined ? undefined : { kind: "muse_code_thread", value: threadId };
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
export function claudeCodeConversationIdentity(
|
|
187
197
|
headers: Headers,
|
|
188
198
|
metadataSessionId?: string,
|
package/src/self_host/config.ts
CHANGED
|
@@ -65,7 +65,10 @@ const MODEL_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*\/\S+$/;
|
|
|
65
65
|
export async function loadGatewayConfig(path: string): Promise<GatewayConfig> {
|
|
66
66
|
const file = Bun.file(path);
|
|
67
67
|
if (!(await file.exists())) {
|
|
68
|
-
throw new Error(
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Gateway config not found at ${path}. Write one from dari-gateway.example.yaml, `
|
|
70
|
+
+ "or set DARI_GATEWAY_CONFIG (in the container image, mount it at /config/dari-gateway.yaml).",
|
|
71
|
+
);
|
|
69
72
|
}
|
|
70
73
|
const text = await file.text();
|
|
71
74
|
const raw: unknown = path.endsWith(".json") ? JSON.parse(text) : Bun.YAML.parse(text);
|
package/src/self_host/main.ts
CHANGED
|
@@ -118,6 +118,10 @@ export async function startSelfHostedGateway(env: Record<string, string> = proce
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (import.meta.main) {
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// Startup errors are configuration mistakes: print the message, not a stack.
|
|
122
|
+
const started = await startSelfHostedGateway().catch((error: unknown) => {
|
|
123
|
+
console.error(`dari-gateway: ${error instanceof Error ? error.message : String(error)}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
installShutdownHandlers(started.server, async () => started.close());
|
|
123
127
|
}
|