@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 +10 -1
- package/dist/types/auth-storage.d.ts +5 -4
- package/dist/types/model-retirements.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-storage.ts +18 -13
- package/src/model-cache.ts +7 -1
- package/src/model-retirements.ts +15 -3
- package/src/model-thinking.ts +3 -3
- package/src/models.json +51 -0
- package/src/provider-models/descriptors.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
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.#
|
|
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 (
|
|
240
|
-
*
|
|
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.
|
|
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.
|
|
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"
|
package/src/auth-storage.ts
CHANGED
|
@@ -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.#
|
|
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 (
|
|
404
|
-
*
|
|
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 #
|
|
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
|
|
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
|
|
package/src/model-cache.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/model-retirements.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
// Retired from advertised catalogs because
|
|
2
|
-
//
|
|
3
|
-
|
|
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
|
|
package/src/model-thinking.ts
CHANGED
|
@@ -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
|
|
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.
|
|
169
|
+
descriptor("xai", "grok-4.7", config => xaiModelManagerOptions(config)),
|
|
170
170
|
catalogDescriptor(
|
|
171
171
|
"deepseek",
|
|
172
172
|
"deepseek-v4-pro",
|