@sayknow-cli/ai 0.5.25 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.5.25] - 2026-09-21
5
+ ## [0.6.0] - 2026-09-24
6
+ ### Fixed
7
+ - Retire `anthropic/claude-3-haiku-20240307`, `claude-3-5-sonnet-20240620` and `claude-3-5-sonnet-20241022`: the Anthropic API answers 404 `not_found_error` for all three (measured 2026-09-23 with a forced tool call). Left in the catalog, the cheapest of them was the first pick for typed decisions on every registry with an Anthropic key.
8
+ - The on-disk model cache now filters retired keys on read. A row written before a retirement carried the dead entry for up to the TTL and the catalog merge brought it straight back.
9
+
10
+ ## [0.5.26] - 2026-09-23
11
+ ### Fixed
12
+ - API-key `/login` now upserts a second key instead of replacing the whole provider pool. Same-key re-login still reuses the existing row.
13
+ - Bundle `xai/grok-4.7` and use it as the xAI default without changing explicitly saved model choices. Preserve its `low`–`xhigh` reasoning range during catalog generation. Cost metadata records the standard (<200k prompt tokens) tariff; the existing cost engine does not represent long-context price tiers.
14
+ - Bundle `anthropic/claude-opus-5-5` (1M context, 128K output, vision, adaptive thinking). The provider default stays `claude-sonnet-4-6`: promoting a Sonnet default to an Opus would raise per-token cost for every user, so selecting Opus 5.5 stays explicit. Cost metadata records the standard tariff ($4/$20, cache read $0.20 at the Opus-5.5-specific 0.05x multiplier, 5-minute cache write $5); the cost engine represents neither the fast-mode tier ($8/$40) nor long-context tiers. Anthropic's server-side default effort (`medium`) is deliberately not encoded as `thinking.defaultLevel`, because `inferDefaultEffort` has no Anthropic rule and catalog regeneration would strip it — every sibling Opus entry omits it for the same reason.
6
15
 
7
16
  ## [0.5.12] - 2026-09-15
8
17
  ### Added
@@ -226,7 +226,7 @@ export interface AuthCredentialStore {
226
226
  }): Promise<void>;
227
227
  /**
228
228
  * Optional async write hook for upserting a single credential. When present,
229
- * `AuthStorage.#upsertOAuthCredential` routes through this instead of the
229
+ * `AuthStorage.#upsertStoredCredential` routes through this instead of the
230
230
  * sync `upsertAuthCredentialForProvider`. `RemoteAuthCredentialStore` uses
231
231
  * it to send the upsert to the broker via `POST /v1/credential`.
232
232
  *
@@ -236,10 +236,11 @@ export interface AuthCredentialStore {
236
236
  upsertAuthCredentialRemote?(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
237
237
  upsertAuthCredentialRemoteIfAbsent?(provider: string, credential: AuthCredential): Promise<AuthCredentialIfAbsentResult>;
238
238
  /**
239
- * Optional async write hook for replace-all semantics (e.g. API-key login
240
- * overwriting any previous keys for the same provider). When present,
239
+ * Optional async write hook for replace-all semantics (logout-style
240
+ * overwrite of every stored credential for a provider). When present,
241
241
  * `AuthStorage.set` routes through this instead of the sync
242
- * `replaceAuthCredentialsForProvider`.
242
+ * `replaceAuthCredentialsForProvider`. API-key login no longer uses this
243
+ * path: it upserts so a second account is added instead of wiping the pool.
243
244
  */
