@gajae-code/ai 0.18.11 → 0.19.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1 -1
  3. package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
  4. package/dist/types/auth-storage.d.ts +98 -13
  5. package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
  6. package/dist/types/model-cache.d.ts +6 -4
  7. package/dist/types/model-manager.d.ts +2 -0
  8. package/dist/types/model-pricing.d.ts +8 -0
  9. package/dist/types/model-thinking.d.ts +11 -0
  10. package/dist/types/openai-completions-compat.d.ts +2 -1
  11. package/dist/types/provider-models/google.d.ts +2 -0
  12. package/dist/types/providers/amazon-bedrock.d.ts +17 -2
  13. package/dist/types/providers/anthropic.d.ts +25 -0
  14. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  15. package/dist/types/providers/google-auth.d.ts +7 -0
  16. package/dist/types/providers/google-gemini-headers.d.ts +11 -6
  17. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  18. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  19. package/dist/types/providers/openai-completions.d.ts +1 -1
  20. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  21. package/dist/types/stream.d.ts +33 -1
  22. package/dist/types/types.d.ts +26 -3
  23. package/dist/types/usage.d.ts +3 -0
  24. package/dist/types/utils/discovery/antigravity.d.ts +16 -1
  25. package/dist/types/utils/idle-iterator.d.ts +3 -2
  26. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -30
  27. package/dist/types/utils/oauth/types.d.ts +9 -0
  28. package/package.json +4 -4
  29. package/src/auth-broker/remote-store.ts +62 -11
  30. package/src/auth-broker/wire-schemas.ts +2 -0
  31. package/src/auth-gateway/server.ts +3 -2
  32. package/src/auth-storage.ts +908 -246
  33. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  34. package/src/bedrock-claude-cache-policy.ts +2 -2
  35. package/src/model-cache.ts +45 -10
  36. package/src/model-manager.ts +18 -6
  37. package/src/model-pricing.d.ts +8 -0
  38. package/src/model-pricing.ts +87 -0
  39. package/src/model-thinking.d.ts +11 -0
  40. package/src/model-thinking.ts +97 -12
  41. package/src/models.json +3176 -1670
  42. package/src/models.ts +10 -3
  43. package/src/openai-completions-compat.d.ts +2 -1
  44. package/src/openai-completions-compat.ts +5 -0
  45. package/src/provider-models/google.ts +11 -1
  46. package/src/provider-models/openai-compat.ts +26 -0
  47. package/src/providers/amazon-bedrock.d.ts +17 -2
  48. package/src/providers/amazon-bedrock.ts +76 -16
  49. package/src/providers/anthropic.d.ts +25 -0
  50. package/src/providers/anthropic.ts +230 -18
  51. package/src/providers/aws-credential-config.d.ts +5 -0
  52. package/src/providers/aws-credential-config.ts +29 -1
  53. package/src/providers/google-auth.d.ts +7 -0
  54. package/src/providers/google-auth.ts +165 -4
  55. package/src/providers/google-gemini-headers.d.ts +11 -6
  56. package/src/providers/google-gemini-headers.ts +16 -11
  57. package/src/providers/google-vertex.ts +2 -4
  58. package/src/providers/kiro-api-key.d.ts +5 -0
  59. package/src/providers/kiro-api-key.ts +379 -68
  60. package/src/providers/ollama.ts +19 -9
  61. package/src/providers/openai-anthropic-shim.ts +1 -0
  62. package/src/providers/openai-chat-server-schema.ts +1 -1
  63. package/src/providers/openai-chat-server.ts +14 -2
  64. package/src/providers/openai-codex-responses.ts +21 -14
  65. package/src/providers/openai-completions.d.ts +1 -1
  66. package/src/providers/openai-completions.ts +65 -4
  67. package/src/providers/openai-responses-server.ts +10 -1
  68. package/src/providers/openai-responses-shared.d.ts +2 -1
  69. package/src/providers/openai-responses-shared.ts +42 -1
  70. package/src/providers/openai-responses.ts +51 -10
  71. package/src/providers/pi-native-server.ts +1 -1
  72. package/src/providers/transform-messages.ts +32 -1
  73. package/src/stream.d.ts +33 -1
  74. package/src/stream.ts +111 -17
  75. package/src/types.d.ts +26 -3
  76. package/src/types.ts +55 -2
  77. package/src/usage/claude.ts +4 -2
  78. package/src/usage.ts +3 -0
  79. package/src/utils/discovery/antigravity.ts +192 -7
  80. package/src/utils/idle-iterator.d.ts +3 -2
  81. package/src/utils/idle-iterator.ts +30 -10
  82. package/src/utils/oauth/anthropic.ts +23 -22
  83. package/src/utils/oauth/api-key-validation.ts +34 -11
  84. package/src/utils/oauth/glm-zcode.d.ts +0 -30
  85. package/src/utils/oauth/glm-zcode.ts +15 -4
  86. package/src/utils/oauth/openai-codex.ts +41 -6
  87. package/src/utils/oauth/types.d.ts +9 -0
  88. package/src/utils/oauth/types.ts +9 -0
  89. package/src/utils/parse-bind.d.ts +35 -0
  90. package/src/utils/proxy.ts +35 -6
