copillm 0.2.9 → 0.3.0-beta.1
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 +69 -1
- package/dist/agentconfig/load.js +9 -1
- package/dist/agentconfig/schema.js +6 -0
- package/dist/auth/accountManager.js +118 -0
- package/dist/auth/accounts.js +161 -0
- package/dist/auth/credentials.js +196 -67
- package/dist/cli/agentEnv.js +2 -1
- package/dist/cli/auth/runAuth.js +206 -9
- package/dist/cli/commands/agents/claude.js +22 -2
- package/dist/cli/commands/agents/codex.js +22 -2
- package/dist/cli/commands/agents/copilot.js +25 -4
- package/dist/cli/commands/agents/pi.js +22 -2
- package/dist/cli/commands/agents/shared.js +57 -0
- package/dist/cli/commands/auth.js +27 -1
- package/dist/cli/copillmFlags.js +8 -0
- package/dist/cli/daemon/runDaemon.js +21 -2
- package/dist/cli/integrations/claudeExport.js +6 -4
- package/dist/cli/integrations/refreshCodex.js +4 -2
- package/dist/cli/integrations/refreshPi.js +4 -2
- package/dist/cli/packageInfo.js +1 -1
- package/dist/config/accountId.js +44 -0
- package/dist/config/home.js +35 -0
- package/dist/integrations/codex/init.js +12 -3
- package/dist/integrations/pi/init.js +4 -3
- package/dist/models/anthropicDefaults.js +13 -4
- package/dist/models/discovery.js +32 -10
- package/dist/server/accountResolver.js +85 -0
- package/dist/server/proxy.js +40 -8
- package/dist/server/routes/debug.js +7 -0
- package/dist/server/routes/models.js +5 -5
- package/dist/server/routes/proxyForward.js +3 -3
- package/dist/server/routes/shared.js +66 -21
- package/package.json +1 -1
package/dist/models/discovery.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { setTimeout as defaultSleep } from "node:timers/promises";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { modelsCachePath, modelsCacheReadPath } from "../config/home.js";
|
|
4
|
+
import { accountModelsCachePath, accountModelsCacheReadPath, modelsCachePath, modelsCacheReadPath } from "../config/home.js";
|
|
5
|
+
import { assertValidAccountId } from "../config/accountId.js";
|
|
5
6
|
import { writeFileSecureAtomic } from "../config/fsSecurity.js";
|
|
6
7
|
import { copilotBaseUrl } from "../config/upstream.js";
|
|
7
8
|
const ModelSchema = z
|
|
@@ -50,7 +51,7 @@ const RETRYABLE_DISCOVERY_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503,
|
|
|
50
51
|
export function accountBaseUrl(accountType) {
|
|
51
52
|
return copilotBaseUrl(accountType);
|
|
52
53
|
}
|
|
53
|
-
export async function listModels(accountType, bearerToken, deps) {
|
|
54
|
+
export async function listModels(accountType, bearerToken, deps, accountId) {
|
|
54
55
|
const fetchImpl = deps?.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
55
56
|
const timeoutMs = deps?.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
56
57
|
try {
|
|
@@ -72,7 +73,7 @@ export async function listModels(accountType, bearerToken, deps) {
|
|
|
72
73
|
if (!parsed.success) {
|
|
73
74
|
throw new ModelDiscoverySchemaError("Model discovery response is invalid.");
|
|
74
75
|
}
|
|
75
|
-
saveModelCache(accountType, parsed.data);
|
|
76
|
+
saveModelCache(accountType, parsed.data, accountId);
|
|
76
77
|
return {
|
|
77
78
|
models: parsed.data,
|
|
78
79
|
source: "live",
|
|
@@ -85,7 +86,7 @@ export async function listModels(accountType, bearerToken, deps) {
|
|
|
85
86
|
if (!canUseCacheFallback(error)) {
|
|
86
87
|
throw error;
|
|
87
88
|
}
|
|
88
|
-
const cached = readModelCache(accountType);
|
|
89
|
+
const cached = readModelCache(accountType, accountId);
|
|
89
90
|
if (!cached) {
|
|
90
91
|
const detail = error instanceof Error ? error.message : "unknown error";
|
|
91
92
|
throw new Error(`Model discovery failed and no cache snapshot is available: ${detail}`);
|
|
@@ -119,7 +120,7 @@ export async function listModels(accountType, bearerToken, deps) {
|
|
|
119
120
|
* specify. Each attempt's own retry budget lives inside `listModels`'s
|
|
120
121
|
* cache-fallback path; this loop runs once per upstream call.
|
|
121
122
|
*/
|
|
122
|
-
export async function listModelsUnion(accountType, bearerToken, attempts = 3, deps) {
|
|
123
|
+
export async function listModelsUnion(accountType, bearerToken, attempts = 3, deps, accountId) {
|
|
123
124
|
const sleepImpl = deps?.sleepImpl ?? ((ms) => defaultSleep(ms));
|
|
124
125
|
const seen = new Map();
|
|
125
126
|
let lastResult = null;
|
|
@@ -127,7 +128,7 @@ export async function listModelsUnion(accountType, bearerToken, attempts = 3, de
|
|
|
127
128
|
let consecutiveFailures = 0;
|
|
128
129
|
for (let i = 0; i < attempts; i += 1) {
|
|
129
130
|
try {
|
|
130
|
-
const result = await listModels(accountType, bearerToken, deps);
|
|
131
|
+
const result = await listModels(accountType, bearerToken, deps, accountId);
|
|
131
132
|
lastResult = result;
|
|
132
133
|
consecutiveFailures = 0;
|
|
133
134
|
for (const model of result.models) {
|
|
@@ -274,17 +275,38 @@ function canUseCacheFallback(error) {
|
|
|
274
275
|
return true;
|
|
275
276
|
}
|
|
276
277
|
const CACHE_FALLBACK_STATUSES = new Set([401, 403, 408, 409, 425, 429]);
|
|
277
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Resolve the model-cache file for an account. `undefined` → the shared
|
|
280
|
+
* `models.cache.json` used by the primary/legacy account and single-account
|
|
281
|
+
* installs; a string → the per-account `models.cache.<id>.json`. The caller
|
|
282
|
+
* (the daemon) decides which based on the account's storage scheme, so a
|
|
283
|
+
* default-account switch never makes two accounts share a catalog file.
|
|
284
|
+
*/
|
|
285
|
+
function modelsCacheWriteFile(accountId) {
|
|
286
|
+
if (accountId === undefined) {
|
|
287
|
+
return modelsCachePath();
|
|
288
|
+
}
|
|
289
|
+
assertValidAccountId(accountId);
|
|
290
|
+
return accountModelsCachePath(accountId);
|
|
291
|
+
}
|
|
292
|
+
function modelsCacheReadFile(accountId) {
|
|
293
|
+
if (accountId === undefined) {
|
|
294
|
+
return modelsCacheReadPath();
|
|
295
|
+
}
|
|
296
|
+
assertValidAccountId(accountId);
|
|
297
|
+
return accountModelsCacheReadPath(accountId);
|
|
298
|
+
}
|
|
299
|
+
function saveModelCache(accountType, models, accountId) {
|
|
278
300
|
const payload = {
|
|
279
301
|
version: 1,
|
|
280
302
|
accountType,
|
|
281
303
|
savedAtIso: new Date().toISOString(),
|
|
282
304
|
models
|
|
283
305
|
};
|
|
284
|
-
writeFileSecureAtomic(
|
|
306
|
+
writeFileSecureAtomic(modelsCacheWriteFile(accountId), JSON.stringify(payload, null, 2), 0o600);
|
|
285
307
|
}
|
|
286
|
-
function readModelCache(accountType) {
|
|
287
|
-
const filePath =
|
|
308
|
+
function readModelCache(accountType, accountId) {
|
|
309
|
+
const filePath = modelsCacheReadFile(accountId);
|
|
288
310
|
if (!fs.existsSync(filePath)) {
|
|
289
311
|
return null;
|
|
290
312
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { CopilotTokenManager } from "../auth/copilotToken.js";
|
|
2
|
+
import { loadStoredCredentialForAccount } from "../auth/credentials.js";
|
|
3
|
+
import { findAccount } from "../auth/accounts.js";
|
|
4
|
+
/**
|
|
5
|
+
* A resolver that knows only the default account. Used to preserve the exact
|
|
6
|
+
* single-account behaviour when the proxy is started without a multi-account
|
|
7
|
+
* resolver (e.g. test harnesses). A prefixed request for any other account
|
|
8
|
+
* resolves to `null` → the proxy returns `account_not_found`.
|
|
9
|
+
*/
|
|
10
|
+
export function singleAccountResolver(input) {
|
|
11
|
+
const def = {
|
|
12
|
+
accountId: input.accountId ?? null,
|
|
13
|
+
githubToken: input.githubToken,
|
|
14
|
+
tokenManager: input.tokenManager,
|
|
15
|
+
accountType: input.accountType,
|
|
16
|
+
cacheId: input.cacheId
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
default: def,
|
|
20
|
+
async resolveById(accountId) {
|
|
21
|
+
if (def.accountId !== null && accountId === def.accountId) {
|
|
22
|
+
return def;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
},
|
|
26
|
+
describe() {
|
|
27
|
+
return { defaultAccountId: def.accountId, activeAccountIds: [] };
|
|
28
|
+
},
|
|
29
|
+
clearAll() {
|
|
30
|
+
def.tokenManager.clear();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The production resolver. Wraps the eagerly-built default account and lazily
|
|
36
|
+
* builds a bearer manager per named account the first time a request for it
|
|
37
|
+
* arrives. Bearer managers are cached for the daemon's lifetime so repeated
|
|
38
|
+
* requests reuse the same (refresh-coalescing) manager.
|
|
39
|
+
*/
|
|
40
|
+
export class DaemonAccountResolver {
|
|
41
|
+
default;
|
|
42
|
+
cache = new Map();
|
|
43
|
+
createTokenManager;
|
|
44
|
+
constructor(input) {
|
|
45
|
+
this.default = input.default;
|
|
46
|
+
this.createTokenManager = input.createTokenManager ?? ((githubToken) => new CopilotTokenManager(githubToken));
|
|
47
|
+
}
|
|
48
|
+
async resolveById(accountId) {
|
|
49
|
+
if (this.default.accountId !== null && accountId === this.default.accountId) {
|
|
50
|
+
return this.default;
|
|
51
|
+
}
|
|
52
|
+
const cached = this.cache.get(accountId);
|
|
53
|
+
if (cached) {
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
const credential = await loadStoredCredentialForAccount(accountId);
|
|
57
|
+
if (!credential) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const record = findAccount(accountId);
|
|
61
|
+
// The cache file follows the account's storage scheme, mirroring the
|
|
62
|
+
// credential store: a legacy-storage account shares `models.cache.json`,
|
|
63
|
+
// a namespaced account gets its own `models.cache.<id>.json`.
|
|
64
|
+
const cacheId = record && record.storage === "legacy" ? undefined : accountId;
|
|
65
|
+
const resolved = {
|
|
66
|
+
accountId,
|
|
67
|
+
githubToken: credential.token,
|
|
68
|
+
tokenManager: this.createTokenManager(credential.token),
|
|
69
|
+
accountType: credential.accountType,
|
|
70
|
+
cacheId
|
|
71
|
+
};
|
|
72
|
+
this.cache.set(accountId, resolved);
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
describe() {
|
|
76
|
+
return { defaultAccountId: this.default.accountId, activeAccountIds: [...this.cache.keys()] };
|
|
77
|
+
}
|
|
78
|
+
clearAll() {
|
|
79
|
+
this.default.tokenManager.clear();
|
|
80
|
+
for (const resolved of this.cache.values()) {
|
|
81
|
+
resolved.tokenManager.clear();
|
|
82
|
+
}
|
|
83
|
+
this.cache.clear();
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/server/proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { singleAccountResolver } from "./accountResolver.js";
|
|
3
4
|
import { attachRequestLifecycle, isBenignSocketError, safeEnd, safeSendJson } from "./requestLifecycle.js";
|
|
4
5
|
import { InvalidRequestShapeError, JsonRequestParseError } from "./errors.js";
|
|
5
6
|
import { ProtocolTranslationError } from "../translation/openaiAnthropic.js";
|
|
@@ -10,6 +11,22 @@ import { handleProxyForward } from "./routes/proxyForward.js";
|
|
|
10
11
|
import { isLocalRequest, resolveRoute, safePathname } from "./routes/shared.js";
|
|
11
12
|
export async function startProxyServer(input) {
|
|
12
13
|
const debugEnabled = input.debug === true;
|
|
14
|
+
const resolver = input.accountResolver ??
|
|
15
|
+
singleAccountResolver({
|
|
16
|
+
tokenManager: input.tokenManager,
|
|
17
|
+
githubToken: input.githubToken ?? "",
|
|
18
|
+
accountType: input.config.accountType
|
|
19
|
+
});
|
|
20
|
+
// Resolve the account a request targets. Returns the default account for an
|
|
21
|
+
// unprefixed request; for an `/<account>` prefix, looks up the named account
|
|
22
|
+
// and answers 404 `account_not_found` when no credential is stored for it.
|
|
23
|
+
const resolveAccountForRoute = async (route) => {
|
|
24
|
+
if (route.accountId === null) {
|
|
25
|
+
return resolver.default;
|
|
26
|
+
}
|
|
27
|
+
const account = await resolver.resolveById(route.accountId);
|
|
28
|
+
return account;
|
|
29
|
+
};
|
|
13
30
|
const server = createServer(async (req, res) => {
|
|
14
31
|
const requestId = randomUUID();
|
|
15
32
|
const startedAt = Date.now();
|
|
@@ -43,13 +60,21 @@ export async function startProxyServer(input) {
|
|
|
43
60
|
handleLivez(res);
|
|
44
61
|
return;
|
|
45
62
|
case "healthz":
|
|
46
|
-
await handleHealthz(res,
|
|
63
|
+
await handleHealthz(res, resolver.default.tokenManager);
|
|
47
64
|
return;
|
|
48
65
|
case "models":
|
|
66
|
+
await handleModels(res, route.kind, resolver.default);
|
|
67
|
+
return;
|
|
49
68
|
case "codex_models":
|
|
50
|
-
case "anthropic_models":
|
|
51
|
-
await
|
|
69
|
+
case "anthropic_models": {
|
|
70
|
+
const account = await resolveAccountForRoute(route);
|
|
71
|
+
if (!account) {
|
|
72
|
+
safeSendJson(res, 404, { error: "account_not_found", detail: `No stored credential for account "${route.accountId}".` });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await handleModels(res, route.kind, account);
|
|
52
76
|
return;
|
|
77
|
+
}
|
|
53
78
|
case "debug":
|
|
54
79
|
if (!debugEnabled) {
|
|
55
80
|
safeSendJson(res, 404, { error: "not_found" });
|
|
@@ -58,9 +83,10 @@ export async function startProxyServer(input) {
|
|
|
58
83
|
await handleDebug(res, {
|
|
59
84
|
config: input.config,
|
|
60
85
|
logger: input.logger,
|
|
61
|
-
tokenManager:
|
|
62
|
-
githubToken:
|
|
63
|
-
port: input.port
|
|
86
|
+
tokenManager: resolver.default.tokenManager,
|
|
87
|
+
githubToken: resolver.default.githubToken,
|
|
88
|
+
port: input.port,
|
|
89
|
+
accounts: resolver.describe()
|
|
64
90
|
});
|
|
65
91
|
return;
|
|
66
92
|
case "not_found":
|
|
@@ -68,18 +94,24 @@ export async function startProxyServer(input) {
|
|
|
68
94
|
return;
|
|
69
95
|
case "openai":
|
|
70
96
|
case "anthropic":
|
|
71
|
-
case "codex_responses":
|
|
97
|
+
case "codex_responses": {
|
|
98
|
+
const account = await resolveAccountForRoute(route);
|
|
99
|
+
if (!account) {
|
|
100
|
+
safeSendJson(res, 404, { error: "account_not_found", detail: `No stored credential for account "${route.accountId}".` });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
72
103
|
await handleProxyForward({
|
|
73
104
|
req,
|
|
74
105
|
res,
|
|
75
106
|
route,
|
|
76
107
|
config: input.config,
|
|
77
|
-
|
|
108
|
+
account,
|
|
78
109
|
logger: input.logger,
|
|
79
110
|
requestId,
|
|
80
111
|
signal: lifecycle.signal
|
|
81
112
|
});
|
|
82
113
|
return;
|
|
114
|
+
}
|
|
83
115
|
}
|
|
84
116
|
}
|
|
85
117
|
catch (error) {
|
|
@@ -48,6 +48,13 @@ export async function handleDebug(res, input) {
|
|
|
48
48
|
bearer_present: input.tokenManager.current !== null,
|
|
49
49
|
bearer_expires_at_unix: input.tokenManager.current?.expiresAtUnix ?? null
|
|
50
50
|
},
|
|
51
|
+
accounts: {
|
|
52
|
+
// Token is never included. Reports the default account id (null for a
|
|
53
|
+
// single-account install) and the named accounts that have served at
|
|
54
|
+
// least one request this daemon lifetime.
|
|
55
|
+
default: input.accounts?.defaultAccountId ?? null,
|
|
56
|
+
active: input.accounts?.activeAccountIds ?? []
|
|
57
|
+
},
|
|
51
58
|
user,
|
|
52
59
|
user_error: userError,
|
|
53
60
|
routes: [
|
|
@@ -4,16 +4,16 @@ import { buildCodexCatalog } from "../codexSchema.js";
|
|
|
4
4
|
import { buildAnthropicModelsResponse } from "../anthropicModelsResponse.js";
|
|
5
5
|
import { tokenErrorToHttpResponse } from "../errors.js";
|
|
6
6
|
import { safeSendJson } from "../requestLifecycle.js";
|
|
7
|
-
export async function handleModels(res, routeKind,
|
|
7
|
+
export async function handleModels(res, routeKind, account) {
|
|
8
8
|
try {
|
|
9
|
-
await tokenManager.ensureToken(false);
|
|
10
|
-
if (!githubToken) {
|
|
9
|
+
await account.tokenManager.ensureToken(false);
|
|
10
|
+
if (!account.githubToken) {
|
|
11
11
|
safeSendJson(res, 503, { error: "github_token_unavailable" });
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
const result = routeKind === "codex_models" || routeKind === "anthropic_models"
|
|
15
|
-
? await listModelsUnion(
|
|
16
|
-
: await listModels(
|
|
15
|
+
? await listModelsUnion(account.accountType, account.githubToken, 3, undefined, account.cacheId)
|
|
16
|
+
: await listModels(account.accountType, account.githubToken, undefined, account.cacheId);
|
|
17
17
|
if (routeKind === "codex_models") {
|
|
18
18
|
safeSendJson(res, 200, buildCodexCatalog(result.models));
|
|
19
19
|
return;
|
|
@@ -22,7 +22,7 @@ function translateRequestBody(routeKind, body) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
export async function handleProxyForward(input) {
|
|
25
|
-
const { req, res, route, config,
|
|
25
|
+
const { req, res, route, config, account, logger, requestId, signal } = input;
|
|
26
26
|
const requestBody = await readJson(req);
|
|
27
27
|
const translatedBody = translateRequestBody(route.kind, requestBody);
|
|
28
28
|
const requestedModel = readRequestedModel(translatedBody);
|
|
@@ -70,8 +70,8 @@ export async function handleProxyForward(input) {
|
|
|
70
70
|
}, "prepared upstream request");
|
|
71
71
|
try {
|
|
72
72
|
const upstream = await postToCopilot({
|
|
73
|
-
tokenManager,
|
|
74
|
-
accountType:
|
|
73
|
+
tokenManager: account.tokenManager,
|
|
74
|
+
accountType: account.accountType,
|
|
75
75
|
body: upstreamBody,
|
|
76
76
|
requestId,
|
|
77
77
|
logger,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stripOneMillionAlias } from "../../translation/openaiAnthropic.js";
|
|
2
|
+
import { isValidAccountId } from "../../config/accountId.js";
|
|
2
3
|
import { JsonRequestParseError } from "../errors.js";
|
|
3
4
|
export async function readJson(req) {
|
|
4
5
|
const chunks = [];
|
|
@@ -119,43 +120,87 @@ export function safePathname(rawUrl) {
|
|
|
119
120
|
return "/";
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
// First path segments that belong to real routes and must never be mistaken
|
|
124
|
+
// for an account prefix. (Direct matching already wins for these, so this is
|
|
125
|
+
// belt-and-suspenders against contrived nested paths.)
|
|
126
|
+
const RESERVED_FIRST_SEGMENTS = new Set([
|
|
127
|
+
"livez",
|
|
128
|
+
"healthz",
|
|
129
|
+
"models",
|
|
130
|
+
"v1",
|
|
131
|
+
"codex",
|
|
132
|
+
"anthropic",
|
|
133
|
+
"_debug"
|
|
134
|
+
]);
|
|
135
|
+
// Only these routes may be addressed with an `/<account>` prefix. The generic
|
|
136
|
+
// `/models` discovery route and all health/debug routes stay global.
|
|
137
|
+
const PREFIXABLE_KINDS = new Set([
|
|
138
|
+
"codex_models",
|
|
139
|
+
"anthropic_models",
|
|
140
|
+
"codex_responses",
|
|
141
|
+
"openai",
|
|
142
|
+
"anthropic"
|
|
143
|
+
]);
|
|
144
|
+
function matchRoute(method, pathname) {
|
|
133
145
|
if (method === "GET" && pathname === "/livez") {
|
|
134
|
-
return { kind: "livez", anthroShape: false };
|
|
146
|
+
return { kind: "livez", anthroShape: false, accountId: null };
|
|
135
147
|
}
|
|
136
148
|
if (method === "GET" && pathname === "/healthz") {
|
|
137
|
-
return { kind: "healthz", anthroShape: false };
|
|
149
|
+
return { kind: "healthz", anthroShape: false, accountId: null };
|
|
138
150
|
}
|
|
139
151
|
if (method === "GET" && (pathname === "/models" || pathname === "/v1/models")) {
|
|
140
|
-
return { kind: "models", anthroShape: false };
|
|
152
|
+
return { kind: "models", anthroShape: false, accountId: null };
|
|
141
153
|
}
|
|
142
154
|
if (method === "GET" && pathname === "/codex/v1/models") {
|
|
143
|
-
return { kind: "codex_models", anthroShape: false };
|
|
155
|
+
return { kind: "codex_models", anthroShape: false, accountId: null };
|
|
144
156
|
}
|
|
145
157
|
if (method === "GET" && pathname === "/anthropic/v1/models") {
|
|
146
|
-
return { kind: "anthropic_models", anthroShape: false };
|
|
158
|
+
return { kind: "anthropic_models", anthroShape: false, accountId: null };
|
|
147
159
|
}
|
|
148
160
|
if (method === "POST" && pathname === "/codex/v1/responses") {
|
|
149
|
-
return { kind: "codex_responses", anthroShape: false };
|
|
161
|
+
return { kind: "codex_responses", anthroShape: false, accountId: null };
|
|
150
162
|
}
|
|
151
163
|
if (method === "GET" && pathname === "/_debug") {
|
|
152
|
-
return { kind: "debug", anthroShape: false };
|
|
164
|
+
return { kind: "debug", anthroShape: false, accountId: null };
|
|
153
165
|
}
|
|
154
166
|
if (method === "POST" && pathname === "/v1/chat/completions") {
|
|
155
|
-
return { kind: "openai", anthroShape: false };
|
|
167
|
+
return { kind: "openai", anthroShape: false, accountId: null };
|
|
156
168
|
}
|
|
157
169
|
if (method === "POST" && (pathname === "/anthropic/v1/messages" || pathname === "/v1/messages")) {
|
|
158
|
-
return { kind: "anthropic", anthroShape: true };
|
|
170
|
+
return { kind: "anthropic", anthroShape: true, accountId: null };
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
export function resolveRoute(method, rawUrl) {
|
|
175
|
+
if (!method || !rawUrl) {
|
|
176
|
+
return { kind: "not_found", anthroShape: false, accountId: null };
|
|
177
|
+
}
|
|
178
|
+
let pathname;
|
|
179
|
+
try {
|
|
180
|
+
pathname = new URL(rawUrl, "http://127.0.0.1").pathname;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { kind: "not_found", anthroShape: false, accountId: null };
|
|
184
|
+
}
|
|
185
|
+
// Try the path as-is first. This keeps every existing (unprefixed) route
|
|
186
|
+
// working unchanged and ensures a reserved first segment like `/codex/...`
|
|
187
|
+
// is always interpreted as a route, never as an account named "codex".
|
|
188
|
+
const direct = matchRoute(method, pathname);
|
|
189
|
+
if (direct) {
|
|
190
|
+
return direct;
|
|
191
|
+
}
|
|
192
|
+
// Otherwise, peel an optional leading `/<account>` segment and re-match the
|
|
193
|
+
// remainder against the prefixable routes.
|
|
194
|
+
const prefixMatch = pathname.match(/^\/([^/]+)(\/.*)$/);
|
|
195
|
+
if (prefixMatch) {
|
|
196
|
+
const candidate = prefixMatch[1];
|
|
197
|
+
const rest = prefixMatch[2];
|
|
198
|
+
if (isValidAccountId(candidate) && !RESERVED_FIRST_SEGMENTS.has(candidate)) {
|
|
199
|
+
const sub = matchRoute(method, rest);
|
|
200
|
+
if (sub && PREFIXABLE_KINDS.has(sub.kind)) {
|
|
201
|
+
return { ...sub, accountId: candidate };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
159
204
|
}
|
|
160
|
-
return { kind: "not_found", anthroShape: false };
|
|
205
|
+
return { kind: "not_found", anthroShape: false, accountId: null };
|
|
161
206
|
}
|