@oh-my-pi/pi-ai 15.4.3 → 15.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/CHANGELOG.md +15 -0
- package/README.md +6 -5
- package/dist/types/auth-broker/refresher.d.ts +1 -1
- package/dist/types/auth-storage.d.ts +1 -0
- package/dist/types/provider-models/openai-compat.d.ts +5 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/oauth/zhipu.d.ts +18 -0
- package/package.json +2 -5
- package/src/auth-broker/refresher.ts +2 -12
- package/src/auth-storage.ts +72 -4
- package/src/provider-models/descriptors.ts +18 -0
- package/src/provider-models/google.ts +8 -9
- package/src/provider-models/openai-compat.ts +72 -0
- package/src/providers/openai-completions-compat.ts +5 -2
- package/src/providers/transform-messages.ts +61 -42
- package/src/stream.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/types.ts +2 -1
- package/src/utils/oauth/zhipu.ts +60 -0
- package/src/utils/schema/fields.ts +1 -0
- package/src/utils/schema/types.ts +1 -2
- package/src/utils/schema/wire.ts +1 -2
- package/dist/types/cli.d.ts +0 -2
- package/src/cli.ts +0 -262
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.0] - 2026-05-26
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `zhipu-coding-plan` provider for Zhipu (智谱) BigModel's domestic coding-plan SKU at `https://open.bigmodel.cn/api/coding/paas/v4`, with dynamic model discovery (`ZHIPU_API_KEY`), zai-format thinking, `reasoning_content` field, and OAuth login flow ([#1340](https://github.com/can1357/oh-my-pi/issues/1340)).
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- Removed the `pi-ai` CLI binary (`packages/ai/src/cli.ts`) and its `bin` entry. Use the in-process equivalent in the omp coding-agent CLI: `omp auth-broker login [provider]`, `omp auth-broker logout [provider]`, and `omp auth-broker list`. The library API (`AuthStorage.login()`, `getOAuthProviders()`, etc.) is unchanged.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fixed delayed `toolResult` emissions so real tool results are emitted in the correct assistant `toolCall` window after handoff/compaction, preventing out-of-order or orphaned tool results
|
|
17
|
+
- Fixed delayed `toolResult` handling for aborted calls so a late real result is emitted instead of a synthetic `aborted` result for the same `toolCallId`
|
|
18
|
+
- Fixed usage polling to disable credentials when OAuth refresh fails definitively (for example `invalid_grant`) and clear cached last-good usage data so stale reports no longer remain visible
|
|
19
|
+
|
|
5
20
|
## [15.4.3] - 2026-05-26
|
|
6
21
|
|
|
7
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1057,13 +1057,14 @@ Official docs: [Application Default Credentials](https://cloud.google.com/docs/a
|
|
|
1057
1057
|
|
|
1058
1058
|
### CLI Login
|
|
1059
1059
|
|
|
1060
|
-
|
|
1060
|
+
Authenticate via the [`omp`](https://omp.sh) coding-agent CLI, which drives this library's OAuth/API-key flows in-process and persists into `agent.db`:
|
|
1061
1061
|
|
|
1062
1062
|
```bash
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1063
|
+
omp auth-broker login # interactive provider selection
|
|
1064
|
+
omp auth-broker login anthropic # login to a specific provider
|
|
1065
|
+
omp auth-broker login vllm # store vLLM API key (or placeholder for local no-auth)
|
|
1066
|
+
omp auth-broker list # list supported providers
|
|
1067
|
+
omp auth-broker logout # interactive — pick a stored credential to remove
|
|
1067
1068
|
```
|
|
1068
1069
|
|
|
1069
1070
|
Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.
|
|
@@ -281,6 +281,7 @@ export type AuthStorageOptions = {
|
|
|
281
281
|
*/
|
|
282
282
|
fetchUsageReports?: (signal?: AbortSignal) => Promise<UsageReport[] | null>;
|
|
283
283
|
};
|
|
284
|
+
export declare function isDefinitiveOAuthFailure(errorMsg: string): boolean;
|
|
284
285
|
type AuthApiKeyOptions = {
|
|
285
286
|
baseUrl?: string;
|
|
286
287
|
modelId?: string;
|
|
@@ -58,6 +58,11 @@ export interface DeepSeekModelManagerConfig {
|
|
|
58
58
|
baseUrl?: string;
|
|
59
59
|
}
|
|
60
60
|
export declare function deepseekModelManagerOptions(config?: DeepSeekModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
61
|
+
export interface ZhipuCodingPlanModelManagerConfig {
|
|
62
|
+
apiKey?: string;
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function zhipuCodingPlanModelManagerOptions(config?: ZhipuCodingPlanModelManagerConfig): ModelManagerOptions<"openai-completions">;
|
|
61
66
|
export interface FireworksModelManagerConfig {
|
|
62
67
|
apiKey?: string;
|
|
63
68
|
baseUrl?: string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface ThinkingConfig {
|
|
|
48
48
|
/** Provider-specific transport used to encode the selected effort. */
|
|
49
49
|
mode: ThinkingControlMode;
|
|
50
50
|
}
|
|
51
|
-
export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "zenmux" | "lm-studio";
|
|
51
|
+
export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "zenmux" | "lm-studio";
|
|
52
52
|
export type Provider = KnownProvider | string;
|
|
53
53
|
import type { Effort } from "./model-thinking";
|
|
54
54
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
|
|
|
7
7
|
email?: string;
|
|
8
8
|
accountId?: string;
|
|
9
9
|
};
|
|
10
|
-
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" | "zai";
|
|
10
|
+
export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" | "zai" | "zhipu-coding-plan";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zhipu Coding Plan login flow.
|
|
3
|
+
*
|
|
4
|
+
* Zhipu BigModel (智谱) provides an OpenAI-compatible API.
|
|
5
|
+
* API docs: https://docs.bigmodel.cn/cn/guide/develop/openai/introduction
|
|
6
|
+
*
|
|
7
|
+
* Simple API key flow:
|
|
8
|
+
* 1. User gets their API key from https://open.bigmodel.cn
|
|
9
|
+
* 2. User pastes the API key into the CLI
|
|
10
|
+
*/
|
|
11
|
+
import type { OAuthController } from "./types";
|
|
12
|
+
/**
|
|
13
|
+
* Login to Zhipu Coding Plan.
|
|
14
|
+
*
|
|
15
|
+
* Opens browser to API keys page, prompts user to paste their API key.
|
|
16
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
17
|
+
*/
|
|
18
|
+
export declare function loginZhipuCodingPlan(options: OAuthController): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.5.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -28,9 +28,6 @@
|
|
|
28
28
|
],
|
|
29
29
|
"main": "./src/index.ts",
|
|
30
30
|
"types": "./dist/types/index.d.ts",
|
|
31
|
-
"bin": {
|
|
32
|
-
"pi-ai": "./src/cli.ts"
|
|
33
|
-
},
|
|
34
31
|
"scripts": {
|
|
35
32
|
"check": "biome check . && bun run check:types",
|
|
36
33
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
@@ -43,7 +40,7 @@
|
|
|
43
40
|
"dependencies": {
|
|
44
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@oh-my-pi/pi-utils": "15.
|
|
43
|
+
"@oh-my-pi/pi-utils": "15.5.0",
|
|
47
44
|
"openai": "^6.36.0",
|
|
48
45
|
"partial-json": "^0.1.7",
|
|
49
46
|
"zod": "4.4.3"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* snapshot pull surfaces a clean delete on the client.
|
|
11
11
|
*/
|
|
12
12
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
13
|
-
import type
|
|
13
|
+
import { type AuthStorage, isDefinitiveOAuthFailure } from "../auth-storage";
|
|
14
14
|
import { DEFAULT_REFRESH_INTERVAL_MS, DEFAULT_REFRESH_SKEW_MS } from "./types";
|
|
15
15
|
|
|
16
16
|
export interface AuthBrokerRefresherOptions {
|
|
@@ -23,16 +23,6 @@ export interface AuthBrokerRefresherOptions {
|
|
|
23
23
|
now?: () => number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const INVALID_GRANT_REGEX = /invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i;
|
|
27
|
-
const TRANSIENT_REGEX = /timeout|network|fetch failed|ECONNREFUSED/i;
|
|
28
|
-
const HTTP_401_403_REGEX = /\b(401|403)\b/;
|
|
29
|
-
|
|
30
|
-
function isDefinitiveFailure(errorMsg: string): boolean {
|
|
31
|
-
if (INVALID_GRANT_REGEX.test(errorMsg)) return true;
|
|
32
|
-
if (HTTP_401_403_REGEX.test(errorMsg) && !TRANSIENT_REGEX.test(errorMsg)) return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
export interface AuthBrokerRefresherSchedule {
|
|
37
27
|
enabled: boolean;
|
|
38
28
|
intervalMs: number;
|
|
@@ -113,7 +103,7 @@ export class AuthBrokerRefresher {
|
|
|
113
103
|
await this.#storage.refreshCredentialById(id);
|
|
114
104
|
} catch (error) {
|
|
115
105
|
const errorMsg = String(error);
|
|
116
|
-
if (
|
|
106
|
+
if (isDefinitiveOAuthFailure(errorMsg)) {
|
|
117
107
|
logger.warn("auth-broker refresh failed definitively; disabling credential", {
|
|
118
108
|
id,
|
|
119
109
|
error: errorMsg,
|
package/src/auth-storage.ts
CHANGED
|
@@ -414,6 +414,29 @@ const OAUTH_REFRESH_SKEW_MS = 60_000;
|
|
|
414
414
|
*/
|
|
415
415
|
const MAX_PENDING_DISABLED_EVENTS = 32;
|
|
416
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Classify an OAuth refresh error as a definitive credential failure (the
|
|
419
|
+
* refresh token is dead — re-login required) versus a transient blip
|
|
420
|
+
* (network/5xx — retry next sweep).
|
|
421
|
+
*
|
|
422
|
+
* Anchored at module scope so all three refresh sites — in-stream
|
|
423
|
+
* {@link AuthStorage.getApiKey}, the usage probe in
|
|
424
|
+
* {@link AuthStorage.fetchUsageReports}, and the auth-broker background
|
|
425
|
+
* refresher — disable rows on the same criteria. A drifting classifier
|
|
426
|
+
* between sites would let stale last-good usage reports surface indefinitely
|
|
427
|
+
* while streaming requests correctly tear the row down.
|
|
428
|
+
*/
|
|
429
|
+
const OAUTH_DEFINITIVE_FAILURE_REGEX =
|
|
430
|
+
/invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i;
|
|
431
|
+
const OAUTH_TRANSIENT_FAILURE_REGEX = /timeout|network|fetch failed|ECONNREFUSED/i;
|
|
432
|
+
const OAUTH_HTTP_AUTH_REGEX = /\b(401|403)\b/;
|
|
433
|
+
|
|
434
|
+
export function isDefinitiveOAuthFailure(errorMsg: string): boolean {
|
|
435
|
+
if (OAUTH_DEFINITIVE_FAILURE_REGEX.test(errorMsg)) return true;
|
|
436
|
+
if (OAUTH_HTTP_AUTH_REGEX.test(errorMsg) && !OAUTH_TRANSIENT_FAILURE_REGEX.test(errorMsg)) return true;
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
417
440
|
type UsageCacheEntry<T> = {
|
|
418
441
|
value: T;
|
|
419
442
|
expiresAt: number;
|
|
@@ -1497,6 +1520,12 @@ export class AuthStorage {
|
|
|
1497
1520
|
await saveApiKeyCredential(apiKey);
|
|
1498
1521
|
return;
|
|
1499
1522
|
}
|
|
1523
|
+
case "zhipu-coding-plan": {
|
|
1524
|
+
const { loginZhipuCodingPlan } = await import("./utils/oauth/zhipu");
|
|
1525
|
+
const apiKey = await loginZhipuCodingPlan(ctrl);
|
|
1526
|
+
await saveApiKeyCredential(apiKey);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1500
1529
|
case "qianfan": {
|
|
1501
1530
|
const { loginQianfan } = await import("./utils/oauth/qianfan");
|
|
1502
1531
|
const apiKey = await loginQianfan(ctrl);
|
|
@@ -1832,9 +1861,50 @@ export class AuthStorage {
|
|
|
1832
1861
|
credential: refreshedCredential,
|
|
1833
1862
|
};
|
|
1834
1863
|
} catch (error) {
|
|
1864
|
+
const errorMsg = String(error);
|
|
1865
|
+
// Definitive failure (invalid_grant / 401 not from a network blip) means
|
|
1866
|
+
// the refresh token itself is dead — probing with the original credential
|
|
1867
|
+
// will 401, the catch below will return null, and #fetchUsageCached's
|
|
1868
|
+
// last-good fallback will surface yesterday's report indefinitely
|
|
1869
|
+
// (including its already-elapsed `resetsAt`). CAS-disable the row and
|
|
1870
|
+
// clear the cache so the credential drops out of the report instead of
|
|
1871
|
+
// freezing in place until the user notices and re-logs in.
|
|
1872
|
+
if (isDefinitiveOAuthFailure(errorMsg)) {
|
|
1873
|
+
const credentialId = this.#findStoredCredentialIdForUsageCredential(
|
|
1874
|
+
request.provider,
|
|
1875
|
+
request.credential,
|
|
1876
|
+
);
|
|
1877
|
+
if (credentialId !== undefined) {
|
|
1878
|
+
const entries = this.#getStoredCredentials(request.provider);
|
|
1879
|
+
const index = entries.findIndex(entry => entry.id === credentialId);
|
|
1880
|
+
if (index !== -1) {
|
|
1881
|
+
const disabled = this.#tryDisableCredentialAtIfMatches(
|
|
1882
|
+
request.provider,
|
|
1883
|
+
index,
|
|
1884
|
+
refreshableCredential,
|
|
1885
|
+
`oauth refresh failed during usage probe: ${errorMsg}`,
|
|
1886
|
+
);
|
|
1887
|
+
if (disabled) {
|
|
1888
|
+
this.#usageLogger?.warn(
|
|
1889
|
+
"Usage credential refresh failed definitively; credential disabled",
|
|
1890
|
+
{ provider: request.provider, credentialId, error: errorMsg },
|
|
1891
|
+
);
|
|
1892
|
+
// Neutralize last-good for this cache key: write a null
|
|
1893
|
+
// entry with an immediately-elapsed expiry so a future
|
|
1894
|
+
// getStale lookup (e.g. on re-login under the same
|
|
1895
|
+
// account identity) can't replay the stale report.
|
|
1896
|
+
this.#usageCache.set(this.#buildUsageReportCacheKey(request), {
|
|
1897
|
+
value: null,
|
|
1898
|
+
expiresAt: 0,
|
|
1899
|
+
});
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1835
1905
|
this.#usageLogger?.debug("Usage credential refresh failed, using original credential", {
|
|
1836
1906
|
provider: request.provider,
|
|
1837
|
-
error:
|
|
1907
|
+
error: errorMsg,
|
|
1838
1908
|
});
|
|
1839
1909
|
}
|
|
1840
1910
|
}
|
|
@@ -2877,9 +2947,7 @@ export class AuthStorage {
|
|
|
2877
2947
|
const errorMsg = String(error);
|
|
2878
2948
|
// Only remove credentials for definitive auth failures
|
|
2879
2949
|
// Keep credentials for transient errors (network, 5xx) and block temporarily
|
|
2880
|
-
const isDefinitiveFailure =
|
|
2881
|
-
/invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i.test(errorMsg) ||
|
|
2882
|
-
(/\b(401|403)\b/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg));
|
|
2950
|
+
const isDefinitiveFailure = isDefinitiveOAuthFailure(errorMsg);
|
|
2883
2951
|
|
|
2884
2952
|
logger.warn("OAuth token refresh failed", {
|
|
2885
2953
|
provider,
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
xaiModelManagerOptions,
|
|
43
43
|
xiaomiModelManagerOptions,
|
|
44
44
|
zenmuxModelManagerOptions,
|
|
45
|
+
zhipuCodingPlanModelManagerOptions,
|
|
45
46
|
} from "./openai-compat";
|
|
46
47
|
import { cursorModelManagerOptions, zaiModelManagerOptions } from "./special";
|
|
47
48
|
|
|
@@ -153,6 +154,17 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
153
154
|
config => fireworksModelManagerOptions(config),
|
|
154
155
|
catalog("Fireworks", ["FIREWORKS_API_KEY"]),
|
|
155
156
|
),
|
|
157
|
+
// Fire Pass does not expose a /v1/models endpoint — the API returns HTTP 403
|
|
158
|
+
// on any catalog-discovery request, so dynamic model listing is not feasible.
|
|
159
|
+
//
|
|
160
|
+
// The single model `kimi-k2.6-turbo` is seeded via the `prevModelsJson`
|
|
161
|
+
// fallback in `generate-models.ts`, which preserves entries from the previous
|
|
162
|
+
// catalog snapshot when a provider does not surface them dynamically.
|
|
163
|
+
//
|
|
164
|
+
// IMPORTANT: Do NOT delete the firepass section from models.json. No
|
|
165
|
+
// descriptor here produces that entry dynamically — removing it from
|
|
166
|
+
// models.json would permanently drop the model from the catalog with no
|
|
167
|
+
// automated mechanism to restore it.
|
|
156
168
|
descriptor("firepass", "kimi-k2.6-turbo", config => firepassModelManagerOptions(config)),
|
|
157
169
|
descriptor("xai", "grok-4-fast-non-reasoning", config => xaiModelManagerOptions(config)),
|
|
158
170
|
catalogDescriptor(
|
|
@@ -281,6 +293,12 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
281
293
|
catalog("ZenMux", ["ZENMUX_API_KEY"]),
|
|
282
294
|
),
|
|
283
295
|
catalogDescriptor("zai", "glm-5.1", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
|
|
296
|
+
catalogDescriptor(
|
|
297
|
+
"zhipu-coding-plan",
|
|
298
|
+
"glm-5.1",
|
|
299
|
+
config => zhipuCodingPlanModelManagerOptions(config),
|
|
300
|
+
catalog("Zhipu Coding Plan", ["ZHIPU_API_KEY"]),
|
|
301
|
+
),
|
|
284
302
|
descriptor("github-copilot", "gpt-4o", config => githubCopilotModelManagerOptions(config)),
|
|
285
303
|
descriptor("google", "gemini-2.5-pro", config => googleModelManagerOptions(config)),
|
|
286
304
|
descriptor("google-vertex", "gemini-3-pro-preview", config => googleVertexModelManagerOptions(config), {
|
|
@@ -40,7 +40,11 @@ export function googleModelManagerOptions(
|
|
|
40
40
|
|
|
41
41
|
export function googleVertexModelManagerOptions(config?: GoogleVertexModelManagerConfig): ModelManagerOptions {
|
|
42
42
|
const project = resolveVertexProject(config);
|
|
43
|
+
const hasApiKey = (config?.apiKey ?? Bun.env.GOOGLE_CLOUD_API_KEY ?? "").trim().length > 0;
|
|
43
44
|
const location = resolveVertexLocation(config);
|
|
45
|
+
if (hasApiKey) {
|
|
46
|
+
return { providerId: "google-vertex" };
|
|
47
|
+
}
|
|
44
48
|
if (project && location) {
|
|
45
49
|
return {
|
|
46
50
|
providerId: "google-vertex",
|
|
@@ -54,15 +58,10 @@ export function googleVertexModelManagerOptions(config?: GoogleVertexModelManage
|
|
|
54
58
|
}),
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
|
-
// API
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// authoritative cached Vertex project catalog on the next refresh.
|
|
62
|
-
const hasApiKey = (config?.apiKey ?? Bun.env.GOOGLE_CLOUD_API_KEY ?? "").trim().length > 0;
|
|
63
|
-
if (hasApiKey) {
|
|
64
|
-
return { providerId: "google-vertex" };
|
|
65
|
-
}
|
|
61
|
+
// With neither ADC project+location nor API key auth configured, drop the
|
|
62
|
+
// bundled static catalog so stale fallbacks (e.g. `gemini-1.5-*`) cannot leak
|
|
63
|
+
// into `/models` alongside an authoritative cached Vertex project catalog on
|
|
64
|
+
// the next refresh.
|
|
66
65
|
return { providerId: "google-vertex", staticModels: [] };
|
|
67
66
|
}
|
|
68
67
|
function resolveVertexProject(config?: GoogleVertexModelManagerConfig): string | undefined {
|
|
@@ -600,6 +600,70 @@ export function deepseekModelManagerOptions(
|
|
|
600
600
|
): ModelManagerOptions<"openai-completions"> {
|
|
601
601
|
return createSimpleOpenAICompletionsOptions("deepseek", "https://api.deepseek.com", config);
|
|
602
602
|
}
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// 6.7 Zhipu Coding Plan
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
export interface ZhipuCodingPlanModelManagerConfig {
|
|
608
|
+
apiKey?: string;
|
|
609
|
+
baseUrl?: string;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function zhipuCodingPlanModelManagerOptions(
|
|
613
|
+
config?: ZhipuCodingPlanModelManagerConfig,
|
|
614
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
615
|
+
const apiKey = config?.apiKey;
|
|
616
|
+
const baseUrl = config?.baseUrl ?? "https://open.bigmodel.cn/api/paas/v4";
|
|
617
|
+
return {
|
|
618
|
+
providerId: "zhipu-coding-plan",
|
|
619
|
+
...(apiKey && {
|
|
620
|
+
fetchDynamicModels: () =>
|
|
621
|
+
fetchOpenAICompatibleModels({
|
|
622
|
+
api: "openai-completions",
|
|
623
|
+
provider: "zhipu-coding-plan",
|
|
624
|
+
baseUrl,
|
|
625
|
+
apiKey,
|
|
626
|
+
mapModel: (
|
|
627
|
+
_entry: OpenAICompatibleModelRecord,
|
|
628
|
+
defaults: Model<"openai-completions">,
|
|
629
|
+
_context: OpenAICompatibleModelMapperContext<"openai-completions">,
|
|
630
|
+
): Model<"openai-completions"> => {
|
|
631
|
+
const id = defaults.id;
|
|
632
|
+
return {
|
|
633
|
+
...defaults,
|
|
634
|
+
reasoning: ZHIPU_REASONING_MODELS[id] === true || id.includes("thinking"),
|
|
635
|
+
input: ZHIPU_VISION_PATTERN.test(id) ? (["text", "image"] as const) : ["text"],
|
|
636
|
+
compat: {
|
|
637
|
+
thinkingFormat: "zai",
|
|
638
|
+
reasoningContentField: "reasoning_content",
|
|
639
|
+
supportsDeveloperRole: false,
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
}),
|
|
644
|
+
}),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Reasoning-capable GLM models on the BigModel coding-plan SKU. Keep this
|
|
649
|
+
// explicit rather than regex-matching `glm-[45]\.\d` so newly-added integers
|
|
650
|
+
// like `glm-5` / `glm-5-turbo` are covered and unrelated future SKUs (e.g.
|
|
651
|
+
// `glm-5-preview`) do not silently flip into thinking mode.
|
|
652
|
+
const ZHIPU_REASONING_MODELS: Readonly<Record<string, true>> = {
|
|
653
|
+
"glm-4.5": true,
|
|
654
|
+
"glm-4.5-air": true,
|
|
655
|
+
"glm-4.6": true,
|
|
656
|
+
"glm-4.7": true,
|
|
657
|
+
"glm-5": true,
|
|
658
|
+
"glm-5-turbo": true,
|
|
659
|
+
"glm-5.1": true,
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// Vision-capable GLM models follow the `glm-<N>[.<N>]v[-<variant>]` shape
|
|
663
|
+
// (e.g. `glm-4v`, `glm-4.5v`, `glm-4v-plus`). The previous `id.includes("v")`
|
|
664
|
+
// check matched anything with a `v` — including the non-vision `glm-5-preview`.
|
|
665
|
+
const ZHIPU_VISION_PATTERN = /^glm-[45](?:\.\d+)?v(?:-|$)/;
|
|
666
|
+
|
|
603
667
|
// ---------------------------------------------------------------------------
|
|
604
668
|
// 7.5 Fireworks
|
|
605
669
|
// ---------------------------------------------------------------------------
|
|
@@ -2184,6 +2248,14 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
|
|
|
2184
2248
|
},
|
|
2185
2249
|
},
|
|
2186
2250
|
),
|
|
2251
|
+
// --- Zhipu Coding Plan ---
|
|
2252
|
+
openAiCompletionsDescriptor("zhipu-coding-plan", "zhipu-coding-plan", "https://open.bigmodel.cn/api/paas/v4", {
|
|
2253
|
+
compat: {
|
|
2254
|
+
thinkingFormat: "zai",
|
|
2255
|
+
reasoningContentField: "reasoning_content",
|
|
2256
|
+
supportsDeveloperRole: false,
|
|
2257
|
+
},
|
|
2258
|
+
}),
|
|
2187
2259
|
];
|
|
2188
2260
|
|
|
2189
2261
|
const filterActiveToolCallModels = (_id: string, m: ModelsDevModel): boolean => {
|
|
@@ -51,6 +51,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
51
51
|
|
|
52
52
|
const isCerebras = provider === "cerebras" || baseUrl.includes("cerebras.ai");
|
|
53
53
|
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
|
|
54
|
+
const isZhipu = provider === "zhipu-coding-plan" || baseUrl.includes("open.bigmodel.cn");
|
|
54
55
|
const isKilo = provider === "kilo" || baseUrl.includes("api.kilo.ai");
|
|
55
56
|
const isKimiModel = model.id.includes("moonshotai/kimi") || /(^|\/)kimi[-.]/i.test(model.id);
|
|
56
57
|
const isMoonshotKimi =
|
|
@@ -97,6 +98,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
97
98
|
baseUrl.includes("fireworks.ai") ||
|
|
98
99
|
isAlibaba ||
|
|
99
100
|
isZai ||
|
|
101
|
+
isZhipu ||
|
|
100
102
|
isKilo ||
|
|
101
103
|
isQwen ||
|
|
102
104
|
provider === "opencode-zen" ||
|
|
@@ -156,6 +158,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
156
158
|
isMistral ||
|
|
157
159
|
isGrok ||
|
|
158
160
|
isZai ||
|
|
161
|
+
isZhipu ||
|
|
159
162
|
isCopilotHost ||
|
|
160
163
|
isZenmuxHost);
|
|
161
164
|
|
|
@@ -194,7 +197,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
194
197
|
// OpenAI's reasoning-API surface.
|
|
195
198
|
supportsDeveloperRole: isOpenAIHost || isAzureHost,
|
|
196
199
|
supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
|
|
197
|
-
supportsReasoningEffort: !isGrok && !isZai,
|
|
200
|
+
supportsReasoningEffort: !isGrok && !isZai && !isZhipu,
|
|
198
201
|
reasoningEffortMap,
|
|
199
202
|
supportsUsageInStreaming: !isCerebras,
|
|
200
203
|
disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel,
|
|
@@ -206,7 +209,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
206
209
|
requiresThinkingAsText: isMistral,
|
|
207
210
|
requiresMistralToolIds: isMistral,
|
|
208
211
|
thinkingFormat:
|
|
209
|
-
isZai || isMoonshotKimi
|
|
212
|
+
isZai || isZhipu || isMoonshotKimi
|
|
210
213
|
? "zai"
|
|
211
214
|
: provider === "openrouter" || baseUrl.includes("openrouter.ai")
|
|
212
215
|
? "openrouter"
|
|
@@ -11,9 +11,9 @@ import type {
|
|
|
11
11
|
} from "../types";
|
|
12
12
|
|
|
13
13
|
const enum ToolCallStatus {
|
|
14
|
-
/**
|
|
14
|
+
/** A tool result has already been emitted for this tool call; later duplicates must be skipped. */
|
|
15
15
|
Resolved = 1,
|
|
16
|
-
/**
|
|
16
|
+
/** A synthetic aborted result was emitted; later real results must be skipped. */
|
|
17
17
|
Aborted = 2,
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -131,9 +131,12 @@ export function transformMessages<TApi extends Api>(
|
|
|
131
131
|
}
|
|
132
132
|
return msg;
|
|
133
133
|
});
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const realToolResultsById = new Map<string, ToolResultMessage>();
|
|
135
|
+
for (const msg of transformed) {
|
|
136
|
+
if (msg.role === "toolResult" && !realToolResultsById.has(msg.toolCallId)) {
|
|
137
|
+
realToolResultsById.set(msg.toolCallId, msg);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
137
140
|
|
|
138
141
|
// Anthropic rejects `tool_result` blocks whose `tool_use_id` does not appear in a prior
|
|
139
142
|
// `tool_use` block. After handoff/compaction folds an assistant turn into a summary
|
|
@@ -148,29 +151,35 @@ export function transformMessages<TApi extends Api>(
|
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
// Second pass:
|
|
152
|
-
//
|
|
154
|
+
// Second pass: ensure each surviving assistant tool call is immediately
|
|
155
|
+
// followed by exactly one corresponding tool result.
|
|
153
156
|
const result: Message[] = [];
|
|
154
157
|
let pendingToolCalls: ToolCall[] = [];
|
|
155
158
|
let pendingAbortedToolCalls = new Map<string, ToolCall>();
|
|
156
159
|
let pendingAbortedTimestamp: number | undefined;
|
|
157
|
-
// Track tool
|
|
160
|
+
// Track which tool calls already have an emitted result so delayed/duplicate
|
|
161
|
+
// toolResult messages cannot create a second provider-visible result.
|
|
158
162
|
const toolCallStatus = new Map<string, ToolCallStatus>();
|
|
159
163
|
|
|
160
164
|
const flushPendingToolCalls = (timestamp: number): void => {
|
|
161
165
|
if (pendingToolCalls.length === 0) return;
|
|
162
166
|
for (const tc of pendingToolCalls) {
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
toolName: tc.name,
|
|
168
|
-
content: [{ type: "text", text: "No result provided" }],
|
|
169
|
-
isError: true,
|
|
170
|
-
timestamp,
|
|
171
|
-
} as ToolResultMessage);
|
|
167
|
+
if (toolCallStatus.has(tc.id)) continue;
|
|
168
|
+
const realToolResult = realToolResultsById.get(tc.id);
|
|
169
|
+
if (realToolResult) {
|
|
170
|
+
result.push(realToolResult);
|
|
172
171
|
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
172
|
+
continue;
|
|
173
173
|
}
|
|
174
|
+
result.push({
|
|
175
|
+
role: "toolResult",
|
|
176
|
+
toolCallId: tc.id,
|
|
177
|
+
toolName: tc.name,
|
|
178
|
+
content: [{ type: "text", text: "No result provided" }],
|
|
179
|
+
isError: true,
|
|
180
|
+
timestamp,
|
|
181
|
+
} as ToolResultMessage);
|
|
182
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
174
183
|
}
|
|
175
184
|
pendingToolCalls = [];
|
|
176
185
|
};
|
|
@@ -178,17 +187,22 @@ export function transformMessages<TApi extends Api>(
|
|
|
178
187
|
const flushPendingAbortedToolCalls = (): void => {
|
|
179
188
|
if (pendingAbortedTimestamp === undefined) return;
|
|
180
189
|
for (const tc of pendingAbortedToolCalls.values()) {
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
isError: true,
|
|
188
|
-
timestamp: pendingAbortedTimestamp,
|
|
189
|
-
} as ToolResultMessage);
|
|
190
|
-
toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
|
|
190
|
+
if (toolCallStatus.has(tc.id)) continue;
|
|
191
|
+
const realToolResult = realToolResultsById.get(tc.id);
|
|
192
|
+
if (realToolResult) {
|
|
193
|
+
result.push(realToolResult);
|
|
194
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Resolved);
|
|
195
|
+
continue;
|
|
191
196
|
}
|
|
197
|
+
result.push({
|
|
198
|
+
role: "toolResult",
|
|
199
|
+
toolCallId: tc.id,
|
|
200
|
+
toolName: tc.name,
|
|
201
|
+
content: [{ type: "text", text: "aborted" }],
|
|
202
|
+
isError: true,
|
|
203
|
+
timestamp: pendingAbortedTimestamp,
|
|
204
|
+
} as ToolResultMessage);
|
|
205
|
+
toolCallStatus.set(tc.id, ToolCallStatus.Aborted);
|
|
192
206
|
}
|
|
193
207
|
result.push({
|
|
194
208
|
role: "developer",
|
|
@@ -236,8 +250,9 @@ export function transformMessages<TApi extends Api>(
|
|
|
236
250
|
}
|
|
237
251
|
|
|
238
252
|
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
|
|
239
|
-
// Keep the assistant message with tool calls intact.
|
|
240
|
-
// otherwise synthesize aborted results
|
|
253
|
+
// Keep the assistant message with tool calls intact. Real tool results are
|
|
254
|
+
// emitted immediately if available; otherwise synthesize aborted results
|
|
255
|
+
// before the next turn boundary.
|
|
241
256
|
result.push(msg);
|
|
242
257
|
pendingAbortedToolCalls = new Map(toolCalls.map(toolCall => [toolCall.id, toolCall] as const));
|
|
243
258
|
pendingAbortedTimestamp = assistantMsg.timestamp;
|
|
@@ -250,6 +265,8 @@ export function transformMessages<TApi extends Api>(
|
|
|
250
265
|
|
|
251
266
|
result.push(msg);
|
|
252
267
|
} else if (msg.role === "toolResult") {
|
|
268
|
+
if (toolCallStatus.has(msg.toolCallId)) continue;
|
|
269
|
+
|
|
253
270
|
if (pendingAbortedToolCalls.has(msg.toolCallId)) {
|
|
254
271
|
pendingAbortedToolCalls.delete(msg.toolCallId);
|
|
255
272
|
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
@@ -257,7 +274,11 @@ export function transformMessages<TApi extends Api>(
|
|
|
257
274
|
continue;
|
|
258
275
|
}
|
|
259
276
|
|
|
260
|
-
if (
|
|
277
|
+
if (pendingToolCalls.some(tc => tc.id === msg.toolCallId)) {
|
|
278
|
+
toolCallStatus.set(msg.toolCallId, ToolCallStatus.Resolved);
|
|
279
|
+
result.push(msg);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
261
282
|
|
|
262
283
|
if (!validToolUseIds.has(msg.toolCallId)) {
|
|
263
284
|
// Orphan `tool_result`: the originating `tool_use` is not present in the
|
|
@@ -272,16 +293,13 @@ export function transformMessages<TApi extends Api>(
|
|
|
272
293
|
// * Anthropic requires the next message after an assistant `tool_use`
|
|
273
294
|
// to be the matching `tool_result`. Inserting a developer message
|
|
274
295
|
// would break that contiguity.
|
|
275
|
-
// *
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
// `realToolResultIds`). Calling it here would convert a legitimate
|
|
279
|
-
// later `tool_result` into a synthetic "aborted" one via the
|
|
280
|
-
// `ToolCallStatus.Aborted` skip-guard.
|
|
296
|
+
// * Flushing pending aborted calls here would wedge synthetic results
|
|
297
|
+
// between the assistant turn and a real result that may still arrive
|
|
298
|
+
// inside the current contiguous result window.
|
|
281
299
|
//
|
|
282
|
-
// Drop the orphan silently in that case; the
|
|
283
|
-
//
|
|
284
|
-
if (pendingToolCalls.
|
|
300
|
+
// Drop the orphan silently in that case; the pending calls will be
|
|
301
|
+
// resolved in their own contiguous result window or at the next boundary.
|
|
302
|
+
if (pendingToolCalls.some(tc => !toolCallStatus.has(tc.id)) || pendingAbortedToolCalls.size > 0) {
|
|
285
303
|
continue;
|
|
286
304
|
}
|
|
287
305
|
// No pending tool-call window: safe to preserve the text payload so the
|
|
@@ -311,11 +329,12 @@ export function transformMessages<TApi extends Api>(
|
|
|
311
329
|
timestamp: messageTimestamp,
|
|
312
330
|
} as UserMessage);
|
|
313
331
|
}
|
|
314
|
-
continue;
|
|
315
332
|
}
|
|
316
333
|
|
|
317
|
-
|
|
318
|
-
result.
|
|
334
|
+
// The matching tool_use exists elsewhere, but this result is not in
|
|
335
|
+
// the currently open result window. Emitting it here would break the
|
|
336
|
+
// provider invariant; the first real result is pulled into the correct
|
|
337
|
+
// slot by the pending-call flush instead.
|
|
319
338
|
} else if (msg.role === "user" || msg.role === "developer") {
|
|
320
339
|
flushPendingToolCalls(messageTimestamp);
|
|
321
340
|
flushPendingAbortedToolCalls();
|
package/src/stream.ts
CHANGED
|
@@ -108,6 +108,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
|
|
|
108
108
|
kilo: "KILO_API_KEY",
|
|
109
109
|
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
|
|
110
110
|
zai: "ZAI_API_KEY",
|
|
111
|
+
"zhipu-coding-plan": "ZHIPU_API_KEY",
|
|
111
112
|
mistral: "MISTRAL_API_KEY",
|
|
112
113
|
minimax: "MINIMAX_API_KEY",
|
|
113
114
|
"minimax-code": "MINIMAX_CODE_API_KEY",
|
package/src/types.ts
CHANGED
package/src/utils/oauth/index.ts
CHANGED
|
@@ -150,6 +150,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
|
|
|
150
150
|
name: "Z.AI (GLM Coding Plan)",
|
|
151
151
|
available: true,
|
|
152
152
|
},
|
|
153
|
+
{
|
|
154
|
+
id: "zhipu-coding-plan",
|
|
155
|
+
name: "Zhipu Coding Plan (智谱)",
|
|
156
|
+
available: true,
|
|
157
|
+
},
|
|
153
158
|
{
|
|
154
159
|
id: "minimax-code",
|
|
155
160
|
name: "MiniMax Coding Plan (International)",
|
|
@@ -328,6 +333,7 @@ export async function refreshOAuthToken(
|
|
|
328
333
|
case "ollama-cloud":
|
|
329
334
|
case "xiaomi":
|
|
330
335
|
case "zai":
|
|
336
|
+
case "zhipu-coding-plan":
|
|
331
337
|
case "qianfan":
|
|
332
338
|
case "venice":
|
|
333
339
|
case "minimax-code":
|
package/src/utils/oauth/types.ts
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zhipu Coding Plan login flow.
|
|
3
|
+
*
|
|
4
|
+
* Zhipu BigModel (智谱) provides an OpenAI-compatible API.
|
|
5
|
+
* API docs: https://docs.bigmodel.cn/cn/guide/develop/openai/introduction
|
|
6
|
+
*
|
|
7
|
+
* Simple API key flow:
|
|
8
|
+
* 1. User gets their API key from https://open.bigmodel.cn
|
|
9
|
+
* 2. User pastes the API key into the CLI
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { validateOpenAICompatibleApiKey } from "./api-key-validation";
|
|
13
|
+
import type { OAuthController } from "./types";
|
|
14
|
+
|
|
15
|
+
const AUTH_URL = "https://open.bigmodel.cn/usercenter/apikeys";
|
|
16
|
+
const API_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
|
|
17
|
+
const VALIDATION_MODEL = "glm-5.1";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Login to Zhipu Coding Plan.
|
|
21
|
+
*
|
|
22
|
+
* Opens browser to API keys page, prompts user to paste their API key.
|
|
23
|
+
* Returns the API key directly (not OAuthCredentials - this isn't OAuth).
|
|
24
|
+
*/
|
|
25
|
+
export async function loginZhipuCodingPlan(options: OAuthController): Promise<string> {
|
|
26
|
+
if (!options.onPrompt) {
|
|
27
|
+
throw new Error("Zhipu Coding Plan login requires onPrompt callback");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Open browser to API keys page
|
|
31
|
+
options.onAuth?.({
|
|
32
|
+
url: AUTH_URL,
|
|
33
|
+
instructions: "Copy your API key from the dashboard",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Prompt user to paste their API key
|
|
37
|
+
const apiKey = await options.onPrompt({
|
|
38
|
+
message: "Paste your Zhipu API key",
|
|
39
|
+
placeholder: "sk-...",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (options.signal?.aborted) {
|
|
43
|
+
throw new Error("Login cancelled");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const trimmed = apiKey.trim();
|
|
47
|
+
if (!trimmed) {
|
|
48
|
+
throw new Error("API key is required");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
options.onProgress?.("Validating API key...");
|
|
52
|
+
await validateOpenAICompatibleApiKey({
|
|
53
|
+
provider: "Zhipu",
|
|
54
|
+
apiKey: trimmed,
|
|
55
|
+
baseUrl: API_BASE_URL,
|
|
56
|
+
model: VALIDATION_MODEL,
|
|
57
|
+
signal: options.signal,
|
|
58
|
+
});
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
@@ -6,6 +6,5 @@ export function isJsonObject(value: unknown): value is JsonObject {
|
|
|
6
6
|
|
|
7
7
|
/** True when `value` is a plain JSON object with no own enumerable keys. */
|
|
8
8
|
export function isJsonObjectEmpty(value: JsonObject): boolean {
|
|
9
|
-
|
|
10
|
-
return true;
|
|
9
|
+
return Object.keys(value).length === 0;
|
|
11
10
|
}
|
package/src/utils/schema/wire.ts
CHANGED
|
@@ -99,8 +99,7 @@ const SCHEMA_ARRAY_KEYS = ["anyOf", "oneOf", "allOf", "prefixItems"] as const;
|
|
|
99
99
|
/** True when `val` is a plain empty object `{}`. */
|
|
100
100
|
function isEmptyObject(val: unknown): val is Record<string, never> {
|
|
101
101
|
if (val === null || typeof val !== "object" || Array.isArray(val)) return false;
|
|
102
|
-
|
|
103
|
-
return true;
|
|
102
|
+
return Object.keys(val).length === 0;
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
function walk(node: unknown): void {
|
package/dist/types/cli.d.ts
DELETED
package/src/cli.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import * as readline from "node:readline";
|
|
3
|
-
import { AuthStorage, SqliteAuthCredentialStore } from "./auth-storage";
|
|
4
|
-
import { getOAuthProviders } from "./utils/oauth";
|
|
5
|
-
import type { OAuthProvider } from "./utils/oauth/types";
|
|
6
|
-
|
|
7
|
-
const PROVIDERS = getOAuthProviders();
|
|
8
|
-
|
|
9
|
-
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
|
10
|
-
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
11
|
-
const input = process.stdin as NodeJS.ReadStream;
|
|
12
|
-
const supportsRawMode = input.isTTY && typeof input.setRawMode === "function";
|
|
13
|
-
const wasRaw = supportsRawMode ? input.isRaw : false;
|
|
14
|
-
let settled = false;
|
|
15
|
-
|
|
16
|
-
const cleanup = () => {
|
|
17
|
-
rl.off("SIGINT", onSigint);
|
|
18
|
-
if (supportsRawMode) {
|
|
19
|
-
input.off("keypress", onKeypress);
|
|
20
|
-
input.setRawMode?.(wasRaw);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const finish = (result: () => void) => {
|
|
25
|
-
if (settled) return;
|
|
26
|
-
settled = true;
|
|
27
|
-
cleanup();
|
|
28
|
-
result();
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const cancel = () => {
|
|
32
|
-
finish(() => reject(new Error("Login cancelled")));
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const onSigint = () => {
|
|
36
|
-
cancel();
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const onKeypress = (_str: string, key: readline.Key) => {
|
|
40
|
-
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
41
|
-
cancel();
|
|
42
|
-
rl.close();
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
if (supportsRawMode) {
|
|
47
|
-
readline.emitKeypressEvents(input, rl);
|
|
48
|
-
input.setRawMode(true);
|
|
49
|
-
input.on("keypress", onKeypress);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
rl.once("SIGINT", onSigint);
|
|
53
|
-
rl.question(question, answer => {
|
|
54
|
-
finish(() => resolve(answer));
|
|
55
|
-
});
|
|
56
|
-
return promise;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function login(provider: OAuthProvider): Promise<void> {
|
|
60
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
61
|
-
const promptFn = (msg: string) => prompt(rl, `${msg} `);
|
|
62
|
-
const store = await SqliteAuthCredentialStore.open();
|
|
63
|
-
const storage = new AuthStorage(store);
|
|
64
|
-
await storage.reload();
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
await storage.login(provider, {
|
|
68
|
-
onAuth(info) {
|
|
69
|
-
const { url, instructions } = info;
|
|
70
|
-
console.log(`\nOpen this URL in your browser:\n${url}`);
|
|
71
|
-
if (instructions) console.log(instructions);
|
|
72
|
-
console.log();
|
|
73
|
-
},
|
|
74
|
-
onProgress(message) {
|
|
75
|
-
console.log(message);
|
|
76
|
-
},
|
|
77
|
-
onPrompt(p) {
|
|
78
|
-
return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
console.log(`\nCredentials saved to ~/.omp/agent/agent.db`);
|
|
82
|
-
} finally {
|
|
83
|
-
store.close();
|
|
84
|
-
rl.close();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function main(): Promise<void> {
|
|
89
|
-
const args = process.argv.slice(2);
|
|
90
|
-
const command = args[0];
|
|
91
|
-
|
|
92
|
-
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
93
|
-
console.log(`Usage: bunx @oh-my-pi/pi-ai <command> [provider]
|
|
94
|
-
|
|
95
|
-
Commands:
|
|
96
|
-
login [provider] Login to a provider
|
|
97
|
-
logout [provider] Logout from a provider
|
|
98
|
-
status Show logged-in providers
|
|
99
|
-
list List available providers
|
|
100
|
-
|
|
101
|
-
Providers:
|
|
102
|
-
anthropic Anthropic (Claude Pro/Max)
|
|
103
|
-
github-copilot GitHub Copilot
|
|
104
|
-
google-gemini-cli Google Gemini CLI
|
|
105
|
-
google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS)
|
|
106
|
-
openai-codex OpenAI Codex (ChatGPT Plus/Pro)
|
|
107
|
-
kimi-code Kimi Code
|
|
108
|
-
kilo Kilo Gateway
|
|
109
|
-
kagi Kagi
|
|
110
|
-
tavily Tavily
|
|
111
|
-
zai Z.AI (GLM Coding Plan)
|
|
112
|
-
deepseek DeepSeek
|
|
113
|
-
nanogpt NanoGPT
|
|
114
|
-
minimax-code MiniMax Coding Plan (International)
|
|
115
|
-
minimax-code-cn MiniMax Coding Plan (China)
|
|
116
|
-
cursor Cursor (Claude, GPT, etc.)
|
|
117
|
-
zenmux ZenMux
|
|
118
|
-
ollama-cloud Ollama Cloud
|
|
119
|
-
|
|
120
|
-
Examples:
|
|
121
|
-
bunx @oh-my-pi/pi-ai login # interactive provider selection
|
|
122
|
-
bunx @oh-my-pi/pi-ai login anthropic # login to specific provider
|
|
123
|
-
bunx @oh-my-pi/pi-ai logout anthropic # logout from specific provider
|
|
124
|
-
bunx @oh-my-pi/pi-ai status # show logged-in providers
|
|
125
|
-
bunx @oh-my-pi/pi-ai list # list providers
|
|
126
|
-
`);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (command === "status") {
|
|
131
|
-
const storage = await SqliteAuthCredentialStore.open();
|
|
132
|
-
try {
|
|
133
|
-
const providers = storage.listProviders();
|
|
134
|
-
if (providers.length === 0) {
|
|
135
|
-
console.log("No credentials stored.");
|
|
136
|
-
console.log(`Use 'bunx @oh-my-pi/pi-ai login' to authenticate.`);
|
|
137
|
-
} else {
|
|
138
|
-
console.log("Logged-in providers:\n");
|
|
139
|
-
for (const provider of providers) {
|
|
140
|
-
const oauth = storage.getOAuth(provider);
|
|
141
|
-
if (oauth) {
|
|
142
|
-
const expires = new Date(oauth.expires);
|
|
143
|
-
const expired = Date.now() >= oauth.expires;
|
|
144
|
-
const status = expired ? "(expired)" : `(expires ${expires.toLocaleString()})`;
|
|
145
|
-
console.log(` ${provider.padEnd(20)} ${status}`);
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const apiKey = storage.getApiKey(provider);
|
|
149
|
-
if (apiKey) {
|
|
150
|
-
console.log(` ${provider.padEnd(20)} (api key)`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} finally {
|
|
155
|
-
storage.close();
|
|
156
|
-
}
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (command === "list") {
|
|
161
|
-
console.log("Available providers:\n");
|
|
162
|
-
for (const p of PROVIDERS) {
|
|
163
|
-
console.log(` ${p.id.padEnd(20)} ${p.name}`);
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (command === "logout") {
|
|
169
|
-
let provider = args[1] as OAuthProvider | undefined;
|
|
170
|
-
const storage = await SqliteAuthCredentialStore.open();
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
if (!provider) {
|
|
174
|
-
const providers = storage.listProviders();
|
|
175
|
-
if (providers.length === 0) {
|
|
176
|
-
console.log("No credentials stored.");
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
181
|
-
console.log("Select a provider to logout:\n");
|
|
182
|
-
for (let i = 0; i < providers.length; i++) {
|
|
183
|
-
console.log(` ${i + 1}. ${providers[i]}`);
|
|
184
|
-
}
|
|
185
|
-
console.log();
|
|
186
|
-
|
|
187
|
-
const choice = await prompt(rl, `Enter number (1-${providers.length}): `);
|
|
188
|
-
rl.close();
|
|
189
|
-
|
|
190
|
-
const index = parseInt(choice, 10) - 1;
|
|
191
|
-
if (index < 0 || index >= providers.length) {
|
|
192
|
-
console.error("Invalid selection");
|
|
193
|
-
process.exit(1);
|
|
194
|
-
}
|
|
195
|
-
provider = providers[index] as OAuthProvider;
|
|
196
|
-
}
|
|
197
|
-
if (!provider) {
|
|
198
|
-
console.error("No provider selected");
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const oauth = storage.getOAuth(provider);
|
|
203
|
-
const apiKey = storage.getApiKey(provider);
|
|
204
|
-
if (!oauth && !apiKey) {
|
|
205
|
-
console.error(`Not logged in to ${provider}`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
storage.deleteProvider(provider);
|
|
210
|
-
console.log(`Logged out from ${provider}`);
|
|
211
|
-
} finally {
|
|
212
|
-
storage.close();
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (command === "login") {
|
|
218
|
-
let provider = args[1] as OAuthProvider | undefined;
|
|
219
|
-
|
|
220
|
-
if (!provider) {
|
|
221
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
222
|
-
console.log("Select a provider:\n");
|
|
223
|
-
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
224
|
-
console.log(` ${i + 1}. ${PROVIDERS[i].name}`);
|
|
225
|
-
}
|
|
226
|
-
console.log();
|
|
227
|
-
|
|
228
|
-
const choice = await prompt(rl, `Enter number (1-${PROVIDERS.length}): `);
|
|
229
|
-
rl.close();
|
|
230
|
-
|
|
231
|
-
const index = parseInt(choice, 10) - 1;
|
|
232
|
-
if (index < 0 || index >= PROVIDERS.length) {
|
|
233
|
-
console.error("Invalid selection");
|
|
234
|
-
process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
provider = PROVIDERS[index].id as OAuthProvider;
|
|
237
|
-
}
|
|
238
|
-
if (!provider) {
|
|
239
|
-
console.error("No provider selected");
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (!PROVIDERS.some(p => p.id === provider)) {
|
|
244
|
-
console.error(`Unknown provider: ${provider}`);
|
|
245
|
-
console.error(`Use 'bunx @oh-my-pi/pi-ai list' to see available providers`);
|
|
246
|
-
process.exit(1);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
console.log(`Logging in to ${provider}…`);
|
|
250
|
-
await login(provider);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.error(`Unknown command: ${command}`);
|
|
255
|
-
console.error(`Use 'bunx @oh-my-pi/pi-ai --help' for usage`);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
main().catch(err => {
|
|
260
|
-
console.error("Error:", err.message);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
});
|