@spinabot/brigade 1.2.2 → 1.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 +18 -4
- package/convex/schema.d.ts +6 -6
- package/convex/sessions.d.ts +4 -4
- package/convex/subagents.d.ts +4 -4
- package/dist/agents/agent-loop.d.ts.map +1 -1
- package/dist/agents/agent-loop.js +70 -10
- package/dist/agents/agent-loop.js.map +1 -1
- package/dist/agents/tools/manage-agent-tool.d.ts.map +1 -1
- package/dist/agents/tools/manage-agent-tool.js +2 -1
- package/dist/agents/tools/manage-agent-tool.js.map +1 -1
- package/dist/agents/tools/manage-provider-tool.d.ts.map +1 -1
- package/dist/agents/tools/manage-provider-tool.js +2 -1
- package/dist/agents/tools/manage-provider-tool.js.map +1 -1
- package/dist/agents/tools/org-tool.d.ts +15 -0
- package/dist/agents/tools/org-tool.d.ts.map +1 -1
- package/dist/agents/tools/org-tool.js +53 -6
- package/dist/agents/tools/org-tool.js.map +1 -1
- package/dist/agents/tools/registry.d.ts.map +1 -1
- package/dist/agents/tools/registry.js +3 -0
- package/dist/agents/tools/registry.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +25 -6
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/login.d.ts +27 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +142 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/flows/web-setup.d.ts +2 -2
- package/dist/cli/flows/web-setup.js +2 -2
- package/dist/cli/program/build-program.d.ts.map +1 -1
- package/dist/cli/program/build-program.js +8 -0
- package/dist/cli/program/build-program.js.map +1 -1
- package/dist/core/auth-bridge.d.ts.map +1 -1
- package/dist/core/auth-bridge.js +75 -25
- package/dist/core/auth-bridge.js.map +1 -1
- package/dist/integrations/cli-login.d.ts +50 -0
- package/dist/integrations/cli-login.d.ts.map +1 -0
- package/dist/integrations/cli-login.js +114 -0
- package/dist/integrations/cli-login.js.map +1 -0
- package/dist/integrations/custom-provider.d.ts +21 -0
- package/dist/integrations/custom-provider.d.ts.map +1 -0
- package/dist/integrations/custom-provider.js +65 -0
- package/dist/integrations/custom-provider.js.map +1 -0
- package/dist/integrations/provider-discovery.d.ts +30 -0
- package/dist/integrations/provider-discovery.d.ts.map +1 -1
- package/dist/integrations/provider-discovery.js +155 -0
- package/dist/integrations/provider-discovery.js.map +1 -1
- package/dist/providers/catalog.d.ts +33 -0
- package/dist/providers/catalog.d.ts.map +1 -1
- package/dist/providers/catalog.js +91 -5
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/validate-key.d.ts.map +1 -1
- package/dist/providers/validate-key.js +20 -6
- package/dist/providers/validate-key.js.map +1 -1
- package/dist/system-prompt/org/render-org-block.js +2 -2
- package/dist/system-prompt/org/render-org-block.js.map +1 -1
- package/dist/ui/onboard-storage-mode.js +24 -21
- package/dist/ui/onboard-storage-mode.js.map +1 -1
- package/dist/ui/onboarding.d.ts +18 -0
- package/dist/ui/onboarding.d.ts.map +1 -1
- package/dist/ui/onboarding.js +576 -43
- package/dist/ui/onboarding.js.map +1 -1
- package/package.json +6 -2
- package/scripts/assets/brigade-favicon.ico +0 -0
- package/scripts/assets/brigade-logo.webp +0 -0
- package/scripts/brand-oauth-page.mjs +92 -0
package/dist/core/auth-bridge.js
CHANGED
|
@@ -52,51 +52,101 @@ function readBrigadeCredentials(agentId) {
|
|
|
52
52
|
// Fall through to env-backed bootstrap.
|
|
53
53
|
}
|
|
54
54
|
for (const profile of Object.values(parsed.profiles ?? {})) {
|
|
55
|
-
if (!profile?.provider
|
|
55
|
+
if (!profile?.provider)
|
|
56
56
|
continue;
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
if (out[profile.provider] !== undefined)
|
|
58
|
+
continue; // first-wins per provider
|
|
59
|
+
// Subscription credentials (OAuth login / setup-token) pass straight
|
|
60
|
+
// through — Pi's AuthStorage handles {type:"oauth"} (auto-refresh) and
|
|
61
|
+
// detects an `sk-ant-oat…` token to switch to Bearer auth.
|
|
62
|
+
if (profile.type === "oauth" || profile.type === "token") {
|
|
63
|
+
const cred = subscriptionProfileToCredential(profile);
|
|
64
|
+
if (cred)
|
|
65
|
+
out[profile.provider] = cred;
|
|
59
66
|
continue;
|
|
60
|
-
// First-wins per provider — matches Primitive #1's no-cooldown path.
|
|
61
|
-
if (out[profile.provider] === undefined) {
|
|
62
|
-
out[profile.provider] = { type: "api_key", key: resolvedKey };
|
|
63
67
|
}
|
|
68
|
+
if (profile.type !== "api_key")
|
|
69
|
+
continue;
|
|
70
|
+
const resolvedKey = resolveProfileKey(profile);
|
|
71
|
+
if (resolvedKey)
|
|
72
|
+
out[profile.provider] = { type: "api_key", key: resolvedKey };
|
|
64
73
|
}
|
|
65
|
-
// Env-backed bootstrap. If a user has ANTHROPIC_API_KEY (
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// take precedence — env is only consulted when a provider has no profile
|
|
69
|
-
// entry.
|
|
74
|
+
// Env-backed bootstrap. If a user has ANTHROPIC_API_KEY (or the OAuth-token
|
|
75
|
+
// fallback ANTHROPIC_OAUTH_TOKEN) exported but never ran `brigade onboard`,
|
|
76
|
+
// surface it so Pi's registry exposes the provider. Profile-stored creds
|
|
77
|
+
// take precedence — env is only consulted when a provider has no profile.
|
|
70
78
|
for (const provider of PROVIDERS) {
|
|
71
|
-
if (
|
|
79
|
+
if (provider.noAuth)
|
|
72
80
|
continue;
|
|
73
81
|
if (out[provider.id] !== undefined)
|
|
74
82
|
continue;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
const envNames = [provider.envVar, ...(provider.envVarFallbacks ?? [])];
|
|
84
|
+
for (const name of envNames) {
|
|
85
|
+
if (!name)
|
|
86
|
+
continue;
|
|
87
|
+
const value = process.env[name];
|
|
88
|
+
if (!value)
|
|
89
|
+
continue;
|
|
90
|
+
out[provider.id] = { type: "api_key", key: value };
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
79
93
|
}
|
|
80
94
|
return out;
|
|
81
95
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
// Resolve a literal-or-ref secret value (key / access / refresh / token).
|
|
97
|
+
// String refs are the legacy `${ENV_VAR}` form; object refs (BrigadeSecretRef)
|
|
98
|
+
// resolve only env source synchronously — file/exec backends are out of scope
|
|
99
|
+
// for the bridge's sync build.
|
|
100
|
+
function resolveRefValue(value, ref) {
|
|
101
|
+
if (value && value.length > 0)
|
|
102
|
+
return value;
|
|
86
103
|
if (!ref)
|
|
87
104
|
return "";
|
|
88
|
-
// String form (legacy): `${ENV_VAR}` literal. Matches the regex agent-loop
|
|
89
|
-
// uses for the same shape.
|
|
90
105
|
if (typeof ref === "string") {
|
|
91
106
|
const m = /^\$\{([A-Z_][A-Z0-9_]*)\}$/.exec(ref);
|
|
92
107
|
if (m && m[1])
|
|
93
108
|
return process.env[m[1]] ?? "";
|
|
94
109
|
return ref;
|
|
95
110
|
}
|
|
96
|
-
|
|
97
|
-
if (ref.source === "env" && ref.id) {
|
|
111
|
+
if (ref.source === "env" && ref.id)
|
|
98
112
|
return process.env[ref.id] ?? "";
|
|
99
|
-
}
|
|
100
113
|
return "";
|
|
101
114
|
}
|
|
115
|
+
function resolveProfileKey(profile) {
|
|
116
|
+
return resolveRefValue(profile.key, profile.keyRef);
|
|
117
|
+
}
|
|
118
|
+
// Map an OAuth-login / setup-token profile to a Pi credential. OAuth →
|
|
119
|
+
// {type:"oauth", access, refresh, expires} (Pi auto-refreshes); token →
|
|
120
|
+
// {type:"api_key", key} so Pi's value-based `sk-ant-oat` Bearer detection
|
|
121
|
+
// fires. Returns null when no secret resolves.
|
|
122
|
+
function subscriptionProfileToCredential(profile) {
|
|
123
|
+
if (profile.type === "oauth") {
|
|
124
|
+
const access = resolveRefValue(profile.access, profile.accessRef);
|
|
125
|
+
if (!access)
|
|
126
|
+
return null;
|
|
127
|
+
const refresh = resolveRefValue(profile.refresh, profile.refreshRef);
|
|
128
|
+
// A durable oauth profile CAN lack `expires` (the Claude/Codex CLI-login
|
|
129
|
+
// path). Coerce a missing/garbage value to 0 so Pi treats the access token
|
|
130
|
+
// as expired and refreshes via the refresh token immediately. Spread
|
|
131
|
+
// `metadata` FIRST so the known oauth fields always win — it carries the
|
|
132
|
+
// extras (Copilot enterprise refresh + availableModelIds) Pi needs.
|
|
133
|
+
const expires = typeof profile.expires === "number" && Number.isFinite(profile.expires)
|
|
134
|
+
? profile.expires
|
|
135
|
+
: 0;
|
|
136
|
+
return {
|
|
137
|
+
...(profile.metadata && typeof profile.metadata === "object" ? profile.metadata : {}),
|
|
138
|
+
type: "oauth",
|
|
139
|
+
access,
|
|
140
|
+
refresh: refresh || undefined,
|
|
141
|
+
expires,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (profile.type === "token") {
|
|
145
|
+
const token = resolveRefValue(profile.token, profile.tokenRef);
|
|
146
|
+
if (!token)
|
|
147
|
+
return null;
|
|
148
|
+
return { type: "api_key", key: token };
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
102
152
|
//# sourceMappingURL=auth-bridge.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-bridge.js","sourceRoot":"","sources":["../../src/core/auth-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAE9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth-bridge.js","sourceRoot":"","sources":["../../src/core/auth-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reuse an already-logged-in CLI's stored credential.
|
|
3
|
+
*
|
|
4
|
+
* Brigade can adopt the token that a vendor's own CLI already minted on this
|
|
5
|
+
* machine — so an operator who has logged into Claude Code or Codex can connect
|
|
6
|
+
* Brigade with no browser flow and no API key. We read the CLI's on-disk
|
|
7
|
+
* credential file directly (file-based only for v1) and hand it back in a normalized
|
|
8
|
+
* shape the onboarding flow persists via the standard profile helpers.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is DEFENSIVE: any missing file, parse failure, or malformed
|
|
11
|
+
* shape returns `null`. We never throw — a failed read just means "no CLI login
|
|
12
|
+
* present" and onboarding falls through to the key / fresh-login path.
|
|
13
|
+
*
|
|
14
|
+
* // TODO: macOS keychain (Claude Code-credentials / Codex Auth) — file-based only for v1.
|
|
15
|
+
*/
|
|
16
|
+
/** Normalized credential returned by a CLI-login reader. */
|
|
17
|
+
export type CliLogin = {
|
|
18
|
+
provider: string;
|
|
19
|
+
type: "oauth";
|
|
20
|
+
access: string;
|
|
21
|
+
refresh: string;
|
|
22
|
+
expires?: number;
|
|
23
|
+
accountId?: string;
|
|
24
|
+
} | {
|
|
25
|
+
provider: string;
|
|
26
|
+
type: "token";
|
|
27
|
+
token: string;
|
|
28
|
+
expires?: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Read Claude Code's stored login from `~/.claude/.credentials.json`.
|
|
32
|
+
*
|
|
33
|
+
* Shape: `{ claudeAiOauth: { accessToken, refreshToken, expiresAt } }`.
|
|
34
|
+
* When a refresh token is present we return an `oauth` credential (Pi can
|
|
35
|
+
* refresh it); otherwise a `token` credential carrying just the access token.
|
|
36
|
+
* Returns `null` on any missing file / parse error / absent access token.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readClaudeCliLogin(): CliLogin | null;
|
|
39
|
+
/**
|
|
40
|
+
* Read Codex's stored login from `${CODEX_HOME || ~/.codex}/auth.json`.
|
|
41
|
+
*
|
|
42
|
+
* Shape: `{ tokens: { access_token, refresh_token, account_id } }`. Codex
|
|
43
|
+
* doesn't store an explicit expiry, so we decode the `exp` claim from the
|
|
44
|
+
* access token JWT (×1000 → epoch-ms). If decoding fails we fall back to the
|
|
45
|
+
* file's mtime + 1 hour — a conservative TTL that triggers a refresh sooner
|
|
46
|
+
* rather than later. Returns `null` on any missing file / parse error / absent
|
|
47
|
+
* access token.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readCodexCliLogin(): CliLogin | null;
|
|
50
|
+
//# sourceMappingURL=cli-login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-login.d.ts","sourceRoot":"","sources":["../../src/integrations/cli-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,4DAA4D;AAC5D,MAAM,MAAM,QAAQ,GACjB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1G;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,IAAI,QAAQ,GAAG,IAAI,CAmBpD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,QAAQ,GAAG,IAAI,CAkCnD"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reuse an already-logged-in CLI's stored credential.
|
|
3
|
+
*
|
|
4
|
+
* Brigade can adopt the token that a vendor's own CLI already minted on this
|
|
5
|
+
* machine — so an operator who has logged into Claude Code or Codex can connect
|
|
6
|
+
* Brigade with no browser flow and no API key. We read the CLI's on-disk
|
|
7
|
+
* credential file directly (file-based only for v1) and hand it back in a normalized
|
|
8
|
+
* shape the onboarding flow persists via the standard profile helpers.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is DEFENSIVE: any missing file, parse failure, or malformed
|
|
11
|
+
* shape returns `null`. We never throw — a failed read just means "no CLI login
|
|
12
|
+
* present" and onboarding falls through to the key / fresh-login path.
|
|
13
|
+
*
|
|
14
|
+
* // TODO: macOS keychain (Claude Code-credentials / Codex Auth) — file-based only for v1.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
/**
|
|
20
|
+
* Read Claude Code's stored login from `~/.claude/.credentials.json`.
|
|
21
|
+
*
|
|
22
|
+
* Shape: `{ claudeAiOauth: { accessToken, refreshToken, expiresAt } }`.
|
|
23
|
+
* When a refresh token is present we return an `oauth` credential (Pi can
|
|
24
|
+
* refresh it); otherwise a `token` credential carrying just the access token.
|
|
25
|
+
* Returns `null` on any missing file / parse error / absent access token.
|
|
26
|
+
*/
|
|
27
|
+
export function readClaudeCliLogin() {
|
|
28
|
+
try {
|
|
29
|
+
const credPath = path.join(homedir(), ".claude", ".credentials.json");
|
|
30
|
+
if (!fs.existsSync(credPath))
|
|
31
|
+
return null;
|
|
32
|
+
const parsed = JSON.parse(fs.readFileSync(credPath, "utf8"));
|
|
33
|
+
const oauth = parsed.claudeAiOauth;
|
|
34
|
+
const access = oauth?.accessToken;
|
|
35
|
+
if (!access)
|
|
36
|
+
return null;
|
|
37
|
+
const refresh = oauth?.refreshToken;
|
|
38
|
+
const expires = oauth?.expiresAt;
|
|
39
|
+
if (refresh) {
|
|
40
|
+
return { provider: "anthropic", type: "oauth", access, refresh, expires };
|
|
41
|
+
}
|
|
42
|
+
return { provider: "anthropic", type: "token", token: access, expires };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read Codex's stored login from `${CODEX_HOME || ~/.codex}/auth.json`.
|
|
50
|
+
*
|
|
51
|
+
* Shape: `{ tokens: { access_token, refresh_token, account_id } }`. Codex
|
|
52
|
+
* doesn't store an explicit expiry, so we decode the `exp` claim from the
|
|
53
|
+
* access token JWT (×1000 → epoch-ms). If decoding fails we fall back to the
|
|
54
|
+
* file's mtime + 1 hour — a conservative TTL that triggers a refresh sooner
|
|
55
|
+
* rather than later. Returns `null` on any missing file / parse error / absent
|
|
56
|
+
* access token.
|
|
57
|
+
*/
|
|
58
|
+
export function readCodexCliLogin() {
|
|
59
|
+
try {
|
|
60
|
+
const codexHome = process.env.CODEX_HOME || path.join(homedir(), ".codex");
|
|
61
|
+
const authPath = path.join(codexHome, "auth.json");
|
|
62
|
+
if (!fs.existsSync(authPath))
|
|
63
|
+
return null;
|
|
64
|
+
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
65
|
+
const tokens = parsed.tokens;
|
|
66
|
+
const access = tokens?.access_token;
|
|
67
|
+
const refresh = tokens?.refresh_token;
|
|
68
|
+
if (!access || !refresh)
|
|
69
|
+
return null;
|
|
70
|
+
let expires = decodeJwtExp(access);
|
|
71
|
+
if (expires === undefined) {
|
|
72
|
+
try {
|
|
73
|
+
expires = fs.statSync(authPath).mtimeMs + 3600_000;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* mtime unavailable — leave expires undefined */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const accountId = tokens?.account_id;
|
|
80
|
+
return {
|
|
81
|
+
provider: "openai-codex",
|
|
82
|
+
type: "oauth",
|
|
83
|
+
access,
|
|
84
|
+
refresh,
|
|
85
|
+
expires,
|
|
86
|
+
...(accountId ? { accountId } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decode the `exp` claim (seconds) of a JWT and return it as epoch-ms, or
|
|
95
|
+
* `undefined` if the token isn't a parseable three-part JWT with a numeric
|
|
96
|
+
* `exp`. Base64url-decodes the middle (payload) segment.
|
|
97
|
+
*/
|
|
98
|
+
function decodeJwtExp(jwt) {
|
|
99
|
+
try {
|
|
100
|
+
const parts = jwt.split(".");
|
|
101
|
+
if (parts.length !== 3)
|
|
102
|
+
return undefined;
|
|
103
|
+
const payloadJson = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
104
|
+
const payload = JSON.parse(payloadJson);
|
|
105
|
+
if (typeof payload.exp === "number" && Number.isFinite(payload.exp)) {
|
|
106
|
+
return payload.exp * 1000;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=cli-login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-login.js","sourceRoot":"","sources":["../../src/integrations/cli-login.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom (catalog-defined) provider registration.
|
|
3
|
+
*
|
|
4
|
+
* Some providers ship a key + a known Anthropic-compatible (or OpenAI-
|
|
5
|
+
* compatible) endpoint we already know from the catalog — GLM, Kimi, Qwen,
|
|
6
|
+
* MiniMax, DeepSeek. Pi-AI doesn't bundle these as built-in providers, so we
|
|
7
|
+
* register them dynamically via the `~/.brigade/models.json` mechanism, the
|
|
8
|
+
* same way Ollama is registered. Each catalog model id becomes a Pi model
|
|
9
|
+
* routed through the provider's `baseUrl` + `api`.
|
|
10
|
+
*
|
|
11
|
+
* We MERGE rather than overwrite — the user (or other providers) may have
|
|
12
|
+
* existing entries in the file we shouldn't clobber.
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeCustomProviderToModelsJson(modelsJsonPath: string, p: {
|
|
15
|
+
id: string;
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
api: "openai-completions" | "anthropic-messages";
|
|
18
|
+
apiKey: string;
|
|
19
|
+
models: string[];
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=custom-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-provider.d.ts","sourceRoot":"","sources":["../../src/integrations/custom-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,wBAAsB,+BAA+B,CACpD,cAAc,EAAE,MAAM,EACtB,CAAC,EAAE;IACF,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB,GACC,OAAO,CAAC,IAAI,CAAC,CAgDf"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom (catalog-defined) provider registration.
|
|
3
|
+
*
|
|
4
|
+
* Some providers ship a key + a known Anthropic-compatible (or OpenAI-
|
|
5
|
+
* compatible) endpoint we already know from the catalog — GLM, Kimi, Qwen,
|
|
6
|
+
* MiniMax, DeepSeek. Pi-AI doesn't bundle these as built-in providers, so we
|
|
7
|
+
* register them dynamically via the `~/.brigade/models.json` mechanism, the
|
|
8
|
+
* same way Ollama is registered. Each catalog model id becomes a Pi model
|
|
9
|
+
* routed through the provider's `baseUrl` + `api`.
|
|
10
|
+
*
|
|
11
|
+
* We MERGE rather than overwrite — the user (or other providers) may have
|
|
12
|
+
* existing entries in the file we shouldn't clobber.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { tryGetRuntimeContext } from "../storage/runtime-context.js";
|
|
17
|
+
export async function writeCustomProviderToModelsJson(modelsJsonPath, p) {
|
|
18
|
+
let existing = { providers: {} };
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(modelsJsonPath, "utf8");
|
|
21
|
+
existing = JSON.parse(raw);
|
|
22
|
+
if (!existing.providers)
|
|
23
|
+
existing.providers = {};
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// File missing or unparseable — start fresh. Pi treats an absent file as no config.
|
|
27
|
+
}
|
|
28
|
+
existing.providers[p.id] = {
|
|
29
|
+
baseUrl: p.baseUrl,
|
|
30
|
+
api: p.api,
|
|
31
|
+
apiKey: p.apiKey,
|
|
32
|
+
models: p.models.map((id) => ({ id, name: id })),
|
|
33
|
+
};
|
|
34
|
+
// In convex mode resolveModelsPath routes to the OS cache dir, which may
|
|
35
|
+
// not exist yet on a fresh machine — a bare write would ENOENT. Filesystem
|
|
36
|
+
// mode: ~/.brigade always exists by this point, so the mkdir is a no-op.
|
|
37
|
+
await fs.mkdir(path.dirname(modelsJsonPath), { recursive: true });
|
|
38
|
+
await fs.writeFile(modelsJsonPath, JSON.stringify(existing, null, 2), "utf8");
|
|
39
|
+
// The coding-plan apiKey is written PLAINTEXT into models.json. Lock the
|
|
40
|
+
// file down to owner-only on POSIX so a shared-host neighbour can't read the
|
|
41
|
+
// key (mirrors the `chmodIfPosix` pattern in src/auth/profiles.ts). No-op on
|
|
42
|
+
// Windows (NTFS perms model differs) and best-effort on filesystems that
|
|
43
|
+
// don't support chmod (e.g. mounted FAT32).
|
|
44
|
+
if (process.platform !== "win32") {
|
|
45
|
+
try {
|
|
46
|
+
await fs.chmod(modelsJsonPath, 0o600);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Filesystem may not support chmod — non-fatal.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Convex mode — the file just written lives in the OS cache (resolveModelsPath
|
|
53
|
+
// routed it there) and is a regenerable mirror; the durable copy is the
|
|
54
|
+
// sealed "models" blob. Push it so a fresh machine re-materialises the
|
|
55
|
+
// catalog at boot.
|
|
56
|
+
const rctx = tryGetRuntimeContext();
|
|
57
|
+
if (rctx?.mode === "convex") {
|
|
58
|
+
await rctx.store.auth
|
|
59
|
+
.writeAuthFileBlob("main", "models", existing)
|
|
60
|
+
.catch((err) => {
|
|
61
|
+
console.error(`brigade: models catalog write to convex failed — ${err.message}`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=custom-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"custom-provider.js","sourceRoot":"","sources":["../../src/integrations/custom-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAErE,MAAM,CAAC,KAAK,UAAU,+BAA+B,CACpD,cAAsB,EACtB,CAMC;IAED,IAAI,QAAQ,GAAwC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACtE,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACtD,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,SAAS;YAAE,QAAQ,CAAC,SAAS,GAAG,EAAE,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACR,oFAAoF;IACrF,CAAC;IAED,QAAQ,CAAC,SAAU,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG;QAC3B,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;KAChD,CAAC;IAEF,yEAAyE;IACzE,2EAA2E;IAC3E,yEAAyE;IACzE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAClE,MAAM,EAAE,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAE9E,yEAAyE;IACzE,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE,4CAA4C;IAC5C,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAClC,IAAI,CAAC;YACJ,MAAM,EAAE,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACR,gDAAgD;QACjD,CAAC;IACF,CAAC;IAED,+EAA+E;IAC/E,wEAAwE;IACxE,uEAAuE;IACvE,mBAAmB;IACnB,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;IACpC,IAAI,IAAI,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI;aACnB,iBAAiB,CAAC,MAAM,EAAE,QAAiB,EAAE,QAAmC,CAAC;aACjF,KAAK,CAAC,CAAC,GAAU,EAAE,EAAE;YACrB,OAAO,CAAC,KAAK,CAAC,oDAAoD,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAClF,CAAC,CAAC,CAAC;IACL,CAAC;AACF,CAAC"}
|
|
@@ -63,4 +63,34 @@ export interface LiveCloudModel {
|
|
|
63
63
|
* never throws. Pricing → `cost.input` is per-Mtok (OpenRouter quotes per-token).
|
|
64
64
|
*/
|
|
65
65
|
export declare function listOpenRouterModels(): Promise<LiveCloudModel[]>;
|
|
66
|
+
/** Last-fetched live models for a subscription provider, or `undefined` if never fetched. */
|
|
67
|
+
export declare function getCachedSubscriptionModels(providerId: string): LiveCloudModel[] | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* GitHub Copilot's per-account model catalog. The token embeds the proxy host
|
|
70
|
+
* (`proxy-ep=…`) which `getGitHubCopilotBaseUrl` rewrites to the api host; we GET
|
|
71
|
+
* `${baseUrl}/models` with Copilot's required editor headers (a plain
|
|
72
|
+
* Authorization isn't enough — the endpoint 400s without the editor/integration
|
|
73
|
+
* headers). Keeps only models the account can actually pick (model_picker_enabled,
|
|
74
|
+
* policy not disabled, tool_calls not explicitly off). Never throws — returns the
|
|
75
|
+
* last good cache or `[]` so the caller falls back to Pi's bundled catalog.
|
|
76
|
+
*/
|
|
77
|
+
export declare function fetchGitHubCopilotModels(copilotToken: string): Promise<LiveCloudModel[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Models a Claude Pro/Max SUBSCRIPTION can use. There is NO live per-account
|
|
80
|
+
* models endpoint for a subscription OAuth token: Anthropic scopes that token to
|
|
81
|
+
* inference only, so `GET /v1/models` returns 401/403 (verified). Instead we
|
|
82
|
+
* surface Pi's bundled Anthropic catalog — it IS the current Claude model family
|
|
83
|
+
* and updates as Pi ships new models — so the picker is always populated and
|
|
84
|
+
* current with ZERO network round-trip (no guaranteed-fail request, no timeout)
|
|
85
|
+
* on every sign-in. (GitHub Copilot DOES expose a live per-account list, so that
|
|
86
|
+
* path stays a real fetch; Anthropic subscriptions simply don't have one.)
|
|
87
|
+
*/
|
|
88
|
+
export declare function fetchAnthropicSubscriptionModels(): LiveCloudModel[];
|
|
89
|
+
/**
|
|
90
|
+
* Warm the live cache for a subscription provider right after OAuth login so the
|
|
91
|
+
* model picker has the account's CURRENT models ready. Best-effort: any failure is
|
|
92
|
+
* swallowed (login must never block on this). `codex` has no live list endpoint —
|
|
93
|
+
* it falls through to Pi's bundled catalog, so this is a no-op for it.
|
|
94
|
+
*/
|
|
95
|
+
export declare function prefetchSubscriptionModels(providerId: string, oauthAccessToken: string): Promise<void>;
|
|
66
96
|
//# sourceMappingURL=provider-discovery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-discovery.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"provider-discovery.d.ts","sourceRoot":"","sources":["../../src/integrations/provider-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,MAAM,WAAW,mBAAmB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC/B,qEAAqE;IACrE,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,IAAI,EAAE,mBAAmB,CAAC;CAC1B;AAyED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC3C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,eAAe,CAAC,CAQ1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF;AAKD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAsDtE;AAeD,6FAA6F;AAC7F,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,SAAS,CAG5F;AAED;;;;;;;;GAQG;AACH,wBAAsB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAiE9F;AAED;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,IAAI,cAAc,EAAE,CA4BnE;AAED;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe5G"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `integrations/ollama.ts` (native /api/tags); this module is for cloud +
|
|
15
15
|
* OpenAI-compatible HTTP endpoints.
|
|
16
16
|
*/
|
|
17
|
+
import { getModels } from "@earendil-works/pi-ai";
|
|
17
18
|
const TIMEOUT_MS = 5000;
|
|
18
19
|
const EMPTY = { exists: false, meta: {} };
|
|
19
20
|
/** OpenRouter's public model list — no key needed, rich metadata. */
|
|
@@ -163,4 +164,158 @@ export async function listOpenRouterModels() {
|
|
|
163
164
|
return openRouterListCache?.models ?? [];
|
|
164
165
|
}
|
|
165
166
|
}
|
|
167
|
+
/* ──────────────────────── subscription live catalogs ──────────────────────── */
|
|
168
|
+
/**
|
|
169
|
+
* Live model catalogs for OAuth subscription providers (GitHub Copilot,
|
|
170
|
+
* Anthropic Claude Pro/Max). Same shape + degrade-to-cache contract as
|
|
171
|
+
* `listOpenRouterModels`, but keyed by providerId because there are several
|
|
172
|
+
* subscription providers (OpenRouter is a singleton). The onboarding picker
|
|
173
|
+
* reads the cache after login via `getCachedSubscriptionModels` and joins the
|
|
174
|
+
* static Pi catalog by id for richer metadata.
|
|
175
|
+
*/
|
|
176
|
+
const SUBSCRIPTION_MODELS_TTL_MS = 5 * 60_000;
|
|
177
|
+
const subscriptionModelsCache = new Map();
|
|
178
|
+
/** Last-fetched live models for a subscription provider, or `undefined` if never fetched. */
|
|
179
|
+
export function getCachedSubscriptionModels(providerId) {
|
|
180
|
+
const e = subscriptionModelsCache.get(providerId);
|
|
181
|
+
return e ? e.models : undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* GitHub Copilot's per-account model catalog. The token embeds the proxy host
|
|
185
|
+
* (`proxy-ep=…`) which `getGitHubCopilotBaseUrl` rewrites to the api host; we GET
|
|
186
|
+
* `${baseUrl}/models` with Copilot's required editor headers (a plain
|
|
187
|
+
* Authorization isn't enough — the endpoint 400s without the editor/integration
|
|
188
|
+
* headers). Keeps only models the account can actually pick (model_picker_enabled,
|
|
189
|
+
* policy not disabled, tool_calls not explicitly off). Never throws — returns the
|
|
190
|
+
* last good cache or `[]` so the caller falls back to Pi's bundled catalog.
|
|
191
|
+
*/
|
|
192
|
+
export async function fetchGitHubCopilotModels(copilotToken) {
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
const cached = subscriptionModelsCache.get("github-copilot");
|
|
195
|
+
if (cached && now - cached.at < SUBSCRIPTION_MODELS_TTL_MS) {
|
|
196
|
+
return cached.models;
|
|
197
|
+
}
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
200
|
+
try {
|
|
201
|
+
const { getGitHubCopilotBaseUrl } = await import("@earendil-works/pi-ai/oauth");
|
|
202
|
+
const baseUrl = getGitHubCopilotBaseUrl(copilotToken);
|
|
203
|
+
const res = await fetch(`${baseUrl}/models`, {
|
|
204
|
+
signal: controller.signal,
|
|
205
|
+
headers: {
|
|
206
|
+
Accept: "application/json",
|
|
207
|
+
Authorization: `Bearer ${copilotToken}`,
|
|
208
|
+
"User-Agent": "GitHubCopilotChat/0.35.0",
|
|
209
|
+
"Editor-Version": "vscode/1.107.0",
|
|
210
|
+
"Editor-Plugin-Version": "copilot-chat/0.35.0",
|
|
211
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
212
|
+
"X-GitHub-Api-Version": "2026-06-01",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
if (!res.ok)
|
|
216
|
+
return cached?.models ?? [];
|
|
217
|
+
const body = (await res.json());
|
|
218
|
+
const list = body?.data;
|
|
219
|
+
if (!Array.isArray(list))
|
|
220
|
+
return cached?.models ?? [];
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const raw of list) {
|
|
223
|
+
const item = raw;
|
|
224
|
+
const id = typeof item.id === "string" ? item.id : undefined;
|
|
225
|
+
if (!id)
|
|
226
|
+
continue;
|
|
227
|
+
// Selectability gate (mirrors Pi's own `isSelectableCopilotModel`): only
|
|
228
|
+
// surface models the account is allowed to pick. Every access is guarded —
|
|
229
|
+
// the response is untyped.
|
|
230
|
+
const policy = item.policy;
|
|
231
|
+
const capabilities = item.capabilities;
|
|
232
|
+
const supports = capabilities?.supports;
|
|
233
|
+
if (item.model_picker_enabled !== true)
|
|
234
|
+
continue;
|
|
235
|
+
if (policy?.state === "disabled")
|
|
236
|
+
continue;
|
|
237
|
+
if (supports?.tool_calls === false)
|
|
238
|
+
continue;
|
|
239
|
+
const maxCtx = capabilities?.limits?.max_context_window_tokens;
|
|
240
|
+
const contextWindow = typeof maxCtx === "number" && maxCtx > 0 ? maxCtx : undefined;
|
|
241
|
+
const name = typeof item.name === "string" && item.name.length > 0 ? item.name : id;
|
|
242
|
+
const input = supports?.vision ? ["text", "image"] : ["text"];
|
|
243
|
+
out.push({
|
|
244
|
+
provider: "github-copilot",
|
|
245
|
+
id,
|
|
246
|
+
name,
|
|
247
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
248
|
+
// Copilot's list doesn't flag reasoning here — leave false; the static
|
|
249
|
+
// catalog join in onboarding fills the richer fields when Pi knows it.
|
|
250
|
+
reasoning: false,
|
|
251
|
+
input,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
subscriptionModelsCache.set("github-copilot", { at: now, models: out });
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return cached?.models ?? [];
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Models a Claude Pro/Max SUBSCRIPTION can use. There is NO live per-account
|
|
266
|
+
* models endpoint for a subscription OAuth token: Anthropic scopes that token to
|
|
267
|
+
* inference only, so `GET /v1/models` returns 401/403 (verified). Instead we
|
|
268
|
+
* surface Pi's bundled Anthropic catalog — it IS the current Claude model family
|
|
269
|
+
* and updates as Pi ships new models — so the picker is always populated and
|
|
270
|
+
* current with ZERO network round-trip (no guaranteed-fail request, no timeout)
|
|
271
|
+
* on every sign-in. (GitHub Copilot DOES expose a live per-account list, so that
|
|
272
|
+
* path stays a real fetch; Anthropic subscriptions simply don't have one.)
|
|
273
|
+
*/
|
|
274
|
+
export function fetchAnthropicSubscriptionModels() {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const cached = subscriptionModelsCache.get("anthropic");
|
|
277
|
+
if (cached && now - cached.at < SUBSCRIPTION_MODELS_TTL_MS) {
|
|
278
|
+
return cached.models;
|
|
279
|
+
}
|
|
280
|
+
let out = [];
|
|
281
|
+
try {
|
|
282
|
+
const catalog = getModels("anthropic");
|
|
283
|
+
out = catalog.map((m) => ({
|
|
284
|
+
provider: "anthropic",
|
|
285
|
+
id: m.id,
|
|
286
|
+
name: m.name ?? m.id,
|
|
287
|
+
contextWindow: m.contextWindow,
|
|
288
|
+
reasoning: m.reasoning ?? false,
|
|
289
|
+
input: m.input ?? ["text"],
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
out = [];
|
|
294
|
+
}
|
|
295
|
+
subscriptionModelsCache.set("anthropic", { at: now, models: out });
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Warm the live cache for a subscription provider right after OAuth login so the
|
|
300
|
+
* model picker has the account's CURRENT models ready. Best-effort: any failure is
|
|
301
|
+
* swallowed (login must never block on this). `codex` has no live list endpoint —
|
|
302
|
+
* it falls through to Pi's bundled catalog, so this is a no-op for it.
|
|
303
|
+
*/
|
|
304
|
+
export async function prefetchSubscriptionModels(providerId, oauthAccessToken) {
|
|
305
|
+
try {
|
|
306
|
+
if (providerId === "github-copilot") {
|
|
307
|
+
await fetchGitHubCopilotModels(oauthAccessToken);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (providerId === "anthropic") {
|
|
311
|
+
// No network — populates the cache from Pi's current Anthropic catalog.
|
|
312
|
+
fetchAnthropicSubscriptionModels();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Other subscription providers (codex) have no live endpoint — no-op.
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Best-effort warm-up — never blocks login; picker falls back to the catalog.
|
|
319
|
+
}
|
|
320
|
+
}
|
|
166
321
|
//# sourceMappingURL=provider-discovery.js.map
|