@gajae-code/ai 0.18.10 → 0.18.12

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 (75) hide show
  1. package/CHANGELOG.md +70 -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 +80 -8
  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/openai-completions-compat.d.ts +2 -1
  9. package/dist/types/providers/anthropic.d.ts +10 -0
  10. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  11. package/dist/types/providers/google-auth.d.ts +7 -0
  12. package/dist/types/providers/google-gemini-headers.d.ts +10 -5
  13. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  14. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  15. package/dist/types/providers/openai-completions.d.ts +1 -1
  16. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  17. package/dist/types/stream.d.ts +33 -1
  18. package/dist/types/types.d.ts +26 -3
  19. package/dist/types/usage.d.ts +3 -0
  20. package/dist/types/utils/discovery/antigravity.d.ts +11 -0
  21. package/dist/types/utils/idle-iterator.d.ts +3 -2
  22. package/dist/types/utils/oauth/types.d.ts +9 -0
  23. package/package.json +4 -4
  24. package/src/auth-broker/remote-store.ts +62 -11
  25. package/src/auth-broker/wire-schemas.ts +2 -0
  26. package/src/auth-gateway/server.ts +3 -2
  27. package/src/auth-storage.ts +647 -194
  28. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  29. package/src/bedrock-claude-cache-policy.ts +2 -2
  30. package/src/model-cache.ts +45 -10
  31. package/src/model-manager.ts +18 -6
  32. package/src/model-thinking.ts +37 -7
  33. package/src/models.json +2931 -1670
  34. package/src/openai-completions-compat.d.ts +2 -1
  35. package/src/openai-completions-compat.ts +5 -0
  36. package/src/provider-models/google.ts +7 -1
  37. package/src/provider-models/openai-compat.ts +26 -0
  38. package/src/providers/amazon-bedrock.ts +20 -11
  39. package/src/providers/anthropic.d.ts +10 -0
  40. package/src/providers/anthropic.ts +139 -4
  41. package/src/providers/aws-credential-config.d.ts +5 -0
  42. package/src/providers/aws-credential-config.ts +29 -1
  43. package/src/providers/google-auth.d.ts +7 -0
  44. package/src/providers/google-auth.ts +165 -4
  45. package/src/providers/google-gemini-headers.d.ts +10 -5
  46. package/src/providers/google-gemini-headers.ts +11 -6
  47. package/src/providers/google-vertex.ts +2 -4
  48. package/src/providers/kiro-api-key.d.ts +5 -0
  49. package/src/providers/kiro-api-key.ts +379 -68
  50. package/src/providers/ollama.ts +19 -9
  51. package/src/providers/openai-anthropic-shim.ts +1 -0
  52. package/src/providers/openai-chat-server-schema.ts +1 -1
  53. package/src/providers/openai-chat-server.ts +14 -2
  54. package/src/providers/openai-codex-responses.ts +21 -14
  55. package/src/providers/openai-completions.d.ts +1 -1
  56. package/src/providers/openai-completions.ts +65 -4
  57. package/src/providers/openai-responses-server.ts +10 -1
  58. package/src/providers/openai-responses-shared.d.ts +2 -1
  59. package/src/providers/openai-responses-shared.ts +42 -1
  60. package/src/providers/openai-responses.ts +51 -10
  61. package/src/providers/pi-native-server.ts +1 -1
  62. package/src/stream.d.ts +33 -1
  63. package/src/stream.ts +107 -13
  64. package/src/types.d.ts +26 -3
  65. package/src/types.ts +55 -2
  66. package/src/usage/claude.ts +4 -2
  67. package/src/usage.ts +3 -0
  68. package/src/utils/discovery/antigravity.ts +144 -2
  69. package/src/utils/idle-iterator.d.ts +3 -2
  70. package/src/utils/idle-iterator.ts +30 -10
  71. package/src/utils/oauth/anthropic.ts +23 -22
  72. package/src/utils/oauth/api-key-validation.ts +34 -11
  73. package/src/utils/oauth/types.d.ts +9 -0
  74. package/src/utils/oauth/types.ts +9 -0
  75. package/src/utils/parse-bind.d.ts +35 -0
@@ -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
  }
@@ -7,6 +7,15 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  profileArn?: string;
10
+ /**
11
+ * Organization the token is scoped to. One Anthropic account can belong to
12
+ * several organizations (e.g. a Team workspace and a personal Max plan); each
13
+ * login is bound to exactly one of them, so the organization is part of the
14
+ * credential's identity.
15
+ */
16
+ organizationId?: string;
17
+ /** Display name of {@link organizationId}. */
18
+ organizationName?: string;
10
19
  };
11
20
  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";
12
21
  export type OAuthProviderId = OAuthProvider | (string & {});
@@ -7,6 +7,15 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  profileArn?: string;
10
+ /**
11
+ * Organization the token is scoped to. One Anthropic account can belong to
12
+ * several organizations (e.g. a Team workspace and a personal Max plan); each
13
+ * login is bound to exactly one of them, so the organization is part of the
14
+ * credential's identity.
15
+ */
16
+ organizationId?: string;
17
+ /** Display name of {@link organizationId}. */
18
+ organizationName?: string;
10
19
  };
11
20
 
12
21
  export type OAuthProvider =
@@ -0,0 +1,35 @@
1
+ export interface ParsedBind {
2
+ hostname: string;
3
+ port: number;
4
+ }
5
+ /**
6
+ * Parse a `host:port` (or bare `port`, which assumes loopback) string.
7
+ *
8
+ * Accepts:
9
+ * - `"4000"` → `127.0.0.1:4000`
10
+ * - `"0.0.0.0:4000"` → as written
11
+ * - `"[::1]:4000"` → as written (brackets retained, Bun handles them)
12
+ *
13
+ * Rejects:
14
+ * - empty input
15
+ * - empty hostname (`":4000"`)
16
+ * - non-integer / out-of-range port
17
+ */
18
+ export declare function parseBind(raw: string): ParsedBind;
19
+ /** True for loopback-only hostnames the auth servers may bind without credentials. */
20
+ export declare function isLoopbackHostname(hostname: string): boolean;
21
+ /**
22
+ * Fail closed when an unauthenticated auth server (empty bearer token set)
23
+ * would bind a non-loopback address: that exposes credential operations to the
24
+ * network with no proof of possession.
25
+ */
26
+ export declare function assertAuthenticatedOrLoopback(bind: ParsedBind, bearerTokenCount: number, serverName: string): void;
27
+ /**
28
+ * Tokenless loopback servers accept a request only when its Host header is the
29
+ * bound hostname and effective port. Default-port forms that stay allowed:
30
+ * a Host with no port, or an explicit :80, on an HTTP listener on port 80;
31
+ * an explicit :443 on a listener on port 443. Any other omitted port is
32
+ * rejected. A DNS-rebound same-origin GET sends the attacker's name and no
33
+ * Origin header.
34
+ */
35
+ export declare function hostHeaderMatchesBind(hostHeader: string | null, bind: ParsedBind): boolean;