@sayknow-cli/ai 0.3.11 → 0.3.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.
@@ -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 {};
@@ -3,6 +3,9 @@
3
3
  * Uses the same format as the official Gemini CLI (v0.35+):
4
4
  * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
5
5
  */
6
+ export declare const GEMINI_CLI_VERSION_ENV = "SKC_AI_GEMINI_CLI_VERSION";
7
+ export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
+ export declare const DEFAULT_GEMINI_CLI_VERSION = "0.50.0";
6
9
  export declare function getGeminiCliUserAgent(modelId?: string): string;
7
10
  export declare const getGeminiCliHeaders: (modelId?: string) => {
8
11
  "User-Agent": string;
@@ -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": "@sayknow-cli/ai",
4
- "version": "0.3.11",
4
+ "version": "0.3.12",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@sayknow-cli/utils": "0.3.11",
46
+ "@sayknow-cli/utils": "0.3.12",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "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 "@sayknow-cli/utils";
6
+ import { getModelDbPath } from "@sayknow-cli/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
+ }
@@ -47,6 +47,7 @@ const DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX: readonly Effort[] = [
47
47
  const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
48
48
  const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
49
49
  const GPT_5_2_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
50
+ const GPT_5_6_PLUS_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
50
51
  const GPT_5_5_DEFAULT_EFFORT = Effort.XHigh;
51
52
 
52
53
  const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.High];
@@ -60,7 +61,18 @@ type SemVer = {
60
61
 
61
62
  type GeminiKind = "pro" | "flash";
62
63
  type AnthropicKind = "opus" | "sonnet";
63
- type OpenAIVariant = "base" | "codex" | "codex-max" | "codex-mini" | "codex-spark" | "mini" | "max" | "nano";
64
+ type OpenAIVariant =
65
+ | "base"
66
+ | "codex"
67
+ | "codex-max"
68
+ | "codex-mini"
69
+ | "codex-spark"
70
+ | "luna"
71
+ | "mini"
72
+ | "max"
73
+ | "nano"
74
+ | "sol"
75
+ | "terra";
64
76
 
65
77
  const CODEX_GPT_5_4_PRIORITY_BY_VARIANT: Partial<Record<OpenAIVariant, number>> = {
66
78
  base: 0,
@@ -465,11 +477,25 @@ function applyGpt55ContextWindow(model: ApiModel<Api>, parsedModel: OpenAIModel)
465
477
  }
466
478
  return false;
467
479
  }
480
+ const GPT_5_6_TIER_VARIANTS: ReadonlySet<OpenAIVariant> = new Set(["base", "sol", "terra", "luna"]);
481
+
482
+ function applyGpt56ContextWindow(model: ApiModel<Api>, parsedModel: OpenAIModel): boolean {
483
+ if (!semverGte(parsedModel.version, "5.6") || !GPT_5_6_TIER_VARIANTS.has(parsedModel.variant)) {
484
+ return false;
485
+ }
486
+ // GPT-5.6 tiers enforce a ~373K usable prompt budget on both transports
487
+ // (matches the openai-codex live catalog), despite the 1M+ marketing window.
488
+ model.contextWindow = 373_000;
489
+ return true;
490
+ }
468
491
 
