@gajae-code/ai 0.15.4 → 0.15.6

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/dist/types/auth-broker/client.d.ts +6 -2
  3. package/dist/types/auth-broker/remote-store.d.ts +14 -2
  4. package/dist/types/auth-broker/types.d.ts +6 -0
  5. package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
  6. package/dist/types/auth-gateway/server.d.ts +39 -5
  7. package/dist/types/auth-gateway/types.d.ts +16 -2
  8. package/dist/types/auth-storage.d.ts +116 -34
  9. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  10. package/dist/types/provider-models/special.d.ts +2 -1
  11. package/dist/types/providers/kiro-api-key.d.ts +50 -0
  12. package/dist/types/providers/kiro-codewhisperer.d.ts +3 -0
  13. package/dist/types/providers/register-builtins.d.ts +12 -12
  14. package/dist/types/stream.d.ts +2 -1
  15. package/dist/types/types.d.ts +35 -24
  16. package/dist/types/utils/fallback-transport.d.ts +7 -0
  17. package/dist/types/utils/json-parse.d.ts +5 -3
  18. package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
  19. package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
  20. package/dist/types/utils/oauth/commandcode.d.ts +1 -0
  21. package/dist/types/utils/oauth/types.d.ts +1 -1
  22. package/dist/types/utils/retry.d.ts +2 -0
  23. package/dist/types/utils/tool-call-healing.d.ts +4 -4
  24. package/package.json +3 -3
  25. package/src/auth-broker/client.ts +41 -13
  26. package/src/auth-broker/redact.ts +25 -1
  27. package/src/auth-broker/remote-store.ts +374 -115
  28. package/src/auth-broker/server.ts +131 -91
  29. package/src/auth-broker/types.ts +6 -0
  30. package/src/auth-broker/wire-schemas.ts +6 -0
  31. package/src/auth-gateway/server.ts +447 -79
  32. package/src/auth-gateway/types.ts +28 -2
  33. package/src/auth-storage.ts +742 -157
  34. package/src/cli.ts +1 -0
  35. package/src/model-thinking.ts +16 -0
  36. package/src/models.json +1054 -0
  37. package/src/models.ts +9 -1
  38. package/src/provider-models/descriptors.ts +3 -1
  39. package/src/provider-models/openai-compat.ts +41 -1
  40. package/src/provider-models/special.ts +15 -3
  41. package/src/providers/anthropic.ts +7 -1
  42. package/src/providers/azure-openai-responses.ts +4 -1
  43. package/src/providers/cursor.ts +256 -101
  44. package/src/providers/gitlab-duo.ts +18 -1
  45. package/src/providers/google-gemini-cli.ts +3 -0
  46. package/src/providers/google-shared.ts +3 -0
  47. package/src/providers/kiro-api-key.d.ts +50 -0
  48. package/src/providers/kiro-api-key.ts +786 -0
  49. package/src/providers/kiro-codewhisperer.d.ts +3 -0
  50. package/src/providers/kiro-codewhisperer.ts +34 -9
  51. package/src/providers/ollama.ts +3 -0
  52. package/src/providers/openai-codex-responses.ts +24 -6
  53. package/src/providers/openai-completions.ts +11 -1
  54. package/src/providers/openai-responses-shared.ts +23 -2
  55. package/src/providers/openai-responses.ts +10 -1
  56. package/src/providers/pi-native-client.ts +1 -0
  57. package/src/providers/pi-native-server.ts +24 -0
  58. package/src/providers/register-builtins.d.ts +12 -12
  59. package/src/providers/register-builtins.ts +16 -3
  60. package/src/stream.d.ts +2 -1
  61. package/src/stream.ts +180 -70
  62. package/src/types.d.ts +35 -24
  63. package/src/types.ts +40 -23
  64. package/src/utils/fallback-transport.d.ts +7 -0
  65. package/src/utils/fallback-transport.ts +21 -4
  66. package/src/utils/json-parse.d.ts +5 -3
  67. package/src/utils/json-parse.ts +6 -6
  68. package/src/utils/oauth/api-key-login.ts +13 -2
  69. package/src/utils/oauth/api-key-validation.ts +242 -41
  70. package/src/utils/oauth/commandcode.ts +17 -0
  71. package/src/utils/oauth/index.ts +20 -5
  72. package/src/utils/oauth/types.d.ts +1 -1
  73. package/src/utils/oauth/types.ts +1 -0
  74. package/src/utils/retry.d.ts +2 -0
  75. package/src/utils/retry.ts +15 -2
  76. package/src/utils/tool-call-healing.d.ts +4 -4
  77. package/src/utils/tool-call-healing.ts +4 -4
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Shared factory for API-key-paste "login" flows.
3
3
  *
