@mupt-ai/dari-gateway 0.3.2 → 0.3.4
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 +10 -11
- package/dari-gateway.example.yaml +1 -1
- package/dist/context/interaction_fingerprint.js +10 -1
- package/dist/context/interaction_fingerprint_format.d.ts +4 -0
- package/dist/context/interaction_fingerprint_format.js +3 -0
- package/dist/context/provider_prompt_accounting.js +52 -4
- package/dist/context/routing_context.d.ts +6 -1
- package/dist/context/routing_context.js +40 -4
- package/dist/ingress/anthropic_messages.js +12 -0
- package/dist/ingress/codex.js +12 -7
- package/dist/interaction/capabilities.d.ts +1 -0
- package/dist/interaction/capabilities.js +6 -0
- package/dist/interaction/document.d.ts +2 -0
- package/dist/interaction/document.js +8 -0
- package/dist/interaction/types.d.ts +7 -1
- package/dist/observability/errors.js +19 -0
- package/dist/platform/platform.d.ts +14 -0
- package/dist/protocol/input/anthropic_messages.js +74 -1
- package/dist/protocol/input/chat_completions.js +6 -0
- package/dist/protocol/input/chat_completions_validation.js +8 -0
- package/dist/protocol/input/openai_responses.js +8 -0
- package/dist/protocol/output/anthropic_messages_output.js +7 -0
- package/dist/providers/key_check.d.ts +31 -0
- package/dist/providers/key_check.js +54 -0
- package/dist/providers/model_capabilities.d.ts +2 -0
- package/dist/providers/model_capabilities.js +15 -0
- package/dist/providers/pi_context.js +13 -5
- 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/runtime/prepare_routed_completion.js +8 -1
- package/dist/self_host/config.d.ts +1 -1
- package/dist/self_host/config.js +2 -1
- package/dist/self_host/main.js +38 -5
- package/dist/self_host/resolve.d.ts +1 -0
- package/dist/self_host/resolve.js +3 -3
- package/dist/strategies/selector_projection.js +3 -0
- package/dist/telemetry/report.d.ts +3 -1
- package/dist/telemetry/report.js +23 -0
- package/package.json +2 -2
- package/src/context/interaction_fingerprint.ts +15 -1
- package/src/context/interaction_fingerprint_format.ts +4 -0
- package/src/context/provider_prompt_accounting.ts +52 -6
- package/src/context/routing_context.ts +56 -2
- package/src/ingress/anthropic_messages.ts +15 -1
- package/src/ingress/codex.ts +12 -9
- package/src/interaction/capabilities.ts +7 -0
- package/src/interaction/document.ts +8 -0
- package/src/interaction/types.ts +8 -1
- package/src/observability/errors.ts +21 -0
- package/src/platform/platform.ts +22 -0
- package/src/protocol/input/anthropic_messages.ts +71 -1
- package/src/protocol/input/chat_completions.ts +6 -0
- package/src/protocol/input/chat_completions_validation.ts +8 -0
- package/src/protocol/input/openai_responses.ts +13 -0
- package/src/protocol/output/anthropic_messages_output.ts +7 -0
- package/src/providers/key_check.ts +76 -0
- package/src/providers/model_capabilities.ts +18 -0
- package/src/providers/pi_context.ts +14 -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/runtime/prepare_routed_completion.ts +12 -1
- package/src/self_host/config.ts +5 -2
- package/src/self_host/main.ts +45 -5
- package/src/self_host/resolve.ts +3 -3
- package/src/strategies/selector_projection.ts +2 -0
- package/src/telemetry/report.ts +34 -1
|
@@ -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,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CandidatePruningAudit } from "@mupt-ai/dari-router/policy-engine";
|
|
2
2
|
|
|
3
3
|
import { interactionConversationPrefix } from "../context/interaction_fingerprint.js";
|
|
4
|
+
import { clampRequestedEffort } from "../context/routing_context.js";
|
|
5
|
+
import { modelMetadataLookup } from "../providers/pi.js";
|
|
4
6
|
import {
|
|
5
7
|
buildRoutingContext,
|
|
6
8
|
derivePrefixLookupHashes,
|
|
@@ -134,7 +136,14 @@ export async function prepareRoutedCompletion(args: {
|
|
|
134
136
|
}
|
|
135
137
|
const access = strictProviderAccess(credentials, args.interaction);
|
|
136
138
|
credentials = access.credentials;
|
|
137
|
-
args = {
|
|
139
|
+
args = {
|
|
140
|
+
...args,
|
|
141
|
+
interaction: clampRequestedEffort(
|
|
142
|
+
access.interaction,
|
|
143
|
+
credentials,
|
|
144
|
+
modelMetadataLookup(credentials.model_configs),
|
|
145
|
+
),
|
|
146
|
+
};
|
|
138
147
|
credentials = await withProviderCredentials(credentials, args.apiKey, args.clientHarness);
|
|
139
148
|
const billable = true;
|
|
140
149
|
const routing = await buildRoutingContext({
|
|
@@ -190,6 +199,8 @@ export async function prepareRoutedCompletion(args: {
|
|
|
190
199
|
identityHit: routing.identityHit,
|
|
191
200
|
hits: routing.hits,
|
|
192
201
|
costEstimates: routing.reportingCostEstimates,
|
|
202
|
+
candidates: candidateResolution.candidates,
|
|
203
|
+
sessionPinKey: sessionPin?.key,
|
|
193
204
|
pruning: reportedPruning(
|
|
194
205
|
candidateResolution.pruning,
|
|
195
206
|
routing.imageCapabilityPrunedModels,
|
package/src/self_host/config.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
* default:
|
|
13
13
|
* models:
|
|
14
14
|
* - anthropic/claude-sonnet-5-5
|
|
15
|
-
* - id: openai/gpt-
|
|
15
|
+
* - id: openai/gpt-6.1-sol
|
|
16
16
|
* thinking: [medium, high]
|
|
17
17
|
* subscriptions: true # serve with connected Claude Code / ChatGPT plans
|
|
18
18
|
* sessionPinning: false # route each session and subagent once, then keep its model
|
|
@@ -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
|
@@ -10,9 +10,11 @@ import { installShutdownHandlers, startServer } from "../http/serve.js";
|
|
|
10
10
|
import { createAutoRoutingSelectorBackend } from "../strategies/auto_routing_selector.js";
|
|
11
11
|
import { createPrioritySelectorBackend } from "../strategies/priority_selector.js";
|
|
12
12
|
import type { SelectorBackend } from "../strategies/routing.js";
|
|
13
|
-
import { loadGatewayConfig } from "./config.js";
|
|
14
13
|
import { createSelfHostedPlatform } from "./platform.js";
|
|
15
|
-
import {
|
|
14
|
+
import { providerForModel } from "@mupt-ai/dari-router/policy-engine";
|
|
15
|
+
import { checkProviderKey } from "../providers/key_check.js";
|
|
16
|
+
import { loadGatewayConfig, type GatewayConfig } from "./config.js";
|
|
17
|
+
import { configuredProviderApiKey, gatewayKeyIndex, providersWithoutCredentials } from "./resolve.js";
|
|
16
18
|
import { PrefixStore } from "./prefix_store.js";
|
|
17
19
|
import { SessionPinStore } from "./session_pin_store.js";
|
|
18
20
|
import { handleSubscriptionRequest, SubscriptionStore } from "./subscriptions/index.js";
|
|
@@ -76,6 +78,8 @@ export async function startSelfHostedGateway(env: Record<string, string> = proce
|
|
|
76
78
|
);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
await checkConfiguredProviderKeys(config, (provider) =>
|
|
82
|
+
configuredProviderApiKey({ config, providerApiKey, env }, provider));
|
|
79
83
|
const autoRoutingKey = env.DARI_AUTO_ROUTING_API_KEY?.trim();
|
|
80
84
|
const selectorBackend: SelectorBackend = autoRoutingKey
|
|
81
85
|
? createAutoRoutingSelectorBackend({
|
|
@@ -109,7 +113,8 @@ export async function startSelfHostedGateway(env: Record<string, string> = proce
|
|
|
109
113
|
"dari-gateway listening",
|
|
110
114
|
JSON.stringify({
|
|
111
115
|
hostname,
|
|
112
|
-
port,
|
|
116
|
+
// The bound port, so PORT=0 reports the port the OS assigned.
|
|
117
|
+
port: server.port,
|
|
113
118
|
routers: Object.keys(config.routers),
|
|
114
119
|
auto_routing: autoRoutingKey ? "dari" : "priority",
|
|
115
120
|
}),
|
|
@@ -117,7 +122,42 @@ export async function startSelfHostedGateway(env: Record<string, string> = proce
|
|
|
117
122
|
return { server, close: () => db.close() };
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Check each provider API key the routers use before serving: a key its
|
|
127
|
+
* provider rejects fails startup, and one no check could answer is reported.
|
|
128
|
+
*/
|
|
129
|
+
async function checkConfiguredProviderKeys(
|
|
130
|
+
config: GatewayConfig,
|
|
131
|
+
providerApiKey: (provider: string) => Promise<string | undefined>,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const providers = new Set(
|
|
134
|
+
Object.values(config.routers).flatMap((router) => router.models.map((model) => providerForModel(model.id))),
|
|
135
|
+
);
|
|
136
|
+
const checks = await Promise.all([...providers].map(async (provider) => {
|
|
137
|
+
const endpoint = config.providers[provider];
|
|
138
|
+
// Endpoints configured without a key variable take no key.
|
|
139
|
+
if (endpoint !== undefined && endpoint.apiKeyEnv === undefined) return null;
|
|
140
|
+
const key = await providerApiKey(provider);
|
|
141
|
+
if (key === undefined) return null;
|
|
142
|
+
return { provider, ...(await checkProviderKey({ provider, key, ...(endpoint ? { endpoint } : {}), fetch })) };
|
|
143
|
+
}));
|
|
144
|
+
const rejected = checks.flatMap((check) => check?.result === "rejected" ? [`${check.provider} (${check.status})`] : []);
|
|
145
|
+
if (rejected.length > 0) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Provider API keys were rejected: ${rejected.join(", ")}. Fix or unset each key before starting the gateway.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const unchecked = checks.flatMap((check) => check?.result === "unchecked" ? [check.provider] : []);
|
|
151
|
+
if (unchecked.length > 0) {
|
|
152
|
+
console.warn("dari-gateway could not check these provider API keys", JSON.stringify({ providers: unchecked }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
if (import.meta.main) {
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
// Startup errors are configuration mistakes: print the message, not a stack.
|
|
158
|
+
const started = await startSelfHostedGateway().catch((error: unknown) => {
|
|
159
|
+
console.error(`dari-gateway: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
});
|
|
162
|
+
installShutdownHandlers(started.server, async () => started.close());
|
|
123
163
|
}
|
package/src/self_host/resolve.ts
CHANGED
|
@@ -86,7 +86,7 @@ export async function resolveSelfHostedRouter(
|
|
|
86
86
|
if (subscription !== null && deps.subscriptions.list(subscription).length > 0) {
|
|
87
87
|
exhausted.add(subscription);
|
|
88
88
|
}
|
|
89
|
-
const apiKey = await
|
|
89
|
+
const apiKey = await configuredProviderApiKey(deps, provider);
|
|
90
90
|
credentials.set(provider, apiKey ? { key: apiKey, source: "user" } : null);
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -154,7 +154,7 @@ export async function resolveSelfHostedRouter(
|
|
|
154
154
|
|
|
155
155
|
// Configured endpoints read their own key variable, or send a placeholder
|
|
156
156
|
// when they take none; known providers use their standard variable.
|
|
157
|
-
async function
|
|
157
|
+
export async function configuredProviderApiKey(
|
|
158
158
|
deps: Pick<SelfHostedResolverDeps, "config" | "providerApiKey" | "env">,
|
|
159
159
|
provider: string,
|
|
160
160
|
): Promise<string | undefined> {
|
|
@@ -226,7 +226,7 @@ export async function providersWithoutCredentials(
|
|
|
226
226
|
: [];
|
|
227
227
|
const connected = plans.some((plan) => plan !== null && deps.subscriptions.list(plan).length > 0);
|
|
228
228
|
if (connected) continue;
|
|
229
|
-
const apiKey = await
|
|
229
|
+
const apiKey = await configuredProviderApiKey(deps, provider);
|
|
230
230
|
if (apiKey === undefined) missing.push(provider);
|
|
231
231
|
}
|
|
232
232
|
return missing;
|
|
@@ -190,6 +190,8 @@ function selectorContentField(
|
|
|
190
190
|
if (part.type === "tool_reference") {
|
|
191
191
|
return { type: "text", text: `Referenced tool: ${part.toolName}` };
|
|
192
192
|
}
|
|
193
|
+
// The selector model is text-only; it sees a marker, not the document bytes.
|
|
194
|
+
if (part.type === "document") return { type: "text", text: "(PDF document omitted from selector input)" };
|
|
193
195
|
return imagePart(part.url, part.detail);
|
|
194
196
|
}),
|
|
195
197
|
};
|
package/src/telemetry/report.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ChainHits,
|
|
12
12
|
type PrefixHit,
|
|
13
13
|
type PricingLookup,
|
|
14
|
+
type RoutingCandidate,
|
|
14
15
|
type RoutingDecision,
|
|
15
16
|
type RoutingStrategy,
|
|
16
17
|
} from "@mupt-ai/dari-router/policy-engine";
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
type ProviderIdentity,
|
|
26
27
|
} from "../interaction/types.js";
|
|
27
28
|
import { captureOperationalError } from "../observability/errors.js";
|
|
28
|
-
import { gatewayPlatform, type RequestTags } from "../platform/platform.js";
|
|
29
|
+
import { gatewayPlatform, type RequestRecordRouter, type RequestTags } from "../platform/platform.js";
|
|
29
30
|
import type {
|
|
30
31
|
ProviderAttemptRecord,
|
|
31
32
|
ProviderAttemptReport,
|
|
@@ -179,6 +180,10 @@ export class RequestReporter {
|
|
|
179
180
|
identityHit?: PrefixHit;
|
|
180
181
|
hits: ChainHits;
|
|
181
182
|
costEstimates: CandidateCostEstimate[];
|
|
183
|
+
// The model and reasoning pairs this request could be routed to.
|
|
184
|
+
candidates: readonly RoutingCandidate[];
|
|
185
|
+
// The session pin this request routed under, when the router pins.
|
|
186
|
+
sessionPinKey?: string;
|
|
182
187
|
// Cache-aware selector-candidate pruning outcome, plus any pre-strategy
|
|
183
188
|
// capability exclusions. Null when custom routing configuration cannot
|
|
184
189
|
// produce candidates.
|
|
@@ -603,6 +608,9 @@ export class RequestReporter {
|
|
|
603
608
|
};
|
|
604
609
|
|
|
605
610
|
gatewayPlatform().recordRequest({
|
|
611
|
+
router: requestRecordRouter(a.credentials),
|
|
612
|
+
candidates: a.candidates.map(({ model, reasoningEffort }) => ({ model, reasoningEffort })),
|
|
613
|
+
...(a.sessionPinKey === undefined ? {} : { sessionPinKey: a.sessionPinKey }),
|
|
606
614
|
row: ingestPayload,
|
|
607
615
|
summary: {
|
|
608
616
|
request_id: a.requestId,
|
|
@@ -786,3 +794,28 @@ function usageIngest(
|
|
|
786
794
|
pricing_known: pricingKnown,
|
|
787
795
|
};
|
|
788
796
|
}
|
|
797
|
+
|
|
798
|
+
function requestRecordRouter(router: ResolvedRouter): RequestRecordRouter {
|
|
799
|
+
return {
|
|
800
|
+
enabledModels: [...router.enabled_models],
|
|
801
|
+
thinkingLevels: Object.fromEntries(
|
|
802
|
+
router.enabled_models.map((model) => [
|
|
803
|
+
model,
|
|
804
|
+
[
|
|
805
|
+
...(router.model_thinking_levels?.[model]
|
|
806
|
+
?? router.model_configs?.[model]?.supported_thinking_levels
|
|
807
|
+
?? []),
|
|
808
|
+
],
|
|
809
|
+
]),
|
|
810
|
+
),
|
|
811
|
+
defaultThinkingLevels: Object.fromEntries(
|
|
812
|
+
router.enabled_models.flatMap((model) => {
|
|
813
|
+
const level = router.model_configs?.[model]?.default_thinking_level;
|
|
814
|
+
return level ? [[model, level]] : [];
|
|
815
|
+
}),
|
|
816
|
+
),
|
|
817
|
+
strict: router.strict === true,
|
|
818
|
+
speculativeRouting: router.speculative_routing === true,
|
|
819
|
+
sessionPinning: router.session_pinning === true,
|
|
820
|
+
};
|
|
821
|
+
}
|