@gajae-code/ai 0.9.2 → 0.9.3

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 CHANGED
@@ -9,6 +9,9 @@
9
9
 
10
10
  ### Fixed
11
11
 
12
+ - Refreshed the default Gemini CLI impersonation version to 0.50.0 so the spoofed User-Agent freshness gate passes for the 0.9.2 release.
13
+ - Hid the non-callable `google-antigravity/gemini-3.1-pro-high` selector from bundled, dynamic, and cached Antigravity catalogs after live Cloud Code Assist calls returned HTTP 400; `google-antigravity/gemini-3.1-pro-low:high` remains the working high-thinking path.
14
+ - Preserved Anthropic tool-use arguments supplied on `content_block_start` when no `input_json_delta` chunks follow, preventing finished tool calls from collapsing back to `{}`.
12
15
  - Refreshed the default Gemini CLI impersonation version to 0.50.0 so the spoofed User-Agent freshness gate passes for the 0.9.2 release.
13
16
 
14
17
  ## [0.9.1] - 2026-07-08
@@ -0,0 +1,6 @@
1
+ export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high"];
2
+ export declare function isRetiredModelKey(provider: string, modelId: string): boolean;
3
+ export declare function isRetiredModel(model: {
4
+ provider: string;
5
+ id: string;
6
+ }): boolean;
@@ -1,6 +1,14 @@
1
- import MODELS from "./models.json";
2
1
  import type { Api, KnownProvider, Model, Usage } from "./types";
3
- export type GeneratedProvider = keyof typeof MODELS;
2
+ /**
3
+ * Static bundled model registry loaded lazily from `models.json`.
4
+ *
5
+ * This module intentionally exposes compile-time defaults only.
6
+ * It does not include runtime discovery, models.dev overlays, or on-disk cache state.
7
+ *
8
+ * For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`.
9
+ */
10
+ type BundledCatalog = typeof import("./models.json");
11
+ export type GeneratedProvider = keyof BundledCatalog;
4
12
  export declare function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi>;
5
13
  export declare function getBundledProviders(): KnownProvider[];
6
14
  export declare function getBundledModels(provider: GeneratedProvider): Model<Api>[];
@@ -10,3 +18,4 @@ export declare function calculateCost<TApi extends Api>(model: Model<TApi>, usag
10
18
  * Returns false if either model is null or undefined.
11
19
  */
12
20
  export declare function modelsAreEqual<TApi extends Api>(a: Model<TApi> | null | undefined, b: Model<TApi> | null | undefined): boolean;
21
+ export {};
@@ -51,6 +51,12 @@ export interface FetchAntigravityDiscoveryModelsOptions {
51
51
  signal?: AbortSignal;
52
52
  /** Optional fetch implementation override for tests. */
53
53
  fetcher?: typeof fetch;
54
+ /**
55
+ * Provider id the caller assigns to returned models. Scopes retired-selector
56
+ * filtering (e.g. `google-gemini-cli` reuses this helper and remaps rows).
57
+ * Default: `google-antigravity`.
58
+ */
59
+ targetProvider?: "google-antigravity" | "google-gemini-cli";
54
60
  }
55
61
  /**
56
62
  * Fetches discoverable Antigravity models and normalizes them into canonical model entries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.9.2",
4
+ "version": "0.9.3",
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,7 +40,7 @@
40
40
  "dependencies": {
41
41
  "@anthropic-ai/sdk": "^0.94.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@gajae-code/utils": "0.9.2",
43
+ "@gajae-code/utils": "0.9.3",
44
44
  "openai": "^6.36.0",
45
45
  "partial-json": "^0.1.7",
46
46
  "zod": "4.4.3"
@@ -3,7 +3,7 @@
3
3
  * Replaces per-provider JSON files with a single cache.db.
4
4
  */
5
5
  import { Database } from "bun:sqlite";
6
- import { getModelDbPath } from "@gajae-code/utils";
6
+ import { getModelDbPath } from "@gajae-code/utils/dirs";
7
7
  import type { Api, Model } from "./types";
8
8
 
9
9
  const CACHE_SCHEMA_VERSION = 3;
@@ -1,8 +1,8 @@
1
1
  import { readModelCache, writeModelCache } from "./model-cache";
2
+ import { isRetiredModel, isRetiredModelKey } from "./model-retirements";
2
3
  import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking";
3
4
  import { type GeneratedProvider, getBundledModels } from "./models";
4
5
  import type { Api, Model, Provider } from "./types";
5
- import { isRecord } from "./utils";
6
6
 
7
7
  const DEFAULT_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
8
8
  const NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
@@ -85,7 +85,14 @@ function passModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
85
85
  }
