@bitkyc08/opencodex 2.40.0 → 2.42.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 +4 -0
- package/gui/dist/assets/index-BU1tE0sr.js +112 -0
- package/gui/dist/assets/index-DL9-iS6J.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/cursor/protobuf-request.ts +41 -21
- package/src/adapters/google.ts +39 -2
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/openai-responses.ts +57 -4
- package/src/bridge.ts +25 -3
- package/src/cli/account-auth.ts +28 -3
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/observe.ts +3 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/auth-api.ts +102 -9
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/effort.ts +15 -2
- package/src/codex/catalog/metadata.ts +119 -9
- package/src/codex/catalog/native-models.ts +71 -0
- package/src/codex/catalog/parsing.ts +5 -3
- package/src/codex/catalog/provider-fetch.ts +166 -28
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/codex/inject.ts +96 -6
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +115 -83
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +24 -3
- package/src/oauth/log.ts +3 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/quota.ts +96 -0
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +82 -45
- package/src/server/live.ts +18 -4
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/oauth-account-routes.ts +10 -3
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +95 -7
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/types/config.ts +10 -1
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-BHe2rl_C.js +0 -112
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
package/src/oauth/chatgpt.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { generatePKCE } from "./pkce";
|
|
|
5
5
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
6
6
|
const AUTH_URL = "https://auth.openai.com/oauth/authorize";
|
|
7
7
|
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
8
|
+
|
|
9
|
+
/** Shared with the deviceauth grant in `./chatgpt-device`: same public PKCE client. */
|
|
10
|
+
export const CHATGPT_CLIENT_ID = CLIENT_ID;
|
|
11
|
+
export const CHATGPT_TOKEN_URL = TOKEN_URL;
|
|
8
12
|
const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
9
13
|
const CALLBACK_PORT = 1455;
|
|
10
14
|
const CALLBACK_PATH = "/auth/callback";
|
|
@@ -46,9 +50,17 @@ export function extractEmail(idToken?: string, accessToken?: string): string | u
|
|
|
46
50
|
return undefined;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
function credsFromToken(data: Record<string, unknown>): OAuthCredentials {
|
|
53
|
+
export function credsFromToken(data: Record<string, unknown>): OAuthCredentials {
|
|
50
54
|
const idToken = typeof data.id_token === "string" ? data.id_token : undefined;
|
|
51
|
-
|
|
55
|
+
// This parses a response from an external boundary, so the access token is
|
|
56
|
+
// validated rather than cast. A 200 carrying no access_token would otherwise
|
|
57
|
+
// resolve a login as successful with an undefined credential, which then gets
|
|
58
|
+
// silently declined at persistence — a success message and no account.
|
|
59
|
+
const accessToken = typeof data.access_token === "string" && data.access_token.length > 0
|
|
60
|
+
? data.access_token
|
|
61
|
+
: undefined;
|
|
62
|
+
if (!accessToken) throw new Error("ChatGPT token response missing access token");
|
|
63
|
+
const refreshToken = typeof data.refresh_token === "string" ? data.refresh_token : "";
|
|
52
64
|
// ?? only guards null/undefined; NaN or a string expires_in would otherwise
|
|
53
65
|
// produce a NaN expiry that never compares as expired, and a negative duration
|
|
54
66
|
// would stamp an already-past expiry — both block refresh semantics.
|
|
@@ -62,7 +74,7 @@ function credsFromToken(data: Record<string, unknown>): OAuthCredentials {
|
|
|
62
74
|
const expires = Number.isFinite(computedExpires) ? computedExpires : Date.now() + 3600 * 1000;
|
|
63
75
|
return {
|
|
64
76
|
access: accessToken,
|
|
65
|
-
refresh:
|
|
77
|
+
refresh: refreshToken,
|
|
66
78
|
expires,
|
|
67
79
|
accountId: extractAccountId(idToken, accessToken),
|
|
68
80
|
email: extractEmail(idToken, accessToken),
|
|
@@ -135,7 +147,22 @@ function safeErrorDescription(resp: Response): Promise<string> {
|
|
|
135
147
|
});
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
|
|
150
|
+
/**
|
|
151
|
+
* How the user proves identity. `browser` runs the localhost:1455 callback flow;
|
|
152
|
+
* `device` runs the deviceauth grant, which needs no local browser or listener
|
|
153
|
+
* and is the only workable path on a headless or remote hub (#3366).
|
|
154
|
+
*/
|
|
155
|
+
export type ChatGPTLoginFlow = "browser" | "device";
|
|
156
|
+
|
|
157
|
+
export async function loginChatGPT(
|
|
158
|
+
ctrl: OAuthController,
|
|
159
|
+
opts?: { forceLogin?: boolean; flow?: ChatGPTLoginFlow },
|
|
160
|
+
): Promise<OAuthCredentials> {
|
|
161
|
+
if (opts?.flow === "device") {
|
|
162
|
+
// Imported lazily so the callback flow does not pay for a module it never uses.
|
|
163
|
+
const { loginChatGPTDevice } = await import("./chatgpt-device");
|
|
164
|
+
return loginChatGPTDevice(ctrl);
|
|
165
|
+
}
|
|
139
166
|
const flow = new ChatGPTOAuthFlow(ctrl);
|
|
140
167
|
if (opts?.forceLogin) flow.forceLogin = true;
|
|
141
168
|
return flow.login();
|
|
@@ -202,11 +202,11 @@ export function rotateGenericOAuthAccountOn429(
|
|
|
202
202
|
// A single stored account has nowhere to go; rotating to itself would just replay the 429.
|
|
203
203
|
if (!set || set.accounts.length < 2) return null;
|
|
204
204
|
|
|
205
|
-
const parsed = parseRetryAfterMs(retryAfterHeader, now);
|
|
205
|
+
const parsed = parseRetryAfterMs(retryAfterHeader, now, { preserveImmediate: true });
|
|
206
206
|
// An account whose allowance is provably spent gets a reset-aligned cooldown instead of
|
|
207
207
|
// the default minute: retrying it every 60s until the window rolls over is pure waste.
|
|
208
208
|
// A Retry-After from upstream still wins — it is the server's own instruction.
|
|
209
|
-
const exhausted = parsed ===
|
|
209
|
+
const exhausted = parsed === undefined ? exhaustedCooldownMs(providerName, failedAccountId, now) : null;
|
|
210
210
|
const cooldownMs = exhausted ?? Math.min(parsed ?? DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS);
|
|
211
211
|
health.set(healthKey(providerName, failedAccountId), {
|
|
212
212
|
cooldownUntil: now + cooldownMs,
|
package/src/oauth/index.ts
CHANGED
|
@@ -34,11 +34,12 @@ import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenReques
|
|
|
34
34
|
import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
35
35
|
import { loginKimi, refreshKimiToken } from "./kimi";
|
|
36
36
|
import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, RefreshIntentIOError } from "./nous";
|
|
37
|
-
import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
|
|
37
|
+
import { loginChatGPT, refreshChatGPTToken, type ChatGPTLoginFlow } from "./chatgpt";
|
|
38
38
|
import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
39
39
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
40
40
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
41
41
|
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
42
|
+
import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
|
|
42
43
|
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
43
44
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
44
45
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
@@ -160,7 +161,17 @@ function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${
|
|
|
160
161
|
function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
|
|
161
162
|
export function sweepExpiredXaiPermanentFailureVerdicts(now=Date.now()):number{let removed=0;for(const[key,until]of permanentRefreshFailures){if(until>now)continue;permanentRefreshFailures.delete(key);removed+=1;}return removed;}
|
|
162
163
|
|
|
163
|
-
export interface LoginOpts {
|
|
164
|
+
export interface LoginOpts {
|
|
165
|
+
forceLogin?: boolean;
|
|
166
|
+
/** When set, persist into this account slot and require matching identity. */
|
|
167
|
+
reauthAccountId?: string;
|
|
168
|
+
/**
|
|
169
|
+
* ChatGPT only: `device` selects the deviceauth grant instead of the
|
|
170
|
+
* localhost:1455 callback flow, for hosts with no browser or no loopback
|
|
171
|
+
* listener (#3366). Ignored by every other provider.
|
|
172
|
+
*/
|
|
173
|
+
flow?: ChatGPTLoginFlow;
|
|
174
|
+
}
|
|
164
175
|
|
|
165
176
|
export interface LoginFlowLifecycle {
|
|
166
177
|
/** Runs after background credential/config persistence settles, before status becomes done. */
|
|
@@ -228,6 +239,16 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
228
239
|
providerConfig: oauthConfig("kimi"),
|
|
229
240
|
defaultModel: oauthDefaultModel("kimi"),
|
|
230
241
|
},
|
|
242
|
+
"meta-muse": {
|
|
243
|
+
login: ctrl => loginMetaMuse(ctrl),
|
|
244
|
+
refresh: refreshMetaMuseToken,
|
|
245
|
+
providerConfig: oauthConfig("meta-muse"),
|
|
246
|
+
defaultModel: oauthDefaultModel("meta-muse"),
|
|
247
|
+
// Static API key that Meta scopes to its own CLI. Never generate unattended traffic
|
|
248
|
+
// on it — same posture as anthropic, for the same reason: the vendor restricts use
|
|
249
|
+
// outside its own client, so every exchange stays attributable to a user action.
|
|
250
|
+
defaultRefreshPolicy: "disabled",
|
|
251
|
+
},
|
|
231
252
|
nous: {
|
|
232
253
|
// Nous Portal device-grant login (RFC 8628) against portal.nousresearch.com.
|
|
233
254
|
// The access token is the per-request inference JWT (scope inference:invoke).
|
|
@@ -271,7 +292,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
271
292
|
defaultRefreshPolicy: "lazy-only",
|
|
272
293
|
},
|
|
273
294
|
chatgpt: {
|
|
274
|
-
login: loginChatGPT,
|
|
295
|
+
login: (ctrl, opts) => loginChatGPT(ctrl, { forceLogin: opts?.forceLogin, flow: opts?.flow }),
|
|
275
296
|
refresh: (rt) => refreshChatGPTToken(rt),
|
|
276
297
|
providerConfig: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const },
|
|
277
298
|
defaultModel: "gpt-5.4",
|
package/src/oauth/log.ts
CHANGED
|
@@ -23,6 +23,9 @@ const FORBIDDEN_NORMALIZED = new Set([
|
|
|
23
23
|
"oauth_code",
|
|
24
24
|
"code_verifier",
|
|
25
25
|
"clientsecret",
|
|
26
|
+
// Device-flow polling handle. Not a token, but it is the bearer of an
|
|
27
|
+
// in-flight authorization and must not be logged.
|
|
28
|
+
"device_auth_id",
|
|
26
29
|
]);
|
|
27
30
|
|
|
28
31
|
function isForbiddenFieldKey(key: string): boolean {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Muse Code credential import.
|
|
3
|
+
*
|
|
4
|
+
* The Muse Code CLI signs in through a browser device-approval flow and stores the
|
|
5
|
+
* result in two places: `~/.config/muse/auth.json` is a POINTER carrying no secret, and
|
|
6
|
+
* the secret itself lives in the macOS Keychain under service
|
|
7
|
+
* `ai.meta.dev.credentials`, account `meta`.
|
|
8
|
+
*
|
|
9
|
+
* Two measured facts shape this module (devlog/_plan/260903_muse_spark_plan_oauth/003):
|
|
10
|
+
*
|
|
11
|
+
* 1. The Keychain payload holds BOTH an `access_token` and an `api_key`, and only the
|
|
12
|
+
* `api_key` authenticates the Model API — the OAuth access token returns 401
|
|
13
|
+
* `invalid_api_key`. So this is a static-key credential, not a refreshable one.
|
|
14
|
+
* 2. Meta scopes that credential to the Muse Code CLI in writing. Reusing it here is an
|
|
15
|
+
* UNSUPPORTED path the repository owner opted into deliberately, which is why the
|
|
16
|
+
* warning below fires before anything is read and why the provider sits in the GUI's
|
|
17
|
+
* HIGH_RISK ToS map.
|
|
18
|
+
*
|
|
19
|
+
* This module never spawns the CLI. A login that finds no credential explains what to
|
|
20
|
+
* run rather than running it: `muse login` is interactive with no machine-readable mode,
|
|
21
|
+
* so a spawned child could outlive cancellation, and polling for the pointer file would
|
|
22
|
+
* be satisfied instantly by the one already on disk — reimporting the OLD account on a
|
|
23
|
+
* force-login.
|
|
24
|
+
*/
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { sanitizeApiKeyValue } from "../providers/api-keys";
|
|
28
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
29
|
+
|
|
30
|
+
const MUSE_POINTER_PATH = join(homedir(), ".config", "muse", "auth.json");
|
|
31
|
+
const KEYCHAIN_SERVICE = "ai.meta.dev.credentials";
|
|
32
|
+
const KEYCHAIN_ACCOUNT = "meta";
|
|
33
|
+
const MODELS_URL = "https://api.meta.ai/v1/models";
|
|
34
|
+
const VALIDATE_TIMEOUT_MS = 10_000;
|
|
35
|
+
const KEYCHAIN_TIMEOUT_MS = 5_000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shown BEFORE any credential is read.
|
|
39
|
+
*
|
|
40
|
+
* `login-cli.ts` passes `onProgress` straight to `console.log` and never reads the
|
|
41
|
+
* registry note, so this is the CLI's only warning surface. The GUI ignores it because
|
|
42
|
+
* `OAuthTosWarningModal` has already been acknowledged by then.
|
|
43
|
+
*/
|
|
44
|
+
const CONSENT_WARNING = [
|
|
45
|
+
"Meta scopes the Muse Code credential to the Muse Code CLI.",
|
|
46
|
+
"Using it here is UNSUPPORTED: Meta does not authorize subscription coverage outside its own CLI,",
|
|
47
|
+
"how these calls settle is not observable from the API, and you should treat every call as billable.",
|
|
48
|
+
"The imported key is copied into OpenCodex's auth store (~/.opencodex/auth.json, 0600).",
|
|
49
|
+
"Supported alternative: the meta-model provider with your own key (META_MODEL_API_KEY).",
|
|
50
|
+
].join(" ");
|
|
51
|
+
|
|
52
|
+
/** The Keychain payload. `access_token` is deliberately unused — it 401s (003 §B). */
|
|
53
|
+
interface MuseKeychainSecret {
|
|
54
|
+
api_key?: unknown;
|
|
55
|
+
access_token?: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface MusePointer {
|
|
59
|
+
providers?: { meta?: { mechanism?: unknown; storage?: unknown; user_email?: unknown } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Injected so tests never touch the real Keychain, filesystem, platform, or network. */
|
|
63
|
+
export interface MuseImportDeps {
|
|
64
|
+
platform?: string;
|
|
65
|
+
readPointer?: () => Promise<string | null>;
|
|
66
|
+
readKeychain?: (signal?: AbortSignal) => Promise<string | null>;
|
|
67
|
+
fetchImpl?: typeof fetch;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function defaultReadPointer(): Promise<string | null> {
|
|
71
|
+
try {
|
|
72
|
+
return await Bun.file(MUSE_POINTER_PATH).text();
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `security` can block indefinitely — the Keychain may raise an interactive approval
|
|
80
|
+
* prompt, and on a headless or locked machine nobody answers it. Without a deadline the
|
|
81
|
+
* login would hang before the validation timeout below is even created, so the bound
|
|
82
|
+
* lives here rather than only around the fetch.
|
|
83
|
+
*/
|
|
84
|
+
async function defaultReadKeychain(signal?: AbortSignal): Promise<string | null> {
|
|
85
|
+
const deadline = signal
|
|
86
|
+
? AbortSignal.any([signal, AbortSignal.timeout(KEYCHAIN_TIMEOUT_MS)])
|
|
87
|
+
: AbortSignal.timeout(KEYCHAIN_TIMEOUT_MS);
|
|
88
|
+
let proc: Bun.Subprocess<"ignore", "pipe", "pipe"> | undefined;
|
|
89
|
+
try {
|
|
90
|
+
proc = Bun.spawn(
|
|
91
|
+
["security", "find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"],
|
|
92
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
93
|
+
);
|
|
94
|
+
const child = proc;
|
|
95
|
+
const finished = Promise.all([new Response(child.stdout).text(), child.exited]);
|
|
96
|
+
const timedOut = new Promise<null>((resolve) => {
|
|
97
|
+
if (deadline.aborted) { resolve(null); return; }
|
|
98
|
+
deadline.addEventListener("abort", () => resolve(null), { once: true });
|
|
99
|
+
});
|
|
100
|
+
const settled = await Promise.race([finished, timedOut]);
|
|
101
|
+
if (settled === null) return null;
|
|
102
|
+
const [out, code] = settled;
|
|
103
|
+
if (code !== 0) return null;
|
|
104
|
+
const trimmed = out.trim();
|
|
105
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
} finally {
|
|
109
|
+
// A prompt still on screen keeps the child alive after the race resolves.
|
|
110
|
+
if (proc && proc.exitCode === null) { try { proc.kill(); } catch { /* already gone */ } }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const INSTALL_HINT =
|
|
115
|
+
"Install it from https://dev.meta.ai/install.sh, run `muse login`, then retry.";
|
|
116
|
+
|
|
117
|
+
function normalizedEmail(value: unknown): string | undefined {
|
|
118
|
+
if (typeof value !== "string") return undefined;
|
|
119
|
+
const trimmed = value.trim().toLowerCase();
|
|
120
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Import the credential the Muse Code CLI already holds.
|
|
125
|
+
*
|
|
126
|
+
* Every refusal names what the user should do. None of them includes the credential.
|
|
127
|
+
*/
|
|
128
|
+
export async function loginMetaMuse(
|
|
129
|
+
ctrl: OAuthController = {},
|
|
130
|
+
deps: MuseImportDeps = {},
|
|
131
|
+
): Promise<OAuthCredentials> {
|
|
132
|
+
// Before ANY read: the CLI has no other warning surface.
|
|
133
|
+
ctrl.onProgress?.(CONSENT_WARNING);
|
|
134
|
+
|
|
135
|
+
const platform = deps.platform ?? process.platform;
|
|
136
|
+
if (platform !== "darwin") {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Meta Muse Code login is macOS-only: the CLI stores its credential in the macOS Keychain, "
|
|
139
|
+
+ "and no other platform's storage has been verified. Use the meta-model provider with your own key instead.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pointerRaw = await (deps.readPointer ?? defaultReadPointer)();
|
|
144
|
+
if (pointerRaw === null) {
|
|
145
|
+
throw new Error(`Muse Code CLI credential not found at ${MUSE_POINTER_PATH}. ${INSTALL_HINT}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let pointer: MusePointer;
|
|
149
|
+
try {
|
|
150
|
+
pointer = JSON.parse(pointerRaw) as MusePointer;
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error(`Muse Code credential file at ${MUSE_POINTER_PATH} is not valid JSON. Run \`muse login\` to rewrite it.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const meta = pointer.providers?.meta;
|
|
156
|
+
if (!meta || meta.mechanism !== "oauth") {
|
|
157
|
+
throw new Error("The Muse Code credential file has no signed-in Meta account. Run `muse login`, then retry.");
|
|
158
|
+
}
|
|
159
|
+
// A different storage backend is a shape we have not measured; refuse rather than guess.
|
|
160
|
+
if (meta.storage !== "keychain") {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Muse Code stored its credential with an unsupported backend (${String(meta.storage)}); only the macOS Keychain is verified.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const secretRaw = await (deps.readKeychain ?? defaultReadKeychain)(ctrl.signal);
|
|
167
|
+
if (secretRaw === null) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
"Could not read the Muse Code credential from the macOS Keychain within 5s. Approve the Keychain prompt, or run `muse login` again.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let secret: MuseKeychainSecret;
|
|
174
|
+
try {
|
|
175
|
+
secret = JSON.parse(secretRaw) as MuseKeychainSecret;
|
|
176
|
+
} catch {
|
|
177
|
+
throw new Error("The Muse Code Keychain entry is not valid JSON. Run `muse login` to rewrite it.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// access_token is present but 401s against the Model API (003 §B) — never fall back to it.
|
|
181
|
+
const apiKey = sanitizeApiKeyValue(secret.api_key);
|
|
182
|
+
if (!apiKey) {
|
|
183
|
+
throw new Error("The Muse Code Keychain entry carries no usable API key. Run `muse login` again.");
|
|
184
|
+
}
|
|
185
|
+
if (!/^LLM\|\d+\|[A-Za-z0-9_-]{10,}$/.test(apiKey)) {
|
|
186
|
+
throw new Error("The Muse Code credential is not in the expected Meta API key format. Run `muse login` again.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ctrl.onProgress?.("Validating the imported Meta credential…");
|
|
190
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
191
|
+
// ctrl.signal is OPTIONAL and the CLI controller supplies none: AbortSignal.any([undefined])
|
|
192
|
+
// throws a TypeError, which would fail every CLI login right after the warning printed.
|
|
193
|
+
const signal = ctrl.signal
|
|
194
|
+
? AbortSignal.any([ctrl.signal, AbortSignal.timeout(VALIDATE_TIMEOUT_MS)])
|
|
195
|
+
: AbortSignal.timeout(VALIDATE_TIMEOUT_MS);
|
|
196
|
+
let response: Response;
|
|
197
|
+
try {
|
|
198
|
+
response = await fetchImpl(MODELS_URL, {
|
|
199
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
|
|
200
|
+
signal,
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (ctrl.signal?.aborted) throw ctrl.signal.reason ?? new DOMException("Meta Muse login aborted", "AbortError");
|
|
204
|
+
throw new Error(`Could not reach the Meta Model API to validate the credential: ${(error as Error).message}`);
|
|
205
|
+
}
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`The Muse Code credential was rejected by the Meta Model API (HTTP ${response.status}). Run \`muse login\` again.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
access: apiKey,
|
|
214
|
+
// Static key: there is nothing to exchange, so refresh carries the same value.
|
|
215
|
+
refresh: apiKey,
|
|
216
|
+
expires: Number.MAX_SAFE_INTEGER,
|
|
217
|
+
// `email`, not `accountId`: the account list masks email for display, and store.ts
|
|
218
|
+
// already falls back to it for slot identity, so multi-account still works.
|
|
219
|
+
...(normalizedEmail(meta.user_email) ? { email: normalizedEmail(meta.user_email) } : {}),
|
|
220
|
+
source: "local-cli",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Static-key refresh, exactly like Command Code's.
|
|
226
|
+
*
|
|
227
|
+
* This deliberately does NOT re-read the Keychain. Generic refresh writes its result into
|
|
228
|
+
* the slot being refreshed, so if the user ran `muse login` with a DIFFERENT account in
|
|
229
|
+
* between, a re-import would silently overwrite one stored identity with another. Only an
|
|
230
|
+
* explicit login may import.
|
|
231
|
+
*/
|
|
232
|
+
export async function refreshMetaMuseToken(apiKey: string): Promise<OAuthCredentials> {
|
|
233
|
+
if (!apiKey) throw new Error("Meta Muse Code API key missing; run `ocx login meta-muse`");
|
|
234
|
+
return { access: apiKey, refresh: apiKey, expires: Number.MAX_SAFE_INTEGER, source: "local-cli" };
|
|
235
|
+
}
|
|
@@ -13,14 +13,27 @@ import { isModelCacheGenerationCurrent } from "../codex/model-cache";
|
|
|
13
13
|
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
14
14
|
|
|
15
15
|
/** Current Antigravity Flash generation. */
|
|
16
|
-
const GEMINI_FLASH_CURRENT = "gemini-3.
|
|
16
|
+
const GEMINI_FLASH_CURRENT = "gemini-3.8-flash";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Previous Flash generation — still served, still picker-visible.
|
|
20
|
+
*
|
|
21
|
+
* 3.6 vanished from CCA the moment 3.7 shipped, which is why RETIRED_FLASH_TIERS exists. 3.8
|
|
22
|
+
* did not do that: Google documents 3.7 Flash as "remains fully supported", and a 2026-09-03
|
|
23
|
+
* :fetchAvailableModels call returns 3.8, 3.7 AND 3.6 wire ids together. Retiring 3.7 here
|
|
24
|
+
* would strand a model the backend is actively serving.
|
|
25
|
+
*/
|
|
26
|
+
const GEMINI_FLASH_PREVIOUS = "gemini-3.7-flash";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wire ID that CCA accepts for the RETIRED-tier redirect target (currently 3.7).
|
|
30
|
+
*
|
|
31
|
+
* Google renamed 3.7 to carry a `-tiered` suffix; the picker-visible ID stays
|
|
32
|
+
* `gemini-3.7-flash` (stripped by `pickerModelIdForDiscoveredWireId`). This constant is named
|
|
33
|
+
* for its ROLE, not for the current generation: 3.8 is current and has no `-tiered` id, so a
|
|
34
|
+
* name like GEMINI_FLASH_WIRE_ID would now point readers at the wrong model.
|
|
22
35
|
*/
|
|
23
|
-
const
|
|
36
|
+
const GEMINI_RETIRED_FLASH_TARGET_WIRE_ID = "gemini-3.7-flash-tiered";
|
|
24
37
|
|
|
25
38
|
/**
|
|
26
39
|
* Retired Flash ids → the reasoning tier they used to encode.
|
|
@@ -60,6 +73,9 @@ const ANTIGRAVITY_WIRE_MODELS = [
|
|
|
60
73
|
];
|
|
61
74
|
|
|
62
75
|
const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record<string, string> = {
|
|
76
|
+
"gemini-3.8-flash-low": "gemini-3.8-flash",
|
|
77
|
+
"gemini-3.8-flash-medium": "gemini-3.8-flash",
|
|
78
|
+
"gemini-3.8-flash-high": "gemini-3.8-flash",
|
|
63
79
|
"gemini-3.1-pro-low": "gemini-3.1-pro",
|
|
64
80
|
"gemini-pro-agent": "gemini-3.1-pro",
|
|
65
81
|
};
|
|
@@ -143,6 +159,9 @@ function collapsesIntoKnownPickerModel(candidateId: string): boolean {
|
|
|
143
159
|
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
144
160
|
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
145
161
|
export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
162
|
+
// No `minimal`: Google documents it as an error for this generation, and CCA exposes only
|
|
163
|
+
// the three tiers.
|
|
164
|
+
"gemini-3.8-flash": ["low", "medium", "high"],
|
|
146
165
|
"gemini-3.7-flash": ["low", "medium", "high"],
|
|
147
166
|
"gemini-3.1-pro": ["low", "high"],
|
|
148
167
|
"claude-sonnet-4-6": ["low", "medium", "high", "max"],
|
|
@@ -151,12 +170,32 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
|
151
170
|
|
|
152
171
|
// ── Effort → wire model map for Gemini base models ──
|
|
153
172
|
const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
173
|
+
// 3.8 publishes one wire id per tier and no `-tiered` row, so its efforts ride the suffix.
|
|
174
|
+
// This is the 3.6 shape, not the 3.7 one.
|
|
175
|
+
"gemini-3.8-flash": {
|
|
176
|
+
low: "gemini-3.8-flash-low",
|
|
177
|
+
medium: "gemini-3.8-flash-medium",
|
|
178
|
+
high: "gemini-3.8-flash-high",
|
|
179
|
+
},
|
|
154
180
|
"gemini-3.1-pro": {
|
|
155
181
|
low: "gemini-3.1-pro-low",
|
|
156
182
|
high: "gemini-pro-agent",
|
|
157
183
|
},
|
|
158
184
|
};
|
|
159
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Base models whose every effort maps to a wire id that ALREADY encodes the tier.
|
|
188
|
+
*
|
|
189
|
+
* Sending `thinkingLevel` beside such a suffix states the effort twice, and CCA does not reject
|
|
190
|
+
* the contradiction — a `-low` wire id paired with `HIGH` returns 200, so the tier that actually
|
|
191
|
+
* ran becomes unknowable from the response. Membership also makes static resolution
|
|
192
|
+
* byte-identical to the discovery path, which never emits a thinking level.
|
|
193
|
+
*
|
|
194
|
+
* `gemini-3.1-pro` is deliberately absent: its `high` rung is `gemini-pro-agent`, which carries
|
|
195
|
+
* no tier suffix, so there the level is the only thing naming the effort.
|
|
196
|
+
*/
|
|
197
|
+
const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set(["gemini-3.8-flash"]);
|
|
198
|
+
|
|
160
199
|
function completeDiscoveredEffortWireModelIds(
|
|
161
200
|
pickerId: string,
|
|
162
201
|
available: ReadonlyMap<string, Record<string, unknown>>,
|
|
@@ -178,6 +217,8 @@ function completeDiscoveredEffortWireModelIds(
|
|
|
178
217
|
|
|
179
218
|
// ── Default effort per Gemini base model ──
|
|
180
219
|
const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
|
|
220
|
+
// Google's documented thinking_level default, and the tier CCA marks `recommended`.
|
|
221
|
+
"gemini-3.8-flash": "medium",
|
|
181
222
|
"gemini-3.1-pro": "high",
|
|
182
223
|
};
|
|
183
224
|
|
|
@@ -198,7 +239,7 @@ const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]);
|
|
|
198
239
|
* Models not listed here use themselves as the wire ID.
|
|
199
240
|
*/
|
|
200
241
|
const ANTIGRAVITY_PICKER_TO_WIRE: Record<string, string> = {
|
|
201
|
-
"gemini-3.7-flash":
|
|
242
|
+
"gemini-3.7-flash": GEMINI_RETIRED_FLASH_TARGET_WIRE_ID,
|
|
202
243
|
};
|
|
203
244
|
|
|
204
245
|
/** Map a picker-visible base model to its CCA wire ID. Identity when no mapping exists. */
|
|
@@ -230,7 +271,7 @@ const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
|
|
|
230
271
|
// because `parseAntigravityAvailableModels` uses THIS map to keep a stale CCA
|
|
231
272
|
// payload from republishing a dead wire id as a picker row.
|
|
232
273
|
...Object.fromEntries(
|
|
233
|
-
Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired,
|
|
274
|
+
Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_RETIRED_FLASH_TARGET_WIRE_ID]),
|
|
234
275
|
),
|
|
235
276
|
};
|
|
236
277
|
|
|
@@ -242,6 +283,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
|
242
283
|
// Picker-visible: collapsed base models only.
|
|
243
284
|
export const ANTIGRAVITY_MODELS = [
|
|
244
285
|
GEMINI_FLASH_CURRENT,
|
|
286
|
+
GEMINI_FLASH_PREVIOUS,
|
|
245
287
|
"gemini-3.1-pro",
|
|
246
288
|
"gemini-3.1-flash-image",
|
|
247
289
|
"claude-sonnet-4-6",
|
|
@@ -255,6 +297,9 @@ function isKnownAntigravityPickerModelId(value: string): boolean {
|
|
|
255
297
|
|
|
256
298
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
257
299
|
const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
300
|
+
"gemini-3.8-flash-low": 1_048_576,
|
|
301
|
+
"gemini-3.8-flash-medium": 1_048_576,
|
|
302
|
+
"gemini-3.8-flash-high": 1_048_576,
|
|
258
303
|
"gemini-3.7-flash-tiered": 1_048_576,
|
|
259
304
|
"gemini-3.1-pro-low": 1_048_576,
|
|
260
305
|
"gemini-pro-agent": 1_048_576,
|
|
@@ -266,6 +311,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
266
311
|
|
|
267
312
|
export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
268
313
|
// Collapsed base IDs — explicit entries for the picker.
|
|
314
|
+
"gemini-3.8-flash": 1_048_576,
|
|
269
315
|
"gemini-3.7-flash": 1_048_576,
|
|
270
316
|
"gemini-3.1-pro": 1_048_576,
|
|
271
317
|
// Wire IDs and aliases via derivation.
|
|
@@ -283,6 +329,7 @@ export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
|
283
329
|
// carries only text and image parts (`OcxImageContent`, src/types.ts) and the Codex
|
|
284
330
|
// catalog normalizes `input_modalities` against a closed enum. Advertising a modality
|
|
285
331
|
// the wire cannot carry would be a promise we break at request time.
|
|
332
|
+
"gemini-3.8-flash": ["text", "image"],
|
|
286
333
|
"gemini-3.7-flash": ["text", "image"],
|
|
287
334
|
"gemini-3.1-pro": ["text", "image"],
|
|
288
335
|
"gemini-3.1-flash-image": ["text", "image"],
|
|
@@ -609,13 +656,15 @@ export function resolveAntigravityEffortWireModel(
|
|
|
609
656
|
};
|
|
610
657
|
}
|
|
611
658
|
|
|
612
|
-
// Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
|
|
613
|
-
//
|
|
614
|
-
//
|
|
659
|
+
// Rule 0: retired Flash id — Google has taken the wire id offline, so route to the 3.7
|
|
660
|
+
// redirect target and carry the tier the retired id encoded. (3.7, not "the current
|
|
661
|
+
// generation": 3.8 is current but these ids were retired onto 3.7, which is still served.)
|
|
662
|
+
// This runs BEFORE the suffix check because those ids are aliases, and rule 1 would drop
|
|
663
|
+
// the tier.
|
|
615
664
|
const retiredTier = retiredAntigravityFlashTier(modelId);
|
|
616
665
|
if (retiredTier) {
|
|
617
666
|
return {
|
|
618
|
-
wireModelId:
|
|
667
|
+
wireModelId: GEMINI_RETIRED_FLASH_TARGET_WIRE_ID,
|
|
619
668
|
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier,
|
|
620
669
|
};
|
|
621
670
|
}
|
|
@@ -638,8 +687,17 @@ export function resolveAntigravityEffortWireModel(
|
|
|
638
687
|
// Rule 2/3: mapped Gemini base model.
|
|
639
688
|
const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
|
|
640
689
|
if (effortMap) {
|
|
641
|
-
|
|
642
|
-
|
|
690
|
+
const suffixTiered = ANTIGRAVITY_SUFFIX_TIER_MODELS.has(modelId);
|
|
691
|
+
// Normalize FIRST for suffix-tiered models. The discovery path clamps max/xhigh/ultra to
|
|
692
|
+
// `high` before its own lookup, so a static path that skipped the clamp answered `medium`
|
|
693
|
+
// for the same request: one input, two tiers, decided by whether discovery happened to run.
|
|
694
|
+
const requested = suffixTiered && effort
|
|
695
|
+
? resolveAntigravityThinkingLevel(effort) ?? effort
|
|
696
|
+
: effort;
|
|
697
|
+
if (requested && requested in effortMap) {
|
|
698
|
+
const wireModelId = effortMap[requested]!;
|
|
699
|
+
// The suffix already names the tier; see ANTIGRAVITY_SUFFIX_TIER_MODELS.
|
|
700
|
+
return suffixTiered ? { wireModelId } : { wireModelId, thinkingLevel: requested };
|
|
643
701
|
}
|
|
644
702
|
const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]!;
|
|
645
703
|
return { wireModelId: effortMap[defaultEffort]! };
|
|
@@ -103,6 +103,21 @@ const COMMAND_CODE_MODEL_EFFORTS = {
|
|
|
103
103
|
// 2026-08-13: direct upstream POST with low/medium/high/xhigh/max all 200,
|
|
104
104
|
// ultra 400; reasoningTokens differentiated 114..253; proxy previously stripped
|
|
105
105
|
// the field so effort changes had no effect).
|
|
106
|
+
//
|
|
107
|
+
// 1.3 shipped 2026-09-02 as the same-shaped successor to 1.2 (Command Code
|
|
108
|
+
// publishes meta/muse-spark-1.3 and meta/muse-spark-1.3-contributor alongside
|
|
109
|
+
// the 1.2 pair, and Zen serves muse-spark-1.3-contributor over the same
|
|
110
|
+
// /responses wire). It carries the 1.2 ladder because it IS the 1.2 spec: the
|
|
111
|
+
// upstream ladder statement is per-family, and a narrower guess here would
|
|
112
|
+
// strip an effort the gateway accepts. Additive — 1.2 and 1.1 stay live.
|
|
113
|
+
"meta/muse-spark-1.3": {
|
|
114
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
115
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.3",
|
|
116
|
+
},
|
|
117
|
+
"meta/muse-spark-1.3-contributor": {
|
|
118
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
119
|
+
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.3-contributor",
|
|
120
|
+
},
|
|
106
121
|
"meta/muse-spark-1.2": {
|
|
107
122
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
108
123
|
profileUrl: "https://commandcode.ai/models/meta-muse-spark-1.2",
|
|
@@ -82,7 +82,10 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
|
82
82
|
"cloudflare-ai": openAi("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1", "https://dash.cloudflare.com/?to=/:account/ai/workers-ai", { supportLevel: "supported", verification: "official", documentationUrl: "https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/", discovery: "static", liveModels: false, models: ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/qwen/qwq-32b"] }),
|
|
83
83
|
cohere: openAi("https://api.cohere.com/compatibility/v1", "https://dashboard.cohere.com/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.cohere.com/reference/list-models", modelsUrl: "https://api.cohere.com/compatibility/v1/models" }),
|
|
84
84
|
friendliai: openAi("https://api.friendli.ai/serverless/v1", "https://suite.friendli.ai", { modelsUrl: "https://api.friendli.ai/serverless/v1/models" }),
|
|
85
|
-
|
|
85
|
+
// `lastVerified` is row-specific here: the model list was re-checked against ai.google.dev
|
|
86
|
+
// on 2026-09-03 when 3.8 was added. Bumping the shared LAST_VERIFIED instead would stamp
|
|
87
|
+
// that date on every other provider row, none of which was re-checked.
|
|
88
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com", dashboardUrl: "https://aistudio.google.com/apikey", adapter: "google", authKind: "key", supportLevel: "supported", verification: "official", documentationUrl: "https://ai.google.dev/api/models", lastVerified: "2026-09-03", discovery: "live", liveModels: true, googleMode: "ai-studio", models: ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"] },
|
|
86
89
|
"github-models": openAi("https://models.github.ai/inference", "https://github.com/settings/tokens", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.github.com/en/github-models/prototyping-with-ai-models", discovery: "static", liveModels: false, models: ["openai/gpt-4.1", "meta/llama-4-scout-17b-16e-instruct"] }),
|
|
87
90
|
groq: openAi("https://api.groq.com/openai/v1", "https://console.groq.com/keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://console.groq.com/docs/api-reference#models" }),
|
|
88
91
|
hackclub: openAi("https://ai.hackclub.com/proxy/v1", "https://ai.hackclub.com", { modelsUrl: "https://ai.hackclub.com/proxy/v1/models" }),
|