pi-hypercharm-provider 1.1.4 → 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/README.md CHANGED
@@ -15,40 +15,41 @@ _Hyperoptimized coding models — DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, GPT
15
15
 
16
16
  ## Features
17
17
 
18
- - **23+ 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
- | Kimi K3 | Text | 1.0M | 131K | Free | Free |
40
- | Llama 3.3 70B Instruct | Text | 128K | 13K | $0.61 | $1.04 |
41
- | Llama 4 Maverick 17B 128E FP8 | Text | 430K | 43K | $0.28 | $0.93 |
42
- | MiniMax M2.7 | Text | 205K | 20K | $0.42 | $1.56 |
43
- | MiniMax M3 | Text | 512K | 512K | Free | Free |
44
- | Qwen3 Coder 480B A35B INT4 | Text | 106K | 11K | $0.60 | $2.08 |
45
- | 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 |
46
47
  | Qwen3.6-Flash | Text + Image | 1.0M | 64K | $1.00 | $4.00 |
47
- | Qwen3.6-Max | Text + Image | 256K | 64K | $2.00 | $12.00 |
48
+ | Qwen3.6-Max | Text | 256K | 64K | $2.00 | $12.00 |
48
49
  | Qwen3.6-Plus | Text + Image | 1.0M | 64K | $2.00 | $6.00 |
49
- | Qwen3.7-Flash | Text | 1.0M | 64K | Free | Free |
50
- | Qwen3.7-Max | Text + Image | 1.0M | 64K | $2.50 | $7.50 |
51
- | 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 |
52
53
  *Costs are per million tokens. Prices subject to change — check [hyper.charm.land](https://hyper.charm.land) for current pricing.*
53
54
 
54
55
  ## Installation
@@ -118,7 +119,7 @@ For reasoning models that support `reasoning_effort`, control thinking depth:
118
119
  pi --provider hypercharm --model deepseek-v4-pro --reasoning-effort max
119
120
  ```
120
121
 
121
- 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.
122
123
 
123
124
  ### Thinking Mode
124
125
 
@@ -156,37 +157,21 @@ Add to your pi configuration for automatic loading:
156
157
  }
157
158
  ```
158
159
 
159
- ### Compat Settings
160
+ ### Catalog and Compat Settings
160
161
 
161
- 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:
162
163
 
163
- **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
164
169
 
165
- - **`thinkingFormat: "deepseek"`** Sends `thinking: {type: "enabled/disabled"}` to toggle thinking mode
166
- - **`supportsReasoningEffort: true`** — Supports `reasoning_effort: "high" | "max"` via `thinkingLevelMap`
167
- - **`requiresReasoningContentOnAssistantMessages: true`** — Replayed assistant messages include empty `reasoning_content` when reasoning is enabled
168
- - **`maxTokensField: "max_tokens"`** — Uses `max_tokens` (not `max_completion_tokens`)
169
-
170
- **Other reasoning models** (GLM, Kimi, Qwen, MiniMax, Gemma, GPT-OSS) use the `openai` thinking format:
171
-
172
- - **`thinkingFormat: "openai"`** — Returns `reasoning_content` in OpenAI format
173
- - **`supportsReasoningEffort: true`** — Accepts `reasoning_effort` parameter on supported models
174
- - **`maxTokensField: "max_tokens"`** — Uses `max_tokens` (not `max_completion_tokens`)
175
- - **`supportsDeveloperRole: true`** — Developer role messages are accepted
176
- - **`supportsStore: false`** — The `store` parameter is not supported
177
-
178
- > **Note:** The HyperCharm `/v1/models` endpoint reports `supports_reasoning: true` for all models,
179
- > including those that don't natively support extended thinking (e.g., Llama 3.3 70B). `models.json`
180
- > 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.
181
171
 
182
172
  ### Patch Overrides
183
173
 
184
- The `patch.json` file contains overrides applied on top of `models.json` data:
185
-
186
- - Correcting API-derived values (e.g., DeepSeek `maxTokens` from 8000 to 384000)
187
- - Adding compat settings that the API doesn't provide
188
- - Setting `thinkingFormat: "deepseek"` + `thinkingLevelMap` for DeepSeek V4 models
189
- - 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.
190
175
 
191
176
  ## Updating Models
192
177
 
@@ -199,10 +184,11 @@ node scripts/update-models.js
199
184
 
200
185
  This will:
201
186
  1. Fetch models from `https://hyper.charm.land/v1/models`
202
- 2. Preserve curated data (reasoning, vision, DeepSeek compat) from existing `models.json`
203
- 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
204
189
  4. Remove custom models now available upstream from `custom-models.json`
205
- 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
206
192
 
207
193
  A GitHub Actions workflow runs this daily and creates a PR if models have changed.
208
194
 
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)
@@ -114,6 +112,9 @@ function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
114
112
  if (!result.reasoning && result.compat?.thinkingFormat) {
115
113
  delete result.compat.thinkingFormat;
116
114
  }
115
+ if (!result.reasoning && result.thinkingLevelMap) {
116
+ delete result.thinkingLevelMap;
117
+ }
117
118
  if (result.compat && Object.keys(result.compat).length === 0) {
118
119
  delete result.compat;
119
120
  }