@@ -1,8 +1,17 @@
1
+ import { logger } from "@gajae-code/utils";
1
2
  import * as z from "zod/v4";
2
3
  import { isRetiredModelKey } from "../../model-retirements";
3
- import { getAntigravityUserAgent } from "../../providers/google-gemini-headers";
4
+ import { getAntigravityUserAgent, getGeminiCliUserAgent } from "../../providers/google-gemini-headers";
4
5
  import type { Model } from "../../types";
5
6
 
7
+ /**
8
+ * Returns the default Antigravity discovery User-Agent.
9
+ * Can be overridden via PI_AI_ANTIGRAVITY_USER_AGENT environment variable.
10
+ */
11
+ export function getDefaultAntigravityUserAgent(): string {
12
+ return getAntigravityUserAgent();
13
+ }
14
+
6
15
  const DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS = [
7
16
  "https://daily-cloudcode-pa.googleapis.com",
8
17
  "https://daily-cloudcode-pa.sandbox.googleapis.com",
@@ -35,6 +44,12 @@ export interface AntigravityDiscoveryApiModel {
35
44
  modelProvider?: string;
36
45
  isInternal?: boolean;
37
46
  supportsVideo?: boolean;
47
+ /** Account-specific entitlement status */
48
+ entitlement?: string;
49
+ /** Whether the model is disabled for this account */
50
+ disabled?: boolean;
51
+ /** Per-account quota/tier information */
52
+ quota?: string;
38
53
  }
39
54
 
40
55
  /**
@@ -81,6 +96,9 @@ const AntigravityDiscoveryApiModelSchema: z.ZodType<AntigravityDiscoveryApiModel
81
96
  modelProvider: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
82
97
  isInternal: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
83
98
  supportsVideo: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
99
+ entitlement: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
100
+ disabled: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
101
+ quota: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
84
102
  })
85
103
  .loose();
86
104
  const AntigravityDiscoveryAgentModelGroupSchema: z.ZodType<AntigravityDiscoveryAgentModelGroup> = z
@@ -154,7 +172,7 @@ export interface FetchAntigravityDiscoveryModelsOptions {
154
172
  token: string;
155
173
  /** Optional endpoint override. Defaults to Antigravity fallback endpoints. */
156
174
  endpoint?: string;
157
- /** Deprecated and ignored for antigravity discovery parity. */
175
+ /** Optional Google Cloud project ID for tier-gated model discovery. */
158
176
  project?: string;
159
177
  /** Optional user agent override. */
160
178
  userAgent?: string;
@@ -168,6 +186,8 @@ export interface FetchAntigravityDiscoveryModelsOptions {
168
186
  * Default: `google-antigravity`.
169
187
  */
170
188
  targetProvider?: "google-antigravity" | "google-gemini-cli";
189
+ /** Optional logger for debug diagnostics. */
190
+ logger?: Pick<typeof logger, "debug">;
171
191
  }
172
192
 
173
193
  /**
@@ -182,27 +202,40 @@ export async function fetchAntigravityDiscoveryModels(
182
202
  const fetcher = options.fetcher ?? fetch;
183
203
  const signal = options.signal ?? AbortSignal.timeout(5_000);
184
204
  const targetProvider = options.targetProvider ?? "google-antigravity";
205
+ const userAgent = options.userAgent ?? getAntigravityDiscoveryUserAgent();
185
206
  const endpoints = options.endpoint
186
207
  ? [trimTrailingSlashes(options.endpoint)]
187
208
  : DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS.map(trimTrailingSlashes);
209
+ const debugLogger = options.logger ?? logger;
188
210
 
189
211
  for (const endpoint of endpoints) {
212
+ const redactedEndpoint = redactAntigravityEndpoint(endpoint);
190
213
  let response: Response;
191
214
  try {
215
+ const requestBody: Record<string, unknown> = {};
216
+ if (options.project) {
217
+ requestBody.project = options.project;
218
+ }
192
219
  response = await fetcher(`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`, {
193
220
  method: "POST",
194
221
  headers: {
195
222
  Authorization: `Bearer ${options.token}`,
196
223
  "Content-Type": "application/json",
197
- "User-Agent": options.userAgent ?? getAntigravityUserAgent(),
224
+ "User-Agent": userAgent,
198
225
  },
199
- body: JSON.stringify({}),
226
+ body: JSON.stringify(requestBody),
200
227
  signal,
201
228
  });
202
229
  } catch {
203
230
  continue;
204
231
  }
205
232
 
233
+ debugLogger.debug("Antigravity discovery endpoint response", {
234
+ endpoint: redactedEndpoint,
235
+ status: response.status,
236
+ statusText: response.statusText,
237
+ });
238
+
206
239
  if (!response.ok) {
207
240
  continue;
208
241
  }
@@ -228,15 +261,60 @@ export async function fetchAntigravityDiscoveryModels(
228
261
  }
229
262
  }
230
263
 
264
+ const debugEnabled = process.env.GJC_DEBUG_ANTIGRAVITY_DISCOVERY === "1";
265
+ const keptModelIds: string[] = [];
266
+ const filteredByDenylist: string[] = [];
267
+ const filteredByRetirement: string[] = [];
268
+ const filteredByInternal: string[] = [];
269
+ const filteredByDisabled: string[] = [];
270
+ const keptModelDetails: Record<string, Partial<AntigravityDiscoveryApiModel>> = {};
271
+ const filteredModelDetails: Record<string, Partial<AntigravityDiscoveryApiModel> & { reason: string }> = {};
272
+
231
273
  const models: Model<"google-gemini-cli">[] = [];
274
+ const modelDropReasons: Record<string, string> = {};
232
275
 
233
276
  for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
234
- if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId) || isRetiredModelKey(targetProvider, modelId)) {
235
- continue;
277
+ let dropReason: string | undefined;
278
+
279
+ if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) {
280
+ dropReason = "denylist";
281
+ } else if (isRetiredModelKey(targetProvider, modelId)) {
282
+ dropReason = "retired";
283
+ } else if (model.disabled === true) {
284
+ dropReason = "disabled";
285
+ } else if (model.isInternal === true && !surfacedModelIds.has(modelId)) {
286
+ dropReason = "isInternal-not-surfaced";
236
287
  }
237
- if (model.isInternal === true && !surfacedModelIds.has(modelId)) {
288
+
289
+ if (dropReason) {
290
+ modelDropReasons[modelId] = dropReason;
291
+ if (debugEnabled) {
292
+ if (dropReason === "denylist") filteredByDenylist.push(modelId);
293
+ else if (dropReason === "retired") filteredByRetirement.push(modelId);
294
+ else if (dropReason === "disabled") filteredByDisabled.push(modelId);
295
+ else filteredByInternal.push(modelId);
296
+ filteredModelDetails[modelId] = {
297
+ ...model,
298
+ reason:
299
+ dropReason === "retired"
300
+ ? `retired-${targetProvider}`
301
+ : dropReason === "isInternal-not-surfaced"
302
+ ? "internal-not-surfaced"
303
+ : dropReason,
304
+ };
305
+ }
238
306
  continue;
239
307
  }
308
+ if (debugEnabled) {
309
+ keptModelIds.push(modelId);
310
+ keptModelDetails[modelId] = {
311
+ displayName: model.displayName,
312
+ isInternal: model.isInternal,
313
+ entitlement: model.entitlement,
314
+ disabled: model.disabled,
315
+ quota: model.quota,
316
+ };
317
+ }
240
318
 
241
319
  const supportsImages = model.supportsImages === true;
242
320
  models.push({
@@ -258,7 +336,102 @@ export async function fetchAntigravityDiscoveryModels(
258
336
  });
259
337
  }
260
338
 
339
+ if (Object.keys(modelDropReasons).length > 0) {
340
+ debugLogger.debug("Antigravity discovery dropped models", {
341
+ endpoint: redactedEndpoint,
342
+ dropReasons: modelDropReasons,
343
+ });
344
+ }
345
+
346
+ debugLogger.debug("Antigravity discovery kept models", {
347
+ endpoint: redactedEndpoint,
348
+ keptModelIds: models.map(m => m.id),
349
+ });
350
+
261
351
  models.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
352
+
353
+ if (debugEnabled) {
354
+ const allRawModelIds = Object.keys(parsed.models ?? {}).sort();
355
+ console.error("[GJC_DEBUG_ANTIGRAVITY_DISCOVERY]");
356
+ console.error(` HTTP Status: ${response.status}`);
357
+ console.error(` User-Agent: ${userAgent}`);
358
+ console.error(` Endpoint: ${endpoint}`);
359
+ console.error(` Total raw models from response: ${allRawModelIds.length}`);
360
+ console.error("\n === KEPT MODELS ===");
361
+ console.error(` Count: ${keptModelIds.length}`);
362
+ for (const modelId of keptModelIds) {
363
+ const details = keptModelDetails[modelId];
364
+ const attrs = [];
365
+ if (details?.entitlement) attrs.push(`entitlement=${details.entitlement}`);
366
+ if (details?.disabled === true) attrs.push("disabled=true");
367
+ if (details?.quota) attrs.push(`quota=${details.quota}`);
368
+ if (details?.isInternal === true) attrs.push("isInternal=true");
369
+ const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
370
+ console.error(` ${modelId}${attrStr}`);
371
+ }
372
+ if (filteredByDenylist.length > 0) {
373
+ console.error(`\n === FILTERED BY DENYLIST ===`);
374
+ console.error(` Count: ${filteredByDenylist.length}`);
375
+ for (const modelId of filteredByDenylist) {
376
+ const details = filteredModelDetails[modelId];
377
+ const attrs = [];
378
+ if (details?.entitlement) attrs.push(`entitlement=${details.entitlement}`);
379
+ if (details?.disabled === true) attrs.push("disabled=true");
380
+ if (details?.quota) attrs.push(`quota=${details.quota}`);
381
+ if (details?.isInternal === true) attrs.push("isInternal=true");
382
+ const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
383
+ console.error(` ${modelId}${attrStr}`);
384
+ }
385
+ }
386
+ if (filteredByRetirement.length > 0) {
387
+ console.error(`\n === FILTERED BY RETIREMENT (${targetProvider}) ===`);
388
+ console.error(` Count: ${filteredByRetirement.length}`);
389
+ for (const modelId of filteredByRetirement) {
390
+ const details = filteredModelDetails[modelId];
391
+ const attrs = [];
392
+ if (details?.entitlement) attrs.push(`entitlement=${details.entitlement}`);
393
+ if (details?.disabled === true) attrs.push("disabled=true");
394
+ if (details?.quota) attrs.push(`quota=${details.quota}`);
395
+ if (details?.isInternal === true) attrs.push("isInternal=true");
396
+ const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
397
+ console.error(` ${modelId}${attrStr}`);
398
+ }
399
+ }
400
+ if (filteredByDisabled.length > 0) {
401
+ console.error(`\n === FILTERED (disabled) ===`);
402
+ console.error(` Count: ${filteredByDisabled.length}`);
403
+ for (const modelId of filteredByDisabled) {
404
+ const details = filteredModelDetails[modelId];
405
+ const attrs = [];
406
+ if (details?.entitlement) attrs.push(`entitlement=${details.entitlement}`);
407
+ if (details?.disabled === true) attrs.push("disabled=true");
408
+ if (details?.quota) attrs.push(`quota=${details.quota}`);
409
+ if (details?.isInternal === true) attrs.push("isInternal=true");
410
+ const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
411
+ console.error(` ${modelId}${attrStr}`);
412
+ }
413
+ }
414
+ if (filteredByInternal.length > 0) {
415
+ console.error(`\n === FILTERED (internal, not surfaced) ===`);
416
+ console.error(` Count: ${filteredByInternal.length}`);
417
+ for (const modelId of filteredByInternal) {
418
+ const details = filteredModelDetails[modelId];
419
+ const attrs = [];
420
+ if (details?.entitlement) attrs.push(`entitlement=${details.entitlement}`);
421
+ if (details?.disabled === true) attrs.push("disabled=true");
422
+ if (details?.quota) attrs.push(`quota=${details.quota}`);
423
+ if (details?.isInternal === true) attrs.push("isInternal=true");
424
+ const attrStr = attrs.length > 0 ? ` [${attrs.join(", ")}]` : "";
425
+ console.error(` ${modelId}${attrStr}`);
426
+ }
427
+ }
428
+ console.error(`\n === SURFACE INFO ===`);
429
+ console.error(` agentModelSorts surfaced: ${surfacedModelIds.size}`);
430
+ if (surfacedModelIds.size > 0) {
431
+ console.error(` ${Array.from(surfacedModelIds).sort().join(", ")}`);
432
+ }
433
+ }
434
+
262
435
  return models;
263
436
  }
264
437
 
@@ -283,3 +456,15 @@ function toPositiveNumber(value: unknown, fallback: number): number {
283
456
  function trimTrailingSlashes(value: string): string {
284
457
  return value.replace(/\/+$/, "");
285
458
  }
459
+
460
+ export function getAntigravityDiscoveryUserAgent(): string {
461
+ return process.env.PI_AI_ANTIGRAVITY_USER_AGENT ? getAntigravityUserAgent() : getGeminiCliUserAgent();
462
+ }
463
+
464
+ function redactAntigravityEndpoint(value: string): string {
465
+ try {
466
+ return new URL(value).origin;
467
+ } catch {
468
+ return "invalid endpoint";
469
+ }
470
+ }
@@ -23,11 +23,12 @@ export declare function getStreamIdleTimeoutRemediationMs(): number;
23
23
  * Returns the idle timeout used for OpenAI-family streaming transports.
24
24
  *
25
25
  * Honors `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
26
+ * A configured model timeout takes precedence over the built-in provider/model floors; environment overrides win over it.
26
27
  * When `provider` is given, long-reasoning hosts (xAI Grok and Grok Build) use that floor instead of the 120s default.
27
28
  * Grok models reached through other OpenAI-compatible hosts (`openrouter/x-ai/grok-*`, kilo, litellm, …) get the
28
29
  * same floor keyed on the model id, because long-reasoning silence is a property of the model (#4797).
29
30
  */
30
- export declare function getOpenAIStreamIdleTimeoutMs(provider?: string, modelId?: string): number | undefined;
31
+ export declare function getOpenAIStreamIdleTimeoutMs(provider?: string, modelId?: string, configuredTimeoutMs?: number): number | undefined;
31
32
  /**
32
33
  * Returns the timeout used while waiting for the first stream event.
33
34
  * The first token can legitimately take longer than later inter-event gaps,
@@ -57,7 +58,7 @@ export declare function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number, fal
57
58
  * window so a short post-connect first-event budget cannot kill legitimate
58
59
  * slow setup.
59
60
  */
60
- export declare function resolveOpenAISdkRequestTimeoutMs(provider: string, streamFirstEventTimeoutOverride?: number, modelId?: string): number | undefined;
61
+ export declare function resolveOpenAISdkRequestTimeoutMs(provider: string, streamFirstEventTimeoutOverride?: number, modelId?: string, configuredIdleTimeoutMs?: number, effectiveIdleTimeoutOverride?: number): number | undefined;
61
62
  /**
62
63
  * Resolves the Anthropic SDK client `timeout` so stalled-before-headers requests
63
64
  * are bounded. The Anthropic first-event watchdog deliberately arms only once
@@ -46,7 +46,7 @@ export function getProviderFirstEventTimeoutFallbackMs(provider: string): number
46
46
  return provider === "kimi-code" ? KIMI_CODE_FIRST_EVENT_TIMEOUT_MS : undefined;
47
47
  }
48
48
 
49
- function normalizeIdleTimeoutMs(value: string | undefined, fallback: number): number | undefined {
49
+ function normalizeIdleTimeoutMs(value: string | undefined, fallback: number | undefined): number | undefined {
50
50
  if (value === undefined) return fallback;
51
51
  const parsed = Number(value);
52
52
  if (!Number.isFinite(parsed)) return fallback;
@@ -86,16 +86,29 @@ export function getStreamIdleTimeoutRemediationMs(): number {
86
86
  * Returns the idle timeout used for OpenAI-family streaming transports.
87
87
  *
88
88
  * Honors `GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS` first (`PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` is the legacy alias). Set `=0` to disable.
89
+ * A configured model timeout takes precedence over the built-in provider/model floors; environment overrides win over it.
89
90
  * When `provider` is given, long-reasoning hosts (xAI Grok and Grok Build) use that floor instead of the 120s default.
90
91
  * Grok models reached through other OpenAI-compatible hosts (`openrouter/x-ai/grok-*`, kilo, litellm, …) get the
91
92
  * same floor keyed on the model id, because long-reasoning silence is a property of the model (#4797).
92
93
  */
93
- export function getOpenAIStreamIdleTimeoutMs(provider?: string, modelId?: string): number | undefined {
94
+ export function getOpenAIStreamIdleTimeoutMs(
95
+ provider?: string,
96
+ modelId?: string,
97
+ configuredTimeoutMs?: number,
98
+ ): number | undefined {
99
+ const builtInFallback =
100
+ getProviderStreamIdleTimeoutFallbackMs(provider ?? "") ??
101
+ (isGrokModelId(modelId) ? LONG_REASONING_STREAM_IDLE_TIMEOUT_MS : undefined) ??
102
+ DEFAULT_STREAM_IDLE_TIMEOUT_MS;
103
+ let configuredFallback: number | undefined = builtInFallback;
104
+ if (configuredTimeoutMs === 0) {
105
+ configuredFallback = undefined;
106
+ } else if (configuredTimeoutMs !== undefined && Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs > 0) {
107
+ configuredFallback = Math.max(1, Math.trunc(configuredTimeoutMs));
108
+ }
94
109
  return normalizeIdleTimeoutMs(
95
110
  $env.GJC_OPENAI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_STREAM_IDLE_TIMEOUT_MS ?? $env.PI_OPENAI_STREAM_IDLE_TIMEOUT_MS,
96
- getProviderStreamIdleTimeoutFallbackMs(provider ?? "") ??
97
- (isGrokModelId(modelId) ? LONG_REASONING_STREAM_IDLE_TIMEOUT_MS : undefined) ??
98
- DEFAULT_STREAM_IDLE_TIMEOUT_MS,
111
+ configuredFallback,
99
112
  );
100
113
  }
101
114
 
@@ -139,19 +152,26 @@ export function resolveOpenAISdkRequestTimeoutMs(
139
152
  provider: string,
140
153
  streamFirstEventTimeoutOverride?: number,
141
154
  modelId?: string,
155
+ configuredIdleTimeoutMs?: number,
156
+ effectiveIdleTimeoutOverride?: number,
142
157
  ): number | undefined {
143
158
  const providerFirstEventFallbackMs = getProviderFirstEventTimeoutFallbackMs(provider);
144
159
  const envSdkTimeoutMs = getStreamFirstEventTimeoutMs(
145
- getOpenAIStreamIdleTimeoutMs(provider, modelId),
160
+ effectiveIdleTimeoutOverride ?? getOpenAIStreamIdleTimeoutMs(provider, modelId, configuredIdleTimeoutMs),
146
161
  providerFirstEventFallbackMs,
147
162
  );
148
163
  if (streamFirstEventTimeoutOverride === 0) return undefined;
164
+ let sdkTimeoutMs: number | undefined;
149
165
  if (streamFirstEventTimeoutOverride !== undefined) {
150
- return providerFirstEventFallbackMs !== undefined
151
- ? streamFirstEventTimeoutOverride
152
- : Math.max(envSdkTimeoutMs ?? 0, streamFirstEventTimeoutOverride);
166
+ sdkTimeoutMs =
167
+ providerFirstEventFallbackMs !== undefined
168
+ ? streamFirstEventTimeoutOverride
169
+ : Math.max(envSdkTimeoutMs ?? 0, streamFirstEventTimeoutOverride);
170
+ } else {
171
+ sdkTimeoutMs = envSdkTimeoutMs;
153
172
  }
154
- return envSdkTimeoutMs;
173
+ if (sdkTimeoutMs === undefined || !Number.isFinite(sdkTimeoutMs) || sdkTimeoutMs <= 0) return undefined;
174
+ return Math.min(Math.trunc(sdkTimeoutMs), MAX_TIMER_DELAY_MS);
155
175
  }
156
176
 
157
177
  /**
@@ -62,15 +62,16 @@ async function postJson(url: string, body: Record<string, string | number>): Pro
62
62
  /**
63
63
  * Decoded shape of Anthropic's `/v1/oauth/token` response (both
64
64
  * `authorization_code` exchange and `refresh_token` refresh return the same
65
- * envelope). The `account` block is inlined alongside the tokens, so we can
66
- * surface `accountId` / `email` on {@link OAuthCredentials} without a separate
67
- * `/api/oauth/profile` round-trip.
65
+ * envelope). The `account` and `organization` blocks are inlined alongside the
66
+ * tokens, so we can surface `accountId` / `email` / `organizationId` on
67
+ * {@link OAuthCredentials} without a separate `/api/oauth/profile` round-trip.
68
68
  */
69
69
  interface AnthropicTokenResponse {
70
70
  access_token: string;
71
71
  refresh_token: string;
72
72
  expires_in: number;
73
73
  account?: { uuid?: string; email_address?: string };
74
+ organization?: { uuid?: string; name?: string };
74
75
  }
75
76
 
76
77
  function parseOAuthTokenResponse(responseBody: string, operation: string): AnthropicTokenResponse {
@@ -83,22 +84,26 @@ function parseOAuthTokenResponse(responseBody: string, operation: string): Anthr
83
84
  }
84
85
  }
85
86
 
87
+ function nonEmptyString(value: unknown): string | undefined {
88
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
89
+ }
90
+
86
91
  /**
87
- * Lift the OAuth response's `account: { uuid, email_address }` block onto
88
- * {@link OAuthCredentials} so downstream identity propagation (e.g.
89
- * `metadata.user_id.account_uuid`, usage tracking) works without a separate
90
- * `/api/oauth/profile` round-trip. Returns `undefined` for either field when
91
- * the response omits it or carries a non-string / empty value.
92
+ * Lift the OAuth response's `account: { uuid, email_address }` and
93
+ * `organization: { uuid, name }` blocks onto {@link OAuthCredentials} so
94
+ * downstream identity propagation (e.g. `metadata.user_id.account_uuid`, usage
95
+ * tracking, one stored credential per organization) works without a separate
96
+ * `/api/oauth/profile` round-trip. Returns `undefined` for any field the
97
+ * response omits or carries as a non-string / empty value.
92
98
  */
93
- function extractAccountFromTokenResponse(data: AnthropicTokenResponse): {
94
- accountId?: string;
95
- email?: string;
96
- } {
97
- const accountUuid = data.account?.uuid;
98
- const emailAddress = data.account?.email_address;
99
+ function extractIdentityFromTokenResponse(
100
+ data: AnthropicTokenResponse,
101
+ ): Pick<OAuthCredentials, "accountId" | "email" | "organizationId" | "organizationName"> {
99
102
  return {
100
- accountId: typeof accountUuid === "string" && accountUuid.length > 0 ? accountUuid : undefined,
101
- email: typeof emailAddress === "string" && emailAddress.length > 0 ? emailAddress : undefined,
103
+ accountId: nonEmptyString(data.account?.uuid),
104
+ email: nonEmptyString(data.account?.email_address),
105
+ organizationId: nonEmptyString(data.organization?.uuid),
106
+ organizationName: nonEmptyString(data.organization?.name),
102
107
  };
103
108
  }
104
109
 
@@ -182,14 +187,12 @@ export class AnthropicOAuthFlow extends OAuthCallbackFlow {
182
187
  }
183
188
 
184
189
  const tokenData = parseOAuthTokenResponse(responseBody, "token exchange");
185
- const { accountId, email } = extractAccountFromTokenResponse(tokenData);
186
190
 
187
191
  return {
188
192
  refresh: tokenData.refresh_token,
189
193
  access: tokenData.access_token,
190
194
  expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
191
- accountId,
192
- email,
195
+ ...extractIdentityFromTokenResponse(tokenData),
193
196
  };
194
197
  }
195
198
  }