4
- * Several providers (Cerebras, Synthetic, Moonshot, Together, NanoGPT, ZenMux)
4
+ * Several providers (Cerebras, Synthetic, Moonshot, Together, NanoGPT, ZenMux,
5
+ * Command Code GOAT)
5
6
  * don't actually implement OAuth — they just ask the user to paste an API key,
6
7
  * optionally validate it, and return the trimmed key.
7
8
  */
@@ -11,6 +12,7 @@ type ChatCompletionsValidation = {
11
12
  provider: string;
12
13
  baseUrl: string;
13
14
  model: string;
15
+ requireInferenceResponse?: boolean;
14
16
  };
15
17
  type ModelsEndpointValidation = {
16
18
  kind: "models-endpoint";
@@ -30,6 +32,7 @@ export type ApiKeyLoginConfig = {
30
32
  placeholder: string;
31
33
  /** Validation strategy, or `null` to skip validation. */
32
34
  validation: ChatCompletionsValidation | ModelsEndpointValidation | null;
35
+ validationProgressMessage?: string;
33
36
  };
34
37
  export declare function createApiKeyLogin(config: ApiKeyLoginConfig): (options: OAuthController) => Promise<string>;
35
38
  export {};
@@ -4,12 +4,17 @@ type OpenAICompatibleValidationOptions = {
4
4
  baseUrl: string;
5
5
  model: string;
6
6
  signal?: AbortSignal;
7
+ fetch?: typeof globalThis.fetch;
8
+ requireInferenceResponse?: boolean;
9
+ timeoutMs?: number;
7
10
  };
8
11
  type ModelListValidationOptions = {
9
12
  provider: string;
10
13
  apiKey: string;
11
14
  modelsUrl: string;
12
15
  signal?: AbortSignal;
16
+ fetch?: typeof globalThis.fetch;
17
+ timeoutMs?: number;
13
18
  };
14
19
  /**
15
20
  * Validate an API key against an OpenAI-compatible chat completions endpoint.
@@ -18,16 +23,17 @@ type ModelListValidationOptions = {
18
23
  */
19
24
  export declare function validateOpenAICompatibleApiKey(options: OpenAICompatibleValidationOptions): Promise<void>;
20
25
  /**
21
- * Validate an API key against a provider models endpoint.
26
+ * Validate a provider models endpoint's reachability and response shape.
22
27
  *
23
28
  * Useful for providers where access to specific models may vary by plan and
24
- * should not block key validation.
29
+ * should not block login; an available model list is not proof that an
30
+ * authenticated inference request will succeed for the supplied key.
25
31
  *
26
32
  * A 200 status alone is NOT accepted: a captive portal, misrouting proxy, or
27
- * broken gateway can answer 200 with an HTML page or an empty JSON object, and
28
- * accepting the key on status alone would store a credential that was never
29
- * actually checked. The body must parse as JSON and carry a recognizable model
30
- * list before the key is considered validated.
33
+ * broken gateway can answer 200 with an HTML page or an empty JSON object.
34
+ * The body must parse as JSON and carry a recognizable model list before the
35
+ * endpoint is considered reachable. This catalog check is not proof that
36
+ * authenticated inference is entitled to use the supplied key.
31
37
  */
32
38
  export declare function validateApiKeyAgainstModelsEndpoint(options: ModelListValidationOptions): Promise<void>;
33
39
  export {};
@@ -0,0 +1 @@
1
+ export declare const loginCommandCode: (options: import("./types").OAuthController) => Promise<string>;
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  };
10
- export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "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" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sglang" | "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" | "opencodex" | "zai";
10
+ export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "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" | "commandcode-goat" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sglang" | "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" | "opencodex" | "zai";
11
11
  export type OAuthProviderId = OAuthProvider | (string & {});