@@ -153,81 +154,76 @@ function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData):
153
154
  }
154
155
  }
155
156
 
156
- const result = Array.from(modelMap.values());
157
-
158
- // Ensure DeepSeek reasoning models have required compat settings.
159
- // Live-fetched models from the SWR pipeline may not have these set.
160
- for (const model of result) {
161
- if (!model.reasoning) continue;
162
- if (isDeepSeekModel(model.id)) {
163
- if (!model.compat) {
164
- model.compat = {
165
- thinkingFormat: "deepseek",
166
- maxTokensField: "max_tokens",
167
- supportsDeveloperRole: true,
168
- supportsStore: false,
169
- supportsReasoningEffort: true,
170
- requiresReasoningContentOnAssistantMessages: true,
171
- };
172
- } else {
173
- if (model.compat.thinkingFormat === undefined) {
174
- model.compat.thinkingFormat = "deepseek";
175
- }
176
- if (model.compat.supportsReasoningEffort === undefined) {
177
- model.compat.supportsReasoningEffort = true;
178
- }
179
- if ((model.compat as any).requiresReasoningContentOnAssistantMessages === undefined) {
180
- (model.compat as any).requiresReasoningContentOnAssistantMessages = true;
181
- }
182
- }
183
- if (!model.thinkingLevelMap) {
184
- model.thinkingLevelMap = {
185
- minimal: null, low: null, medium: null, high: "high", max: "max",
186
- };
187
- }
188
- }
189
- }
190
-
191
- return result;
192
- }
193
-
194
- function isDeepSeekModel(id: string): boolean {
195
- return /^deepseek-v/.test(id);
157
+ return Array.from(modelMap.values());
196
158
  }
197
159
 
198
160
  // ─── Stale-While-Revalidate Model Sync ────────────────────────────────────────
199
161
 
200
162
  const PROVIDER_ID = "hypercharm";
201
163
  const BASE_URL = "https://hyper.charm.land/v1";
202
- const MODELS_URL = `${BASE_URL}/models`;
164
+ const MODELS_URL = `${BASE_URL}/provider`;
203
165
  const CACHE_DIR = path.join(getAgentDir(), "cache");
204
166
  const CACHE_PATH = path.join(CACHE_DIR, `${PROVIDER_ID}-models.json`);
205
167
  const LIVE_FETCH_TIMEOUT_MS = 8000;
206
168
 
207
- /** Transform a model from the HyperCharm /v1/models API to JsonModel format. */
208
- function transformApiModel(apiModel: any): JsonModel | null {
209
- if (!apiModel.id) return null;
210
-
211
- const cost = apiModel.cost?.usd || {};
212
- const toPerM = (v: any) => {
213
- const n = typeof v === "string" ? parseFloat(v) : (v || 0);
214
- // API returns $/M directly; round to 6 decimals to preserve sub-cent cache prices.
215
- 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,
216
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;
217
206
 
218
207
  return {
219
208
  id: apiModel.id,
220
- name: apiModel.display_name || apiModel.id,
221
- reasoning: false, // API supports_reasoning is unreliable; patch.json corrects
222
- 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"],
223
213
  cost: {
224
- input: toPerM(cost["1m_in"]),
225
- output: toPerM(cost["1m_out"]),
226
- cacheRead: toPerM(cost["1m_in_cache"]),
227
- 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,
228
218
  },
229
219
  contextWindow: apiModel.context_window || 0,
230
- 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
+ },
231
227
  };
232
228
  }
233
229
 
@@ -239,7 +235,7 @@ async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<Js
239
235
  });
240
236
  if (!response.ok) return null;
241
237
  const data = await response.json();
242
- const apiModels = Array.isArray(data) ? data : (data.data || []);
238
+ const apiModels = Array.isArray(data) ? data : (data.models || data.data || []);
243
239
  if (!Array.isArray(apiModels) || apiModels.length === 0) return null;
244
240
  return apiModels.map(transformApiModel).filter((m): m is JsonModel => m !== null);
245
241
  } catch {
@@ -273,20 +269,13 @@ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[])
273
269
  const embedded = embeddedMap.get(liveModel.id);
274
270
  seen.add(liveModel.id);
275
271
  if (embedded) {
276
- // Self-heal: live API pricing is authoritative field-by-field. Prefer the
277
- // live cost when the API reports it (non-zero); fall back to embedded when
278
- // the API is silent (0) so curated cacheRead/cacheWrite isn't clobbered and
279
- // providers whose /models endpoint exposes no pricing keep their curated
280
- // 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.
281
275
  result.push({
282
276
  ...liveModel,
283
277
  ...embedded,
284
- cost: {
285
- input: liveModel.cost.input || embedded.cost.input,
286
- output: liveModel.cost.output || embedded.cost.output,
287
- cacheRead: liveModel.cost.cacheRead || embedded.cost.cacheRead,
288
- cacheWrite: liveModel.cost.cacheWrite || embedded.cost.cacheWrite,
289
- },
278
+ cost: liveModel.cost,
290
279
  contextWindow: liveModel.contextWindow || embedded.contextWindow,
291
280
  });
292
281
  } else {