@mupt-ai/dari-gateway 0.2.1 → 0.3.0
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 +8 -4
- package/dist/context/pricing.js +5 -1
- package/dist/http/handler.js +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/ingress/chat_completions.js +8 -1
- package/dist/ingress/codex.js +4 -0
- package/dist/ingress/models.js +13 -1
- package/dist/ingress/muse_code.d.ts +2 -0
- package/dist/ingress/muse_code.js +8 -0
- package/dist/ingress/muse_models.d.ts +9 -0
- package/dist/ingress/muse_models.js +32 -0
- package/dist/interaction/types.d.ts +1 -0
- package/dist/models/catalog.d.ts +35 -0
- package/dist/models/catalog.js +232 -0
- package/dist/models/execution_provider.d.ts +2 -1
- package/dist/models/execution_provider.js +5 -1
- package/dist/models/provider_defaults.js +3 -0
- package/dist/models/runtime_overrides.d.ts +3 -0
- package/dist/models/runtime_overrides.js +17 -0
- package/dist/models/subscriptions.d.ts +8 -0
- package/dist/models/subscriptions.js +37 -0
- package/dist/platform/platform.d.ts +5 -1
- package/dist/protocol/input/chat_completions.js +3 -1
- package/dist/protocol/input/chat_completions_validation.js +2 -1
- package/dist/providers/pi.js +12 -6
- package/dist/providers/pi_options.js +13 -1
- package/dist/providers/provider_call.js +7 -3
- package/dist/providers/subscription_limits.d.ts +4 -2
- package/dist/providers/subscription_limits.js +19 -3
- package/dist/runtime/explicit_model.d.ts +3 -0
- package/dist/runtime/explicit_model.js +10 -6
- package/dist/runtime/prepare_routed_completion.js +24 -5
- package/dist/runtime/strict_provider_access.d.ts +6 -0
- package/dist/runtime/strict_provider_access.js +40 -0
- package/dist/self_host/resolve.js +11 -15
- package/dist/self_host/subscriptions/oauth.d.ts +10 -6
- package/dist/self_host/subscriptions/oauth.js +101 -6
- package/dist/self_host/subscriptions/store.js +1 -0
- package/dist/types.d.ts +4 -1
- package/package.json +2 -2
- package/src/context/pricing.ts +5 -2
- package/src/http/handler.ts +7 -0
- package/src/index.ts +9 -1
- package/src/ingress/chat_completions.ts +8 -1
- package/src/ingress/codex.ts +4 -0
- package/src/ingress/models.ts +13 -1
- package/src/ingress/muse_code.ts +9 -0
- package/src/ingress/muse_models.ts +36 -0
- package/src/interaction/types.ts +2 -0
- package/src/models/catalog.ts +310 -0
- package/src/models/execution_provider.ts +4 -1
- package/src/models/provider_defaults.ts +3 -0
- package/src/models/runtime_overrides.ts +22 -0
- package/src/models/subscriptions.ts +54 -0
- package/src/platform/platform.ts +5 -1
- package/src/protocol/input/chat_completions.ts +3 -1
- package/src/protocol/input/chat_completions_validation.ts +2 -1
- package/src/providers/pi.ts +21 -6
- package/src/providers/pi_options.ts +17 -1
- package/src/providers/provider_call.ts +7 -3
- package/src/providers/subscription_limits.ts +21 -4
- package/src/runtime/explicit_model.ts +15 -16
- package/src/runtime/prepare_routed_completion.ts +22 -3
- package/src/runtime/strict_provider_access.ts +42 -0
- package/src/self_host/resolve.ts +12 -26
- package/src/self_host/subscriptions/oauth.ts +139 -8
- package/src/self_host/subscriptions/store.ts +1 -0
- package/src/types.ts +20 -3
|
@@ -23,7 +23,10 @@ export type GatewayPlatform = {
|
|
|
23
23
|
* Optional. Supply the credential for one enabled model of this request,
|
|
24
24
|
* consulted after `resolveRouter` and before its provider keys (for the
|
|
25
25
|
* self-hosted gateway, its environment keys), so an embedder can serve
|
|
26
|
-
* per-tenant keys from its own store. Undefined keeps the resolved key.
|
|
26
|
+
* per-tenant keys from its own store. Undefined keeps the resolved key. A
|
|
27
|
+
* credential with `subscription` serves the model on that plan, even on a
|
|
28
|
+
* strict router; a plan that cannot serve the model for this client (see
|
|
29
|
+
* SubscriptionKind) is ignored.
|
|
27
30
|
*/
|
|
28
31
|
resolveProviderCredential?(request: ProviderCredentialRequest): Promise<ProviderCredential | undefined>;
|
|
29
32
|
/** Persist one completed turn's prefix entry and/or refresh a read entry. */
|
|
@@ -59,6 +62,7 @@ export type ProviderCredentialRequest = {
|
|
|
59
62
|
routerId: string;
|
|
60
63
|
organizationId: string;
|
|
61
64
|
apiKey: string;
|
|
65
|
+
clientHarness?: ClientHarness;
|
|
62
66
|
};
|
|
63
67
|
export type PrefixStateWrite = {
|
|
64
68
|
organization_id: string;
|
|
@@ -54,7 +54,9 @@ export function internalChatCompletionsToInteraction(request) {
|
|
|
54
54
|
const tools = chatTools(request.tools);
|
|
55
55
|
toolsByWireName(tools);
|
|
56
56
|
const responseFormat = toInteractionResponseFormat(request.response_format);
|
|
57
|
-
|
|
57
|
+
// OpenAI spells the non-reasoning effort "none"; Dari spells it "off".
|
|
58
|
+
const requestedEffort = request.reasoning_effort;
|
|
59
|
+
const reasoningEffort = requestedEffort === "none" ? "off" : request.reasoning_effort;
|
|
58
60
|
return {
|
|
59
61
|
sourceProtocol: request.source_protocol ?? "openai_chat",
|
|
60
62
|
model: request.model,
|
|
@@ -184,7 +184,8 @@ function validateReasoning(request) {
|
|
|
184
184
|
if (raw.reasoning !== undefined) {
|
|
185
185
|
throw new HttpError(400, "reasoning is not supported; use reasoning_effort.", "invalid_request_error", "reasoning");
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
const effort = request.reasoning_effort;
|
|
188
|
+
if (effort !== undefined && effort !== "none" && !isReasoningEffort(effort)) {
|
|
188
189
|
throw new HttpError(400, "reasoning_effort is not supported.", "unsupported_reasoning_effort", "reasoning_effort");
|
|
189
190
|
}
|
|
190
191
|
}
|
package/dist/providers/pi.js
CHANGED
|
@@ -13,7 +13,7 @@ import { REASONING_EFFORTS, isRecord, nativeModelId, providerForModel, } from "@
|
|
|
13
13
|
import { HttpError } from "../http/errors.js";
|
|
14
14
|
import { effectiveContextWindow } from "../models/context_limits.js";
|
|
15
15
|
import { defaultBaseUrl } from "../models/provider_defaults.js";
|
|
16
|
-
import { contextWindowOverride, inputModalitiesOverride, runtimeCompat } from "../models/runtime_overrides.js";
|
|
16
|
+
import { contextWindowOverride, inputModalitiesOverride, providerThinkingLevelOverrides, runtimeCompat, withThinkingLevelOverrides, } from "../models/runtime_overrides.js";
|
|
17
17
|
import { BEDROCK_API } from "./model_capabilities.js";
|
|
18
18
|
import { buildContext } from "./pi_context.js";
|
|
19
19
|
import { providerIdentityFromModel } from "../interaction/provider_continuation_state.js";
|
|
@@ -175,15 +175,20 @@ export function buildModel(pi, modelId, pricing, config) {
|
|
|
175
175
|
// A null catalog value delegates to Pi's registry when it knows the model.
|
|
176
176
|
// Custom OpenAI-compatible models are not necessarily registered, so keep
|
|
177
177
|
// the public effort set permissive and let the provider validate the wire
|
|
178
|
-
// value instead of silently treating an unknown model as off-only.
|
|
179
|
-
|
|
178
|
+
// value instead of silently treating an unknown model as off-only. A
|
|
179
|
+
// built-in provider with its own thinking levels (Meta) serves ids newer
|
|
180
|
+
// than the registry the same way, with only its key configured.
|
|
181
|
+
const providerThinking = providerThinkingLevelOverrides(provider);
|
|
182
|
+
const fallbackThinkingLevels = (config !== undefined || providerThinking !== undefined) &&
|
|
183
|
+
configuredThinkingLevels == null &&
|
|
184
|
+
registered === undefined
|
|
180
185
|
? REASONING_EFFORTS
|
|
181
186
|
: undefined;
|
|
182
187
|
const effectiveThinkingLevels = Array.isArray(configuredThinkingLevels)
|
|
183
188
|
? configuredThinkingLevels
|
|
184
189
|
: fallbackThinkingLevels;
|
|
185
190
|
const configuredThinking = effectiveThinkingLevels
|
|
186
|
-
? thinkingMetadata(effectiveThinkingLevels, registered)
|
|
191
|
+
? thinkingMetadata(effectiveThinkingLevels, registered ?? { thinkingLevelMap: providerThinking })
|
|
187
192
|
: null;
|
|
188
193
|
const defaultThinkingLevel = config?.default_thinking_level ?? undefined;
|
|
189
194
|
const api = config?.api ?? registered?.api ?? defaultApi(provider);
|
|
@@ -243,7 +248,7 @@ function thinkingMetadata(supportedLevels, registered) {
|
|
|
243
248
|
}
|
|
244
249
|
function resolveModelMetadata(pi, provider, modelId, nativeId) {
|
|
245
250
|
const registry = registryProvider(provider, modelId);
|
|
246
|
-
return pi.getModel?.(registry, nativeId) ?? pi.getModel?.(registry, modelId);
|
|
251
|
+
return withThinkingLevelOverrides(provider, pi.getModel?.(registry, nativeId) ?? pi.getModel?.(registry, modelId));
|
|
247
252
|
}
|
|
248
253
|
// Dari's "azure" provider has no Pi registry of its own. One Foundry resource
|
|
249
254
|
// serves OpenAI deployments through Pi's Azure OpenAI Responses registry and
|
|
@@ -307,7 +312,8 @@ function executionPricing(pricing, config) {
|
|
|
307
312
|
return pricing ?? modelPricingFromConfig(config);
|
|
308
313
|
}
|
|
309
314
|
function defaultApi(provider) {
|
|
310
|
-
|
|
315
|
+
// Meta recommends Responses: its Chat Completions drops reasoning between turns.
|
|
316
|
+
if (provider === "openai" || provider === "xai" || provider === "meta")
|
|
311
317
|
return "openai-responses";
|
|
312
318
|
if (provider === "anthropic")
|
|
313
319
|
return "anthropic-messages";
|
|
@@ -23,8 +23,11 @@ export function buildOptions(request, auth, selectedModel, model, pi, sessionId,
|
|
|
23
23
|
const providerAuth = typeof auth === "string" ? { type: "bearer", apiKey: auth } : auth;
|
|
24
24
|
const { generation, toolChoice, parallelToolCalls, responseFormat } = request;
|
|
25
25
|
const headers = {};
|
|
26
|
+
const bearerHeader = sendsBearerToMessages(providerAuth, model);
|
|
27
|
+
if (bearerHeader)
|
|
28
|
+
headers.authorization = `Bearer ${providerAuth.apiKey}`;
|
|
26
29
|
const options = {
|
|
27
|
-
...authenticationOptions(providerAuth, api),
|
|
30
|
+
...(bearerHeader ? {} : authenticationOptions(providerAuth, api)),
|
|
28
31
|
maxRetries: 0,
|
|
29
32
|
timeoutMs,
|
|
30
33
|
signal,
|
|
@@ -38,6 +41,9 @@ export function buildOptions(request, auth, selectedModel, model, pi, sessionId,
|
|
|
38
41
|
if (isClaudeCodeOAuth(providerAuth, api)) {
|
|
39
42
|
headers["user-agent"] = request.claudeCodeUserAgent ?? CLAUDE_CODE_USER_AGENT;
|
|
40
43
|
}
|
|
44
|
+
if (model.provider === "meta" && request.museCodeClientId !== undefined) {
|
|
45
|
+
headers["x-client-id"] = request.museCodeClientId;
|
|
46
|
+
}
|
|
41
47
|
// Pi's Anthropic transport honors compat.supportsTemperature; the OpenAI
|
|
42
48
|
// transports forward temperature regardless, so drop it here for models
|
|
43
49
|
// that reject it instead of failing the whole request.
|
|
@@ -173,6 +179,12 @@ function authenticationOptions(auth, api) {
|
|
|
173
179
|
},
|
|
174
180
|
};
|
|
175
181
|
}
|
|
182
|
+
// Anthropic Messages endpoints outside Anthropic that take the key as a bearer
|
|
183
|
+
// token (Claude Code's ANTHROPIC_AUTH_TOKEN) and reject x-api-key.
|
|
184
|
+
const BEARER_MESSAGES_PROVIDERS = new Set(["meta"]);
|
|
185
|
+
function sendsBearerToMessages(auth, model) {
|
|
186
|
+
return auth.type === "bearer" && isAnthropic(model.api) && BEARER_MESSAGES_PROVIDERS.has(model.provider);
|
|
187
|
+
}
|
|
176
188
|
function isClaudeCodeOAuth(auth, api) {
|
|
177
189
|
return auth.type === "bearer" && isAnthropic(api) && auth.apiKey.includes("sk-ant-oat");
|
|
178
190
|
}
|
|
@@ -19,8 +19,10 @@ export function prepareProviderExecution(args) {
|
|
|
19
19
|
}
|
|
20
20
|
const provider = executionProviderForModel(args.credentials, model);
|
|
21
21
|
const supplied = args.credentials.provider_credentials?.[model];
|
|
22
|
-
const providerKeySource =
|
|
23
|
-
|
|
22
|
+
const providerKeySource = keySourceForModel(args.credentials, model);
|
|
23
|
+
// A strict router serves a plan only when the platform supplied it for this
|
|
24
|
+
// request, never a pooled or personal one it resolved.
|
|
25
|
+
if (args.credentials.strict === true && providerKeySource === "subscription" && !supplied) {
|
|
24
26
|
throw new HttpError(400, `Strict routers do not serve '${model}' from a personal subscription.`, "strict_subscription_refused", "model");
|
|
25
27
|
}
|
|
26
28
|
const auth = supplied
|
|
@@ -47,7 +49,9 @@ export function prepareProviderExecution(args) {
|
|
|
47
49
|
: {}),
|
|
48
50
|
provider,
|
|
49
51
|
providerKeySource,
|
|
50
|
-
|
|
52
|
+
// Only resolved personal subscriptions are held at their usage limit;
|
|
53
|
+
// the platform manages the plans it supplies.
|
|
54
|
+
...(providerKeySource === "subscription" && !supplied && args.credentials.attributed_user_id
|
|
51
55
|
? {
|
|
52
56
|
subscriptionUserId: args.credentials.attributed_user_id,
|
|
53
57
|
subscriptionCredentialId: args.credentials.subscription_credential_ids?.[provider],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ProviderKeySource } from "../types.js";
|
|
2
2
|
import type { CapturedProviderResponse } from "./response_capture.js";
|
|
3
|
-
export type SubscriptionOAuthProvider = "anthropic_claude_code" | "openai_codex";
|
|
3
|
+
export type SubscriptionOAuthProvider = "anthropic_claude_code" | "openai_codex" | "meta_muse";
|
|
4
4
|
export type SubscriptionUsageLimit = {
|
|
5
5
|
oauthProvider: SubscriptionOAuthProvider;
|
|
6
6
|
resetsAt: Date;
|
|
@@ -12,7 +12,9 @@ export declare function subscriptionOAuthProviderFor(provider: string, keySource
|
|
|
12
12
|
/**
|
|
13
13
|
* Recognize a personal subscription that has run out of quota. Claude Code
|
|
14
14
|
* answers 429 with the unified rate-limit headers; the ChatGPT backend answers
|
|
15
|
-
* 429 with a structured `error.code` and `resets_at`.
|
|
15
|
+
* 429 with a structured `error.code` and `resets_at`. Meta documents no
|
|
16
|
+
* usage-limit signature for Muse Code plans, so any 429 on one holds it until
|
|
17
|
+
* the reset Meta gives, if any. The Codex WebSocket
|
|
16
18
|
* transport bypasses fetch, so its usage-limit error only survives as message
|
|
17
19
|
* text and is matched there without a reset time.
|
|
18
20
|
*/
|
|
@@ -8,6 +8,7 @@ const FAILURE_MARKER = "subscription_usage_limit";
|
|
|
8
8
|
const SUBSCRIPTION_LABEL = {
|
|
9
9
|
anthropic_claude_code: "Claude Code",
|
|
10
10
|
openai_codex: "ChatGPT",
|
|
11
|
+
meta_muse: "Muse Code",
|
|
11
12
|
};
|
|
12
13
|
export function subscriptionOAuthProviderFor(provider, keySource) {
|
|
13
14
|
if (keySource !== "subscription")
|
|
@@ -16,12 +17,16 @@ export function subscriptionOAuthProviderFor(provider, keySource) {
|
|
|
16
17
|
return "anthropic_claude_code";
|
|
17
18
|
if (provider === "openai" || provider === "openai-codex")
|
|
18
19
|
return "openai_codex";
|
|
20
|
+
if (provider === "meta")
|
|
21
|
+
return "meta_muse";
|
|
19
22
|
return null;
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
25
|
* Recognize a personal subscription that has run out of quota. Claude Code
|
|
23
26
|
* answers 429 with the unified rate-limit headers; the ChatGPT backend answers
|
|
24
|
-
* 429 with a structured `error.code` and `resets_at`.
|
|
27
|
+
* 429 with a structured `error.code` and `resets_at`. Meta documents no
|
|
28
|
+
* usage-limit signature for Muse Code plans, so any 429 on one holds it until
|
|
29
|
+
* the reset Meta gives, if any. The Codex WebSocket
|
|
25
30
|
* transport bypasses fetch, so its usage-limit error only survives as message
|
|
26
31
|
* text and is matched there without a reset time.
|
|
27
32
|
*/
|
|
@@ -33,6 +38,8 @@ export function detectSubscriptionUsageLimit(args) {
|
|
|
33
38
|
if (oauthProvider === "anthropic_claude_code") {
|
|
34
39
|
return anthropicUsageLimit(args.captured, now);
|
|
35
40
|
}
|
|
41
|
+
if (oauthProvider === "meta_muse")
|
|
42
|
+
return museUsageLimit(args.captured, now);
|
|
36
43
|
return codexUsageLimit(args.captured, providerFailureMessage(args.failure), now);
|
|
37
44
|
}
|
|
38
45
|
function anthropicUsageLimit(captured, now) {
|
|
@@ -76,6 +83,15 @@ function codexUsageLimit(captured, failureMessage, now) {
|
|
|
76
83
|
reason: "ChatGPT usage limit reached.",
|
|
77
84
|
};
|
|
78
85
|
}
|
|
86
|
+
function museUsageLimit(captured, now) {
|
|
87
|
+
if (captured === undefined || captured.status !== 429)
|
|
88
|
+
return null;
|
|
89
|
+
return {
|
|
90
|
+
oauthProvider: "meta_muse",
|
|
91
|
+
resetsAt: laterReset(codexErrorBody(captured.bodyText)?.resets_at, captured.headers, now),
|
|
92
|
+
reason: "Muse Code usage limit reached.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
79
95
|
function codexErrorBody(bodyText) {
|
|
80
96
|
try {
|
|
81
97
|
const parsed = JSON.parse(bodyText);
|
|
@@ -137,12 +153,12 @@ export function subscriptionUsageLimitFromFailure(failure) {
|
|
|
137
153
|
return null;
|
|
138
154
|
const oauthProvider = marker.oauth_provider;
|
|
139
155
|
const resetsAt = typeof marker.resets_at === "string" ? new Date(marker.resets_at) : null;
|
|
140
|
-
if (
|
|
156
|
+
if (typeof oauthProvider !== "string" || !(oauthProvider in SUBSCRIPTION_LABEL)
|
|
141
157
|
|| resetsAt === null || Number.isNaN(resetsAt.getTime())) {
|
|
142
158
|
return null;
|
|
143
159
|
}
|
|
144
160
|
return {
|
|
145
|
-
oauthProvider,
|
|
161
|
+
oauthProvider: oauthProvider,
|
|
146
162
|
resetsAt,
|
|
147
163
|
reason: typeof marker.reason === "string" ? marker.reason : "Subscription usage limit reached.",
|
|
148
164
|
...(marker.switched === true ? { switched: true } : {}),
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { HttpError } from "../http/errors.js";
|
|
1
2
|
import type { RoutedDecision } from "../strategies/route_decision.js";
|
|
2
3
|
import type { PreparedRoutedCompletion } from "./prepare_routed_completion.js";
|
|
3
4
|
export declare function explicitModelDecision(prepared: PreparedRoutedCompletion): RoutedDecision | null;
|
|
4
5
|
export declare function strictModelDecision(prepared: PreparedRoutedCompletion): RoutedDecision;
|
|
6
|
+
export declare function modelNotEnabled(modelId: string): HttpError;
|
|
7
|
+
export declare function strictModelRequired(): HttpError;
|
|
@@ -8,9 +8,8 @@ export function explicitModelDecision(prepared) {
|
|
|
8
8
|
const modelId = explicitModelId(prepared.interaction.model);
|
|
9
9
|
if (modelId === null)
|
|
10
10
|
return null;
|
|
11
|
-
if (!prepared.credentials.enabled_models.includes(modelId))
|
|
12
|
-
throw
|
|
13
|
-
}
|
|
11
|
+
if (!prepared.credentials.enabled_models.includes(modelId))
|
|
12
|
+
throw modelNotEnabled(modelId);
|
|
14
13
|
const candidates = prepared.routing.candidateResolution.candidates.filter((candidate) => candidate.model === modelId);
|
|
15
14
|
if (candidates.length === 0) {
|
|
16
15
|
throw new HttpError(400, `Model '${modelId}' cannot serve this request.`, "model_unavailable", "model");
|
|
@@ -40,9 +39,8 @@ function pickCandidate(prepared, modelId, candidates) {
|
|
|
40
39
|
refuses everything else instead of substituting. */
|
|
41
40
|
export function strictModelDecision(prepared) {
|
|
42
41
|
const modelId = explicitModelId(prepared.interaction.model);
|
|
43
|
-
if (modelId === null)
|
|
44
|
-
throw
|
|
45
|
-
}
|
|
42
|
+
if (modelId === null)
|
|
43
|
+
throw strictModelRequired();
|
|
46
44
|
const { decision } = explicitModelDecision(prepared);
|
|
47
45
|
const requested = prepared.interaction.reasoning?.effort
|
|
48
46
|
?? prepared.credentials.model_configs?.[modelId]?.default_thinking_level;
|
|
@@ -51,3 +49,9 @@ export function strictModelDecision(prepared) {
|
|
|
51
49
|
}
|
|
52
50
|
return { decision };
|
|
53
51
|
}
|
|
52
|
+
export function modelNotEnabled(modelId) {
|
|
53
|
+
return new HttpError(400, `Model '${modelId}' is not enabled on this router.`, "model_not_enabled", "model");
|
|
54
|
+
}
|
|
55
|
+
export function strictModelRequired() {
|
|
56
|
+
return new HttpError(400, "This router is strict: request a pinned model as 'dari/model/<id>'.", "strict_model_required", "model");
|
|
57
|
+
}
|
|
@@ -2,11 +2,13 @@ import { interactionConversationPrefix } from "../context/interaction_fingerprin
|
|
|
2
2
|
import { buildRoutingContext, derivePrefixLookupHashes, } from "../context/routing_context.js";
|
|
3
3
|
import { isBackgroundRequest } from "../interaction/types.js";
|
|
4
4
|
import { executionProviderForModel } from "../models/execution_provider.js";
|
|
5
|
+
import { codexModelConfig, subscriptionServes } from "../models/subscriptions.js";
|
|
5
6
|
import { gatewayPlatform } from "../platform/platform.js";
|
|
6
7
|
import { resolveRouter } from "../strategies/routing.js";
|
|
7
8
|
import { RequestReporter } from "../telemetry/report.js";
|
|
8
9
|
import { RoutingBillingFinalization } from "../telemetry/billing_finalization.js";
|
|
9
10
|
import { requestTags } from "./request_tags.js";
|
|
11
|
+
import { strictProviderAccess } from "./strict_provider_access.js";
|
|
10
12
|
import { scopedHarnessConversationId, scopedHarnessSessionId, } from "./harness_identity/index.js";
|
|
11
13
|
export async function prepareRoutedCompletion(args) {
|
|
12
14
|
const tags = requestTags(args.tagsHeader);
|
|
@@ -79,7 +81,10 @@ export async function prepareRoutedCompletion(args) {
|
|
|
79
81
|
}
|
|
80
82
|
credentials = conversationCredentials;
|
|
81
83
|
}
|
|
82
|
-
|
|
84
|
+
const access = strictProviderAccess(credentials, args.interaction);
|
|
85
|
+
credentials = access.credentials;
|
|
86
|
+
args = { ...args, interaction: access.interaction };
|
|
87
|
+
credentials = await withProviderCredentials(credentials, args.apiKey, args.clientHarness);
|
|
83
88
|
const billable = true;
|
|
84
89
|
const routing = await buildRoutingContext({
|
|
85
90
|
routerId: prefixFingerprintRouterId,
|
|
@@ -134,7 +139,7 @@ export async function prepareRoutedCompletion(args) {
|
|
|
134
139
|
}
|
|
135
140
|
// Ask the platform for each enabled model's credential up front, so provider
|
|
136
141
|
// execution and fallback stay synchronous.
|
|
137
|
-
async function withProviderCredentials(credentials, apiKey) {
|
|
142
|
+
async function withProviderCredentials(credentials, apiKey, clientHarness) {
|
|
138
143
|
const platform = gatewayPlatform();
|
|
139
144
|
if (platform.resolveProviderCredential === undefined)
|
|
140
145
|
return credentials;
|
|
@@ -146,14 +151,28 @@ async function withProviderCredentials(credentials, apiKey) {
|
|
|
146
151
|
routerId: credentials.router_id,
|
|
147
152
|
organizationId: credentials.organization_id,
|
|
148
153
|
apiKey,
|
|
154
|
+
...(clientHarness ? { clientHarness } : {}),
|
|
149
155
|
}),
|
|
150
156
|
]));
|
|
151
157
|
const provider_credentials = {};
|
|
158
|
+
const model_configs = { ...credentials.model_configs };
|
|
152
159
|
for (const [model, credential] of resolved) {
|
|
153
|
-
if (credential?.apiKey.trim())
|
|
154
|
-
|
|
160
|
+
if (!credential?.apiKey.trim())
|
|
161
|
+
continue;
|
|
162
|
+
const plan = credential.subscription;
|
|
163
|
+
if (plan !== undefined && !subscriptionServes(plan, model, clientHarness))
|
|
164
|
+
continue;
|
|
165
|
+
provider_credentials[model] = credential;
|
|
166
|
+
if (plan === "openai_codex") {
|
|
167
|
+
// Keep the catalog's limits and thinking levels; only the serving
|
|
168
|
+
// endpoint changes.
|
|
169
|
+
model_configs[model] = {
|
|
170
|
+
...model_configs[model],
|
|
171
|
+
...codexModelConfig(model, credentials.model_prices?.[model] ?? null),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
155
174
|
}
|
|
156
|
-
return { ...credentials, provider_credentials };
|
|
175
|
+
return { ...credentials, provider_credentials, model_configs };
|
|
157
176
|
}
|
|
158
177
|
// Core's cache-aware pruning audit, extended with the context layer's image
|
|
159
178
|
// capability exclusions. The exclusions survive on their own when custom
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { InteractionRequest } from "../interaction/types.js";
|
|
2
|
+
import type { ResolvedRouter } from "../types.js";
|
|
3
|
+
export declare function strictProviderAccess(credentials: ResolvedRouter, interaction: InteractionRequest): {
|
|
4
|
+
credentials: ResolvedRouter;
|
|
5
|
+
interaction: InteractionRequest;
|
|
6
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getBuiltinModels, getBuiltinProviders } from "@mupt-ai/pi-ai/providers/all";
|
|
2
|
+
import { HttpError } from "../http/errors.js";
|
|
3
|
+
import { explicitModelId } from "../protocol/input/router_model.js";
|
|
4
|
+
import { modelNotEnabled, strictModelRequired } from "./explicit_model.js";
|
|
5
|
+
// Resolve only the requested catalog model, never turn a provider grant into
|
|
6
|
+
// selector candidates. Unknown IDs fail closed; a grant-only router has no
|
|
7
|
+
// candidates to route among, so it refuses them here with the strict errors.
|
|
8
|
+
export function strictProviderAccess(credentials, interaction) {
|
|
9
|
+
const providers = credentials.strict_providers;
|
|
10
|
+
if (credentials.strict !== true || !providers?.length)
|
|
11
|
+
return { credentials, interaction };
|
|
12
|
+
const requested = explicitModelId(interaction.model);
|
|
13
|
+
const grantOnly = credentials.enabled_models.length === 0;
|
|
14
|
+
if (requested === null) {
|
|
15
|
+
if (grantOnly)
|
|
16
|
+
throw strictModelRequired();
|
|
17
|
+
return { credentials, interaction };
|
|
18
|
+
}
|
|
19
|
+
const matches = new Set();
|
|
20
|
+
for (const provider of providers) {
|
|
21
|
+
if (!getBuiltinProviders().includes(provider)) {
|
|
22
|
+
throw new HttpError(400, `Unknown strict provider '${provider}'.`, "unknown_provider");
|
|
23
|
+
}
|
|
24
|
+
for (const model of getBuiltinModels(provider)) {
|
|
25
|
+
const id = `${provider}/${model.id}`;
|
|
26
|
+
if (requested === id)
|
|
27
|
+
matches.add(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const model = [...matches][0];
|
|
31
|
+
if (model === undefined) {
|
|
32
|
+
if (grantOnly)
|
|
33
|
+
throw modelNotEnabled(requested);
|
|
34
|
+
return { credentials, interaction };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
credentials: { ...credentials, enabled_models: [...new Set([...credentials.enabled_models, model])] },
|
|
38
|
+
interaction: { ...interaction, model: `dari/model/${model}` },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { nativeModelId, providerForModel, } from "@mupt-ai/dari-router/policy-engine";
|
|
3
3
|
import { HttpError } from "../http/errors.js";
|
|
4
|
+
import { codexModelConfig } from "../models/subscriptions.js";
|
|
4
5
|
import { registeredModelPrice } from "../providers/pi.js";
|
|
5
6
|
import { freshAccessToken, } from "./subscriptions/index.js";
|
|
6
7
|
// A self-hosted gateway is one tenant; these stand in for the hosted
|
|
7
8
|
// organization and the user a personal subscription belongs to.
|
|
8
9
|
export const SELF_HOSTED_ORGANIZATION_ID = "self-hosted";
|
|
9
10
|
export const SELF_HOSTED_USER_ID = "self-hosted";
|
|
10
|
-
const CODEX_BACKEND_URL = "https://chatgpt.com/backend-api";
|
|
11
11
|
/**
|
|
12
12
|
* Resolve a configured router for one request. Each provider is served by a
|
|
13
13
|
* connected personal subscription when the router allows it and one is
|
|
@@ -129,12 +129,14 @@ export function gatewayKeyIndex(keys, presented) {
|
|
|
129
129
|
}
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
|
-
// Claude Code plans serve only Claude Code itself
|
|
133
|
-
// client through the Codex backend.
|
|
132
|
+
// Claude Code plans serve only Claude Code itself and Muse Code plans only
|
|
133
|
+
// Muse Code; ChatGPT plans serve any client through the Codex backend.
|
|
134
134
|
function subscriptionProviderFor(provider, clientHarness) {
|
|
135
135
|
if (provider === "anthropic") {
|
|
136
136
|
return clientHarness === "claude_code" ? "anthropic_claude_code" : null;
|
|
137
137
|
}
|
|
138
|
+
if (provider === "meta")
|
|
139
|
+
return clientHarness === "muse_code" ? "meta_muse" : null;
|
|
138
140
|
if (provider === "openai")
|
|
139
141
|
return "openai_codex";
|
|
140
142
|
return null;
|
|
@@ -146,24 +148,18 @@ function servingProviders(provider, credential) {
|
|
|
146
148
|
? [provider, "openai-codex"]
|
|
147
149
|
: [provider];
|
|
148
150
|
}
|
|
149
|
-
function codexModelConfig(modelId, price) {
|
|
150
|
-
return {
|
|
151
|
-
provider: "openai-codex",
|
|
152
|
-
native_model_id: nativeModelId(modelId),
|
|
153
|
-
api: "openai-codex-responses",
|
|
154
|
-
base_url: CODEX_BACKEND_URL,
|
|
155
|
-
// A plan bills no tokens; the catalog price only feeds cost estimates.
|
|
156
|
-
cost: price ?? { input: 0, output: 0 },
|
|
157
|
-
fast_mode: false,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
151
|
function thinkingLevels(router) {
|
|
161
152
|
return Object.fromEntries(router.models.flatMap((model) => model.thinking ? [[model.id, model.thinking]] : []));
|
|
162
153
|
}
|
|
154
|
+
const SUBSCRIPTION_PLAN_NAMES = {
|
|
155
|
+
anthropic_claude_code: "Claude Code",
|
|
156
|
+
openai_codex: "ChatGPT",
|
|
157
|
+
meta_muse: "Muse Code",
|
|
158
|
+
};
|
|
163
159
|
function unavailableRouterError(routerId, exhausted) {
|
|
164
160
|
if (exhausted.size > 0) {
|
|
165
161
|
const plans = [...exhausted]
|
|
166
|
-
.map((provider) => provider
|
|
162
|
+
.map((provider) => SUBSCRIPTION_PLAN_NAMES[provider])
|
|
167
163
|
.join(" and ");
|
|
168
164
|
return new HttpError(424, `Every connected ${plans} subscription is at its usage limit, and no API key can serve router ${routerId} instead.`, "personal_subscription_unavailable");
|
|
169
165
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth protocols for personal subscriptions: Claude Code (PKCE, the user
|
|
3
|
-
* pastes back the `code#state` Anthropic shows)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* pastes back the `code#state` Anthropic shows), ChatGPT/Codex (device code,
|
|
4
|
+
* the gateway polls for approval), and Muse Code (Meta's device code, then the
|
|
5
|
+
* subscription's Model API key minted from the sign-in token). Every network
|
|
6
|
+
* call goes through the injected `fetch`.
|
|
6
7
|
*/
|
|
7
|
-
export type SubscriptionProvider = "anthropic_claude_code" | "openai_codex";
|
|
8
|
+
export type SubscriptionProvider = "anthropic_claude_code" | "openai_codex" | "meta_muse";
|
|
8
9
|
export declare const SUBSCRIPTION_PROVIDERS: readonly SubscriptionProvider[];
|
|
9
10
|
export type OAuthFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
10
11
|
export type OAuthOptions = {
|
|
@@ -14,7 +15,8 @@ export type OAuthOptions = {
|
|
|
14
15
|
/** The provider refused or could not be reached (HTTP 502 at the edge). */
|
|
15
16
|
export declare class SubscriptionOAuthProviderError extends Error {
|
|
16
17
|
readonly status?: number | undefined;
|
|
17
|
-
|
|
18
|
+
readonly errorCode?: string | undefined;
|
|
19
|
+
constructor(message: string, status?: number | undefined, errorCode?: string | undefined);
|
|
18
20
|
}
|
|
19
21
|
/** The user's input or flow state is wrong (HTTP 400 at the edge). */
|
|
20
22
|
export declare class SubscriptionOAuthInputError extends Error {
|
|
@@ -37,10 +39,12 @@ export type OAuthTokens = {
|
|
|
37
39
|
export declare const CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
38
40
|
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
39
41
|
export declare const CODEX_APPROVAL_PENDING = "OpenAI approval is still pending. Enter the code at OpenAI, finish approval, then try again.";
|
|
42
|
+
export declare const MUSE_CODE_CLIENT_ID = "1031625952748946";
|
|
43
|
+
export declare const META_APPROVAL_PENDING = "Meta approval is still pending. Enter the code at Meta, finish approval, then try again.";
|
|
40
44
|
export declare function startOAuth(provider: SubscriptionProvider, options: OAuthOptions): Promise<OAuthAuthorization>;
|
|
41
45
|
/**
|
|
42
46
|
* Finish a flow. Claude Code needs the pasted authorization response; the
|
|
43
|
-
* device-code
|
|
47
|
+
* device-code flows ignore it and ask the provider whether the user approved.
|
|
44
48
|
*/
|
|
45
49
|
export declare function completeOAuth(provider: SubscriptionProvider, flow: Record<string, string>, authorizationResponse: string, options: OAuthOptions): Promise<OAuthTokens>;
|
|
46
50
|
export declare function refreshOAuth(credential: {
|