244
245
  replaceAuthCredentialsRemote?(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
245
246
  /**
@@ -1,4 +1,4 @@
1
- export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high"];
1
+ export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high", "anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-sonnet-20240620", "anthropic/claude-3-5-sonnet-20241022"];
2
2
  export declare function isRetiredModelKey(provider: string, modelId: string): boolean;
3
3
  export declare function isRetiredModel(model: {
4
4
  provider: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/ai",
4
- "version": "0.5.25",
4
+ "version": "0.6.0",
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.5.25",
46
+ "@sayknow-cli/utils": "0.6.0",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -387,7 +387,7 @@ export interface AuthCredentialStore {
387
387
  markCredentialSuspect?(credentialId: number, opts?: { signal?: AbortSignal }): Promise<void>;
388
388
  /**
389
389
  * Optional async write hook for upserting a single credential. When present,
390
- * `AuthStorage.#upsertOAuthCredential` routes through this instead of the
390
+ * `AuthStorage.#upsertStoredCredential` routes through this instead of the
391
391
  * sync `upsertAuthCredentialForProvider`. `RemoteAuthCredentialStore` uses
392
392
  * it to send the upsert to the broker via `POST /v1/credential`.
393
393
  *
@@ -400,10 +400,11 @@ export interface AuthCredentialStore {
400
400
  credential: AuthCredential,
401
401
  ): Promise<AuthCredentialIfAbsentResult>;
402
402
  /**
403
- * Optional async write hook for replace-all semantics (e.g. API-key login
404
- * overwriting any previous keys for the same provider). When present,
403
+ * Optional async write hook for replace-all semantics (logout-style
404
+ * overwrite of every stored credential for a provider). When present,
405
405
  * `AuthStorage.set` routes through this instead of the sync
406
- * `replaceAuthCredentialsForProvider`.
406
+ * `replaceAuthCredentialsForProvider`. API-key login no longer uses this
407
+ * path: it upserts so a second account is added instead of wiping the pool.
407
408
  */
408
409
  replaceAuthCredentialsRemote?(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
409
410
  /**
@@ -1550,7 +1551,7 @@ export class AuthStorage {
1550
1551
  };
1551
1552
  }
1552
1553
 
1553
- async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
1554
+ async #upsertStoredCredential(provider: string, credential: AuthCredential): Promise<void> {
1554
1555
  const stored = this.#store.upsertAuthCredentialRemote
1555
1556
  ? await this.#store.upsertAuthCredentialRemote(provider, credential)
1556
1557
  : this.#store.upsertAuthCredentialForProvider(provider, credential);
@@ -1562,6 +1563,17 @@ export class AuthStorage {
1562
1563
  this.#invalidateUsageCacheForProvider(provider);
1563
1564
  }
1564
1565
 
1566
+ async #dropStoredApiKeysForProvider(provider: string): Promise<void> {
1567
+ const remaining = this.#getCredentialsForProvider(provider).filter(credential => credential.type !== "api_key");
1568
+ if (remaining.length === this.#getCredentialsForProvider(provider).length) return;
1569
+ await this.set(provider, remaining);
1570
+ }
1571
+
1572
+ async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
1573
+ await this.#dropStoredApiKeysForProvider(provider);
1574
+ await this.#upsertStoredCredential(provider, credential);
1575
+ }
1576
+
1565
1577
  #invalidateUsageCacheForProvider(provider: string): void {
1566
1578
  this.#usageRequestInFlight.clear();
1567
1579
  this.#usageReportsInFlight.clear();
@@ -1696,7 +1708,7 @@ export class AuthStorage {
1696
1708
  let credentials: OAuthCredentials;
1697
1709
  const saveApiKeyCredential = async (apiKey: string): Promise<void> => {
1698
1710
  const newCredential: ApiKeyCredential = { type: "api_key", key: apiKey };
1699
- await this.set(provider, newCredential);
1711
+ await this.#upsertStoredCredential(provider, newCredential);
1700
1712
  };
1701
1713
  const manualCodeInput = () => ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" });
1702
1714
  switch (provider) {
@@ -2048,13 +2060,6 @@ export class AuthStorage {
2048
2060
  }
2049
2061
  }
2050
2062
  const newCredential: OAuthCredential = { type: "oauth", ...credentials };
2051
- if (provider === "xai") {
2052
- const existingOAuthCredentials = this.#getCredentialsForProvider(provider).filter(
2053
- (credential): credential is OAuthCredential => credential.type === "oauth",
2054
- );
2055
- await this.set(provider, [...existingOAuthCredentials, newCredential]);
2056
- return;
2057
- }
2058
2063
  await this.#upsertOAuthCredential(provider, newCredential);
2059
2064
  }
2060
2065
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { Database } from "bun:sqlite";
6
6
  import { getModelDbPath } from "@sayknow-cli/utils/dirs";
7
+ import { isRetiredModelKey } from "./model-retirements";
7
8
  import type { Api, Model } from "./types";
8
9
 
9
10
  const CACHE_SCHEMA_VERSION = 3;
@@ -96,7 +97,12 @@ export function readModelCache<TApi extends Api>(
96
97
  if (!row || row.version !== CACHE_SCHEMA_VERSION) {
97
98
  return null;
98
99
  }
99
- const models = JSON.parse(row.models) as Model<TApi>[];
100
+ // A row written before a model was retired still carries it for up to the TTL;
101
+ // the bundled catalog filters retirements, so the cache must too or the merge
102
+ // brings the dead entry straight back.
103
+ const models = (JSON.parse(row.models) as Model<TApi>[]).filter(
104
+ model => !isRetiredModelKey(model.provider ?? providerId, model.id),
105
+ );
100
106
  const ageMs = now() - row.updated_at;
101
107
  const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
102
108
  return {
@@ -1,6 +1,18 @@
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;
1
+ // Retired from advertised catalogs because the provider rejects live calls.
2
+ //
3
+ // google-antigravity/gemini-3.1-pro-high: Cloud Code Assist answers HTTP 400. The
4
+ // callable high-thinking path is gemini-3.1-pro-low:high.
5
+ //
6
+ // anthropic/claude-3-*: the Anthropic API answers 404 `not_found_error` for all three
7
+ // (measured 2026-09-23 with a forced tool call). Left in the catalog, the cheapest of
8
+ // them ($0.25/MTok) was the first pick for typed decisions, so the "small hosted model"
9
+ // fallback silently answered nothing on every registry that had an Anthropic key.
10
+ export const RETIRED_MODEL_KEYS = [
11
+ "google-antigravity/gemini-3.1-pro-high",
12
+ "anthropic/claude-3-haiku-20240307",
13
+ "anthropic/claude-3-5-sonnet-20240620",
14
+ "anthropic/claude-3-5-sonnet-20241022",
15
+ ] as const;
4
16
 
5
17
  const RETIRED_MODEL_KEY_SET = new Set<string>(RETIRED_MODEL_KEYS);
6
18
 
@@ -475,12 +475,12 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
475
475
  if (model.provider !== "opencode-go" && model.id === "minimax-m3") {
476
476
  model.contextWindow = 512_000;
477
477
  }
478
- // xAI Grok 4.5/4.6: official reasoning docs (docs.x.ai) expose
479
- // low/medium/high, with xhigh only on grok-4.6+. Catalog snapshots and
478
+ // xAI Grok 4.5/4.6/4.7: official reasoning docs (docs.x.ai) expose
479
+ // low/medium/high, with xhigh on the reviewed 4.6 and 4.7 models. Catalog snapshots and
480
480
  // openai-compat historically clamped everything to high because
481
481
  // supportsReasoningEffort was false for Grok; pin the documented ranges so
482
482
  // profile suffixes and requireSupportedEffort stay honest across regen.
483
- if (model.provider === "xai" && /^grok-4\.6(?:$|[-.])/.test(model.id)) {
483
+ if (model.provider === "xai" && /^grok-4\.(?:6|7)(?:$|[-.])/.test(model.id)) {
484
484
  if (model.thinking) {
485
485
  model.thinking = {
486
486
  ...model.thinking,
package/src/models.json CHANGED
@@ -5005,6 +5005,31 @@
5005
5005
  "maxLevel": "max"
5006
5006
  }
5007
5007
  },
5008
+ "claude-opus-5-5": {
5009
+ "id": "claude-opus-5-5",
5010
+ "name": "Anthropic Opus 5.5",
5011
+ "api": "anthropic-messages",
5012
+ "provider": "anthropic",
5013
+ "baseUrl": "https://api.anthropic.com",
5014
+ "reasoning": true,
5015
+ "input": [
5016
+ "text",
5017
+ "image"
5018
+ ],
5019
+ "cost": {
5020
+ "input": 4,
5021
+ "output": 20,
5022
+ "cacheRead": 0.2,
5023
+ "cacheWrite": 5
5024
+ },
5025
+ "contextWindow": 1000000,
5026
+ "maxTokens": 128000,
5027
+ "thinking": {
5028
+ "mode": "anthropic-adaptive",
5029
+ "minLevel": "minimal",
5030
+ "maxLevel": "max"
5031
+ }
5032
+ },
5008
5033
  "claude-sonnet-4-0": {
5009
5034
  "id": "claude-sonnet-4-0",
5010
5035
  "name": "Anthropic Sonnet 4 (latest)",
@@ -90861,6 +90886,32 @@
90861
90886
  "defaultLevel": "high"
90862
90887
  }
90863
90888
  },
90889
+ "grok-4.7": {
90890
+ "id": "grok-4.7",
90891
+ "name": "Grok 4.7",
90892
+ "api": "openai-completions",
90893
+ "provider": "xai",
90894
+ "baseUrl": "https://api.x.ai/v1",
90895
+ "reasoning": true,
90896
+ "input": [
90897
+ "text",
90898
+ "image"
90899
+ ],
90900
+ "cost": {
90901
+ "input": 2,
90902
+ "output": 6,
90903
+ "cacheRead": 0.5,
90904
+ "cacheWrite": 0
90905
+ },
90906
+ "contextWindow": 500000,
90907
+ "maxTokens": 500000,
90908
+ "thinking": {
90909
+ "mode": "effort",
90910
+ "minLevel": "low",
90911
+ "maxLevel": "xhigh",
90912
+ "defaultLevel": "high"
90913
+ }
90914
+ },
90864
90915
  "grok-beta": {
90865
90916
  "id": "grok-beta",
90866
90917
  "name": "Grok Beta",
@@ -166,7 +166,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
166
166
  config => fuguModelManagerOptions(config),
167
167
  catalog("Sakana Fugu", ["FUGU_API_KEY"]),
168
168
  ),
169
- descriptor("xai", "grok-4.6", config => xaiModelManagerOptions(config)),
169
+ descriptor("xai", "grok-4.7", config => xaiModelManagerOptions(config)),
170
170
  catalogDescriptor(
171
171
  "deepseek",
172
172
  "deepseek-v4-pro",