@@ -221,13 +224,11 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
221
224
  }
222
225
 
223
226
  const data = parseOAuthTokenResponse(responseBody, "token refresh");
224
- const { accountId, email } = extractAccountFromTokenResponse(data);
225
227
 
226
228
  return {
227
229
  refresh: data.refresh_token || refreshToken,
228
230
  access: data.access_token,
229
231
  expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
230
- accountId,
231
- email,
232
+ ...extractIdentityFromTokenResponse(data),
232
233
  };
233
234
  }
@@ -193,19 +193,42 @@ export async function validateOpenAICompatibleApiKey(options: OpenAICompatibleVa
193
193
  typeof parsed === "object" && parsed !== null && "choices" in parsed
194
194
  ? (parsed as { choices?: unknown }).choices
195
195
  : undefined;
196
- const firstChoice = Array.isArray(choices) ? choices[0] : undefined;
197
- const message =
198
- typeof firstChoice === "object" &&
199
- firstChoice !== null &&
200
- "message" in firstChoice &&
201
- typeof (firstChoice as { message?: unknown }).message === "object" &&
202
- !Array.isArray((firstChoice as { message?: unknown }).message)
203
- ? (firstChoice as { message: Record<string, unknown> }).message
204
- : undefined;
205
- const content = message?.content;
206
- if (!(typeof content === "string" && content.trim().length > 0)) {
196
+ if (!Array.isArray(choices) || choices.length === 0) {
207
197
  throw validationFailure(options.provider, options.apiKey, "the inference probe returned no choices");
208
198
  }
199
+ const firstChoice: unknown = choices[0];
200
+ if (
201
+ typeof firstChoice !== "object" ||
202
+ firstChoice === null ||
203
+ Array.isArray(firstChoice) ||
204
+ !("message" in firstChoice)
205
+ ) {
206
+ throw validationFailure(
207
+ options.provider,
208
+ options.apiKey,
209
+ "the inference probe returned choices without a valid message",
210
+ );
211
+ }
212
+ const choice = firstChoice as Record<string, unknown>;
213
+ const rawMessage = choice.message;
214
+ if (typeof rawMessage !== "object" || rawMessage === null || Array.isArray(rawMessage)) {
215
+ throw validationFailure(
216
+ options.provider,
217
+ options.apiKey,
218
+ "the inference probe returned choices without a valid message",
219
+ );
220
+ }
221
+ const message = rawMessage as Record<string, unknown>;
222
+ const hasContent = typeof message.content === "string" && message.content.trim().length > 0;
223
+ const hasReasoningContent =
224
+ typeof message.reasoning_content === "string" && message.reasoning_content.trim().length > 0;
225
+ if (!hasContent && !hasReasoningContent && choice.finish_reason !== "length") {
226
+ throw validationFailure(
227
+ options.provider,
228
+ options.apiKey,
229
+ "the inference probe returned choices with empty content",
230
+ );
231
+ }
209
232
  }
210
233
  return;
211
234
  }
