@gajae-code/ai 0.6.4 → 0.7.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 +6 -0
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +3 -1
- package/dist/types/auth-broker/types.d.ts +6 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
- package/dist/types/auth-storage.d.ts +17 -0
- package/dist/types/provider-models/special.d.ts +3 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/oauth/glm-zcode.d.ts +80 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-broker/client.ts +15 -0
- package/src/auth-broker/remote-store.ts +20 -0
- package/src/auth-broker/server.ts +27 -0
- package/src/auth-broker/types.ts +12 -1
- package/src/auth-broker/wire-schemas.ts +17 -0
- package/src/auth-storage.ts +129 -11
- package/src/cli.ts +1 -0
- package/src/model-thinking.ts +11 -10
- package/src/models.json +65 -7
- package/src/models.ts +12 -5
- package/src/provider-models/descriptors.ts +7 -1
- package/src/provider-models/openai-compat.ts +6 -0
- package/src/provider-models/special.ts +12 -0
- package/src/providers/anthropic.ts +2 -2
- package/src/stream.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/oauth/glm-zcode.ts +321 -0
- package/src/utils/oauth/index.ts +10 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthCredential } from "../auth-storage";
|
|
2
|
-
import type { CredentialDisableResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types";
|
|
2
|
+
import type { CredentialDisableResponse, CredentialIfAbsentUploadResponse, CredentialRefreshResponse, CredentialUploadResponse, HealthzResponse, SnapshotResponse, SnapshotStreamEvent, UsageResponse } from "./types";
|
|
3
3
|
export interface AuthBrokerClientOptions {
|
|
4
4
|
/** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
|
|
5
5
|
url: string;
|
|
@@ -63,4 +63,5 @@ export declare class AuthBrokerClient {
|
|
|
63
63
|
refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse>;
|
|
64
64
|
disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
|
|
65
65
|
uploadCredential(provider: string, credential: AuthCredential, signal?: AbortSignal): Promise<CredentialUploadResponse>;
|
|
66
|
+
uploadCredentialIfAbsent(provider: string, credential: AuthCredential, signal?: AbortSignal): Promise<CredentialIfAbsentUploadResponse>;
|
|
66
67
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AuthCredential, type AuthCredentialStore, type OAuthCredential, type StoredAuthCredential } from "../auth-storage";
|
|
1
|
+
import { type AuthCredential, type AuthCredentialIfAbsentResult, type AuthCredentialStore, type OAuthCredential, type StoredAuthCredential } from "../auth-storage";
|
|
2
2
|
import type { Provider } from "../types";
|
|
3
3
|
import type { UsageReport } from "../usage";
|
|
4
4
|
import type { OAuthCredentials } from "../utils/oauth/types";
|
|
@@ -44,6 +44,7 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
44
44
|
}): Promise<void>;
|
|
45
45
|
replaceAuthCredentialsForProvider(_provider: string, _credentials: AuthCredential[]): StoredAuthCredential[];
|
|
46
46
|
upsertAuthCredentialForProvider(_provider: string, _credential: AuthCredential): StoredAuthCredential[];
|
|
47
|
+
upsertAuthCredentialForProviderIfAbsent(_provider: string, _credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
47
48
|
deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void;
|
|
48
49
|
/**
|
|
49
50
|
* Upsert a single credential through the broker. The broker server is the
|
|
@@ -53,6 +54,7 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
53
54
|
* any concurrent peer (refresh, generation bump) stays in sync.
|
|
54
55
|
*/
|
|
55
56
|
upsertAuthCredentialRemote(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
|
|
57
|
+
upsertAuthCredentialRemoteIfAbsent(provider: string, credential: AuthCredential): Promise<AuthCredentialIfAbsentResult>;
|
|
56
58
|
/**
|
|
57
59
|
* Replace-all semantics: disable every active credential for the provider,
|
|
58
60
|
* then upload each of the new credentials. Used by API-key login so a new
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* clients use `access` tokens directly and call back to the broker when a
|
|
6
6
|
* credential expires or a 401 surfaces on a supposedly-fresh credential.
|
|
7
7
|
*/
|
|
8
|
-
import type { AuthCredential, AuthCredentialSnapshot, AuthCredentialSnapshotEntry } from "../auth-storage";
|
|
8
|
+
import type { AuthCredential, AuthCredentialIfAbsentReason, AuthCredentialSnapshot, AuthCredentialSnapshotEntry } from "../auth-storage";
|
|
9
9
|
import type { UsageReport } from "../usage";
|
|
10
10
|
/** GET /v1/healthz response body. */
|
|
11
11
|
export interface HealthzResponse {
|
|
@@ -56,6 +56,11 @@ export interface CredentialUploadRequest {
|
|
|
56
56
|
export interface CredentialUploadResponse {
|
|
57
57
|
entries: AuthCredentialSnapshotEntry[];
|
|
58
58
|
}
|
|
59
|
+
export interface CredentialIfAbsentUploadResponse {
|
|
60
|
+
inserted: boolean;
|
|
61
|
+
reason: AuthCredentialIfAbsentReason;
|
|
62
|
+
entries: AuthCredentialSnapshotEntry[];
|
|
63
|
+
}
|
|
59
64
|
/**
|
|
60
65
|
* SSE event kinds emitted on `GET /v1/snapshot/stream`. The same value is set
|
|
61
66
|
* as the SSE `event:` name (load-bearing for clients) **and** embedded as a
|
|
@@ -410,3 +410,33 @@ export declare const credentialUploadResponseSchema: z.ZodObject<{
|
|
|
410
410
|
identityKey: z.ZodNullable<z.ZodString>;
|
|
411
411
|
}, z.core.$strict>>;
|
|
412
412
|
}, z.core.$strict>;
|
|
413
|
+
export declare const credentialIfAbsentUploadResponseSchema: z.ZodObject<{
|
|
414
|
+
inserted: z.ZodBoolean;
|
|
415
|
+
reason: z.ZodEnum<{
|
|
416
|
+
inserted: "inserted";
|
|
417
|
+
"skipped-existing": "skipped-existing";
|
|
418
|
+
"skipped-existing-config": "skipped-existing-config";
|
|
419
|
+
"skipped-existing-env": "skipped-existing-env";
|
|
420
|
+
"skipped-existing-fallback": "skipped-existing-fallback";
|
|
421
|
+
"skipped-existing-runtime": "skipped-existing-runtime";
|
|
422
|
+
"skipped-invalid": "skipped-invalid";
|
|
423
|
+
}>;
|
|
424
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
425
|
+
id: z.ZodNumber;
|
|
426
|
+
provider: z.ZodString;
|
|
427
|
+
credential: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
428
|
+
type: z.ZodLiteral<"oauth">;
|
|
429
|
+
access: z.ZodString;
|
|
430
|
+
expires: z.ZodNumber;
|
|
431
|
+
enterpriseUrl: z.ZodOptional<z.ZodString>;
|
|
432
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
433
|
+
email: z.ZodOptional<z.ZodString>;
|
|
434
|
+
accountId: z.ZodOptional<z.ZodString>;
|
|
435
|
+
refresh: z.ZodLiteral<"__remote__">;
|
|
436
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
437
|
+
type: z.ZodLiteral<"api_key">;
|
|
438
|
+
key: z.ZodString;
|
|
439
|
+
}, z.core.$strict>], "type">;
|
|
440
|
+
identityKey: z.ZodNullable<z.ZodString>;
|
|
441
|
+
}, z.core.$strict>>;
|
|
442
|
+
}, z.core.$strict>;
|
|
@@ -102,6 +102,19 @@ export interface AuthCredentialSnapshotEntry {
|
|
|
102
102
|
credential: SnapshotCredential;
|
|
103
103
|
identityKey: string | null;
|
|
104
104
|
}
|
|
105
|
+
export type AuthCredentialIfAbsentReason = "inserted" | "skipped-existing" | "skipped-existing-runtime" | "skipped-existing-config" | "skipped-existing-env" | "skipped-existing-fallback" | "skipped-invalid";
|
|
106
|
+
export interface AuthCredentialIfAbsentResult {
|
|
107
|
+
inserted: boolean;
|
|
108
|
+
reason: AuthCredentialIfAbsentReason;
|
|
109
|
+
provider: string;
|
|
110
|
+
entries: StoredAuthCredential[];
|
|
111
|
+
}
|
|
112
|
+
export interface AuthCredentialIfAbsentSnapshotResult {
|
|
113
|
+
inserted: boolean;
|
|
114
|
+
reason: AuthCredentialIfAbsentReason;
|
|
115
|
+
provider: string;
|
|
116
|
+
entries: AuthCredentialSnapshotEntry[];
|
|
117
|
+
}
|
|
105
118
|
/**
|
|
106
119
|
* Wire-shaped snapshot exported by {@link AuthStorage.exportSnapshot} and
|
|
107
120
|
* served by the auth-broker server on `GET /v1/snapshot`.
|
|
@@ -128,6 +141,7 @@ export interface AuthCredentialStore {
|
|
|
128
141
|
tryDisableAuthCredentialIfMatches(id: number, expectedData: string, disabledCause: string): boolean;
|
|
129
142
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
|
|
130
143
|
upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
|
|
144
|
+
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
131
145
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
|
|
132
146
|
getCache(key: string, options?: {
|
|
133
147
|
includeExpired?: boolean;
|
|
@@ -201,6 +215,7 @@ export interface AuthCredentialStore {
|
|
|
201
215
|
* post-write read path is consistent.
|
|
202
216
|
*/
|
|
203
217
|
upsertAuthCredentialRemote?(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
|
|
218
|
+
upsertAuthCredentialRemoteIfAbsent?(provider: string, credential: AuthCredential): Promise<AuthCredentialIfAbsentResult>;
|
|
204
219
|
/**
|
|
205
220
|
* Optional async write hook for replace-all semantics (e.g. API-key login
|
|
206
221
|
* overwriting any previous keys for the same provider). When present,
|
|
@@ -417,6 +432,7 @@ export declare class AuthStorage {
|
|
|
417
432
|
* Set credential for a provider.
|
|
418
433
|
*/
|
|
419
434
|
set(provider: string, credential: AuthCredentialEntry): Promise<void>;
|
|
435
|
+
importCredentialIfAbsent(provider: string, credential: AuthCredential): Promise<AuthCredentialIfAbsentSnapshotResult>;
|
|
420
436
|
/**
|
|
421
437
|
* Remove credential for a provider.
|
|
422
438
|
*/
|
|
@@ -615,6 +631,7 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
615
631
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
616
632
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
|
|
617
633
|
upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
|
|
634
|
+
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
618
635
|
updateAuthCredential(id: number, credential: AuthCredential): void;
|
|
619
636
|
deleteAuthCredential(id: number, disabledCause: string): void;
|
|
620
637
|
/**
|
|
@@ -14,3 +14,6 @@ export declare function cursorModelManagerOptions(config?: CursorModelManagerCon
|
|
|
14
14
|
export interface ZaiModelManagerConfig {
|
|
15
15
|
}
|
|
16
16
|
export declare function zaiModelManagerOptions(_config?: ZaiModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
|
|
17
|
+
export interface GlmZcodeModelManagerConfig {
|
|
18
|
+
}
|
|
19
|
+
export declare function glmZcodeModelManagerOptions(_config?: GlmZcodeModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
|
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" | "azure-openai" | "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" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "lm-studio";
|
|
51
|
+
export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "azure-openai" | "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" | "glm-zcode" | "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" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "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) */
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM ZCode OAuth flow (UNOFFICIAL, opt-in).
|
|
3
|
+
*
|
|
4
|
+
* Replicates the reverse-engineered ZCode desktop-app login to Z.AI/GLM. This
|
|
5
|
+
* is NOT an official Z.AI OAuth client: it reuses ZCode's authorize page,
|
|
6
|
+
* broker, and a custom-protocol redirect. It may break at any time and may
|
|
7
|
+
* violate ZCode/Z.AI Terms of Service. Endpoints and the client id are
|
|
8
|
+
* overridable via `ZCODE_OAUTH_*` environment variables.
|
|
9
|
+
*
|
|
10
|
+
* Login flow:
|
|
11
|
+
* 1. Authorize: GET {authorize}?redirect_uri=zcode://oauth/callback&response_type=code&client_id=...&state=...
|
|
12
|
+
* (custom-protocol redirect → a CLI cannot catch it, so the user pastes the code/redirect URL)
|
|
13
|
+
* 2. Broker: POST {broker} { provider: "zai", code, redirect_uri, state }
|
|
14
|
+
* → { code: 0, data: { token: <ZCode JWT>, zai: { access_token: <upstream Z.AI token> }, expires_in } }
|
|
15
|
+
*
|
|
16
|
+
* Credential mapping (verified against the ZCode host bundle):
|
|
17
|
+
* - `access` = the **ZCode JWT** (`data.token`). This is the GLM coding-plan
|
|
18
|
+
* model credential: ZCode stores it under the `zcodejwttoken`
|
|
19
|
+
* key and sends it as `Authorization: Bearer` to the coding-plan
|
|
20
|
+
* gateway `${ZCODE_PLAN_ANTHROPIC_BASE_URL}` (default
|
|
21
|
+
* https://zcode.z.ai/api/v1/zcode-plan/anthropic), which
|
|
22
|
+
* validates the ZCode session and injects the upstream GLM key
|
|
23
|
+
* server-side. Model traffic does NOT go to api.z.ai directly.
|
|
24
|
+
* - `refresh` = the upstream Z.AI OAuth access token (`data.zai.access_token`),
|
|
25
|
+
* kept for identity/userinfo only. ZCode's separate z/login
|
|
26
|
+
* "business token" is used by ZCode for billing/userinfo, NOT
|
|
27
|
+
* model calls, so it is intentionally never minted here.
|
|
28
|
+
*
|
|
29
|
+
* The ZCode JWT reaches the Anthropic-messages request as a plain bearer
|
|
30
|
+
* automatically (the gateway base is not api.anthropic.com). This provider must
|
|
31
|
+
* NEVER force `isOAuth=true`, which would route GLM into the Claude-Code OAuth
|
|
32
|
+
* header branch (claude-cli UA, `claude_` tool prefixes, Claude system prompt).
|
|
33
|
+
*/
|
|
34
|
+
import { OAuthCallbackFlow } from "./callback-server";
|
|
35
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
36
|
+
export declare const GLM_ZCODE_REFRESH_SKEW_MS: number;
|
|
37
|
+
/** Default endpoints / client id. Override via the matching `ZCODE_OAUTH_*` env vars. */
|
|
38
|
+
export declare const GLM_ZCODE_OAUTH_AUTHORIZE_URL = "https://chat.z.ai/api/oauth/authorize";
|
|
39
|
+
export declare const GLM_ZCODE_OAUTH_CLIENT_ID = "client_P8X5CMWmlaRO9gyO-KSqtg";
|
|
40
|
+
export declare const GLM_ZCODE_OAUTH_REDIRECT_URI = "zcode://oauth/callback";
|
|
41
|
+
export declare const GLM_ZCODE_OAUTH_BROKER_TOKEN_URL = "https://zcode.z.ai/api/v1/oauth/token";
|
|
42
|
+
export declare const GLM_ZCODE_USERINFO_URL = "https://chat.z.ai/api/oauth/userinfo";
|
|
43
|
+
/**
|
|
44
|
+
* Default coding-plan ("start plan") Anthropic gateway base. ZCode derives this
|
|
45
|
+
* as `${zcodeBackend}/api/v1/zcode-plan/anthropic`. Model requests go here
|
|
46
|
+
* (NOT api.z.ai), authenticated with the ZCode JWT. Override via
|
|
47
|
+
* `ZCODE_PLAN_ANTHROPIC_BASE_URL`. Exported for the model descriptor / catalog.
|
|
48
|
+
*/
|
|
49
|
+
export declare const GLM_ZCODE_PLAN_ANTHROPIC_BASE_URL = "https://zcode.z.ai/api/v1/zcode-plan/anthropic";
|
|
50
|
+
type FetchImpl = typeof globalThis.fetch;
|
|
51
|
+
/**
|
|
52
|
+
* The provider is configured whenever a client id is available. The real ZCode
|
|
53
|
+
* client id ships as the default, so this is true unless explicitly cleared.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isGlmZcodeOAuthConfigured(): boolean;
|
|
56
|
+
export interface GlmZcodeOAuthFlowOptions {
|
|
57
|
+
fetch?: FetchImpl;
|
|
58
|
+
}
|
|
59
|
+
export declare class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
|
|
60
|
+
#private;
|
|
61
|
+
constructor(ctrl: OAuthController, options?: GlmZcodeOAuthFlowOptions);
|
|
62
|
+
generateAuthUrl(state: string, redirectUri: string): Promise<{
|
|
63
|
+
url: string;
|
|
64
|
+
instructions?: string;
|
|
65
|
+
}>;
|
|
66
|
+
exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
67
|
+
}
|
|
68
|
+
export declare function loginGlmZcode(ctrl: OAuthController, options?: GlmZcodeOAuthFlowOptions): Promise<OAuthCredentials>;
|
|
69
|
+
export interface GlmZcodeRefreshOptions {
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
fetch?: FetchImpl;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The ZCode session JWT is the model credential. ZCode mints it from a one-time
|
|
75
|
+
* authorization code via the broker and exposes no documented refresh grant, so
|
|
76
|
+
* there is no autonomous refresh: an expired credential requires re-login
|
|
77
|
+
* (`/login glm-zcode`). Never return an expired credential as valid.
|
|
78
|
+
*/
|
|
79
|
+
export declare function refreshGlmZcodeToken(_credentials: OAuthCredentials, _options?: AbortSignal | GlmZcodeRefreshOptions): Promise<OAuthCredentials>;
|
|
80
|
+
export {};
|
|
@@ -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" | "xai" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "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" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "zai";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.
|
|
46
|
+
"@gajae-code/utils": "0.7.0",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
|
@@ -11,6 +11,7 @@ import type { AuthCredential } from "../auth-storage";
|
|
|
11
11
|
import type {
|
|
12
12
|
CredentialDisableRequest,
|
|
13
13
|
CredentialDisableResponse,
|
|
14
|
+
CredentialIfAbsentUploadResponse,
|
|
14
15
|
CredentialRefreshResponse,
|
|
15
16
|
CredentialUploadRequest,
|
|
16
17
|
CredentialUploadResponse,
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
} from "./types";
|
|
22
23
|
import {
|
|
23
24
|
credentialDisableResponseSchema,
|
|
25
|
+
credentialIfAbsentUploadResponseSchema,
|
|
24
26
|
credentialRefreshResponseSchema,
|
|
25
27
|
credentialUploadResponseSchema,
|
|
26
28
|
healthzResponseSchema,
|
|
@@ -262,6 +264,19 @@ export class AuthBrokerClient {
|
|
|
262
264
|
}) as Promise<CredentialUploadResponse>;
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
async uploadCredentialIfAbsent(
|
|
268
|
+
provider: string,
|
|
269
|
+
credential: AuthCredential,
|
|
270
|
+
signal?: AbortSignal,
|
|
271
|
+
): Promise<CredentialIfAbsentUploadResponse> {
|
|
272
|
+
const body: CredentialUploadRequest = { provider, credential };
|
|
273
|
+
return this.#request("POST", "/v1/credential/if-absent", {
|
|
274
|
+
body,
|
|
275
|
+
schema: credentialIfAbsentUploadResponseSchema,
|
|
276
|
+
signal,
|
|
277
|
+
}) as Promise<CredentialIfAbsentUploadResponse>;
|
|
278
|
+
}
|
|
279
|
+
|
|
265
280
|
async #request<TSchema extends ZodType>(
|
|
266
281
|
method: "GET" | "POST",
|
|
267
282
|
path: string,
|
|
@@ -11,6 +11,7 @@ import { scheduler } from "node:timers/promises";
|
|
|
11
11
|
import { logger } from "@gajae-code/utils";
|
|
12
12
|
import {
|
|
13
13
|
type AuthCredential,
|
|
14
|
+
type AuthCredentialIfAbsentResult,
|
|
14
15
|
type AuthCredentialSnapshotEntry,
|
|
15
16
|
type AuthCredentialStore,
|
|
16
17
|
type OAuthCredential,
|
|
@@ -308,6 +309,15 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
308
309
|
);
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
upsertAuthCredentialForProviderIfAbsent(
|
|
313
|
+
_provider: string,
|
|
314
|
+
_credential: AuthCredential,
|
|
315
|
+
): AuthCredentialIfAbsentResult {
|
|
316
|
+
throw new Error(
|
|
317
|
+
"RemoteAuthCredentialStore is read-only on the client. Use `gjc auth-broker login <provider>` to mutate credentials.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
311
321
|
deleteAuthCredentialsForProvider(_provider: string, _disabledCause: string): void {
|
|
312
322
|
throw new Error(
|
|
313
323
|
"RemoteAuthCredentialStore is read-only on the client. Use `gjc auth-broker logout <provider>` to mutate credentials.",
|
|
@@ -328,6 +338,16 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
328
338
|
return this.listAuthCredentials(provider);
|
|
329
339
|
}
|
|
330
340
|
|
|
341
|
+
async upsertAuthCredentialRemoteIfAbsent(
|
|
342
|
+
provider: string,
|
|
343
|
+
credential: AuthCredential,
|
|
344
|
+
): Promise<AuthCredentialIfAbsentResult> {
|
|
345
|
+
const { inserted, reason, entries } = await this.#client.uploadCredentialIfAbsent(provider, credential);
|
|
346
|
+
this.#applyProviderEntries(provider, entries);
|
|
347
|
+
this.#maybeRefreshSnapshot("upload-if-absent");
|
|
348
|
+
return { inserted, reason, provider, entries: this.listAuthCredentials(provider) };
|
|
349
|
+
}
|
|
350
|
+
|
|
331
351
|
/**
|
|
332
352
|
* Replace-all semantics: disable every active credential for the provider,
|
|
333
353
|
* then upload each of the new credentials. Used by API-key login so a new
|
|
@@ -15,6 +15,7 @@ import { parseBind } from "../utils/parse-bind";
|
|
|
15
15
|
import { AuthBrokerRefresher, type AuthBrokerRefresherSchedule } from "./refresher";
|
|
16
16
|
import type {
|
|
17
17
|
CredentialDisableResponse,
|
|
18
|
+
CredentialIfAbsentUploadResponse,
|
|
18
19
|
CredentialRefreshResponse,
|
|
19
20
|
CredentialUploadResponse,
|
|
20
21
|
HealthzResponse,
|
|
@@ -592,6 +593,32 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
592
593
|
const response: CredentialDisableResponse = { ok: true };
|
|
593
594
|
return json(200, response);
|
|
594
595
|
}
|
|
596
|
+
if (req.method === "POST" && pathname === "/v1/credential/if-absent") {
|
|
597
|
+
const parsed = await parseBody(req, credentialUploadRequestSchema);
|
|
598
|
+
if (!parsed.ok) return parsed.response;
|
|
599
|
+
const { provider, credential } = parsed.data;
|
|
600
|
+
try {
|
|
601
|
+
const result = await opts.storage.importCredentialIfAbsent(provider, credential);
|
|
602
|
+
logger.info("auth-broker credential import-if-absent", {
|
|
603
|
+
provider,
|
|
604
|
+
type: credential.type,
|
|
605
|
+
inserted: result.inserted,
|
|
606
|
+
reason: result.reason,
|
|
607
|
+
providerTotal: result.entries.length,
|
|
608
|
+
peer,
|
|
609
|
+
});
|
|
610
|
+
const response: CredentialIfAbsentUploadResponse = {
|
|
611
|
+
inserted: result.inserted,
|
|
612
|
+
reason: result.reason,
|
|
613
|
+
entries: result.entries,
|
|
614
|
+
};
|
|
615
|
+
return json(200, response);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
618
|
+
logger.warn("auth-broker upload-if-absent failed", { provider, type: credential.type, peer });
|
|
619
|
+
return json(500, { error: message });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
595
622
|
if (req.method === "POST" && pathname === "/v1/credential") {
|
|
596
623
|
const parsed = await parseBody(req, credentialUploadRequestSchema);
|
|
597
624
|
if (!parsed.ok) return parsed.response;
|
package/src/auth-broker/types.ts
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* credential expires or a 401 surfaces on a supposedly-fresh credential.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AuthCredential,
|
|
11
|
+
AuthCredentialIfAbsentReason,
|
|
12
|
+
AuthCredentialSnapshot,
|
|
13
|
+
AuthCredentialSnapshotEntry,
|
|
14
|
+
} from "../auth-storage";
|
|
10
15
|
import type { UsageReport } from "../usage";
|
|
11
16
|
|
|
12
17
|
/** GET /v1/healthz response body. */
|
|
@@ -68,6 +73,12 @@ export interface CredentialUploadResponse {
|
|
|
68
73
|
entries: AuthCredentialSnapshotEntry[];
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
export interface CredentialIfAbsentUploadResponse {
|
|
77
|
+
inserted: boolean;
|
|
78
|
+
reason: AuthCredentialIfAbsentReason;
|
|
79
|
+
entries: AuthCredentialSnapshotEntry[];
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
/**
|
|
72
83
|
* SSE event kinds emitted on `GET /v1/snapshot/stream`. The same value is set
|
|
73
84
|
* as the SSE `event:` name (load-bearing for clients) **and** embedded as a
|
|
@@ -185,6 +185,15 @@ export const credentialDisableResponseSchema = z
|
|
|
185
185
|
.strict();
|
|
186
186
|
|
|
187
187
|
// ─── Upload ────────────────────────────────────────────────────────────────
|
|
188
|
+
const credentialIfAbsentReasonSchema = z.enum([
|
|
189
|
+
"inserted",
|
|
190
|
+
"skipped-existing",
|
|
191
|
+
"skipped-existing-runtime",
|
|
192
|
+
"skipped-existing-config",
|
|
193
|
+
"skipped-existing-env",
|
|
194
|
+
"skipped-existing-fallback",
|
|
195
|
+
"skipped-invalid",
|
|
196
|
+
]);
|
|
188
197
|
|
|
189
198
|
export const credentialUploadRequestSchema = z
|
|
190
199
|
.object({
|
|
@@ -198,3 +207,11 @@ export const credentialUploadResponseSchema = z
|
|
|
198
207
|
entries: z.array(credentialSnapshotEntrySchema),
|
|
199
208
|
})
|
|
200
209
|
.strict();
|
|
210
|
+
|
|
211
|
+
export const credentialIfAbsentUploadResponseSchema = z
|
|
212
|
+
.object({
|
|
213
|
+
inserted: z.boolean(),
|
|
214
|
+
reason: credentialIfAbsentReasonSchema,
|
|
215
|
+
entries: z.array(credentialSnapshotEntrySchema),
|
|
216
|
+
})
|
|
217
|
+
.strict();
|
package/src/auth-storage.ts
CHANGED
|
@@ -151,6 +151,29 @@ export interface AuthCredentialSnapshotEntry {
|
|
|
151
151
|
identityKey: string | null;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
export type AuthCredentialIfAbsentReason =
|
|
155
|
+
| "inserted"
|
|
156
|
+
| "skipped-existing"
|
|
157
|
+
| "skipped-existing-runtime"
|
|
158
|
+
| "skipped-existing-config"
|
|
159
|
+
| "skipped-existing-env"
|
|
160
|
+
| "skipped-existing-fallback"
|
|
161
|
+
| "skipped-invalid";
|
|
162
|
+
|
|
163
|
+
export interface AuthCredentialIfAbsentResult {
|
|
164
|
+
inserted: boolean;
|
|
165
|
+
reason: AuthCredentialIfAbsentReason;
|
|
166
|
+
provider: string;
|
|
167
|
+
entries: StoredAuthCredential[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface AuthCredentialIfAbsentSnapshotResult {
|
|
171
|
+
inserted: boolean;
|
|
172
|
+
reason: AuthCredentialIfAbsentReason;
|
|
173
|
+
provider: string;
|
|
174
|
+
entries: AuthCredentialSnapshotEntry[];
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
/**
|
|
155
178
|
* Wire-shaped snapshot exported by {@link AuthStorage.exportSnapshot} and
|
|
156
179
|
* served by the auth-broker server on `GET /v1/snapshot`.
|
|
@@ -182,6 +205,7 @@ export interface AuthCredentialStore {
|
|
|
182
205
|
tryDisableAuthCredentialIfMatches(id: number, expectedData: string, disabledCause: string): boolean;
|
|
183
206
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
|
|
184
207
|
upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
|
|
208
|
+
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
185
209
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
|
|
186
210
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
187
211
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
@@ -254,6 +278,10 @@ export interface AuthCredentialStore {
|
|
|
254
278
|
* post-write read path is consistent.
|
|
255
279
|
*/
|
|
256
280
|
upsertAuthCredentialRemote?(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
|
|
281
|
+
upsertAuthCredentialRemoteIfAbsent?(
|
|
282
|
+
provider: string,
|
|
283
|
+
credential: AuthCredential,
|
|
284
|
+
): Promise<AuthCredentialIfAbsentResult>;
|
|
257
285
|
/**
|
|
258
286
|
* Optional async write hook for replace-all semantics (e.g. API-key login
|
|
259
287
|
* overwriting any previous keys for the same provider). When present,
|
|
@@ -1234,6 +1262,56 @@ export class AuthStorage {
|
|
|
1234
1262
|
this.#resetProviderAssignments(provider);
|
|
1235
1263
|
}
|
|
1236
1264
|
|
|
1265
|
+
#toSnapshotEntries(provider: string, stored: StoredAuthCredential[]): AuthCredentialSnapshotEntry[] {
|
|
1266
|
+
return stored.map(entry => {
|
|
1267
|
+
const persisted = entry.credential;
|
|
1268
|
+
const redacted: SnapshotCredential =
|
|
1269
|
+
persisted.type === "api_key" ? persisted : { ...persisted, refresh: REMOTE_REFRESH_SENTINEL };
|
|
1270
|
+
return {
|
|
1271
|
+
id: entry.id,
|
|
1272
|
+
provider: entry.provider,
|
|
1273
|
+
credential: redacted,
|
|
1274
|
+
identityKey: resolveCredentialIdentityKey(provider, persisted),
|
|
1275
|
+
};
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
#snapshotSkipResult(provider: string, reason: AuthCredentialIfAbsentReason): AuthCredentialIfAbsentSnapshotResult {
|
|
1280
|
+
return {
|
|
1281
|
+
inserted: false,
|
|
1282
|
+
reason,
|
|
1283
|
+
provider,
|
|
1284
|
+
entries: this.exportSnapshot().credentials.filter(entry => entry.provider === provider),
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async importCredentialIfAbsent(
|
|
1289
|
+
provider: string,
|
|
1290
|
+
credential: AuthCredential,
|
|
1291
|
+
): Promise<AuthCredentialIfAbsentSnapshotResult> {
|
|
1292
|
+
if (this.#runtimeOverrides.has(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-runtime");
|
|
1293
|
+
if (this.#configOverrides.has(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-config");
|
|
1294
|
+
if (this.#getCredentialsForProvider(provider).length > 0)
|
|
1295
|
+
return this.#snapshotSkipResult(provider, "skipped-existing");
|
|
1296
|
+
if (getEnvApiKey(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-env");
|
|
1297
|
+
if (this.#fallbackResolver?.(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-fallback");
|
|
1298
|
+
|
|
1299
|
+
const result = this.#store.upsertAuthCredentialRemoteIfAbsent
|
|
1300
|
+
? await this.#store.upsertAuthCredentialRemoteIfAbsent(provider, credential)
|
|
1301
|
+
: this.#store.upsertAuthCredentialForProviderIfAbsent(provider, credential);
|
|
1302
|
+
this.#setStoredCredentials(
|
|
1303
|
+
provider,
|
|
1304
|
+
result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
1305
|
+
);
|
|
1306
|
+
this.#resetProviderAssignments(provider);
|
|
1307
|
+
return {
|
|
1308
|
+
inserted: result.inserted,
|
|
1309
|
+
reason: result.reason,
|
|
1310
|
+
provider: result.provider,
|
|
1311
|
+
entries: this.#toSnapshotEntries(provider, result.entries),
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1237
1315
|
async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
|
|
1238
1316
|
const stored = this.#store.upsertAuthCredentialRemote
|
|
1239
1317
|
? await this.#store.upsertAuthCredentialRemote(provider, credential)
|
|
@@ -1515,6 +1593,14 @@ export class AuthStorage {
|
|
|
1515
1593
|
});
|
|
1516
1594
|
break;
|
|
1517
1595
|
}
|
|
1596
|
+
case "glm-zcode": {
|
|
1597
|
+
const { loginGlmZcode } = await import("./utils/oauth/glm-zcode");
|
|
1598
|
+
credentials = await loginGlmZcode({
|
|
1599
|
+
...ctrl,
|
|
1600
|
+
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1601
|
+
});
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1518
1604
|
case "fireworks": {
|
|
1519
1605
|
const { loginFireworks } = await import("./utils/oauth/fireworks");
|
|
1520
1606
|
const apiKey = await loginFireworks(ctrl);
|
|
@@ -3343,17 +3429,7 @@ export class AuthStorage {
|
|
|
3343
3429
|
stored.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
3344
3430
|
);
|
|
3345
3431
|
this.#resetProviderAssignments(provider);
|
|
3346
|
-
return
|
|
3347
|
-
const persisted = entry.credential;
|
|
3348
|
-
const redacted: SnapshotCredential =
|
|
3349
|
-
persisted.type === "api_key" ? persisted : { ...persisted, refresh: REMOTE_REFRESH_SENTINEL };
|
|
3350
|
-
return {
|
|
3351
|
-
id: entry.id,
|
|
3352
|
-
provider: entry.provider,
|
|
3353
|
-
credential: redacted,
|
|
3354
|
-
identityKey: resolveCredentialIdentityKey(provider, persisted),
|
|
3355
|
-
};
|
|
3356
|
-
});
|
|
3432
|
+
return this.#toSnapshotEntries(provider, stored);
|
|
3357
3433
|
}
|
|
3358
3434
|
|
|
3359
3435
|
/**
|
|
@@ -3989,6 +4065,48 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
3989
4065
|
return result;
|
|
3990
4066
|
}
|
|
3991
4067
|
|
|
4068
|
+
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult {
|
|
4069
|
+
let serialized: SerializedCredentialRecord | null;
|
|
4070
|
+
try {
|
|
4071
|
+
serialized = serializeCredential(provider, credential);
|
|
4072
|
+
} catch {
|
|
4073
|
+
serialized = null;
|
|
4074
|
+
}
|
|
4075
|
+
if (!serialized) {
|
|
4076
|
+
return {
|
|
4077
|
+
inserted: false,
|
|
4078
|
+
reason: "skipped-invalid",
|
|
4079
|
+
provider,
|
|
4080
|
+
entries: this.listAuthCredentials(provider),
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
const writeIfAbsent = this.#db.transaction(
|
|
4085
|
+
(providerName: string, record: SerializedCredentialRecord): AuthCredentialIfAbsentResult => {
|
|
4086
|
+
const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
|
|
4087
|
+
const existing: StoredAuthCredential[] = [];
|
|
4088
|
+
for (const row of existingRows) {
|
|
4089
|
+
const activeCredential = deserializeCredential(row);
|
|
4090
|
+
if (!activeCredential) continue;
|
|
4091
|
+
existing.push(toStoredAuthCredential(row, activeCredential));
|
|
4092
|
+
}
|
|
4093
|
+
if (existing.length > 0) {
|
|
4094
|
+
return { inserted: false, reason: "skipped-existing", provider: providerName, entries: existing };
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
this.#insertStmt.get(providerName, record.credentialType, record.data, record.identityKey);
|
|
4098
|
+
return {
|
|
4099
|
+
inserted: true,
|
|
4100
|
+
reason: "inserted",
|
|
4101
|
+
provider: providerName,
|
|
4102
|
+
entries: this.listAuthCredentials(providerName),
|
|
4103
|
+
};
|
|
4104
|
+
},
|
|
4105
|
+
);
|
|
4106
|
+
|
|
4107
|
+
return writeIfAbsent.immediate(provider, serialized);
|
|
4108
|
+
}
|
|
4109
|
+
|
|
3992
4110
|
/**
|
|
3993
4111
|
* Hard-deletes disabled rows for a provider when an active row with the same identity exists.
|
|
3994
4112
|
* This prevents unbounded accumulation of soft-deleted credentials while preserving
|