pi-hypercharm-provider 1.1.3 → 1.1.5
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/AGENTS.md +1 -0
- package/README.md +37 -48
- package/deprecated-models.json +1 -0
- package/index.ts +100 -78
- package/models.json +462 -104
- package/package.json +1 -1
- package/patch.json +1 -155
- package/scripts/update-models.js +136 -99
package/AGENTS.md
CHANGED
|
@@ -7,6 +7,7 @@ The following files are **idempotent** and regenerated by `scripts/update-models
|
|
|
7
7
|
| File | Why it's auto-generated |
|
|
8
8
|
|------|------------------------|
|
|
9
9
|
| `models.json` | Built from the provider API. `update-models.js` fetches models, preserves curated data for known IDs, and writes this file. |
|
|
10
|
+
| `deprecated-models.json` | Graveyard for models the API delisted. update-models.js stamps them with deprecatedAt and pi keeps serving them for a 2-week grace period, then evicts them. Never edit by hand. |
|
|
10
11
|
| `README.md` (model table) | The table under `## Available Models` is replaced in-place by `update-models.js` after merging base models → patch → custom models. |
|
|
11
12
|
|
|
12
13
|
## Correct Files to Edit
|
package/README.md
CHANGED
|
@@ -15,37 +15,41 @@ _Hyperoptimized coding models — DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, GPT
|
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **
|
|
19
|
-
- **DeepSeek Native Thinking** — Uses the `deepseek` thinking format with `reasoning_effort`
|
|
18
|
+
- **24+ AI Models** including DeepSeek V4 Flash/Pro, GLM 5/5.1, Kimi K2.5/K2.6, Qwen3.6/3.7, MiniMax M2.7, Gemma 4, GPT-OSS, and Llama
|
|
19
|
+
- **DeepSeek Native Thinking** — Uses the `deepseek` thinking format for Charm Hyper requests, with native `reasoning_effort` on models that publish levels
|
|
20
20
|
- **OpenAI-compatible API** via Charm Hyper's `/v1/chat/completions` endpoint
|
|
21
|
-
- **
|
|
22
|
-
- **Reasoning Models** with
|
|
23
|
-
- **
|
|
21
|
+
- **Official Catalog Sync** from Charm's typed `/v1/provider` endpoint, matching `@charmland/pi-hyper-provider`
|
|
22
|
+
- **Reasoning Models** with provider-published on/off states and exact effort levels
|
|
23
|
+
- **Attachment Support** for models the official catalog marks as attachment-capable
|
|
24
24
|
|
|
25
25
|
## Available Models
|
|
26
26
|
|
|
27
27
|
| Model | Type | Context | Max Tokens | Input Cost | Output Cost |
|
|
28
28
|
|-------|------|---------|------------|------------|-------------|
|
|
29
29
|
| DeepSeek V4 Flash | Text | 1.0M | 384K | $0.20 | $0.40 |
|
|
30
|
+
| DeepSeek V4 Flash 0731 | Text | 1.0M | 384K | $0.15 | $0.30 |
|
|
30
31
|
| DeepSeek V4 Pro | Text | 1.0M | 384K | $2.40 | $4.80 |
|
|
31
|
-
| Gemma 4 26B A4B | Text
|
|
32
|
-
| GLM
|
|
33
|
-
| GLM-5 | Text | 203K |
|
|
32
|
+
| Gemma 4 26B A4B | Text | 256K | 26K | $0.13 | $0.43 |
|
|
33
|
+
| GLM-5 | Text | 203K | 20K | $0.85 | $2.62 |
|
|
34
|
+
| GLM-5.1 | Text | 203K | 131K | $1.52 | $4.79 |
|
|
34
35
|
| GLM-5.2 | Text | 1.0M | 128K | $1.40 | $4.40 |
|
|
35
|
-
|
|
|
36
|
-
| Kimi K2.5 | Text
|
|
36
|
+
| gpt-oss-120b | Text | 131K | 13K | $0.19 | $0.70 |
|
|
37
|
+
| Kimi K2.5 | Text | 262K | 26K | $0.56 | $2.82 |
|
|
37
38
|
| Kimi K2.6 | Text + Image | 262K | 262K | $0.95 | $4.00 |
|
|
38
|
-
| Kimi K2.7 Code | Text | 256K | 16K | $0.95 | $4.00 |
|
|
39
|
-
|
|
|
40
|
-
| Llama
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
39
|
+
| Kimi K2.7 Code | Text + Image | 256K | 16K | $0.95 | $4.00 |
|
|
40
|
+
| Kimi K3 | Text + Image | 1.0M | 131K | $3.27 | $16.33 |
|
|
41
|
+
| Llama 3.3 70B Instruct | Text | 128K | 13K | $0.51 | $1.04 |
|
|
42
|
+
| Llama 4 Maverick 17B 128E Instruct FP8 | Text | 430K | 43K | $0.28 | $0.93 |
|
|
43
|
+
| MiniMax M2.7 | Text | 205K | 20K | $0.44 | $1.72 |
|
|
44
|
+
| MiniMax M3 | Text + Image | 512K | 512K | $0.33 | $1.31 |
|
|
45
|
+
| Qwen3 Coder 480B A35B Instruct INT4 Mixed AR | Text | 106K | 11K | $0.57 | $2.13 |
|
|
46
|
+
| Qwen3 Next 80B A3B Instruct | Text | 262K | 26K | $0.12 | $1.14 |
|
|
44
47
|
| Qwen3.6-Flash | Text + Image | 1.0M | 64K | $1.00 | $4.00 |
|
|
45
|
-
| Qwen3.6-Max | Text
|
|
48
|
+
| Qwen3.6-Max | Text | 256K | 64K | $2.00 | $12.00 |
|
|
46
49
|
| Qwen3.6-Plus | Text + Image | 1.0M | 64K | $2.00 | $6.00 |
|
|
47
|
-
| Qwen3.7-
|
|
48
|
-
| Qwen3.7-
|
|
50
|
+
| Qwen3.7-Flash | Text + Image | 1.0M | 64K | $0.20 | $0.80 |
|
|
51
|
+
| Qwen3.7-Max | Text | 1.0M | 64K | $2.50 | $7.50 |
|
|
52
|
+
| Qwen3.7-Plus | Text + Image | 1.0M | 64K | $1.20 | $4.80 |
|
|
49
53
|
*Costs are per million tokens. Prices subject to change — check [hyper.charm.land](https://hyper.charm.land) for current pricing.*
|
|
50
54
|
|
|
51
55
|
## Installation
|
|
@@ -115,7 +119,7 @@ For reasoning models that support `reasoning_effort`, control thinking depth:
|
|
|
115
119
|
pi --provider hypercharm --model deepseek-v4-pro --reasoning-effort max
|
|
116
120
|
```
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
Supported levels are model-specific and come from Charm's `/v1/provider` catalog (for example `low`/`medium`/`high` on Kimi K2.6). Models with no published levels support the Hyper thinking on state through Pi's `max` level.
|
|
119
123
|
|
|
120
124
|
### Thinking Mode
|
|
121
125
|
|
|
@@ -153,37 +157,21 @@ Add to your pi configuration for automatic loading:
|
|
|
153
157
|
}
|
|
154
158
|
```
|
|
155
159
|
|
|
156
|
-
### Compat Settings
|
|
160
|
+
### Catalog and Compat Settings
|
|
157
161
|
|
|
158
|
-
|
|
162
|
+
Model metadata matches Charm's official [`@charmland/pi-hyper-provider`](https://github.com/charmbracelet/pi-hyper-provider) transform:
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
- canonical `/v1/provider` names, prices, context windows, output caps, attachment flags, and `can_reason`
|
|
165
|
+
- `thinkingFormat: "deepseek"`, which maps Pi thinking levels onto Hyper's `thinking` envelope
|
|
166
|
+
- `reasoning_effort` only when the catalog publishes concrete level names
|
|
167
|
+
- `maxTokensField: "max_tokens"` and `supportsStore: false`
|
|
168
|
+
- zero `cacheWrite`, because Hyper reports discounted cached-output pricing, not cache-write cost
|
|
161
169
|
|
|
162
|
-
|
|
163
|
-
- **`supportsReasoningEffort: true`** — Supports `reasoning_effort: "high" | "max"` via `thinkingLevelMap`
|
|
164
|
-
- **`requiresReasoningContentOnAssistantMessages: true`** — Replayed assistant messages include empty `reasoning_content` when reasoning is enabled
|
|
165
|
-
- **`maxTokensField: "max_tokens"`** — Uses `max_tokens` (not `max_completion_tokens`)
|
|
166
|
-
|
|
167
|
-
**Other reasoning models** (GLM, Kimi, Qwen, MiniMax, Gemma, GPT-OSS) use the `openai` thinking format:
|
|
168
|
-
|
|
169
|
-
- **`thinkingFormat: "openai"`** — Returns `reasoning_content` in OpenAI format
|
|
170
|
-
- **`supportsReasoningEffort: true`** — Accepts `reasoning_effort` parameter on supported models
|
|
171
|
-
- **`maxTokensField: "max_tokens"`** — Uses `max_tokens` (not `max_completion_tokens`)
|
|
172
|
-
- **`supportsDeveloperRole: true`** — Developer role messages are accepted
|
|
173
|
-
- **`supportsStore: false`** — The `store` parameter is not supported
|
|
174
|
-
|
|
175
|
-
> **Note:** The HyperCharm `/v1/models` endpoint reports `supports_reasoning: true` for all models,
|
|
176
|
-
> including those that don't natively support extended thinking (e.g., Llama 3.3 70B). `models.json`
|
|
177
|
-
> curates reasoning flags based on known model capabilities; `patch.json` adds compat overrides.
|
|
170
|
+
`patch.json` is reserved only for a verified provider regression and is currently empty.
|
|
178
171
|
|
|
179
172
|
### Patch Overrides
|
|
180
173
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
- Correcting API-derived values (e.g., DeepSeek `maxTokens` from 8000 to 384000)
|
|
184
|
-
- Adding compat settings that the API doesn't provide
|
|
185
|
-
- Setting `thinkingFormat: "deepseek"` + `thinkingLevelMap` for DeepSeek V4 models
|
|
186
|
-
- Overriding pricing when official rates change
|
|
174
|
+
`patch.json` is applied on top of `models.json` only for verified endpoint corrections. It is currently empty because every live field comes from Charm's canonical provider catalog.
|
|
187
175
|
|
|
188
176
|
## Updating Models
|
|
189
177
|
|
|
@@ -196,10 +184,11 @@ node scripts/update-models.js
|
|
|
196
184
|
|
|
197
185
|
This will:
|
|
198
186
|
1. Fetch models from `https://hyper.charm.land/v1/models`
|
|
199
|
-
2.
|
|
200
|
-
3. Apply overrides from `patch.json`
|
|
187
|
+
2. Regenerate `models.json` as pure metadata from Charm's typed `/v1/provider` catalog
|
|
188
|
+
3. Apply overrides from `patch.json` only when building the README
|
|
201
189
|
4. Remove custom models now available upstream from `custom-models.json`
|
|
202
|
-
5.
|
|
190
|
+
5. Reconcile delisted models through the 14-day `deprecated-models.json` grace layer
|
|
191
|
+
6. Update `models.json` and the README model table
|
|
203
192
|
|
|
204
193
|
A GitHub Actions workflow runs this daily and creates a PR if models have changed.
|
|
205
194
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/index.ts
CHANGED
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|
* Registers HyperCharm (hyper.charm.land) as a custom provider using the
|
|
5
5
|
* openai-completions API. Base URL: https://hyper.charm.land/v1
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* pricing, context
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* it reports true for Llama 3.3 70B which doesn't support extended thinking).
|
|
13
|
-
* The models.json embeds curated reasoning flags; patch.json corrects compat.
|
|
7
|
+
* Model metadata comes from Charm's typed official-catalog endpoint,
|
|
8
|
+
* /v1/provider, matching @charmland/pi-hyper-provider. It provides canonical
|
|
9
|
+
* names, pricing, context and output limits, reasoning levels, and attachment
|
|
10
|
+
* support. patch.json remains available for verified endpoint regressions, but
|
|
11
|
+
* currently contains no overrides.
|
|
14
12
|
*
|
|
15
13
|
* Model resolution strategy: Stale-While-Revalidate
|
|
16
14
|
* 1. Serve stale immediately: disk cache → embedded models.json (zero-latency)
|
|
@@ -39,6 +37,7 @@ import { getAgentDir, type ExtensionAPI, type ModelRegistry } from "@earendil-wo
|
|
|
39
37
|
import modelsData from "./models.json" with { type: "json" };
|
|
40
38
|
import customModelsData from "./custom-models.json" with { type: "json" };
|
|
41
39
|
import patchData from "./patch.json" with { type: "json" };
|
|
40
|
+
import deprecatedData from "./deprecated-models.json" with { type: "json" };
|
|
42
41
|
import fs from "fs";
|
|
43
42
|
import path from "path";
|
|
44
43
|
|
|
@@ -113,6 +112,9 @@ function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
|
|
|
113
112
|
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
114
113
|
delete result.compat.thinkingFormat;
|
|
115
114
|
}
|
|
115
|
+
if (!result.reasoning && result.thinkingLevelMap) {
|
|
116
|
+
delete result.thinkingLevelMap;
|
|
117
|
+
}
|
|
116
118
|
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
117
119
|
delete result.compat;
|
|
118
120
|
}
|
|
@@ -124,7 +126,10 @@ function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
|
|
|
124
126
|
function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData): JsonModel[] {
|
|
125
127
|
const modelMap = new Map<string, JsonModel>();
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
// Seed with the base list plus grace-period deprecated models so patch.json
|
|
130
|
+
// entries apply to deprecated models exactly as while the model was live
|
|
131
|
+
// (withDeprecated keeps live data on id conflicts).
|
|
132
|
+
for (const model of withDeprecated(base)) {
|
|
128
133
|
modelMap.set(model.id, model);
|
|
129
134
|
}
|
|
130
135
|
|
|
@@ -149,81 +154,76 @@ function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData):
|
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// Ensure DeepSeek reasoning models have required compat settings.
|
|
155
|
-
// Live-fetched models from the SWR pipeline may not have these set.
|
|
156
|
-
for (const model of result) {
|
|
157
|
-
if (!model.reasoning) continue;
|
|
158
|
-
if (isDeepSeekModel(model.id)) {
|
|
159
|
-
if (!model.compat) {
|
|
160
|
-
model.compat = {
|
|
161
|
-
thinkingFormat: "deepseek",
|
|
162
|
-
maxTokensField: "max_tokens",
|
|
163
|
-
supportsDeveloperRole: true,
|
|
164
|
-
supportsStore: false,
|
|
165
|
-
supportsReasoningEffort: true,
|
|
166
|
-
requiresReasoningContentOnAssistantMessages: true,
|
|
167
|
-
};
|
|
168
|
-
} else {
|
|
169
|
-
if (model.compat.thinkingFormat === undefined) {
|
|
170
|
-
model.compat.thinkingFormat = "deepseek";
|
|
171
|
-
}
|
|
172
|
-
if (model.compat.supportsReasoningEffort === undefined) {
|
|
173
|
-
model.compat.supportsReasoningEffort = true;
|
|
174
|
-
}
|
|
175
|
-
if ((model.compat as any).requiresReasoningContentOnAssistantMessages === undefined) {
|
|
176
|
-
(model.compat as any).requiresReasoningContentOnAssistantMessages = true;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (!model.thinkingLevelMap) {
|
|
180
|
-
model.thinkingLevelMap = {
|
|
181
|
-
minimal: null, low: null, medium: null, high: "high", max: "max",
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return result;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function isDeepSeekModel(id: string): boolean {
|
|
191
|
-
return /^deepseek-v/.test(id);
|
|
157
|
+
return Array.from(modelMap.values());
|
|
192
158
|
}
|
|
193
159
|
|
|
194
160
|
// ─── Stale-While-Revalidate Model Sync ────────────────────────────────────────
|
|
195
161
|
|
|
196
162
|
const PROVIDER_ID = "hypercharm";
|
|
197
163
|
const BASE_URL = "https://hyper.charm.land/v1";
|
|
198
|
-
const MODELS_URL = `${BASE_URL}/
|
|
164
|
+
const MODELS_URL = `${BASE_URL}/provider`;
|
|
199
165
|
const CACHE_DIR = path.join(getAgentDir(), "cache");
|
|
200
166
|
const CACHE_PATH = path.join(CACHE_DIR, `${PROVIDER_ID}-models.json`);
|
|
201
167
|
const LIVE_FETCH_TIMEOUT_MS = 8000;
|
|
202
168
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
169
|
+
const PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
170
|
+
|
|
171
|
+
const ON_OFF_THINKING_LEVEL_MAP: Record<string, string | null> = {
|
|
172
|
+
off: "off",
|
|
173
|
+
minimal: null,
|
|
174
|
+
low: null,
|
|
175
|
+
medium: null,
|
|
176
|
+
high: null,
|
|
177
|
+
xhigh: null,
|
|
178
|
+
max: "max",
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function buildThinkingLevelMap(levels: string[]): Record<string, string | null> | undefined {
|
|
182
|
+
if (levels.length === 0) return undefined;
|
|
183
|
+
const available = new Set(levels);
|
|
184
|
+
const result: Record<string, string | null> = {
|
|
185
|
+
off: available.has("off") ? "off" : available.has("none") ? "none" : null,
|
|
212
186
|
};
|
|
187
|
+
for (const level of PI_THINKING_LEVELS) {
|
|
188
|
+
result[level] = available.has(level) ? level : null;
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Transform a model from Charm's official typed Hyper /v1/provider catalog. */
|
|
194
|
+
function transformApiModel(apiModel: any): JsonModel | null {
|
|
195
|
+
if (typeof apiModel.id !== "string" || apiModel.id.length === 0) return null;
|
|
196
|
+
|
|
197
|
+
const reasoningLevels = Array.isArray(apiModel.reasoning_levels)
|
|
198
|
+
? apiModel.reasoning_levels.filter((level: any) => typeof level === "string")
|
|
199
|
+
: [];
|
|
200
|
+
const supportsReasoningEffort = reasoningLevels.length > 0;
|
|
201
|
+
const thinkingLevelMap = supportsReasoningEffort
|
|
202
|
+
? buildThinkingLevelMap(reasoningLevels)
|
|
203
|
+
: apiModel.can_reason === true
|
|
204
|
+
? ON_OFF_THINKING_LEVEL_MAP
|
|
205
|
+
: undefined;
|
|
213
206
|
|
|
214
207
|
return {
|
|
215
208
|
id: apiModel.id,
|
|
216
|
-
name: apiModel.
|
|
217
|
-
reasoning:
|
|
218
|
-
|
|
209
|
+
name: apiModel.name || apiModel.id,
|
|
210
|
+
reasoning: apiModel.can_reason === true,
|
|
211
|
+
thinkingLevelMap,
|
|
212
|
+
input: apiModel.supports_attachments === true ? ["text", "image"] : ["text"],
|
|
219
213
|
cost: {
|
|
220
|
-
input:
|
|
221
|
-
output:
|
|
222
|
-
cacheRead:
|
|
223
|
-
cacheWrite:
|
|
214
|
+
input: apiModel.cost_per_1m_in || 0,
|
|
215
|
+
output: apiModel.cost_per_1m_out || 0,
|
|
216
|
+
cacheRead: apiModel.cost_per_1m_in_cached || 0,
|
|
217
|
+
cacheWrite: 0,
|
|
224
218
|
},
|
|
225
219
|
contextWindow: apiModel.context_window || 0,
|
|
226
|
-
maxTokens: apiModel.
|
|
220
|
+
maxTokens: apiModel.default_max_tokens || apiModel.context_window || 0,
|
|
221
|
+
compat: {
|
|
222
|
+
supportsStore: false,
|
|
223
|
+
supportsReasoningEffort,
|
|
224
|
+
thinkingFormat: "deepseek",
|
|
225
|
+
maxTokensField: "max_tokens",
|
|
226
|
+
},
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -235,7 +235,7 @@ async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<Js
|
|
|
235
235
|
});
|
|
236
236
|
if (!response.ok) return null;
|
|
237
237
|
const data = await response.json();
|
|
238
|
-
const apiModels = Array.isArray(data) ? data : (data.data || []);
|
|
238
|
+
const apiModels = Array.isArray(data) ? data : (data.models || data.data || []);
|
|
239
239
|
if (!Array.isArray(apiModels) || apiModels.length === 0) return null;
|
|
240
240
|
return apiModels.map(transformApiModel).filter((m): m is JsonModel => m !== null);
|
|
241
241
|
} catch {
|
|
@@ -269,20 +269,13 @@ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[])
|
|
|
269
269
|
const embedded = embeddedMap.get(liveModel.id);
|
|
270
270
|
seen.add(liveModel.id);
|
|
271
271
|
if (embedded) {
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
// providers whose /models endpoint exposes no pricing keep their curated
|
|
276
|
-
// cost. Curation (reasoning/input/compat/name) still wins via ...embedded.
|
|
272
|
+
// The official /v1/provider catalog is authoritative for pricing, including
|
|
273
|
+
// legitimately zero-priced preview models. Curation (reasoning/input/compat/name)
|
|
274
|
+
// still wins via ...embedded.
|
|
277
275
|
result.push({
|
|
278
276
|
...liveModel,
|
|
279
277
|
...embedded,
|
|
280
|
-
cost:
|
|
281
|
-
input: liveModel.cost.input || embedded.cost.input,
|
|
282
|
-
output: liveModel.cost.output || embedded.cost.output,
|
|
283
|
-
cacheRead: liveModel.cost.cacheRead || embedded.cost.cacheRead,
|
|
284
|
-
cacheWrite: liveModel.cost.cacheWrite || embedded.cost.cacheWrite,
|
|
285
|
-
},
|
|
278
|
+
cost: liveModel.cost,
|
|
286
279
|
contextWindow: liveModel.contextWindow || embedded.contextWindow,
|
|
287
280
|
});
|
|
288
281
|
} else {
|
|
@@ -298,6 +291,35 @@ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[])
|
|
|
298
291
|
return result;
|
|
299
292
|
}
|
|
300
293
|
|
|
294
|
+
// Grace period for delisted models. When the provider API stops listing a
|
|
295
|
+
// model, update-models.js moves its last-known definition into
|
|
296
|
+
// deprecated-models.json (stamped with deprecatedAt) instead of dropping it.
|
|
297
|
+
// For 14 days the model keeps working here so in-flight sessions and saved
|
|
298
|
+
// model settings do not break; afterwards it is evicted permanently.
|
|
299
|
+
const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
300
|
+
|
|
301
|
+
// Grace-period deprecated models with deprecation metadata stripped.
|
|
302
|
+
function activeDeprecatedModels(): JsonModel[] {
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
const result: JsonModel[] = [];
|
|
305
|
+
for (const entry of Object.values(deprecatedData as Record<string, JsonModel & { deprecatedAt?: string }>)) {
|
|
306
|
+
if (!entry?.id) continue;
|
|
307
|
+
const removedAt = Date.parse(entry.deprecatedAt ?? "");
|
|
308
|
+
if (Number.isNaN(removedAt) || now - removedAt > DEPRECATED_MODEL_TTL_MS) continue;
|
|
309
|
+
const model = { ...entry } as JsonModel & { deprecatedAt?: string };
|
|
310
|
+
delete model.deprecatedAt;
|
|
311
|
+
result.push(model);
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Append grace-period deprecated models the list does not already have (live data wins).
|
|
317
|
+
function withDeprecated(models: JsonModel[]): JsonModel[] {
|
|
318
|
+
const seen = new Set(models.map((m) => m.id));
|
|
319
|
+
const extras = activeDeprecatedModels().filter((m) => !seen.has(m.id));
|
|
320
|
+
return extras.length > 0 ? [...models, ...extras] : models;
|
|
321
|
+
}
|
|
322
|
+
|
|
301
323
|
function loadStaleModels(embeddedModels: JsonModel[]): JsonModel[] {
|
|
302
324
|
const cached = loadCachedModels();
|
|
303
325
|
if (!cached || cached.length === 0) return embeddedModels;
|