@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
|
@@ -2,13 +2,17 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
export const SUBSCRIPTION_PROVIDERS = [
|
|
3
3
|
"anthropic_claude_code",
|
|
4
4
|
"openai_codex",
|
|
5
|
+
"meta_muse",
|
|
5
6
|
];
|
|
6
7
|
/** The provider refused or could not be reached (HTTP 502 at the edge). */
|
|
7
8
|
export class SubscriptionOAuthProviderError extends Error {
|
|
8
9
|
status;
|
|
9
|
-
|
|
10
|
+
errorCode;
|
|
11
|
+
// `errorCode` is the provider's OAuth `error` code, such as `authorization_pending`.
|
|
12
|
+
constructor(message, status, errorCode) {
|
|
10
13
|
super(message);
|
|
11
14
|
this.status = status;
|
|
15
|
+
this.errorCode = errorCode;
|
|
12
16
|
this.name = "SubscriptionOAuthProviderError";
|
|
13
17
|
}
|
|
14
18
|
}
|
|
@@ -36,6 +40,20 @@ const CODEX_TOKEN_URL = `${OPENAI_AUTH_BASE_URL}/oauth/token`;
|
|
|
36
40
|
const CODEX_REDIRECT_URI = `${OPENAI_AUTH_BASE_URL}/deviceauth/callback`;
|
|
37
41
|
const CODEX_ACCOUNT_CLAIM = "https://api.openai.com/auth";
|
|
38
42
|
export const CODEX_APPROVAL_PENDING = "OpenAI approval is still pending. Enter the code at OpenAI, finish approval, then try again.";
|
|
43
|
+
// Muse Code's own device sign-in (`muse login`), as its public launcher runs it.
|
|
44
|
+
export const MUSE_CODE_CLIENT_ID = "1031625952748946";
|
|
45
|
+
const META_AUTH_BASE_URL = "https://auth.meta.com";
|
|
46
|
+
const META_DEVICE_AUTHORIZATION_URL = `${META_AUTH_BASE_URL}/oidc/device/authorization/`;
|
|
47
|
+
const META_TOKEN_URL = `${META_AUTH_BASE_URL}/oidc/device/token/`;
|
|
48
|
+
// Muse Code mints its subscription's Model API key from the sign-in token here.
|
|
49
|
+
const MUSE_MINT_URL = "https://api.meta.ai/muse-code/key";
|
|
50
|
+
// Meta states no lifetime for a minted key, and Muse Code mints a fresh one at
|
|
51
|
+
// every start, so the key is minted again on this cadence.
|
|
52
|
+
const MUSE_KEY_LIFETIME_MS = 12 * 60 * 60_000;
|
|
53
|
+
// Marks a stored sign-in refresh token, which is exchanged for a fresh sign-in
|
|
54
|
+
// token before each mint. Without it the stored value is the sign-in token.
|
|
55
|
+
const MUSE_SIGN_IN_REFRESH_PREFIX = "refresh:";
|
|
56
|
+
export const META_APPROVAL_PENDING = "Meta approval is still pending. Enter the code at Meta, finish approval, then try again.";
|
|
39
57
|
export async function startOAuth(provider, options) {
|
|
40
58
|
const expiresAt = new Date((options.now ?? new Date()).getTime() + FLOW_TIMEOUT_MS);
|
|
41
59
|
if (provider === "anthropic_claude_code") {
|
|
@@ -60,6 +78,18 @@ export async function startOAuth(provider, options) {
|
|
|
60
78
|
flow: { code_verifier: verifier, state },
|
|
61
79
|
};
|
|
62
80
|
}
|
|
81
|
+
if (provider === "meta_muse") {
|
|
82
|
+
const payload = await postForm(options.fetch, META_DEVICE_AUTHORIZATION_URL, { client_id: MUSE_CODE_CLIENT_ID }, "Meta device code request");
|
|
83
|
+
const complete = payload.verification_uri_complete;
|
|
84
|
+
return {
|
|
85
|
+
authorizationUrl: typeof complete === "string" && complete.startsWith("https://")
|
|
86
|
+
? complete
|
|
87
|
+
: requiredString(payload, "verification_uri"),
|
|
88
|
+
expiresAt,
|
|
89
|
+
userCode: requiredString(payload, "user_code"),
|
|
90
|
+
flow: { device_code: requiredString(payload, "device_code") },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
63
93
|
const payload = await postJson(options.fetch, `${CODEX_DEVICE_AUTH_URL}/usercode`, { client_id: CODEX_CLIENT_ID }, "OpenAI device code request");
|
|
64
94
|
const userCode = requiredString({ user_code: payload.user_code || payload.usercode }, "user_code");
|
|
65
95
|
return {
|
|
@@ -71,7 +101,7 @@ export async function startOAuth(provider, options) {
|
|
|
71
101
|
}
|
|
72
102
|
/**
|
|
73
103
|
* Finish a flow. Claude Code needs the pasted authorization response; the
|
|
74
|
-
* device-code
|
|
104
|
+
* device-code flows ignore it and ask the provider whether the user approved.
|
|
75
105
|
*/
|
|
76
106
|
export async function completeOAuth(provider, flow, authorizationResponse, options) {
|
|
77
107
|
const now = options.now ?? new Date();
|
|
@@ -95,6 +125,31 @@ export async function completeOAuth(provider, flow, authorizationResponse, optio
|
|
|
95
125
|
}, "Anthropic subscription token exchange");
|
|
96
126
|
return claudeTokens(token, now, null);
|
|
97
127
|
}
|
|
128
|
+
if (provider === "meta_muse") {
|
|
129
|
+
let token;
|
|
130
|
+
try {
|
|
131
|
+
token = await postForm(options.fetch, META_TOKEN_URL, {
|
|
132
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
133
|
+
device_code: requiredString(flow, "device_code"),
|
|
134
|
+
client_id: MUSE_CODE_CLIENT_ID,
|
|
135
|
+
}, "Meta device approval check");
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof SubscriptionOAuthProviderError) {
|
|
139
|
+
if (error.errorCode === "authorization_pending" || error.errorCode === "slow_down") {
|
|
140
|
+
throw new SubscriptionOAuthInputError(META_APPROVAL_PENDING);
|
|
141
|
+
}
|
|
142
|
+
if (error.errorCode === "access_denied") {
|
|
143
|
+
throw new SubscriptionOAuthInputError("The sign-in was declined on Meta. Start again to retry.");
|
|
144
|
+
}
|
|
145
|
+
if (error.errorCode === "expired_token") {
|
|
146
|
+
throw new SubscriptionOAuthInputError("The Meta sign-in code expired. Start again.");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
return await museTokens(requiredString(token, "access_token"), museRefreshToken(token), options.fetch, now);
|
|
152
|
+
}
|
|
98
153
|
// The provider, not user-supplied input, supplies the authorization code.
|
|
99
154
|
let approval;
|
|
100
155
|
try {
|
|
@@ -129,6 +184,23 @@ export async function refreshOAuth(credential, options) {
|
|
|
129
184
|
}, "Anthropic subscription token refresh");
|
|
130
185
|
return claudeTokens(token, now, credential.refreshToken);
|
|
131
186
|
}
|
|
187
|
+
if (credential.provider === "meta_muse") {
|
|
188
|
+
let stored = credential.refreshToken;
|
|
189
|
+
let signInToken = stored;
|
|
190
|
+
if (stored.startsWith(MUSE_SIGN_IN_REFRESH_PREFIX)) {
|
|
191
|
+
const token = await postForm(options.fetch, META_TOKEN_URL, {
|
|
192
|
+
grant_type: "refresh_token",
|
|
193
|
+
refresh_token: stored.slice(MUSE_SIGN_IN_REFRESH_PREFIX.length),
|
|
194
|
+
client_id: MUSE_CODE_CLIENT_ID,
|
|
195
|
+
}, "Meta sign-in refresh");
|
|
196
|
+
signInToken = requiredString(token, "access_token");
|
|
197
|
+
stored = museRefreshToken(token) ?? stored;
|
|
198
|
+
}
|
|
199
|
+
// A lapsed plan fails routing like any other refresh failure.
|
|
200
|
+
return await museTokens(signInToken, stored, options.fetch, now).catch((error) => {
|
|
201
|
+
throw error instanceof SubscriptionOAuthInputError ? new SubscriptionOAuthProviderError(error.message) : error;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
132
204
|
const token = await postJson(options.fetch, CODEX_TOKEN_URL, {
|
|
133
205
|
grant_type: "refresh_token",
|
|
134
206
|
refresh_token: credential.refreshToken,
|
|
@@ -171,6 +243,27 @@ function claudeTokens(payload, now, fallbackRefreshToken) {
|
|
|
171
243
|
accountEmail: typeof email === "string" && email ? email : null,
|
|
172
244
|
};
|
|
173
245
|
}
|
|
246
|
+
function museRefreshToken(token) {
|
|
247
|
+
const value = token.refresh_token;
|
|
248
|
+
return typeof value === "string" && value ? MUSE_SIGN_IN_REFRESH_PREFIX + value : null;
|
|
249
|
+
}
|
|
250
|
+
// Mints the plan's Model API key from the Meta sign-in token.
|
|
251
|
+
async function museTokens(signInToken, refreshToken, fetch, now) {
|
|
252
|
+
const minted = await requestJson(fetch, MUSE_MINT_URL, "{}", "application/json", "Muse Code subscription key mint", { Authorization: `Bearer ${signInToken}`, "x-api-version": "1.0.0" });
|
|
253
|
+
// A key minted without an active plan bills Model API pay-as-you-go.
|
|
254
|
+
if (minted.is_subs_active !== true) {
|
|
255
|
+
throw new SubscriptionOAuthInputError("This Meta account has no active Muse Code subscription. Subscribe at https://accountscenter.meta.com/muse_code/, then connect again.");
|
|
256
|
+
}
|
|
257
|
+
const email = minted.user_email;
|
|
258
|
+
return {
|
|
259
|
+
provider: "meta_muse",
|
|
260
|
+
accessToken: requiredString(minted, "api_key"),
|
|
261
|
+
refreshToken: refreshToken ?? signInToken,
|
|
262
|
+
expiresAt: new Date(now.getTime() + MUSE_KEY_LIFETIME_MS),
|
|
263
|
+
accountId: null,
|
|
264
|
+
accountEmail: typeof email === "string" && email ? email : null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
174
267
|
function codexTokens(payload, now, fallbackRefreshToken, fallbackAccountId) {
|
|
175
268
|
const accessToken = requiredString(payload, "access_token");
|
|
176
269
|
const refreshToken = refreshTokenFrom(payload, fallbackRefreshToken);
|
|
@@ -242,12 +335,12 @@ function postJson(fetch, url, payload, operation) {
|
|
|
242
335
|
function postForm(fetch, url, payload, operation) {
|
|
243
336
|
return requestJson(fetch, url, new URLSearchParams(payload).toString(), "application/x-www-form-urlencoded", operation);
|
|
244
337
|
}
|
|
245
|
-
async function requestJson(fetch, url, body, contentType, operation) {
|
|
338
|
+
async function requestJson(fetch, url, body, contentType, operation, headers = {}) {
|
|
246
339
|
let response;
|
|
247
340
|
try {
|
|
248
341
|
response = await fetch(url, {
|
|
249
342
|
method: "POST",
|
|
250
|
-
headers: { Accept: "application/json", "Content-Type": contentType, "User-Agent": "Dari/1.0" },
|
|
343
|
+
headers: { Accept: "application/json", "Content-Type": contentType, "User-Agent": "Dari/1.0", ...headers },
|
|
251
344
|
body,
|
|
252
345
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
253
346
|
});
|
|
@@ -256,8 +349,10 @@ async function requestJson(fetch, url, body, contentType, operation) {
|
|
|
256
349
|
throw new SubscriptionOAuthProviderError(`${operation} could not reach the provider.`);
|
|
257
350
|
}
|
|
258
351
|
if (!response.ok) {
|
|
259
|
-
|
|
260
|
-
|
|
352
|
+
// Only the RFC 6749 `error` code is kept; the rest may carry provider detail.
|
|
353
|
+
const failure = await response.json().catch(() => undefined);
|
|
354
|
+
const errorCode = isRecord(failure) && typeof failure.error === "string" ? failure.error : undefined;
|
|
355
|
+
throw new SubscriptionOAuthProviderError(`${operation} failed with provider status ${response.status}.`, response.status, errorCode);
|
|
261
356
|
}
|
|
262
357
|
let decoded;
|
|
263
358
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -18,10 +18,12 @@ export type AwsAssumeRoleProviderAuth = {
|
|
|
18
18
|
region: string;
|
|
19
19
|
};
|
|
20
20
|
export type ResolvedProviderAuth = BearerProviderAuth | AwsSigV4ProviderAuth;
|
|
21
|
+
export type SubscriptionKind = "openai_codex" | "anthropic_claude_code" | "meta_muse";
|
|
21
22
|
export type ProviderCredential = {
|
|
22
23
|
apiKey: string;
|
|
23
24
|
baseUrl?: string;
|
|
24
25
|
headers?: Record<string, string>;
|
|
26
|
+
subscription?: SubscriptionKind;
|
|
25
27
|
};
|
|
26
28
|
export type ProviderAuth = ResolvedProviderAuth | AwsAssumeRoleProviderAuth;
|
|
27
29
|
export type AwsProviderAuth = AwsSigV4ProviderAuth | AwsAssumeRoleProviderAuth;
|
|
@@ -39,7 +41,7 @@ export type PublicEvalCatalogCard = Omit<RouterEval, "scores"> & {
|
|
|
39
41
|
updated_at: string | null;
|
|
40
42
|
scores: PublicEvalCatalogScore[];
|
|
41
43
|
};
|
|
42
|
-
export type ClientHarness = "claude_code";
|
|
44
|
+
export type ClientHarness = "claude_code" | "muse_code";
|
|
43
45
|
export type ResolvedRouterModelConfig = {
|
|
44
46
|
provider: string;
|
|
45
47
|
native_model_id: string;
|
|
@@ -74,6 +76,7 @@ export type ResolvedRouter = {
|
|
|
74
76
|
speculative_routing?: boolean;
|
|
75
77
|
primary_retries?: number;
|
|
76
78
|
strict?: boolean;
|
|
79
|
+
strict_providers?: string[];
|
|
77
80
|
model_fallback_enabled?: boolean;
|
|
78
81
|
fallback_requires_different_provider?: boolean;
|
|
79
82
|
evals?: RouterEval[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mupt-ai/dari-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@aws-sdk/client-sts": "3.1063.0",
|
|
52
|
-
"@mupt-ai/dari-router": "0.2.
|
|
52
|
+
"@mupt-ai/dari-router": "0.2.5",
|
|
53
53
|
"@mupt-ai/pi-ai": "1.4.0-mupt.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/context/pricing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModelPricing, PricingLookup } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
import { registeredModelPrice } from "../providers/pi.js";
|
|
2
3
|
import type { ResolvedRouter } from "../types.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
5
|
export function pricingLookupForRouter(credentials: ResolvedRouter): PricingLookup {
|
|
6
6
|
return (model: string): ModelPricing | null => modelPricingForRouter(credentials, model);
|
|
7
7
|
}
|
|
@@ -10,7 +10,10 @@ export function modelPricingForRouter(
|
|
|
10
10
|
credentials: ResolvedRouter,
|
|
11
11
|
model: string,
|
|
12
12
|
): ModelPricing | null {
|
|
13
|
-
|
|
13
|
+
// A model the platform neither prices nor configures is served from Pi's
|
|
14
|
+
// catalog (see buildModel), so its catalog price is the one it is billed at.
|
|
15
|
+
const price = credentials.model_prices?.[model]
|
|
16
|
+
?? (credentials.model_configs?.[model] === undefined ? registeredModelPrice(model) : null);
|
|
14
17
|
if (!price || typeof price.input !== "number" || typeof price.output !== "number") return null;
|
|
15
18
|
// Null cached rates mean the provider publishes none: bill cached prompt
|
|
16
19
|
// buckets at the input rate, never for free.
|
package/src/http/handler.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
handleResponsesWebSocketFallback,
|
|
4
4
|
} from "../ingress/codex.js";
|
|
5
5
|
import { handleModels } from "../ingress/models.js";
|
|
6
|
+
import { handleMuseCodeModels } from "../ingress/muse_models.js";
|
|
6
7
|
import { handleChatCompletions } from "../ingress/chat_completions.js";
|
|
7
8
|
import { handleAnthropicMessages } from "../ingress/anthropic_messages.js";
|
|
8
9
|
import { bindGatewayPlatform, type GatewayPlatform } from "../platform/platform.js";
|
|
@@ -31,6 +32,12 @@ export function createRequestHandler(options: GatewayHandlerOptions) {
|
|
|
31
32
|
) {
|
|
32
33
|
return Response.json({ status: "ok" });
|
|
33
34
|
}
|
|
35
|
+
// Muse Code reads its model list from its base URL's origin; `dari muse`
|
|
36
|
+
// forwards it to the router's own path.
|
|
37
|
+
const museModels = url.pathname.match(/^(?:\/([^/]+))?\/muse-code\/models$/);
|
|
38
|
+
if (request.method === "GET" && museModels !== null) {
|
|
39
|
+
return await handleMuseCodeModels(request, museModels[1] ?? DEFAULT_ROUTER_ALIAS);
|
|
40
|
+
}
|
|
34
41
|
// Claude Code probes this first-party endpoint before its Messages call
|
|
35
42
|
// when ANTHROPIC_BASE_URL points at a compatible gateway.
|
|
36
43
|
if (request.method === "HEAD" && url.pathname === "/api/hello") {
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,15 @@ export type {
|
|
|
16
16
|
ResolveRouterRequest,
|
|
17
17
|
SubscriptionHold,
|
|
18
18
|
} from "./platform/platform.js";
|
|
19
|
+
export {
|
|
20
|
+
DEFAULT_CATALOG_PROVIDERS,
|
|
21
|
+
listModelCatalog,
|
|
22
|
+
type CatalogModel,
|
|
23
|
+
type CatalogModelPrice,
|
|
24
|
+
type CatalogPriceTier,
|
|
25
|
+
type ModelCatalogOptions,
|
|
26
|
+
} from "./models/catalog.js";
|
|
19
27
|
export { createAutoRoutingSelectorBackend } from "./strategies/auto_routing_selector.js";
|
|
20
28
|
export { createPrioritySelectorBackend } from "./strategies/priority_selector.js";
|
|
21
29
|
export { createLlmSelectorBackend, type SelectorBackend } from "./strategies/routing.js";
|
|
22
|
-
export type { ProviderCredential, ResolvedRouter } from "./types.js";
|
|
30
|
+
export type { ProviderCredential, ResolvedRouter, SubscriptionKind } from "./types.js";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
openCodeRequestContext,
|
|
12
12
|
} from "../runtime/harness_identity/index.js";
|
|
13
13
|
import { parseJsonRequest } from "./json.js";
|
|
14
|
+
import { museCodeClientId } from "./muse_code.js";
|
|
14
15
|
|
|
15
16
|
export async function handleChatCompletions(
|
|
16
17
|
request: Request,
|
|
@@ -22,15 +23,21 @@ export async function handleChatCompletions(
|
|
|
22
23
|
const payload = await parseJsonRequest<ChatCompletionRequest>(request);
|
|
23
24
|
const interaction = chatCompletionsToInteraction({ ...payload, source_protocol: "openai_chat" });
|
|
24
25
|
const agentContext = openCodeRequestContext(request.headers);
|
|
26
|
+
const museClientId = museCodeClientId(request.headers);
|
|
25
27
|
return await handleRoutedCompletion({
|
|
26
28
|
request,
|
|
27
29
|
routerId,
|
|
28
30
|
apiKey,
|
|
29
|
-
payload:
|
|
31
|
+
payload: {
|
|
32
|
+
...interaction,
|
|
33
|
+
...(agentContext === undefined ? {} : { agentContext }),
|
|
34
|
+
...(museClientId === undefined ? {} : { museCodeClientId: museClientId }),
|
|
35
|
+
},
|
|
30
36
|
responseBody: chatCompletionResponse,
|
|
31
37
|
streamBody: chatCompletionsSseStream,
|
|
32
38
|
errorResponse,
|
|
33
39
|
selectorBackend,
|
|
40
|
+
clientHarness: museClientId === undefined ? undefined : "muse_code",
|
|
34
41
|
harnessConversationIdentity: chatCompletionsConversationIdentity(
|
|
35
42
|
request.headers,
|
|
36
43
|
payload.prompt_cache_key,
|
package/src/ingress/codex.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { openAIResponsesResponse } from "../protocol/output/openai_responses.js"
|
|
|
5
5
|
import { openAIResponsesSseStream } from "../protocol/output/openai_responses_stream.js";
|
|
6
6
|
import { openAIResponsesToInteraction } from "../protocol/input/openai_responses.js";
|
|
7
7
|
import { parseJsonRequest } from "./json.js";
|
|
8
|
+
import { museCodeClientId } from "./muse_code.js";
|
|
8
9
|
import { handleRoutedCompletion } from "../runtime/routed_completion.js";
|
|
9
10
|
import type { SelectorBackend } from "../strategies/routing.js";
|
|
10
11
|
import {
|
|
@@ -24,10 +25,12 @@ export async function handleResponses(
|
|
|
24
25
|
const interaction = openAIResponsesToInteraction(rawPayload);
|
|
25
26
|
const requestPurpose = codexRequestPurpose(request.headers, rawPayload);
|
|
26
27
|
const agentContext = codexRequestContext(request.headers, rawPayload);
|
|
28
|
+
const museClientId = museCodeClientId(request.headers);
|
|
27
29
|
const payload = {
|
|
28
30
|
...interaction,
|
|
29
31
|
...(requestPurpose === undefined ? {} : { requestPurpose }),
|
|
30
32
|
...(agentContext === undefined ? {} : { agentContext }),
|
|
33
|
+
...(museClientId === undefined ? {} : { museCodeClientId: museClientId }),
|
|
31
34
|
};
|
|
32
35
|
const harnessConversationIdentity = codexConversationIdentity(
|
|
33
36
|
request.headers,
|
|
@@ -47,6 +50,7 @@ export async function handleResponses(
|
|
|
47
50
|
streamBody: openAIResponsesSseStream,
|
|
48
51
|
errorResponse,
|
|
49
52
|
selectorBackend,
|
|
53
|
+
clientHarness: museClientId === undefined ? undefined : "muse_code",
|
|
50
54
|
harnessConversationIdentity,
|
|
51
55
|
});
|
|
52
56
|
} catch (error) {
|
package/src/ingress/models.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
Codex reads the Codex catalog; Anthropic-SDK clients (Claude Desktop /
|
|
3
3
|
Cowork gateway probes, Claude Code) read the Anthropic list shape. The
|
|
4
4
|
handler detects the caller, authenticates in that protocol's auth shape,
|
|
5
|
-
and selects the matching catalog adapter.
|
|
5
|
+
and selects the matching catalog adapter. `?detail=1` instead returns the
|
|
6
|
+
model catalog (models/catalog.ts), optionally narrowed with
|
|
7
|
+
`provider=<id>[,<id>...]`. */
|
|
6
8
|
|
|
7
9
|
import { anthropicToken, bearerToken } from "../http/auth.js";
|
|
8
10
|
import { errorResponse } from "../http/errors.js";
|
|
9
11
|
import { captureRequestError } from "../observability/errors.js";
|
|
10
12
|
import { anthropicErrorResponse } from "./anthropic_messages.js";
|
|
13
|
+
import { listModelCatalog } from "../models/catalog.js";
|
|
11
14
|
import { modelSupportsImageInput } from "../providers/model_capabilities.js";
|
|
12
15
|
import { modelMetadataLookup } from "../providers/pi.js";
|
|
13
16
|
import { resolveRouter } from "../strategies/routing.js";
|
|
@@ -26,6 +29,15 @@ export async function handleModels(request: Request, routerId: string): Promise<
|
|
|
26
29
|
// Resolution authenticates the key and confirms the router exists even
|
|
27
30
|
// when the response body (Anthropic shape) does not depend on it.
|
|
28
31
|
const router = await resolveRouter({ routerId, apiKey });
|
|
32
|
+
const query = new URL(request.url).searchParams;
|
|
33
|
+
if (query.get("detail") === "1") {
|
|
34
|
+
const providers = query.get("provider")?.split(",").map((id) => id.trim()).filter(Boolean);
|
|
35
|
+
const data = await listModelCatalog({
|
|
36
|
+
...(providers?.length ? { providers } : {}),
|
|
37
|
+
signal: request.signal,
|
|
38
|
+
});
|
|
39
|
+
return Response.json({ object: "list", data }, { headers: { "Cache-Control": "no-store" } });
|
|
40
|
+
}
|
|
29
41
|
if (anthropicClient) {
|
|
30
42
|
return Response.json(anthropicModelsResponse(), {
|
|
31
43
|
headers: {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Muse Code names its surface (TUI, `muse exec`, desktop, `muse serve`) in
|
|
2
|
+
// x-client-id on every model request.
|
|
3
|
+
const MUSE_CODE_CLIENT_IDS = new Set(["tbh:tui", "tbh:exec", "tbh:desktop", "tbh:serve"]);
|
|
4
|
+
|
|
5
|
+
/** Muse Code's x-client-id when the request comes from Muse Code. */
|
|
6
|
+
export function museCodeClientId(headers: Headers): string | undefined {
|
|
7
|
+
const clientId = headers.get("x-client-id")?.trim().toLowerCase();
|
|
8
|
+
return clientId !== undefined && MUSE_CODE_CLIENT_IDS.has(clientId) ? clientId : undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bearerToken } from "../http/auth.js";
|
|
2
|
+
import { errorResponse } from "../http/errors.js";
|
|
3
|
+
import { resolveRouter } from "../strategies/routing.js";
|
|
4
|
+
import type { ResolvedRouter } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Muse Code fetches its model list from its base URL's origin
|
|
8
|
+
* (`/muse-code/models`) before its first request. A list of only the router's
|
|
9
|
+
* models lets Muse Code pick from them and start on the first; `dari/routing`
|
|
10
|
+
* leads it only on the Autorouting plan.
|
|
11
|
+
*/
|
|
12
|
+
export function museCodeModelsResponse(
|
|
13
|
+
router: Pick<ResolvedRouter, "enabled_models" | "autorouting_enabled">,
|
|
14
|
+
): Record<string, unknown> {
|
|
15
|
+
const ids = [
|
|
16
|
+
...(router.autorouting_enabled === false ? [] : ["dari/routing"]),
|
|
17
|
+
...router.enabled_models.map((model) => `dari/model/${model}`),
|
|
18
|
+
];
|
|
19
|
+
return {
|
|
20
|
+
data: ids.map((id, index) => ({
|
|
21
|
+
id,
|
|
22
|
+
display_label: id === "dari/routing" ? "Dari Routing" : id.slice("dari/model/".length),
|
|
23
|
+
is_default: index === 0,
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleMuseCodeModels(request: Request, routerId: string): Promise<Response> {
|
|
29
|
+
try {
|
|
30
|
+
// Only Muse Code fetches this list, so its plans apply even without a Meta key.
|
|
31
|
+
const router = await resolveRouter({ routerId, apiKey: bearerToken(request.headers), clientHarness: "muse_code" });
|
|
32
|
+
return Response.json(museCodeModelsResponse(router), { headers: { "Cache-Control": "no-store" } });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return errorResponse(error);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/interaction/types.ts
CHANGED
|
@@ -235,6 +235,8 @@ export type InteractionRequest = {
|
|
|
235
235
|
agentContext?: AgentRequestContext;
|
|
236
236
|
// Claude Code's own user agent; Anthropic gates subscription models on it.
|
|
237
237
|
claudeCodeUserAgent?: string;
|
|
238
|
+
// Muse Code's own x-client-id, which Meta attributes its requests by.
|
|
239
|
+
museCodeClientId?: string;
|
|
238
240
|
anthropicSafeguards?: AnthropicSafeguards;
|
|
239
241
|
model?: string;
|
|
240
242
|
instructions?: string;
|