@@ -1,33 +1,3 @@
1
- /**
2
- * GLM ZCode OAuth flow (UNOFFICIAL, opt-in).
3
- *
4
- * Replicates how the ZCode desktop app turns a Z.AI login into usable GLM model
5
- * access. This is NOT an official Z.AI OAuth client: it reuses ZCode's authorize
6
- * page, broker, and a custom-protocol redirect. It may break at any time and may
7
- * violate ZCode/Z.AI Terms of Service. Endpoints/client id are overridable via
8
- * `ZCODE_OAUTH_*` environment variables.
9
- *
10
- * Verified end-to-end against the ZCode host bundle (`resolveZaiApiKey` /
11
- * `resolveBizApiKey`) and live traffic:
12
- * 1. Authorize: GET {authorize}?redirect_uri=zcode://oauth/callback&response_type=code&client_id=...&state=...
13
- * (custom-protocol redirect → a CLI cannot catch it, so the user pastes the code/redirect URL)
14
- * 2. Broker: POST {broker} { provider:"zai", code, redirect_uri, state }
15
- * → { data: { token: <ZCode JWT>, zai: { access_token: <upstream Z.AI token> } } }
16
- * 3. Business: POST {z/login} { token: <upstream Z.AI token> } → { data: { access_token: <business token> } }
17
- * 4. Provision: with the business token, GET getCustomerInfo → default org/project,
18
- * GET/POST .../api_keys (find/create a key named "zcode-api-key"),
19
- * GET .../api_keys/copy/{id} → secretKey ⇒ a real Z.AI API key "{id}.{secret}".
20
- *
21
- * Credential mapping:
22
- * - `access` = the provisioned **Z.AI API key** ("{id}.{secret}"). Model requests go to
23
- * `https://api.z.ai/api/anthropic/v1/messages` with `Authorization: Bearer <key>`
24
- * (exactly like a dashboard key) — NO zcode.z.ai gateway, NO captcha.
25
- * - `refresh` = the upstream Z.AI OAuth access token (used to re-provision the key).
26
- * The API key is long-lived, so `expires` is set far in the future.
27
- *
28
- * This provider must NEVER force `isOAuth=true`: the key is a plain Z.AI API key, and
29
- * api.z.ai is not api.anthropic.com, so the Anthropic path already emits a plain bearer.
30
- */
31
1
  import { OAuthCallbackFlow } from "./callback-server";
