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 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
- - **20+ 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 with `reasoning_effort` and 384K max output for DeepSeek V4 models
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
- - **Cost Tracking** with per-model pricing from the API
22
- - **Reasoning Models** with `reasoning_effort` parameter support on select models
23
- - **Vision Support** for image-capable models (Kimi K2.5/K2.6, GLM 5.1, Qwen3.6/3.7, Gemma 4)
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 + Image | 256K | 26K | $0.12 | $0.41 |
32
- | GLM 5.1 | Text + Image | 203K | 131K | $1.40 | $4.40 |
33
- | GLM-5 | Text | 203K | 20K | $0.81 | $2.59 |
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
- | GPT-OSS 120B | Text | 131K | 13K | $0.17 | $0.56 |
36
- | Kimi K2.5 | Text + Image | 262K | 26K | $0.52 | $2.72 |
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
- | Llama 3.3 70B Instruct | Text | 128K | 13K | $0.61 | $1.04 |
40
- | Llama 4 Maverick 17B 128E FP8 | Text | 430K | 43K | $0.28 | $0.93 |
41
- | MiniMax M2.7 | Text | 205K | 20K | $0.42 | $1.56 |
42
- | Qwen3 Coder 480B A35B INT4 | Text | 106K | 11K | $0.60 | $2.08 |
43
- | Qwen3 Next 80B A3B | Text | 262K | 26K | $0.12 | $1.14 |
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 + Image | 256K | 64K | $2.00 | $12.00 |
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-Max | Text + Image | 1.0M | 64K | $2.50 | $7.50 |
48
- | Qwen3.7-Plus | Text | 1.0M | 64K | $1.20 | $4.80 |
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
- Values: `none`, `low`, `medium`, `high`, `max`
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
- Charm Hyper uses an OpenAI-compatible API. Compatibility settings are managed via `patch.json`:
162
+ Model metadata matches Charm's official [`@charmland/pi-hyper-provider`](https://github.com/charmbracelet/pi-hyper-provider) transform:
159
163
 
160
- **DeepSeek models** use the `deepseek` thinking format (identical to the native DeepSeek API):
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
- - **`thinkingFormat: "deepseek"`** Sends `thinking: {type: "enabled/disabled"}` to toggle thinking mode
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
- The `patch.json` file contains overrides applied on top of `models.json` data:
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. Preserve curated data (reasoning, vision, DeepSeek compat) from existing `models.json`
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. Update `models.json` and the README model table
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
- * HyperCharm provides hyperoptimized coding models via an OpenAI-compatible API.
8
- * The /v1/models endpoint returns structured metadata including reasoning flags,
9
- * pricing, context windows, and max output tokens.
10
- *
11
- * Note: The API's `supports_reasoning` flag is unreliable for some models (e.g.,
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
- for (const model of base) {
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
- const result = Array.from(modelMap.values());
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}/models`;
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
- /** Transform a model from the HyperCharm /v1/models API to JsonModel format. */
204
- function transformApiModel(apiModel: any): JsonModel | null {
205
- if (!apiModel.id) return null;
206
-
207
- const cost = apiModel.cost?.usd || {};
208
- const toPerM = (v: any) => {
209
- const n = typeof v === "string" ? parseFloat(v) : (v || 0);
210
- // API returns $/M directly; round to 6 decimals to preserve sub-cent cache prices.
211
- return Math.round(n * 1e6) / 1e6;
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.display_name || apiModel.id,
217
- reasoning: false, // API supports_reasoning is unreliable; patch.json corrects
218
- input: ["text"],
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: toPerM(cost["1m_in"]),
221
- output: toPerM(cost["1m_out"]),
222
- cacheRead: toPerM(cost["1m_in_cache"]),
223
- cacheWrite: toPerM(cost["1m_out_cache"]),
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.max_output_tokens || 0,
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
- // Self-heal: live API pricing is authoritative field-by-field. Prefer the
273
- // live cost when the API reports it (non-zero); fall back to embedded when
274
- // the API is silent (0) so curated cacheRead/cacheWrite isn't clobbered and
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;