12
12
  export type OAuthPrompt = {
13
13
  message: string;
@@ -24,4 +24,6 @@ export declare function callWithCopilotModelRetry<T>(fn: () => Promise<T>, optio
24
24
  signal?: AbortSignal;
25
25
  retryBaseDelayMs?: number;
26
26
  fallbackManaged?: boolean;
27
+ requestMaxRetries?: number;
28
+ disableProviderRetries?: boolean;
27
29
  }): Promise<T>;
@@ -22,10 +22,10 @@ export interface HealedToolCall {
22
22
  readonly name: string;
23
23
  readonly arguments: string;
24
24
  /**
25
- * Whether the raw leaked payload spelled a printable non-ASCII character as a
26
- * `\uXXXX` escape. Captured BEFORE the normalizing round-trip below, which
27
- * decodes escapes into literal characters and would otherwise erase the only
28
- * evidence that the text is unverifiable.
25
+ * Whether the raw leaked payload carried unsafe Unicode argument data.
26
+ * Captured before the normalizing round-trip below, which would otherwise
27
+ * erase malformed escape evidence. Valid `\uXXXX` spellings decode
28
+ * canonically and leave this false.
29
29
  */
30
30
  readonly escapedNonAsciiArguments: boolean;
31
31
  readonly escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.15.4",
4
+ "version": "0.15.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -40,8 +40,8 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/natives": "0.15.4",
44
- "@gajae-code/utils": "0.15.4",
43
+ "@gajae-code/natives": "0.15.6",
44
+ "@gajae-code/utils": "0.15.6",
45
45
  "openai": "^6.36.0",
46
46
  "partial-json": "^0.1.7",
47
47
  "zod": "4.4.3"
@@ -8,6 +8,7 @@
8
8
  import { readSseEvents } from "@gajae-code/utils";
9
9
  import type { ZodType, infer as zInfer } from "zod/v4";
10
10
  import type { AuthCredential } from "../auth-storage";
11
+ import type { Provider } from "../types";
11
12
  import type {
12
13
  CredentialDisableRequest,
13
14
  CredentialDisableResponse,
@@ -34,6 +35,8 @@ import {
34
35
  usageResponseSchema,
35
36
  } from "./wire-schemas";
36
37
 
38
+ export const AUTH_BROKER_EPOCH_HEADER = "X-GJC-Auth-Broker-Epoch";
39
+
37
40
  export interface AuthBrokerClientOptions {
38
41
  /** Base URL (e.g. `https://broker.tailnet:8765`). Trailing slashes are trimmed. */
39
42
  url: string;
@@ -80,24 +83,27 @@ export class AuthBrokerCredentialMetadataUnsupportedError extends AuthBrokerErro
80
83
 
81
84
  export interface FetchSnapshotOptions {
82
85
  ifGenerationGt?: number;
86
+ ifEpoch?: string;
83
87
  waitMs?: number;
84
88
  signal?: AbortSignal;
85
89
  }
86
90
 
87
91
  export type FetchSnapshotResult =
88
92
  | { status: 200; snapshot: SnapshotResponse; generation: number }
89
- | { status: 304; generation: number };
93
+ | { status: 304; generation: number; epoch?: string };
90
94
 
91
- function parseGenerationTag(header: string | null): number | undefined {
95
+ function parseSnapshotTag(header: string | null): { epoch?: string; generation: number } | undefined {
92
96
  if (!header) return undefined;
93
97
  let value = header.trim();
94
98
  if (value.startsWith("W/")) value = value.slice(2).trim();
95
99
  if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
96
100
  value = value.slice(1, -1);
97
101
  }
98
- const generation = Number(value);
102
+ const separator = value.lastIndexOf(":");
103
+ const epoch = separator > 0 ? value.slice(0, separator) : undefined;
104
+ const generation = Number(separator > 0 ? value.slice(separator + 1) : value);
99
105
  if (!Number.isInteger(generation) || generation < 0) return undefined;
100
- return generation;
106
+ return { epoch, generation };
101
107
  }
102
108
 
103
109
  const DEFAULT_TIMEOUT_MS = 10_000;
@@ -136,6 +142,7 @@ export class AuthBrokerClient {
136
142
  try {
137
143
  return (await this.#request("GET", "/v1/credentials/metadata", {
138
144
  schema: credentialMetadataResponseSchema,
145
+ headers: { [AUTH_BROKER_EPOCH_HEADER]: "1" },
139
146
  signal,
140
147
  })) as CredentialMetadataResponse;
141
148
  } catch (error) {
@@ -150,7 +157,11 @@ export class AuthBrokerClient {
150
157
  if (opts.waitMs !== undefined) query.set("wait", String(opts.waitMs));
151
158
  const path = `/v1/snapshot${query.size > 0 ? `?${query.toString()}` : ""}`;
152
159
  const headers: Record<string, string> = {};
153
- if (opts.ifGenerationGt !== undefined) headers["If-None-Match"] = `"${opts.ifGenerationGt}"`;
160
+ headers[AUTH_BROKER_EPOCH_HEADER] = "1";
161
+ if (opts.ifGenerationGt !== undefined) {
162
+ const validator = opts.ifEpoch ? `${opts.ifEpoch}:${opts.ifGenerationGt}` : String(opts.ifGenerationGt);
163
+ headers["If-None-Match"] = `"${validator}"`;
164
+ }
154
165
  const timeoutMs =
155
166
  opts.waitMs !== undefined && opts.waitMs > 0 ? Math.max(this.#timeoutMs, opts.waitMs + 1000) : undefined;
156
167
  const response = await this.#fetchRaw("GET", path, {
@@ -159,9 +170,13 @@ export class AuthBrokerClient {
159
170
  signal: opts.signal,
160
171
  timeoutMs,
161
172
  });
162
- const etagGeneration = parseGenerationTag(response.headers.get("etag"));
173
+ const etag = parseSnapshotTag(response.headers.get("etag"));
163
174
  if (response.status === 304) {
164
- return { status: 304, generation: etagGeneration ?? opts.ifGenerationGt ?? 0 };
175
+ return {
176
+ status: 304,
177
+ generation: etag?.generation ?? opts.ifGenerationGt ?? 0,
178
+ ...(etag?.epoch ? { epoch: etag.epoch } : {}),
179
+ };
165
180
  }
166
181
  const text = await response.text();
167
182
  const raw = this.#parseJson(text, response.status);
@@ -173,7 +188,7 @@ export class AuthBrokerClient {
173
188
  });
174
189
  }
175
190
  const snapshot = validated.data as SnapshotResponse;
176
- return { status: 200, snapshot, generation: etagGeneration ?? snapshot.generation };
191
+ return { status: 200, snapshot, generation: etag?.generation ?? snapshot.generation };
177
192
  }
178
193
 
179
194
  /**
@@ -190,6 +205,7 @@ export class AuthBrokerClient {
190
205
  const headers: Record<string, string> = {
191
206
  Accept: "text/event-stream",
192
207
  Authorization: `Bearer ${this.#token}`,
208
+ [AUTH_BROKER_EPOCH_HEADER]: "1",
193
209
  };
194
210
  if (opts.signal?.aborted) {
195
211
  throw new AuthBrokerError("Auth broker request aborted", { cause: opts.signal.reason });
@@ -258,12 +274,13 @@ export class AuthBrokerClient {
258
274
  }
259
275
  }
260
276
 
261
- fetchUsage(signal?: AbortSignal): Promise<UsageResponse> {
277
+ fetchUsage(signal?: AbortSignal, provider?: Provider): Promise<UsageResponse> {
262
278
  // Validates the envelope (`generatedAt`, `reports[].provider`, `limits`,
263
279
  // `metadata`) but leaves provider-specific extension fields permissive so
264
280
  // the broker can ship new shapes ahead of the client. `raw` is accepted
265
281
  // but normally stripped by the broker before send.
266
- return this.#request("GET", "/v1/usage", { schema: usageResponseSchema, signal }) as Promise<UsageResponse>;
282
+ const path = provider ? `/v1/usage/scoped?provider=${encodeURIComponent(provider)}` : "/v1/usage";
283
+ return this.#request("GET", path, { schema: usageResponseSchema, signal }) as Promise<UsageResponse>;
267
284
  }
268
285
 
269
286
  async refreshCredential(id: number, signal?: AbortSignal): Promise<CredentialRefreshResponse> {
@@ -285,8 +302,13 @@ export class AuthBrokerClient {
285
302
  }) as Promise<CredentialRefreshResponse>;
286
303
  }
287
304
 
288
- async disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse> {
289
- const body: CredentialDisableRequest = { cause };
305
+ async disableCredential(
306
+ id: number,
307
+ cause: string,
308
+ signal?: AbortSignal,
309
+ expectedRevision?: number,
310
+ ): Promise<CredentialDisableResponse> {
311
+ const body: CredentialDisableRequest = { cause, ...(expectedRevision === undefined ? {} : { expectedRevision }) };
290
312
  return this.#request("POST", `/v1/credential/${id}/disable`, {
291
313
  body,
292
314
  schema: credentialDisableResponseSchema,
@@ -323,7 +345,13 @@ export class AuthBrokerClient {
323
345
  async #request<TSchema extends ZodType>(
324
346
  method: "GET" | "POST",
325
347
  path: string,
326
- opts: { schema: TSchema; auth?: boolean; body?: unknown; signal?: AbortSignal },
348
+ opts: {
349
+ schema: TSchema;
350
+ auth?: boolean;
351
+ body?: unknown;
352
+ signal?: AbortSignal;
353
+ headers?: Record<string, string>;
354
+ },
327
355
  ): Promise<zInfer<TSchema>> {
328
356
  const response = await this.#fetchRaw(method, path, opts);
329
357
  const text = await response.text();
@@ -7,8 +7,32 @@
7
7
  export function cleanReason(value: unknown): string | undefined {
8
8
  if (value === undefined || value === null) return undefined;
9
9
  let reason = value instanceof Error ? value.message : String(value);
10
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(reason)) return "Credential diagnostic unavailable.";
11
+ if (reason.includes("\\")) return "Credential diagnostic unavailable.";
12
+ if (/["']?authorization(?:[_-]header)?["']?\s*[:=]/i.test(reason)) return "Credential diagnostic unavailable.";
13
+ if (
14
+ /\b["']?(?:key|api[_-]?key|client[_-]?secret|clientSecret|token|secret|password|access|refresh|cookie|credential)(?:[_-](?:token|key|secret|header|headers))?["']?\s*[:=]/i.test(
15
+ reason,
16
+ )
17
+ ) {
18
+ return "Credential diagnostic unavailable.";
19
+ }
20
+ if (/\b(?:bearer|basic)\s+["']/i.test(reason)) return "Credential diagnostic unavailable.";
21
+ if (/^No credential with id=\d+$/i.test(reason)) return reason;
22
+ if (/\b(?:api\s*[-_]?key|access\s*[-_]?token|refresh\s*[-_]?token|credential)\b/i.test(reason))
23
+ return "Credential diagnostic unavailable.";
10
24
  reason = reason.replace(/bearer\s+[^\s,;]+/gi, "Bearer [redacted]");
11
- reason = reason.replace(/(api[_-]?key|token|secret|authorization)[=:]\s*[^\s,;]+/gi, "$1=[redacted]");
25
+ reason = reason.replace(/basic\s+[^\s,;]+/gi, "Basic [redacted]");
26
+ reason = reason.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, "$1[redacted]@");
27
+ reason = reason.replace(/\b([a-z][a-z0-9+.-]*:\/\/[^\s<>"']*?)(?:[?#][^\s<>"']*)/gi, "$1");
28
+ reason = reason.replace(
29
+ /((?:\\?["']?(?:key|api[_-]?key|client[_-]?secret|clientSecret|token|secret|authorization|password|access|refresh|cookie|credential)(?:[_-](?:token|key|secret|header|headers))?\\?["']?)\s*:\s*)\\?(["'])(?:\\.|(?!\2)[^\\])*\2/gi,
30
+ "$1$2[redacted]$2",
31
+ );
32
+ reason = reason.replace(
33
+ /((?:key|api[_-]?key|client[_-]?secret|clientSecret|token|secret|authorization|password|access|refresh|cookie|credential)(?:[_-](?:token|key|secret|header|headers))?)\s*[:=]\s*[^\s,;]+/gi,
34
+ "$1=[redacted]",
35
+ );
12
36
  reason = reason.replace(/[\r\n\t ]+/g, " ").trim();
13
37
  if (reason.length > 256) reason = `${reason.slice(0, 253)}...`;
14
38
  return reason || undefined;