opencode-auth-load-balancer 0.4.0 → 0.5.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 +25 -6
- package/dist/accounts.d.ts +13 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1878 -1530
- package/dist/providers/anthropic/constants.d.ts +27 -0
- package/dist/providers/anthropic/version.d.ts +21 -0
- package/dist/providers/kimi/adapter.d.ts +13 -0
- package/dist/providers/kimi/constants.d.ts +36 -0
- package/dist/providers/kimi/key.d.ts +18 -0
- package/dist/providers/kimi/oauth.d.ts +22 -0
- package/dist/providers/kimi/profile.d.ts +10 -0
- package/dist/providers/kimi/usage.d.ts +12 -0
- package/dist/providers/oauth-callback.d.ts +14 -0
- package/dist/providers/openai/oauth.d.ts +6 -3
- package/dist/providers/types.d.ts +49 -0
- package/package.json +4 -2
- package/tui/auth-load-balancer-tui.logic.ts +14 -4
- package/tui/auth-load-balancer-tui.view.tsx +25 -3
|
@@ -83,6 +83,33 @@ export declare const FALLBACK_CLAUDE_CODE_VERSION = "2.1.258";
|
|
|
83
83
|
* `cch` fingerprint algorithm and the newest version starts 400ing).
|
|
84
84
|
*/
|
|
85
85
|
export declare const CLAUDE_CODE_VERSION_ENV = "OPENCODE_AUTH_LB_ANTHROPIC_CLAUDE_CODE_VERSION";
|
|
86
|
+
/**
|
|
87
|
+
* The structured error code Anthropic answers a version-gated model with:
|
|
88
|
+
*
|
|
89
|
+
* ```json
|
|
90
|
+
* {"type":"error","error":{"type":"invalid_request_error",
|
|
91
|
+
* "message":"Claude Code 2.1.278 does not support this model; version 2.1.280 or newer is required.",
|
|
92
|
+
* "details":{"error_code":"claude_code_version_too_old"}}}
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* Matched as a SUBSTRING of the raw body rather than through a parsed path
|
|
96
|
+
* (`error.details.error_code`): the envelope's shape is Anthropic's to change,
|
|
97
|
+
* and this string appearing anywhere in a rejection is both sufficient — no
|
|
98
|
+
* other error carries it — and immune to a reshuffle that would silently break
|
|
99
|
+
* a path lookup. See `recoverClaudeCodeVersion` in version.ts.
|
|
100
|
+
*/
|
|
101
|
+
export declare const CLAUDE_CODE_VERSION_GATE_CODE = "claude_code_version_too_old";
|
|
102
|
+
/**
|
|
103
|
+
* The MINIMUM version that rejection's human-readable message demands.
|
|
104
|
+
*
|
|
105
|
+
* Anchored on the `or newer is required` tail rather than the first version in
|
|
106
|
+
* the string, because the message names TWO: the one we just sent ("Claude
|
|
107
|
+
* Code 2.1.278 does not support this model") and the one being asked for
|
|
108
|
+
* ("version 2.1.280 or newer is required"). Capturing the leading one would
|
|
109
|
+
* adopt the version that just failed — a no-op the recovery would correctly
|
|
110
|
+
* refuse to retry, i.e. a silent regression to the old stuck behavior.
|
|
111
|
+
*/
|
|
112
|
+
export declare const CLAUDE_CODE_REQUIRED_VERSION_RE: RegExp;
|
|
86
113
|
/**
|
|
87
114
|
* npm dist-tags for the Claude Code CLI — `{"latest":"2.1.258",…}`, ~56 bytes.
|
|
88
115
|
* Deliberately NOT `/@anthropic-ai/claude-code/latest`, which returns the whole
|
|
@@ -20,6 +20,27 @@ export declare function usageUserAgent(): string;
|
|
|
20
20
|
* usable answer, so a failure does NOT refresh the TTL).
|
|
21
21
|
*/
|
|
22
22
|
export declare function refreshClaudeCodeVersion(now: number): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* REACTIVE recovery, driven by the gate's own rejection.
|
|
25
|
+
*
|
|
26
|
+
* `primeClaudeCodeVersion` resolves once at startup and then trusts the cache
|
|
27
|
+
* for `CLAUDE_CODE_VERSION_TTL_MS`. A model that launches INSIDE that window is
|
|
28
|
+
* therefore rejected for as long as the TTL has left to run — up to a full day
|
|
29
|
+
* of hard failures — even when npm already tags the version Anthropic is asking
|
|
30
|
+
* for, and even though the rejection itself NAMES that version. No polling
|
|
31
|
+
* cadence fixes this in general either: Anthropic can gate a model before npm
|
|
32
|
+
* tags the release, which is why the env pin exists as a manual escape hatch.
|
|
33
|
+
*
|
|
34
|
+
* So learn from the rejection instead. Adopt the minimum it demands, then ask
|
|
35
|
+
* npm for the real `latest` in the same breath — the minimum only clears the
|
|
36
|
+
* model that just failed, whereas `latest` is what the NEXT launch will want.
|
|
37
|
+
*
|
|
38
|
+
* Returns whether the reported version actually MOVED, which is precisely the
|
|
39
|
+
* condition under which a retry can succeed; the caller spends a second
|
|
40
|
+
* round-trip on true and passes the rejection through untouched on false.
|
|
41
|
+
* `res` is consumed, so callers that still need the body must pass a clone.
|
|
42
|
+
*/
|
|
43
|
+
export declare function recoverClaudeCodeVersion(res: Response, now: number): Promise<boolean>;
|
|
23
44
|
/**
|
|
24
45
|
* Startup hook. Awaits only the LOCAL cache read — cheap, and enough to have
|
|
25
46
|
* the right version in hand before the loader returns the fetch — then leaves
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ProviderAdapter } from '../types';
|
|
2
|
+
import { type KimiDeployment } from './constants';
|
|
3
|
+
/**
|
|
4
|
+
* Kimi Code adapter for one deployment. A subscription joins the pool either
|
|
5
|
+
* through a device-code OAuth login (`startDeviceLogin`, refreshed by
|
|
6
|
+
* `refresh`) or as a pasted API key (`authorize`/`exchange` +
|
|
7
|
+
* `tokensFromApiKey`). opencode already shapes the requests as
|
|
8
|
+
* OpenAI-compatible chat completions, so they pass through untouched, and
|
|
9
|
+
* inference responses carry no quota headers: usage comes from `/usages`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createKimiAdapter(deployment: KimiDeployment): ProviderAdapter;
|
|
12
|
+
export declare const kimiCodeAdapter: ProviderAdapter;
|
|
13
|
+
export declare const kimiCodeGlobalAdapter: ProviderAdapter;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export { OAUTH_HTTP_TIMEOUT_MS, USAGE_HTTP_TIMEOUT_MS } from '../http-timeouts';
|
|
2
|
+
/**
|
|
3
|
+
* One Kimi Code deployment. kimi.com and kimi.ai are separate hosts with
|
|
4
|
+
* separate accounts, and opencode's models.dev catalog lists each as its own
|
|
5
|
+
* provider — so each gets its own adapter, and its own pool.
|
|
6
|
+
*/
|
|
7
|
+
export interface KimiDeployment {
|
|
8
|
+
/** opencode provider id (models.dev). */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** OpenAI-compatible API base (the catalog's `api`); `/usages` and `/me` live here too. */
|
|
11
|
+
readonly baseUrl: string;
|
|
12
|
+
/** OAuth host for the device-code login and token refresh. */
|
|
13
|
+
readonly oauthHost: string;
|
|
14
|
+
/** Console page where a subscriber creates the API key the key login asks for. */
|
|
15
|
+
readonly consoleUrl: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const KIMI_CODE: KimiDeployment;
|
|
18
|
+
export declare const KIMI_CODE_GLOBAL: KimiDeployment;
|
|
19
|
+
/** Kimi Code's public OAuth client, shared by both deployments (no secret). */
|
|
20
|
+
export declare const KIMI_OAUTH_CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
21
|
+
/**
|
|
22
|
+
* `X-Msh-Platform` / `X-Msh-Version` sent to the OAuth host. Kimi's SDK asks
|
|
23
|
+
* every host to name itself rather than borrow its CLI's `kimi_code_cli`, so
|
|
24
|
+
* this names the plugin; the version is this integration's, bumped when its
|
|
25
|
+
* handling of the login protocol changes.
|
|
26
|
+
*/
|
|
27
|
+
export declare const KIMI_DEVICE_PLATFORM = "opencode_auth_load_balancer";
|
|
28
|
+
export declare const KIMI_DEVICE_VERSION = "1";
|
|
29
|
+
/** Local wall-clock budget for one device-code login (Kimi's own clients use 15 min). */
|
|
30
|
+
export declare const DEVICE_LOGIN_TIMEOUT_MS: number;
|
|
31
|
+
/**
|
|
32
|
+
* `expires` stamped on a static-key row: a key does not expire client-side,
|
|
33
|
+
* so `needsRefresh` must never fire. Finite, so the pool store's
|
|
34
|
+
* `Number.isFinite(expires)` normalization keeps it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const STATIC_KEY_EXPIRES: number;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TokenSet } from '../../types';
|
|
2
|
+
import { type KimiDeployment } from './constants';
|
|
3
|
+
/**
|
|
4
|
+
* Map a Kimi Code API key onto the pool's token shape: the key is the bearer,
|
|
5
|
+
* with no refresh token and an expiry that never comes due. `accountId` is a
|
|
6
|
+
* fingerprint of the key — `addAccount` dedups on it — which a login upgrades
|
|
7
|
+
* to the Kimi account id; this bare form serves the startup import, where no
|
|
8
|
+
* network call can run.
|
|
9
|
+
*/
|
|
10
|
+
export declare function tokensFromApiKey(key: string): TokenSet;
|
|
11
|
+
/**
|
|
12
|
+
* The pasted key, checked against `GET /me` before it can join the pool: a
|
|
13
|
+
* typo, a Moonshot open-platform key, or a stray URL fails the login instead
|
|
14
|
+
* of adding a row that only ever 401s. The row is keyed by the Kimi account
|
|
15
|
+
* behind the key, so a second key — or an OAuth login — for the same
|
|
16
|
+
* subscription replaces its row instead of double-counting one quota.
|
|
17
|
+
*/
|
|
18
|
+
export declare function exchange(deployment: KimiDeployment, input: string): Promise<TokenSet | null>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TokenSet } from '../../types';
|
|
2
|
+
import type { DeviceLogin } from '../types';
|
|
3
|
+
import { type KimiDeployment } from './constants';
|
|
4
|
+
/** The poll loop's clock — a seam so tests need not wait out real intervals. */
|
|
5
|
+
export interface PollClock {
|
|
6
|
+
readonly now: () => number;
|
|
7
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Start a device-code login: the URL (user code embedded) to approve in a
|
|
11
|
+
* browser, and `complete` — the wait for that approval. The approved login is
|
|
12
|
+
* keyed by the Kimi account behind it (`GET /me`), like a key login, so one
|
|
13
|
+
* subscription stays one pool row however it signs in.
|
|
14
|
+
*/
|
|
15
|
+
export declare function startDeviceLogin(deployment: KimiDeployment, clock?: PollClock): Promise<DeviceLogin>;
|
|
16
|
+
/**
|
|
17
|
+
* Refresh a Kimi Code OAuth row. Kimi answers a revoked grant with 400
|
|
18
|
+
* `invalid_grant`, 401 or 403, and each means re-login: 400/401 already carry
|
|
19
|
+
* the status refresh.ts parks rows on, so only 403 is spelled out as
|
|
20
|
+
* `invalid_grant`. A key row has no refresh token to spend at all.
|
|
21
|
+
*/
|
|
22
|
+
export declare function refreshOAuth(deployment: KimiDeployment, refreshToken: string): Promise<TokenSet>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** What `GET /me` says about the account behind a bearer. */
|
|
2
|
+
export interface KimiProfile {
|
|
3
|
+
/** Kimi's account id: one per subscription, whichever key or login reaches it. */
|
|
4
|
+
readonly userId?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `GET {baseUrl}/me` with an API key or an OAuth access token. Null when the
|
|
8
|
+
* bearer is refused or the call fails, so it doubles as the key check.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchProfile(baseUrl: string, bearer: string): Promise<KimiProfile | null>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PoolAccount, UsageSnapshot } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse `GET /usages` into the pool's snapshot, one window at a time: the
|
|
4
|
+
* ratio shape (`usages.limit_5h` / `limit_7d`) first, the counter shape as
|
|
5
|
+
* that window's fallback. Null when neither window parses, so a foreign or
|
|
6
|
+
* broken body keeps the last-known snapshot instead of reading as 0% used.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseUsages(json: unknown, now: number): UsageSnapshot | null;
|
|
9
|
+
/** `GET {baseUrl}/usages` — free, spends no quota. Null on any failure. */
|
|
10
|
+
export declare function getUsages(baseUrl: string, key: string): Promise<unknown>;
|
|
11
|
+
/** The adapter's `fetchUsage` for one deployment. */
|
|
12
|
+
export declare function usageFetcher(baseUrl: string): (account: PoolAccount, now: number) => Promise<UsageSnapshot | null>;
|
|
@@ -98,3 +98,17 @@ export declare function parseCallbackInput(input: string, options?: {
|
|
|
98
98
|
code: string;
|
|
99
99
|
state: string;
|
|
100
100
|
} | null;
|
|
101
|
+
/**
|
|
102
|
+
* `parseCallbackInput`, plus the authorization code on its own — what a user
|
|
103
|
+
* naturally copies out of the callback's `?code=…` when the login prompt asks
|
|
104
|
+
* for "the authorization code". A bare code carries no `state`, so it comes
|
|
105
|
+
* back as `state: null` and the caller skips its CSRF comparison for it. PKCE
|
|
106
|
+
* keeps that safe: a code only redeems together with the `code_verifier`
|
|
107
|
+
* generated for THIS login attempt, so a code minted for any other attempt is
|
|
108
|
+
* useless here. A pasted callback still returns its `state` for the caller to
|
|
109
|
+
* enforce.
|
|
110
|
+
*/
|
|
111
|
+
export declare function parseCodeInput(input: string): {
|
|
112
|
+
code: string;
|
|
113
|
+
state: string | null;
|
|
114
|
+
} | null;
|
|
@@ -3,9 +3,12 @@ import type { AuthorizeRequest } from '../types';
|
|
|
3
3
|
/** Begin the ChatGPT PKCE authorization flow. */
|
|
4
4
|
export declare function authorize(): Promise<AuthorizeRequest>;
|
|
5
5
|
/**
|
|
6
|
-
* Exchange
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Exchange the pasted login result for tokens. Returns null on failure. The
|
|
7
|
+
* redirect lands on http://localhost:1455/auth/callback, a page that never
|
|
8
|
+
* loads — so the user pastes either that whole URL from the address bar (or
|
|
9
|
+
* its `code=…&state=…` query) or just the authorization code itself, which is
|
|
10
|
+
* what the login prompt asks for. A pasted `state` must match this attempt's;
|
|
11
|
+
* a bare code has none to compare (see `parseCodeInput`).
|
|
9
12
|
*/
|
|
10
13
|
export declare function exchange(input: string, verifier: string, redirectUri: string, expectedState?: string): Promise<TokenSet | null>;
|
|
11
14
|
/** Refresh an access token. Throws on failure; message includes the HTTP status. */
|
|
@@ -28,6 +28,17 @@ export interface AuthorizeRequest {
|
|
|
28
28
|
verifier: string;
|
|
29
29
|
state: string;
|
|
30
30
|
redirectUri: string;
|
|
31
|
+
/** Login prompt text; absent = the OAuth default ("Paste the authorization code here:"). */
|
|
32
|
+
instructions?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A device-code login (RFC 8628) in flight: where to approve it, and the wait for approval. */
|
|
35
|
+
export interface DeviceLogin {
|
|
36
|
+
/** Verification URL to open (the user code rides in it). */
|
|
37
|
+
url: string;
|
|
38
|
+
/** Prompt shown with the URL, e.g. the user code to confirm. */
|
|
39
|
+
instructions: string;
|
|
40
|
+
/** Resolve with the approved tokens, or null when denied, expired, or failed. */
|
|
41
|
+
complete(): Promise<TokenSet | null>;
|
|
31
42
|
}
|
|
32
43
|
/**
|
|
33
44
|
* A request-body rewrite that downgrades the requested model to a cheaper
|
|
@@ -79,6 +90,24 @@ export interface ProviderAdapter {
|
|
|
79
90
|
exchange(input: string, verifier: string, redirectUri: string, state: string): Promise<TokenSet | null>;
|
|
80
91
|
/** Refresh an access token. Throws on failure (callers handle invalid_grant). */
|
|
81
92
|
refresh(refreshToken: string): Promise<TokenSet>;
|
|
93
|
+
/**
|
|
94
|
+
* Start a device-code login, for providers whose OAuth has no redirect to
|
|
95
|
+
* paste back (Kimi Code). Registered as an `auto` login method ahead of the
|
|
96
|
+
* paste flow; absent on Claude/Codex.
|
|
97
|
+
*/
|
|
98
|
+
startDeviceLogin?(): Promise<DeviceLogin>;
|
|
99
|
+
/**
|
|
100
|
+
* Present on providers that also take a static API key (Kimi Code): maps a
|
|
101
|
+
* key onto the pool's TokenSet. A key has no refresh token behind it, so
|
|
102
|
+
* its presence switches three credential paths:
|
|
103
|
+
* - the paste login (`authorize`/`exchange`) collects a key, and its
|
|
104
|
+
* callback hands opencode `{ key }`, stored as a plain `api` credential;
|
|
105
|
+
* - the auth loader also imports opencode's `api` credential, which
|
|
106
|
+
* OAuth-only providers ignore;
|
|
107
|
+
* - a 401 parks a key row (one without a refresh token) for a re-login
|
|
108
|
+
* instead of cooling it down — no refresh can repair a revoked key.
|
|
109
|
+
*/
|
|
110
|
+
tokensFromApiKey?(key: string): TokenSet;
|
|
82
111
|
/** Set auth + provider headers on an outgoing request for the chosen account. */
|
|
83
112
|
applyAuth(headers: Headers, account: PoolAccount): void;
|
|
84
113
|
/** Optionally rewrite the request URL (e.g. add ?beta=true, swap base URL). */
|
|
@@ -136,4 +165,24 @@ export interface ProviderAdapter {
|
|
|
136
165
|
* downgraded body. null = treat the 429 as a normal account rotation.
|
|
137
166
|
*/
|
|
138
167
|
planReactiveFallback?(res: Response, body: string, now: number, models: readonly string[]): ReactiveModelFallback | null;
|
|
168
|
+
/**
|
|
169
|
+
* REACTIVE recovery from a CLIENT-VERSION gate: a provider may reject an
|
|
170
|
+
* otherwise-valid request until the plugin claims to be a newer client
|
|
171
|
+
* (Anthropic answers a too-new model with 400 `claude_code_version_too_old`,
|
|
172
|
+
* naming the minimum it wants). Such a status classifies as `ok` — the fetch
|
|
173
|
+
* loop is one statement away from handing it to the caller — so this is the
|
|
174
|
+
* last point at which it can become a recoverable event instead of a failed
|
|
175
|
+
* turn.
|
|
176
|
+
*
|
|
177
|
+
* Return true ONLY when the version the adapter will now report actually
|
|
178
|
+
* changed: the fetch loop reads that as "a retry can succeed" and restarts
|
|
179
|
+
* the request once. Returning true without a change would buy a second,
|
|
180
|
+
* byte-identical round-trip and the same rejection.
|
|
181
|
+
*
|
|
182
|
+
* `res` is a CLONE the implementation may consume freely; the original is
|
|
183
|
+
* still returned to the caller when this resolves false. Absent on providers
|
|
184
|
+
* with no such gate (OpenAI) — the fetch loop treats that as "nothing to
|
|
185
|
+
* recover".
|
|
186
|
+
*/
|
|
187
|
+
recoverClientVersion?(res: Response, now: number): Promise<boolean>;
|
|
139
188
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-auth-load-balancer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Load-balance opencode requests across multiple Anthropic (Claude) and OpenAI (Codex) OAuth accounts, weighted primarily by weekly usage with drain-before-reset scheduling.",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Load-balance opencode requests across multiple Anthropic (Claude) and OpenAI (Codex) OAuth accounts and Kimi Code subscriptions, weighted primarily by weekly usage with drain-before-reset scheduling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "owjs3901",
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
"claude",
|
|
63
63
|
"openai",
|
|
64
64
|
"codex",
|
|
65
|
+
"kimi",
|
|
66
|
+
"kimi-code",
|
|
65
67
|
"oauth",
|
|
66
68
|
"load-balancer"
|
|
67
69
|
],
|
|
@@ -62,6 +62,8 @@ export interface PoolAccount {
|
|
|
62
62
|
/** LEGACY pre-tier-map field (folded into `modelCooldownsUntil.opus` for display until the server migrates the file). */
|
|
63
63
|
opusCooldownUntil?: number
|
|
64
64
|
disabledReason?: string | null
|
|
65
|
+
/** Empty on a static API-key row (Kimi Code), which re-logs in with a new key. */
|
|
66
|
+
refresh?: string
|
|
65
67
|
}
|
|
66
68
|
/** Loosely-typed view of the on-disk pool JSON (one shape for reads AND read-modify-writes). */
|
|
67
69
|
export interface PoolShape {
|
|
@@ -276,16 +278,24 @@ export function clearReloginTargetInPool(path: string = POOL_FILE): void {
|
|
|
276
278
|
* opencode aggregates auth methods from every plugin registered for a provider,
|
|
277
279
|
* so this plugin's position is not stable. Prefer its pooled-login label, then
|
|
278
280
|
* fall back to the provider's first OAuth method when no such label is present.
|
|
281
|
+
* A provider can offer both an account login and an API-key login (Kimi
|
|
282
|
+
* Code), so `apiKey` picks the kind that matches how the row was added.
|
|
279
283
|
*/
|
|
280
284
|
export function pickAuthMethodIndex(
|
|
281
285
|
methods: readonly { type?: string; label?: string }[] | undefined,
|
|
286
|
+
apiKey = false,
|
|
282
287
|
): number | null {
|
|
283
|
-
const pooled =
|
|
288
|
+
const pooled = (method: { type?: string; label?: string }): boolean =>
|
|
289
|
+
method.type === 'oauth' &&
|
|
290
|
+
(method.label?.toLowerCase().includes('load balancer') ?? false)
|
|
291
|
+
const sameKind = methods?.findIndex(
|
|
284
292
|
(method) =>
|
|
285
|
-
method
|
|
286
|
-
method.label?.toLowerCase().includes('
|
|
293
|
+
pooled(method) &&
|
|
294
|
+
(method.label?.toLowerCase().includes('api key') ?? false) === apiKey,
|
|
287
295
|
)
|
|
288
|
-
if (
|
|
296
|
+
if (sameKind !== undefined && sameKind >= 0) return sameKind
|
|
297
|
+
const anyPooled = methods?.findIndex(pooled)
|
|
298
|
+
if (anyPooled !== undefined && anyPooled >= 0) return anyPooled
|
|
289
299
|
const oauth = methods?.findIndex((method) => method.type === 'oauth')
|
|
290
300
|
return oauth !== undefined && oauth >= 0 ? oauth : null
|
|
291
301
|
}
|
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
const PROVIDER_NAMES: Record<string, string> = {
|
|
58
58
|
anthropic: 'Claude',
|
|
59
59
|
openai: 'Codex',
|
|
60
|
+
'kimi-code-plan-cn': 'Kimi',
|
|
61
|
+
'kimi-code-plan-global': 'Kimi (kimi.ai)',
|
|
60
62
|
}
|
|
61
63
|
/** Shared fallback lookup used by both the bottom bar and sidebar group labels. */
|
|
62
64
|
function providerLabel(id: string): string {
|
|
@@ -200,6 +202,8 @@ interface Row extends WindowDisplay {
|
|
|
200
202
|
rank: number | null
|
|
201
203
|
state: string
|
|
202
204
|
manuallyDisabled: boolean
|
|
205
|
+
/** A static API-key row: re-login asks for a new key, not an OAuth sign-in. */
|
|
206
|
+
apiKey: boolean
|
|
203
207
|
}
|
|
204
208
|
interface Group {
|
|
205
209
|
provider: string
|
|
@@ -264,6 +268,7 @@ function SidebarPanel(props: {
|
|
|
264
268
|
// tier annotation forever until the server heals the file.
|
|
265
269
|
state: stateOf(sa, tierResets(a, now), now),
|
|
266
270
|
manuallyDisabled: a.disabledReason === MANUAL_DISABLED_REASON,
|
|
271
|
+
apiKey: a.refresh === '',
|
|
267
272
|
}
|
|
268
273
|
})
|
|
269
274
|
return { provider: providerLabel(providerID), rows }
|
|
@@ -306,12 +311,13 @@ function SidebarPanel(props: {
|
|
|
306
311
|
id: string,
|
|
307
312
|
label: string,
|
|
308
313
|
providerID: string,
|
|
314
|
+
apiKey: boolean,
|
|
309
315
|
): Promise<void> {
|
|
310
316
|
try {
|
|
311
317
|
const methods = (await props.api.client.provider.auth()).data?.[
|
|
312
318
|
providerID
|
|
313
319
|
]
|
|
314
|
-
const method = pickAuthMethodIndex(methods)
|
|
320
|
+
const method = pickAuthMethodIndex(methods, apiKey)
|
|
315
321
|
if (method === null) {
|
|
316
322
|
props.api.ui.toast({
|
|
317
323
|
variant: 'error',
|
|
@@ -372,6 +378,15 @@ function SidebarPanel(props: {
|
|
|
372
378
|
}
|
|
373
379
|
|
|
374
380
|
if (auth.method === 'auto') {
|
|
381
|
+
// A device-code login (Kimi Code) completes in the browser: show where
|
|
382
|
+
// to approve it for as long as the callback waits.
|
|
383
|
+
dialog().replace(() =>
|
|
384
|
+
props.api.ui.DialogAlert({
|
|
385
|
+
title: `Re-login "${label}"`,
|
|
386
|
+
message: `${auth.instructions}\n\n${auth.url}`,
|
|
387
|
+
onConfirm: () => dialog().clear(),
|
|
388
|
+
}),
|
|
389
|
+
)
|
|
375
390
|
await completeRelogin()
|
|
376
391
|
return
|
|
377
392
|
}
|
|
@@ -423,6 +438,7 @@ function SidebarPanel(props: {
|
|
|
423
438
|
label: string,
|
|
424
439
|
providerID: string,
|
|
425
440
|
manuallyDisabled: boolean,
|
|
441
|
+
apiKey: boolean,
|
|
426
442
|
): void {
|
|
427
443
|
const items: { title: string; run: () => void }[] = [
|
|
428
444
|
{ title: 'Rename', run: () => openRename(id, label) },
|
|
@@ -440,7 +456,7 @@ function SidebarPanel(props: {
|
|
|
440
456
|
{
|
|
441
457
|
title: 'Re-login — re-authorize with the provider',
|
|
442
458
|
run: () => {
|
|
443
|
-
void openRelogin(id, label, providerID)
|
|
459
|
+
void openRelogin(id, label, providerID, apiKey)
|
|
444
460
|
},
|
|
445
461
|
},
|
|
446
462
|
{ title: 'Delete — remove from pool', run: () => openDelete(id, label) },
|
|
@@ -503,7 +519,13 @@ function SidebarPanel(props: {
|
|
|
503
519
|
{(r) => (
|
|
504
520
|
<box
|
|
505
521
|
onMouseUp={() =>
|
|
506
|
-
openMenu(
|
|
522
|
+
openMenu(
|
|
523
|
+
r.id,
|
|
524
|
+
r.label,
|
|
525
|
+
r.providerID,
|
|
526
|
+
r.manuallyDisabled,
|
|
527
|
+
r.apiKey,
|
|
528
|
+
)
|
|
507
529
|
}
|
|
508
530
|
>
|
|
509
531
|
<text
|