469
492
  function applyOpenAICatalogPolicy(model: ApiModel<Api>, parsedModel: OpenAIModel): void {
470
493
  if (applyGpt55ContextWindow(model, parsedModel)) {
471
494
  return;
472
495
  }
496
+ if (applyGpt56ContextWindow(model, parsedModel)) {
497
+ return;
498
+ }
473
499
  // OpenAI code backend models: 400K figure includes output budget; input window is 272K.
474
500
  if (parsedModel.variant.startsWith("codex") && parsedModel.variant !== "codex-spark") {
475
501
  model.contextWindow = 272000;
@@ -582,6 +608,9 @@ function inferOpenAISupportedEfforts(model: OpenAIModel): readonly Effort[] {
582
608
  if (model.variant === "codex-mini" && semverEqual(model.version, "5.1")) {
583
609
  return GPT_5_1_CODEX_MINI_EFFORTS;
584
610
  }
611
+ if (semverGte(model.version, "5.6")) {
612
+ return GPT_5_6_PLUS_EFFORTS;
613
+ }
585
614
  if (semverGte(model.version, "5.2")) {
586
615
  return GPT_5_2_PLUS_EFFORTS;
587
616
  }
@@ -714,7 +743,10 @@ function parseAnthropicModel(modelId: string): AnthropicModel | null {
714
743
  }
715
744
 
716
745
  function parseOpenAIModel(modelId: string): OpenAIModel | null {
717
- const match = /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|mini|max|nano))?$/.exec(modelId);
746
+ const match =
747
+ /gpt-(\d+(?:\.\d+){0,2})(?:-(codex-spark|codex-mini|codex-max|codex|luna|mini|max|nano|sol|terra))?$/.exec(
748
+ modelId,
749
+ );
718
750
  if (!match) {
719
751
  return null;
720
752
  }
package/src/models.ts CHANGED
@@ -1,26 +1,46 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { isRetiredModelKey } from "./model-retirements";
1
3
  import { applyGeneratedModelPolicies, enrichModelThinking } from "./model-thinking";
2
- import MODELS from "./models.json" with { type: "json" };
4
+ // `with { type: "file" }` is embedded by `bun build --compile` and resolves to
5
+ // the bunfs path inside standalone binaries (and to the on-disk path in dev).
6
+ // A plain `createRequire` of a `.json` listed as an extra compile entrypoint is
7
+ // NOT emitted into the bunfs, and its cwd-fallback masks the failure whenever
8
+ // the process runs inside a repo checkout — see PR body for the minimal repro.
9
+ import modelsJsonPath from "./models.json" with { type: "file" };
3
10
  import type { Api, KnownProvider, Model, Usage } from "./types";
4
11
  import { isClaudeForcedToolChoiceIncapableModelId } from "./utils/tool-choice-capability";
5
12
 
6
13
  /**
7
- * Static bundled model registry loaded from `models.json`.
14
+ * Static bundled model registry loaded lazily from `models.json`.
8
15
  *
9
16
  * This module intentionally exposes compile-time defaults only.
10
17
  * It does not include runtime discovery, models.dev overlays, or on-disk cache state.
11
18
  *
12
19
  * For runtime-aware resolution, use `createModelManager()` / `resolveProviderModels()`.
13
20
  */
14
- const providerNames = Object.keys(MODELS) as KnownProvider[];
21
+ type BundledCatalog = typeof import("./models.json");
22
+
23
+ let bundledCatalog: BundledCatalog | undefined;
24
+ let providerNames: KnownProvider[] | undefined;
15
25
  const providerModelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
16
26
 
27
+ function getBundledCatalog(): BundledCatalog {
28
+ // TS types a .json import as its contents; at runtime `with { type: "file" }`
29
+ // yields the file path (bunfs path in compiled binaries, disk path in dev).
30
+ bundledCatalog ??= JSON.parse(readFileSync(modelsJsonPath as unknown as string, "utf8")) as BundledCatalog;
31
+ return bundledCatalog;
32
+ }
33
+
17
34
  function getProviderModels(provider: GeneratedProvider): Map<string, Model<Api>> | undefined {
18
35
  const cached = providerModelRegistry.get(provider);
19
36
  if (cached) return cached;
20
- const models = MODELS[provider];
37
+ const models = getBundledCatalog()[provider];
21
38
  if (!models) return undefined;
22
39
  const providerModels = new Map<string, Model<Api>>();
23
40
  for (const [id, model] of Object.entries(models)) {
41
+ if (isRetiredModelKey(provider, id)) {
42
+ continue;
43
+ }
24
44
  providerModels.set(id, applyBundledCompatDefaults(enrichModelThinking(model as Model<Api>)));
25
45
  }
26
46
  providerModelRegistry.set(provider, providerModels);
@@ -52,7 +72,7 @@ function applyBundledCompatDefaults(model: Model<Api>): Model<Api> {
52
72
  return policyModels[0] ?? normalized;
53
73
  }
54
74
 
55
- export type GeneratedProvider = keyof typeof MODELS;
75
+ export type GeneratedProvider = keyof BundledCatalog;
56
76
 
57
77
  export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvider, modelId: string): Model<TApi> {
58
78
  const providerModels = getProviderModels(provider);
@@ -62,6 +82,7 @@ export function getBundledModel<TApi extends Api = Api>(provider: GeneratedProvi
62
82
  export function getBundledProviders(): KnownProvider[] {
63
83
  // Defensive copy: the old eager path returned a fresh Array.from(...), so
64
84
  // callers may freely mutate their result without corrupting enumeration.
85
+ providerNames ??= Object.keys(getBundledCatalog()) as KnownProvider[];
65
86
  return providerNames.slice();
66
87
  }
67
88
 
@@ -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;
@@ -586,15 +586,12 @@ const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "t
586
586
  export const applyClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
587
587
  if (!prefixOverride) return name;
588
588
  if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
589
- const prefix = prefixOverride.toLowerCase();
590
- if (name.toLowerCase().startsWith(prefix)) return name;
591
589
  return `${prefixOverride}${name}`;
592
590
  };
593
591
 
594
592
  export const stripClaudeToolPrefix = (name: string, prefixOverride: string = claudeToolPrefix) => {
595
593
  if (!prefixOverride) return name;
596
- const prefix = prefixOverride.toLowerCase();
597
- if (!name.toLowerCase().startsWith(prefix)) return name;
594
+ if (!name.startsWith(prefixOverride)) return name;
598
595
  return name.slice(prefixOverride.length);
599
596
  };
600
597
 
@@ -1325,6 +1322,25 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1325
1322
  | (ToolCall & { partialJson: string })
1326
1323
  ) & { index: number };
1327
1324
  const blocks = output.content as Block[];
1325
+ const blocksByAnthropicIndex = new Map<number, Block>();
1326
+ const getBlockByAnthropicIndex = (anthropicIndex: number) => {
1327
+ const block = blocksByAnthropicIndex.get(anthropicIndex);
1328
+ if (!block) return { block: undefined, contentIndex: -1 };
1329
+ return { block, contentIndex: blocks.indexOf(block) };
1330
+ };
1331
+ const trackBlockByAnthropicIndex = (anthropicIndex: number, block: Block) => {
1332
+ // A duplicate start for an active index is a provider-envelope violation;
1333
+ // finalize the orphaned block so no internal stream fields leak into output.
1334
+ const orphaned = blocksByAnthropicIndex.get(anthropicIndex);
1335
+ if (orphaned) {
1336
+ if (orphaned.type === "toolCall" && orphaned.partialJson.trim()) {
1337
+ orphaned.arguments = parseStreamingJson(orphaned.partialJson);
1338
+ }
1339
+ delete (orphaned as { index?: number }).index;
1340
+ delete (orphaned as { partialJson?: string }).partialJson;
1341
+ }
1342
+ blocksByAnthropicIndex.set(anthropicIndex, block);
1343
+ };
1328
1344
  const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
1329
1345
  const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
1330
1346
  stream.push({ type: "start", partial: output });
@@ -1334,6 +1350,8 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1334
1350
  let providerRetryAttempt = 0;
1335
1351
  let thinkingRepairAttempted = false;
1336
1352
  while (true) {
1353
+ // Retries reset output.content; drop stale block correlations from the aborted attempt.
1354
+ blocksByAnthropicIndex.clear();
1337
1355
  activeAbortTracker = createAbortSourceTracker(options?.signal);
1338
1356
  const firstEventTimeoutAbortError = new Error(
1339
1357
  "Anthropic stream timed out while waiting for the first event",
@@ -1403,6 +1421,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1403
1421
  index: event.index,
1404
1422
  };
1405
1423
  output.content.push(block);
1424
+ trackBlockByAnthropicIndex(event.index, block);
1406
1425
  stream.push({
1407
1426
  type: "text_start",
1408
1427
  contentIndex: output.content.length - 1,
@@ -1416,6 +1435,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1416
1435
  index: event.index,
1417
1436
  };
1418
1437
  output.content.push(block);
1438
+ trackBlockByAnthropicIndex(event.index, block);
1419
1439
  stream.push({
1420
1440
  type: "thinking_start",
1421
1441
  contentIndex: output.content.length - 1,
@@ -1428,6 +1448,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1428
1448
  index: event.index,
1429
1449
  };
1430
1450
  output.content.push(block);
1451
+ trackBlockByAnthropicIndex(event.index, block);
1431
1452
  } else if (event.content_block.type === "tool_use") {
1432
1453
  streamedReplayUnsafeContent = true;
1433
1454
  const block: Block = {
@@ -1441,6 +1462,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1441
1462
  index: event.index,
1442
1463
  };
1443
1464
  output.content.push(block);
1465
+ trackBlockByAnthropicIndex(event.index, block);
1444
1466
  stream.push({
1445
1467
  type: "toolcall_start",
1446
1468
  contentIndex: output.content.length - 1,
@@ -1449,8 +1471,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1449
1471
  }
1450
1472
  } else if (event.type === "content_block_delta") {
1451
1473
  if (event.delta.type === "text_delta") {
1452
- const index = blocks.findIndex(b => b.index === event.index);
1453
- const block = blocks[index];
1474
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1454
1475
  if (block && block.type === "text") {
1455
1476
  block.text += event.delta.text;
1456
1477
  stream.push({
@@ -1461,8 +1482,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1461
1482
  });
1462
1483
  }
1463
1484
  } else if (event.delta.type === "thinking_delta") {
1464
- const index = blocks.findIndex(b => b.index === event.index);
1465
- const block = blocks[index];
1485
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1466
1486
  if (block && block.type === "thinking") {
1467
1487
  block.thinking += event.delta.thinking;
1468
1488
  stream.push({
@@ -1473,8 +1493,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1473
1493
  });
1474
1494
  }
1475
1495
  } else if (event.delta.type === "input_json_delta") {
1476
- const index = blocks.findIndex(b => b.index === event.index);
1477
- const block = blocks[index];
1496
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1478
1497
  if (block && block.type === "toolCall") {
1479
1498
  block.partialJson += event.delta.partial_json;
1480
1499
  block.arguments = parseStreamingJson(block.partialJson);
@@ -1486,17 +1505,16 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1486
1505
  });
1487
1506
  }
1488
1507
  } else if (event.delta.type === "signature_delta") {
1489
- const index = blocks.findIndex(b => b.index === event.index);
1490
- const block = blocks[index];
1508
+ const { block } = getBlockByAnthropicIndex(event.index);
1491
1509
  if (block && block.type === "thinking") {
1492
1510
  block.thinkingSignature = block.thinkingSignature || "";
1493
1511
  block.thinkingSignature += event.delta.signature;
1494
1512
  }
1495
1513
  }
1496
1514
  } else if (event.type === "content_block_stop") {
1497
- const index = blocks.findIndex(b => b.index === event.index);
1498
- const block = blocks[index];
1515
+ const { block, contentIndex: index } = getBlockByAnthropicIndex(event.index);
1499
1516
  if (block) {
1517
+ blocksByAnthropicIndex.delete(event.index);
1500
1518
  delete (block as { index?: number }).index;
1501
1519
  if (block.type === "text") {
1502
1520
  stream.push({
@@ -1513,7 +1531,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1513
1531
  partial: output,
1514
1532
  });
1515
1533
  } else if (block.type === "toolCall") {
1516
- block.arguments = parseStreamingJson(block.partialJson);
1534
+ if (block.partialJson.trim()) {
1535
+ block.arguments = parseStreamingJson(block.partialJson);
1536
+ }
1517
1537
  delete (block as { partialJson?: string }).partialJson;
1518
1538
  stream.push({
1519
1539
  type: "toolcall_end",
@@ -2243,7 +2263,11 @@ function buildParams(
2243
2263
  params.tools = convertTools(
2244
2264
  context.tools,
2245
2265
  isOAuthToken,
2246
- disableStrictTools || model.provider === "github-copilot",
2266
+ // The Claude Code OAuth surface mishandles `strict: true` tools:
2267
+ // streamed tool_use blocks arrive with empty/undefined arguments and
2268
+ // occasionally corrupted names (works with PI_NO_STRICT=1). Never
2269
+ // request strict tool use on OAuth requests.
2270
+ disableStrictTools || isOAuthToken || model.provider === "github-copilot",
2247
2271
  getAnthropicCompat(model).supportsEagerToolInputStreaming,
2248
2272
  );
2249
2273
  }
@@ -3,8 +3,13 @@
3
3
  * Uses the same format as the official Gemini CLI (v0.35+):
4
4
  * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
5
5
  */
6
+ export const GEMINI_CLI_VERSION_ENV = "SKC_AI_GEMINI_CLI_VERSION";
7
+ export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
8
+ export const DEFAULT_GEMINI_CLI_VERSION = "0.50.0";
9
+
6
10
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
7
- const version = process.env.SKC_AI_GEMINI_CLI_VERSION || process.env.PI_AI_GEMINI_CLI_VERSION || "0.49.0";
11
+ const version =
12
+ process.env[GEMINI_CLI_VERSION_ENV] || process.env[LEGACY_GEMINI_CLI_VERSION_ENV] || DEFAULT_GEMINI_CLI_VERSION;
8
13
  const platform = process.platform === "win32" ? "win32" : process.platform;
9
14
  const arch = process.arch === "x64" ? "x64" : process.arch;
10
15
  return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
@@ -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
  }