32
2
  import type { OAuthController, OAuthCredentials } from "./types";
33
3
  export declare const GLM_ZCODE_REFRESH_SKEW_MS: number;
@@ -4,8 +4,9 @@
4
4
  * Replicates how the ZCode desktop app turns a Z.AI login into usable GLM model
5
5
  * access. This is NOT an official Z.AI OAuth client: it reuses ZCode's authorize
6
6
  * page, broker, and a custom-protocol redirect. It may break at any time and may
7
- * violate ZCode/Z.AI Terms of Service. Endpoints/client id are overridable via
8
- * `ZCODE_OAUTH_*` environment variables.
7
+ * violate ZCode/Z.AI Terms of Service. Endpoints and the client id are
8
+ * overridable via `ZCODE_OAUTH_*` from the launching shell or user-owned env
9
+ * files. A project `.env` is not a trusted source for those values.
9
10
  *
10
11
  * Verified end-to-end against the ZCode host bundle (`resolveZaiApiKey` /
11
12
  * `resolveBizApiKey`) and live traffic:
@@ -28,6 +29,7 @@
28
29
  * This provider must NEVER force `isOAuth=true`: the key is a plain Z.AI API key, and
29
30
  * api.z.ai is not api.anthropic.com, so the Anthropic path already emits a plain bearer.