86
86
  const out: Model<TApi>[] = [];
87
87
  for (const item of value) {
88
- if (item === null || typeof item !== "object" || typeof (item as { id: unknown }).id !== "string") {
88
+ if (item === null || typeof item !== "object") {
89
+ continue;
90
+ }
91
+ const candidate = item as { id?: unknown; provider?: unknown };
92
+ if (typeof candidate.id !== "string") {
93
+ continue;
94
+ }
95
+ if (typeof candidate.provider === "string" && isRetiredModelKey(candidate.provider, candidate.id)) {
89
96
  continue;
90
97
  }
91
98
  out.push(enrichModelThinking(item as Model<TApi>));
@@ -382,7 +389,7 @@ function normalizeModelList<TApi extends Api>(value: unknown): Model<TApi>[] {
382
389
  }
383
390
  const models: Model<TApi>[] = [];
384
391
  for (const item of value) {
385
- if (isModelLike(item)) {
392
+ if (isModelLike(item) && !isRetiredModel(item)) {
386
393
  models.push(enrichModelThinking(item as Model<TApi>));
387
394
  }
388
395
  }
@@ -441,6 +448,10 @@ function isModelLike(value: unknown): value is Model<Api> {
441
448
  return true;
442
449
  }
443
450
 
451
+ function isRecord(value: unknown): value is Record<string, unknown> {
452
+ return typeof value === "object" && value !== null;
453
+ }
454
+
444
455
  function isModelInputArray(value: unknown): value is ("text" | "image")[] {
445
456
  if (!Array.isArray(value) || value.length === 0) {
446
457
  return false;
@@ -0,0 +1,13 @@
1
+ // Retired from advertised catalogs because Cloud Code Assist rejects live calls
2
+ // with HTTP 400. The callable high-thinking path is gemini-3.1-pro-low:high.
3
+ export const RETIRED_MODEL_KEYS = ["google-antigravity/gemini-3.1-pro-high"] as const;
4
+
5
+ const RETIRED_MODEL_KEY_SET = new Set<string>(RETIRED_MODEL_KEYS);
6
+
7
+ export function isRetiredModelKey(provider: string, modelId: string): boolean {
8
+ return RETIRED_MODEL_KEY_SET.has(`${provider}/${modelId}`);
9
+ }
10
+
11
+ export function isRetiredModel(model: { provider: string; id: string }): boolean {
12
+ return isRetiredModelKey(model.provider, model.id);
13
+ }
package/src/models.json CHANGED
@@ -10978,35 +10978,6 @@
10978
10978
  ]
10979
10979
  }
10980
10980
  },
10981
- "gemini-3.1-pro-high": {
10982
- "id": "gemini-3.1-pro-high",
10983
- "name": "Gemini 3.1 Pro (High) (Antigravity)",
10984
- "api": "google-gemini-cli",
10985
- "provider": "google-antigravity",
10986
- "baseUrl": "https://daily-cloudcode-pa.sandbox.googleapis.com",
10987
- "reasoning": true,
10988
- "input": [
10989
- "text",
10990
- "image"
10991
- ],
10992
- "cost": {
10993
- "input": 0,
10994
- "output": 0,
10995
- "cacheRead": 0,
10996
- "cacheWrite": 0
10997
- },
10998
- "contextWindow": 1048576,
10999
- "maxTokens": 65535,
11000
- "thinking": {
11001
- "mode": "google-level",
11002
- "minLevel": "low",
11003
- "maxLevel": "high",
11004
- "levels": [
11005
- "low",
11006
- "high"
11007
- ]
11008
- }
11009
- },
11010
10981
  "gemini-3.1-pro-low": {
11011
10982
  "id": "gemini-3.1-pro-low",
11012
10983
  "name": "Gemini 3.1 Pro (Low) (Antigravity)",
package/src/models.ts CHANGED
@@ -1,26 +1,42 @@
1
+ import { createRequire } from "node:module";
2
+ import { isRetiredModelKey } from "./model-retirements";
1
3
  import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking";
2
- import MODELS from "./models.json" with { type: "json" };
3
4
  import type { Api, KnownProvider, Model, Usage } from "./types";
4
5
  import { isClaudeForcedToolChoiceIncapableModelId } from "./utils/tool-choice-capability";
5
6
 
6
7
  /**
7
- * Static bundled model registry loaded from `models.json`.
8
+ * Static bundled model registry loaded lazily from `models.json`.
8
9
  *
9
10
  * This module intentionally exposes compile-time defaults only.
10
11
  * It does not include runtime discovery, models.dev overlays, or on-disk cache state.
11
12
  *
12
13
  * For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`.
13
14
  */
14
- const providerNames = Object.keys(MODELS) as KnownProvider[];
15
+ type BundledCatalog = typeof import("./models.json");
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const COMPILED_MODELS_PATH = "./packages/ai/src/models.json";
19
+ let bundledCatalog: BundledCatalog | undefined;
20
+ let providerNames: KnownProvider[] | undefined;
15
21
  const providerModelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
16
22
 
23
+ function getBundledCatalog(): BundledCatalog {
24
+ bundledCatalog ??= require(
25
+ process.env.PI_COMPILED === "true" ? COMPILED_MODELS_PATH : "./models.json",
26
+ ) as BundledCatalog;
27
+ return bundledCatalog;
28
+ }
29
+
17
30
  function getProviderModels(provider: GeneratedProvider): Map<string, Model<Api>> | undefined {
18
31
  const cached = providerModelRegistry.get(provider);
19
32
  if (cached) return cached;
20
- const models = MODELS[provider];
33
+ const models = getBundledCatalog()[provider];
21
34
  if (!models) return undefined;
22
35
  const providerModels = new Map<string, Model<Api>>();
23
36
  for (const [id, model] of Object.entries(models)) {
37
+ if (isRetiredModelKey(provider, id)) {
38
+ continue;
39
+ }
24
40
  providerModels.set(id, applyBundledCompatDefaults(enrichModelThinking(model as Model<Api>)));
25
41
  }
26
42
  providerModelRegistry.set(provider, providerModels);
@@ -52,7 +68,7 @@ function applyBundledCompatDefaults(model: Model<Api>): Model<Api> {
52
68
  return policyModels[0] ?? normalized;
53
69
  }
54
70
 
55
- export type GeneratedProvider = keyof typeof MODELS;
71
+ export type GeneratedProvider = keyof BundledCatalog;
56
72
 
57
73
  export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi> {
58
74
  const providerModels = getProviderModels(provider);
@@ -62,6 +78,7 @@ export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvi
62
78
  export function getBundledProviders(): KnownProvider[] {
63
79
  // Defensive copy: the old eager path returned a fresh Array.from(...), so
64
80
  // callers may freely mutate their result without corrupting enumeration.
81
+ providerNames ??= Object.keys(getBundledCatalog()) as KnownProvider[];
65
82
  return providerNames.slice();
66
83
  }
67
84
 
@@ -75,6 +75,7 @@ export function googleGeminiCliModelManagerOptions(
75
75
  const models = await fetchAntigravityDiscoveryModels({
76
76
  token,
77
77
  endpoint,
78
+ targetProvider: "google-gemini-cli",
78
79
  });
79
80
  if (models === null) {
80
81
  return null;
@@ -1513,7 +1513,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1513
1513
  partial: output,
1514
1514
  });
1515
1515
  } else if (block.type === "toolCall") {
1516
- block.arguments = parseStreamingJson(block.partialJson);
1516
+ if (block.partialJson.trim()) {
1517
+ block.arguments = parseStreamingJson(block.partialJson);
1518
+ }
1517
1519
  delete (block as { partialJson?: string }).partialJson;
1518
1520
  stream.push({
1519
1521
  type: "toolcall_end",
package/src/stream.ts CHANGED
@@ -12,22 +12,15 @@ import {
12
12
  import type { BedrockOptions } from "./providers/amazon-bedrock";
13
13
  import type { AnthropicOptions } from "./providers/anthropic";
14
14
  import type { CursorOptions } from "./providers/cursor";
15
- import { isGitLabDuoModel, streamGitLabDuo } from "./providers/gitlab-duo";
16
15
  import type { GoogleOptions } from "./providers/google";
17
16
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
18
17
  import type { GoogleVertexOptions } from "./providers/google-vertex";
19
- import { isKimiModel, streamKimi } from "./providers/kimi";
20
18
  import type { OllamaChatOptions } from "./providers/ollama";
21
19
  import type { OpenAICompletionsOptions } from "./providers/openai-completions";
22
- import { streamPiNative } from "./providers/pi-native-client";
23
20
  // Heavy provider stream functions are imported lazily via register-builtins,
24
- // which wraps each provider module in a dynamic import. This keeps the
25
- // AWS SDK, google-auth-library, @google/genai, @bufbuild/protobuf, and
26
- // other provider SDKs out of the CLI startup parse graph. The
27
- // gitlab-duo / kimi / synthetic providers stay eager because their modules
28
- // export routing predicates (isGitLabDuoModel, isKimiModel, isSyntheticModel)
29
- // that must be callable synchronously before streaming begins, and their
30
- // modules are thin wrappers with no heavy SDK dependencies.
21
+ // which wraps each provider module in a dynamic import. Thin provider routing
22
+ // modules are also loaded lazily below by returning an outer stream and piping
23
+ // the dynamically imported inner stream into it.
31
24
  import {
32
25
  streamAnthropic,
33
26
  streamAzureOpenAIResponses,
@@ -41,7 +34,6 @@ import {
41
34
  streamOpenAICompletions,
42
35
  streamOpenAIResponses,
43
36
  } from "./providers/register-builtins";
44
- import { isSyntheticModel, streamSynthetic } from "./providers/synthetic";
45
37
  import type {
46
38
  Api,
47
39
  AssistantMessage,
@@ -240,6 +232,45 @@ export function formatProviderCredentialHint(provider: string): string {
240
232
  }
241
233
  return parts.join(" ");
242
234
  }
235
+ function pipeAssistantStream(
236
+ outer: AssistantMessageEventStream,
237
+ inner: AssistantMessageEventStream,
238
+ signal?: AbortSignal,
239
+ ): void {
240
+ void (async () => {
241
+ try {
242
+ for await (const event of inner) {
243
+ outer.push(event);
244
+ // The inner provider stream owns abort semantics (it receives the
245
+ // same signal), but stop forwarding as soon as the consumer
246
+ // aborted so a misbehaving inner stream cannot keep the pipe
247
+ // buffering events indefinitely.
248
+ if (signal?.aborted && !outer.done) {
249
+ outer.end(await inner.result());
250
+ return;
251
+ }
252
+ }
253
+ if (!outer.done) outer.end(await inner.result());
254
+ } catch (error) {
255
+ outer.fail(error);
256
+ }
257
+ })();
258
+ }
259
+
260
+ function streamFromLazyImport(
261
+ createInner: () => Promise<AssistantMessageEventStream>,
262
+ signal?: AbortSignal,
263
+ ): AssistantMessageEventStream {
264
+ const outer = new AssistantMessageEventStream();
265
+ void (async () => {
266
+ try {
267
+ pipeAssistantStream(outer, await createInner(), signal);
268
+ } catch (error) {
269
+ outer.fail(error);
270
+ }
271
+ })();
272
+ return outer;
273
+ }
243
274
 
244
275
  /**
245
276
  * Build an actionable "missing API key" error for a provider, used by the
@@ -262,15 +293,21 @@ export function stream<TApi extends Api>(
262
293
  return customApiProvider.stream(model, context, options as StreamOptions);
263
294
  }
264
295
 
265
- if (isGitLabDuoModel(model)) {
296
+ if (model.provider === "gitlab-duo") {
266
297
  const apiKey = (options as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
267
298
  if (!apiKey) {
268
299
  throw new Error(formatMissingApiKeyError(model.provider));
269
300
  }
270
- return streamGitLabDuo(model, context, {
271
- ...(options as SimpleStreamOptions | undefined),
272
- apiKey,
273
- });
301
+ return streamFromLazyImport(
302
+ async () => {
303
+ const { streamGitLabDuo } = await import("./providers/gitlab-duo");
304
+ return streamGitLabDuo(model, context, {
305
+ ...(options as SimpleStreamOptions | undefined),
306
+ apiKey,
307
+ });
308
+ },
309
+ (options as StreamOptions | undefined)?.signal,
310
+ );
274
311
  }
275
312
 
276
313
  // Vertex AI uses Application Default Credentials, not API keys
@@ -446,7 +483,10 @@ export function streamSimple<TApi extends Api>(
446
483
  // extension-registered APIs can't accidentally override a configured
447
484
  // pi-native transport.
448
485
  if (model.transport === "pi-native") {
449
- return streamPiNative(model, context, options);
486
+ return streamFromLazyImport(async () => {
487
+ const { streamPiNative } = await import("./providers/pi-native-client");
488
+ return streamPiNative(model, context, options);
489
+ }, options?.signal);
450
490
  }
451
491
 
452
492
  // Check custom API registry (extension-provided APIs)
@@ -471,31 +511,40 @@ export function streamSimple<TApi extends Api>(
471
511
  }
472
512
 
473
513
  // GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
474
- if (isGitLabDuoModel(model)) {
475
- return streamGitLabDuo(model, context, {
476
- ...options,
477
- apiKey,
478
- });
514
+ if (model.provider === "gitlab-duo") {
515
+ return streamFromLazyImport(async () => {
516
+ const { streamGitLabDuo } = await import("./providers/gitlab-duo");
517
+ return streamGitLabDuo(model, context, {
518
+ ...options,
519
+ apiKey,
520
+ });
521
+ }, options?.signal);
479
522
  }
480
523
 
481
524
  // Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
482
- if (isKimiModel(model)) {
483
- // Pass raw SimpleStreamOptions - streamKimi handles mapping internally
484
- return streamKimi(model as Model<"openai-completions">, context, {
485
- ...options,
486
- apiKey,
487
- format: options?.kimiApiFormat ?? "anthropic",
488
- });
525
+ if (model.provider === "kimi-code") {
526
+ return streamFromLazyImport(async () => {
527
+ const { streamKimi } = await import("./providers/kimi");
528
+ // Pass raw SimpleStreamOptions - streamKimi handles mapping internally
529
+ return streamKimi(model as Model<"openai-completions">, context, {
530
+ ...options,
531
+ apiKey,
532
+ format: options?.kimiApiFormat ?? "anthropic",
533
+ });
534
+ }, options?.signal);
489
535
  }
490
536
 
491
537
  // Synthetic - route to dedicated handler that wraps OpenAI or Anthropic API
492
- if (isSyntheticModel(model)) {
493
- // Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
494
- return streamSynthetic(model as Model<"openai-completions">, context, {
495
- ...options,
496
- apiKey,
497
- format: options?.syntheticApiFormat ?? "openai", // Default to OpenAI format
498
- });
538
+ if (model.provider === "synthetic") {
539
+ return streamFromLazyImport(async () => {
540
+ const { streamSynthetic } = await import("./providers/synthetic");
541
+ // Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
542
+ return streamSynthetic(model as Model<"openai-completions">, context, {
543
+ ...options,
544
+ apiKey,
545
+ format: options?.syntheticApiFormat ?? "openai", // Default to OpenAI format
546
+ });
547
+ }, options?.signal);
499
548
  }
500
549
 
501
550
  const providerOptions = mapOptionsForApi(model, options, apiKey);
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod/v4";
2
+ import { isRetiredModelKey } from "../../model-retirements";
2
3
  import { getAntigravityUserAgent } from "../../providers/google-gemini-headers";
3
4
  import type { Model } from "../../types";
4
- import { toPositiveNumber } from "../../utils";
5
5
 
6
6
  const DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS = [
7
7
  "https://daily-cloudcode-pa.googleapis.com",
@@ -162,6 +162,12 @@ export interface FetchAntigravityDiscoveryModelsOptions {
162
162
  signal?: AbortSignal;
163
163
  /** Optional fetch implementation override for tests. */
164
164
  fetcher?: typeof fetch;
165
+ /**
166
+ * Provider id the caller assigns to returned models. Scopes retired-selector
167
+ * filtering (e.g. `google-gemini-cli` reuses this helper and remaps rows).
168
+ * Default: `google-antigravity`.
169
+ */
170
+ targetProvider?: "google-antigravity" | "google-gemini-cli";
165
171
  }
166
172
 
167
173
  /**
@@ -174,6 +180,7 @@ export async function fetchAntigravityDiscoveryModels(
174
180
  options: FetchAntigravityDiscoveryModelsOptions,
175
181
  ): Promise<Model<"google-gemini-cli">[] | null> {
176
182
  const fetcher = options.fetcher ?? fetch;
183
+ const targetProvider = options.targetProvider ?? "google-antigravity";
177
184
  const endpoints = options.endpoint
178
185
  ? [trimTrailingSlashes(options.endpoint)]
179
186
  : DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS.map(trimTrailingSlashes);
@@ -214,7 +221,7 @@ export async function fetchAntigravityDiscoveryModels(
214
221
  const models: Model<"google-gemini-cli">[] = [];
215
222
 
216
223
  for (const [modelId, model] of Object.entries(parsed.models ?? {})) {
217
- if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId)) {
224
+ if (ANTIGRAVITY_DISCOVERY_DENYLIST.has(modelId) || isRetiredModelKey(targetProvider, modelId)) {
218
225
  continue;
219
226
  }
220
227
  if (model.isInternal === true) {
@@ -256,6 +263,13 @@ function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscovery
256
263
  return parsed.data;
257
264
  }
258
265
 
266
+ function toPositiveNumber(value: unknown, fallback: number): number {
267
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
268
+ return fallback;
269
+ }
270
+ return value;
271
+ }
272
+
259
273
  function trimTrailingSlashes(value: string): string {
260
274
  return value.replace(/\/+$/, "");
261
275
  }