30
31
  */
32
+ import { $credentialEnv } from "@gajae-code/utils";
31
33
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions, parseCallbackInput } from "./callback-server";
32
34
  import type { OAuthController, OAuthCredentials } from "./types";
33
35
 
@@ -52,9 +54,18 @@ export const GLM_ZCODE_ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic";
52
54
 
53
55
  type FetchImpl = typeof globalThis.fetch;
54
56
 
57
+ /**
58
+ * Trusted override for one `ZCODE_OAUTH_*` name.
59
+ *
60
+ * These values choose where the authorization code and upstream token are sent.
61
+ * `$credentialEnv` keeps the launching shell and user-owned env files and skips
62
+ * a project `.env`. A shell value is also skipped when it repeats that file
63
+ * or the project declaration is dynamic, and when no other trusted source
64
+ * sets the name an empty value still uses the built-in endpoint.
65
+ */
55
66
  function envOr(name: string, fallback: string): string {
56
- const value = process.env[name];
57
- return value && value.trim().length > 0 ? value.trim() : fallback;
67
+ const value = $credentialEnv(name)?.trim();
68
+ return value ? value : fallback;
58
69
  }
59
70
 
60
71
  function resolveAuthorizeUrl(): string {
@@ -63,6 +63,41 @@ interface PKCE {
63
63
  challenge: string;
64
64
  }
65
65
 
66
+ /**
67
+ * Extract a readable error message from OpenAI OAuth error response.
68
+ * Handles both string errors (error + error_description) and object errors (code/type + message).
69
+ */
70
+ function extractOpenAIErrorMessage(body: unknown): string {
71
+ if (typeof body !== "object" || body === null) return "";
72
+
73
+ const obj = body as Record<string, unknown>;
74
+ const error = obj.error;
75
+
76
+ if (typeof error === "string") {
77
+ // String error: "invalid_grant", optionally explained by error_description.
78
+ // Parts are joined so an empty error string never leaves a dangling separator.
79
+ const desc = obj.error_description;
80
+ const parts = desc !== undefined && typeof desc === "string" && desc.length > 0 ? [error, desc] : [error];
81
+ return parts.filter(part => part.length > 0).join(": ");
82
+ }
83
+
84
+ if (typeof error === "object" && error !== null) {
85
+ // Object error: {message, type, code}. `code` wins over `type`, but only when
86
+ // it is a usable string — a numeric or empty code must fall back to the type
87
+ // rather than silently dropping both.
88
+ const err = error as Record<string, unknown>;
89
+ const code = typeof err.code === "string" && err.code.length > 0 ? err.code : err.type;
90
+ const msg = err.message;
91
+
92
+ const parts: string[] = [];
93
+ if (typeof code === "string" && code.length > 0) parts.push(code);
94
+ if (typeof msg === "string" && msg.length > 0) parts.push(msg);
95
+ return parts.join(": ");
96
+ }
97
+
98
+ return "";
99
+ }
100
+
66
101
  class OpenAICodexOAuthFlow extends OAuthCallbackFlow {
67
102
  constructor(
68
103
  ctrl: OAuthController,
@@ -120,9 +155,9 @@ async function exchangeCodeForToken(code: string, verifier: string, redirectUri:
120
155
  if (!tokenResponse.ok) {
121
156
  let detail = `${tokenResponse.status}`;
122
157
  try {
123
- const body = (await tokenResponse.json()) as { error?: string; error_description?: string };
124
- if (body.error)
125
- detail = `${tokenResponse.status} ${body.error}${body.error_description ? `: ${body.error_description}` : ""}`;
158
+ const body = await tokenResponse.json();
159
+ const errorMsg = extractOpenAIErrorMessage(body);
160
+ if (errorMsg) detail = `${tokenResponse.status} ${errorMsg}`;
126
161
  } catch {}
127
162
  throw new Error(`Token exchange failed: ${detail}`);
128
163
  }
@@ -272,9 +307,9 @@ export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAu
272
307
  if (!response.ok) {
273
308
  let detail = `${response.status}`;
274
309
  try {
275
- const body = (await response.json()) as { error?: string; error_description?: string };
276
- if (body.error)
277
- detail = `${response.status} ${body.error}${body.error_description ? `: ${body.error_description}` : ""}`;
310
+ const body = await response.json();
311
+ const errorMsg = extractOpenAIErrorMessage(body);
312
+ if (errorMsg) detail = `${response.status} ${errorMsg}`;
278
313
  } catch {}
279
314
  throw new Error(`OpenAI Codex token refresh failed: ${detail}`);
280
315
  }