pi-hypercharm-provider 1.1.4 → 1.2.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/.pi/fabric/mesh/state.json +106 -0
- package/AGENTS.md +2 -1
- package/README.md +81 -50
- package/deprecated-models.json +34 -1
- package/index.ts +951 -291
- package/models.json +436 -125
- package/package.json +9 -2
- package/patch.json +1 -155
- package/pnpm-workspace.yaml +15 -0
- package/scripts/update-models.js +181 -101
- package/status.ts +305 -0
- package/tests/status.smoke.ts +146 -0
- package/tsconfig.json +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hypercharm-provider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "HyperCharm provider extension for pi - Access DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, and GPT-OSS models through the Charm Hyper API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
"gemma",
|
|
22
22
|
"gpt-oss"
|
|
23
23
|
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
26
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
27
|
+
"@types/node": "^24.10.1",
|
|
28
|
+
"typescript": "6.0.3"
|
|
29
|
+
},
|
|
24
30
|
"author": "",
|
|
25
31
|
"license": "MIT",
|
|
26
32
|
"pi": {
|
|
@@ -31,7 +37,8 @@
|
|
|
31
37
|
"scripts": {
|
|
32
38
|
"clean": "echo 'nothing to clean'",
|
|
33
39
|
"build": "echo 'nothing to build'",
|
|
34
|
-
"check": "
|
|
40
|
+
"check": "tsc --noEmit && node tests/status.smoke.ts",
|
|
41
|
+
"smoke": "node tests/status.smoke.ts",
|
|
35
42
|
"update-models": "node scripts/update-models.js"
|
|
36
43
|
}
|
|
37
44
|
}
|
package/patch.json
CHANGED
|
@@ -1,155 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"deepseek-v4-flash": {
|
|
3
|
-
"maxTokens": 384000,
|
|
4
|
-
"thinkingLevelMap": {
|
|
5
|
-
"minimal": null,
|
|
6
|
-
"low": null,
|
|
7
|
-
"medium": null,
|
|
8
|
-
"high": "high",
|
|
9
|
-
"max": "max"
|
|
10
|
-
},
|
|
11
|
-
"compat": {
|
|
12
|
-
"thinkingFormat": "deepseek",
|
|
13
|
-
"maxTokensField": "max_tokens",
|
|
14
|
-
"supportsDeveloperRole": true,
|
|
15
|
-
"supportsStore": false,
|
|
16
|
-
"supportsReasoningEffort": true,
|
|
17
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"deepseek-v4-pro": {
|
|
21
|
-
"maxTokens": 384000,
|
|
22
|
-
"thinkingLevelMap": {
|
|
23
|
-
"minimal": null,
|
|
24
|
-
"low": null,
|
|
25
|
-
"medium": null,
|
|
26
|
-
"high": "high",
|
|
27
|
-
"max": "max"
|
|
28
|
-
},
|
|
29
|
-
"compat": {
|
|
30
|
-
"thinkingFormat": "deepseek",
|
|
31
|
-
"maxTokensField": "max_tokens",
|
|
32
|
-
"supportsDeveloperRole": true,
|
|
33
|
-
"supportsStore": false,
|
|
34
|
-
"supportsReasoningEffort": true,
|
|
35
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
"gemma-4-26b-a4b-it": {
|
|
39
|
-
"compat": {
|
|
40
|
-
"thinkingFormat": "openai",
|
|
41
|
-
"maxTokensField": "max_tokens",
|
|
42
|
-
"supportsDeveloperRole": true,
|
|
43
|
-
"supportsStore": false,
|
|
44
|
-
"supportsReasoningEffort": true
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
"glm-5": {
|
|
48
|
-
"compat": {
|
|
49
|
-
"thinkingFormat": "openai",
|
|
50
|
-
"maxTokensField": "max_tokens",
|
|
51
|
-
"supportsDeveloperRole": true,
|
|
52
|
-
"supportsStore": false,
|
|
53
|
-
"supportsReasoningEffort": true
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
"glm-5.1": {
|
|
57
|
-
"compat": {
|
|
58
|
-
"thinkingFormat": "openai",
|
|
59
|
-
"maxTokensField": "max_tokens",
|
|
60
|
-
"supportsDeveloperRole": true,
|
|
61
|
-
"supportsStore": false,
|
|
62
|
-
"supportsReasoningEffort": true
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
"gpt-oss-120b": {
|
|
66
|
-
"compat": {
|
|
67
|
-
"thinkingFormat": "openai",
|
|
68
|
-
"maxTokensField": "max_tokens",
|
|
69
|
-
"supportsDeveloperRole": true,
|
|
70
|
-
"supportsStore": false,
|
|
71
|
-
"supportsReasoningEffort": true
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"kimi-k2.5": {
|
|
75
|
-
"compat": {
|
|
76
|
-
"thinkingFormat": "openai",
|
|
77
|
-
"maxTokensField": "max_tokens",
|
|
78
|
-
"supportsDeveloperRole": true,
|
|
79
|
-
"supportsStore": false,
|
|
80
|
-
"supportsReasoningEffort": true
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
"kimi-k2.6": {
|
|
84
|
-
"compat": {
|
|
85
|
-
"thinkingFormat": "openai",
|
|
86
|
-
"maxTokensField": "max_tokens",
|
|
87
|
-
"supportsDeveloperRole": true,
|
|
88
|
-
"supportsStore": false,
|
|
89
|
-
"supportsReasoningEffort": true
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
"minimax-m2.7": {
|
|
93
|
-
"compat": {
|
|
94
|
-
"thinkingFormat": "openai",
|
|
95
|
-
"maxTokensField": "max_tokens",
|
|
96
|
-
"supportsDeveloperRole": true,
|
|
97
|
-
"supportsStore": false,
|
|
98
|
-
"supportsReasoningEffort": true
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
"qwen3.6-flash": {
|
|
102
|
-
"compat": {
|
|
103
|
-
"thinkingFormat": "openai",
|
|
104
|
-
"maxTokensField": "max_tokens",
|
|
105
|
-
"supportsDeveloperRole": true,
|
|
106
|
-
"supportsStore": false,
|
|
107
|
-
"supportsReasoningEffort": true
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
"qwen3.6-max": {
|
|
111
|
-
"compat": {
|
|
112
|
-
"thinkingFormat": "openai",
|
|
113
|
-
"maxTokensField": "max_tokens",
|
|
114
|
-
"supportsDeveloperRole": true,
|
|
115
|
-
"supportsStore": false,
|
|
116
|
-
"supportsReasoningEffort": true
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
"qwen3.6-plus": {
|
|
120
|
-
"compat": {
|
|
121
|
-
"thinkingFormat": "openai",
|
|
122
|
-
"maxTokensField": "max_tokens",
|
|
123
|
-
"supportsDeveloperRole": true,
|
|
124
|
-
"supportsStore": false,
|
|
125
|
-
"supportsReasoningEffort": true
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
"qwen3.7-max": {
|
|
129
|
-
"compat": {
|
|
130
|
-
"thinkingFormat": "openai",
|
|
131
|
-
"maxTokensField": "max_tokens",
|
|
132
|
-
"supportsDeveloperRole": true,
|
|
133
|
-
"supportsStore": false,
|
|
134
|
-
"supportsReasoningEffort": true
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
"qwen3-coder-480b-a35b-instruct-int4-mixed-ar": {
|
|
138
|
-
"compat": {
|
|
139
|
-
"thinkingFormat": "openai",
|
|
140
|
-
"maxTokensField": "max_tokens",
|
|
141
|
-
"supportsDeveloperRole": true,
|
|
142
|
-
"supportsStore": false,
|
|
143
|
-
"supportsReasoningEffort": true
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
"qwen3-next-80b-a3b-instruct": {
|
|
147
|
-
"compat": {
|
|
148
|
-
"thinkingFormat": "openai",
|
|
149
|
-
"maxTokensField": "max_tokens",
|
|
150
|
-
"supportsDeveloperRole": true,
|
|
151
|
-
"supportsStore": false,
|
|
152
|
-
"supportsReasoningEffort": true
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
1
|
+
{}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
allowBuilds:
|
|
2
|
+
'@google/genai': set this to true or false
|
|
3
|
+
protobufjs: set this to true or false
|
|
4
|
+
onlyBuiltDependencies:
|
|
5
|
+
- '@google/genai'
|
|
6
|
+
- protobufjs
|
|
7
|
+
|
|
8
|
+
minimumReleaseAgeExclude:
|
|
9
|
+
- '@earendil-works/pi-agent-core@0.84.0 || 0.84.1'
|
|
10
|
+
- '@earendil-works/pi-ai@0.84.0 || 0.84.1'
|
|
11
|
+
- '@earendil-works/pi-client@0.84.0 || 0.84.1'
|
|
12
|
+
- '@earendil-works/pi-coding-agent@0.84.0 || 0.84.1'
|
|
13
|
+
- '@earendil-works/pi-protocol@0.84.0 || 0.84.1'
|
|
14
|
+
- '@earendil-works/pi-telemetry@0.84.0 || 0.84.1'
|
|
15
|
+
- '@earendil-works/pi-tui@0.84.0 || 0.84.1'
|
package/scripts/update-models.js
CHANGED
|
@@ -1,31 +1,133 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Update HyperCharm models from
|
|
4
|
+
* Update HyperCharm models from Charm's official typed provider catalog
|
|
5
5
|
*
|
|
6
|
-
* Fetches models from https://hyper.charm.land/v1/
|
|
7
|
-
* - models.json:
|
|
8
|
-
* - README.md:
|
|
6
|
+
* Fetches models from https://hyper.charm.land/v1/provider and updates:
|
|
7
|
+
* - models.json: canonical API-owned metadata used by @charmland/pi-hyper-provider
|
|
8
|
+
* - README.md: model table with patch.json overrides applied
|
|
9
9
|
*
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Note: supports_reasoning is unreliable for some models (reports true for
|
|
15
|
-
* Llama 3.3 70B which doesn't support extended thinking). models.json
|
|
16
|
-
* curates reasoning flags based on known model capabilities; patch.json
|
|
17
|
-
* adds compat flags and corrections.
|
|
10
|
+
* The endpoint provides canonical names, $/M pricing, context/output limits,
|
|
11
|
+
* can_reason, optional reasoning levels, and attachment support. models.json is
|
|
12
|
+
* pure API data. patch.json is reserved for verified endpoint regressions and
|
|
13
|
+
* currently contains no overrides.
|
|
18
14
|
*
|
|
19
15
|
* Merge order for README: models.json → apply patch.json → merge custom-models.json
|
|
16
|
+
*
|
|
17
|
+
* API key: the stored `hypercharm` credential in ~/.pi/agent/auth.json wins, then
|
|
18
|
+
* the HYPERCHARM_API_KEY environment variable. The script refuses to run without one.
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
import fs from 'fs';
|
|
22
|
+
import os from 'os';
|
|
23
|
+
import { execSync } from 'child_process';
|
|
23
24
|
import path from 'path';
|
|
24
25
|
import { fileURLToPath } from 'url';
|
|
25
26
|
|
|
26
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
// pi's agent directory: PI_CODING_AGENT_DIR (with ~ expansion) or ~/.pi/agent.
|
|
30
|
+
function piAgentDir() {
|
|
31
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
32
|
+
if (envDir) {
|
|
33
|
+
return envDir.startsWith('~/') || envDir === '~'
|
|
34
|
+
? path.join(os.homedir(), envDir.slice(1))
|
|
35
|
+
: envDir;
|
|
36
|
+
}
|
|
37
|
+
return path.join(os.homedir(), '.pi', 'agent');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const AUTH_JSON_PATH = path.join(piAgentDir(), 'auth.json');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a configured value using pi's semantics (resolve-config-value.ts in
|
|
44
|
+
* pi-mono): "!command" runs via the shell (10s timeout) and uses trimmed
|
|
45
|
+
* stdout; "$VAR" / "${VAR}" interpolate environment variables ("$$" escapes a
|
|
46
|
+
* literal "$", "$!" a literal "!"); anything else is a literal. Returns
|
|
47
|
+
* undefined when a referenced env var is unset or a command fails.
|
|
48
|
+
*/
|
|
49
|
+
function resolveConfigValue(config, env) {
|
|
50
|
+
if (typeof config !== 'string' || config.length === 0) return undefined;
|
|
51
|
+
if (config.startsWith('!')) {
|
|
52
|
+
try {
|
|
53
|
+
const out = execSync(config.slice(1), {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
timeout: 10000,
|
|
56
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
57
|
+
});
|
|
58
|
+
return out.trim() || undefined;
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
64
|
+
let resolved = '';
|
|
65
|
+
let index = 0;
|
|
66
|
+
while (index < config.length) {
|
|
67
|
+
const dollar = config.indexOf('$', index);
|
|
68
|
+
if (dollar < 0) {
|
|
69
|
+
resolved += config.slice(index);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
resolved += config.slice(index, dollar);
|
|
73
|
+
const next = config[dollar + 1];
|
|
74
|
+
let name;
|
|
75
|
+
if (next === '$' || next === '!') {
|
|
76
|
+
resolved += next;
|
|
77
|
+
index = dollar + 2;
|
|
78
|
+
continue;
|
|
79
|
+
} else if (next === '{') {
|
|
80
|
+
const end = config.indexOf('}', dollar + 2);
|
|
81
|
+
if (end < 0) {
|
|
82
|
+
resolved += '$';
|
|
83
|
+
index = dollar + 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const inner = config.slice(dollar + 2, end);
|
|
87
|
+
if (!ENV_NAME_RE.test(inner)) {
|
|
88
|
+
resolved += config.slice(dollar, end + 1);
|
|
89
|
+
index = end + 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
name = inner;
|
|
93
|
+
index = end + 1;
|
|
94
|
+
} else {
|
|
95
|
+
const match = config.slice(dollar + 1).match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
96
|
+
if (!match) {
|
|
97
|
+
resolved += '$';
|
|
98
|
+
index = dollar + 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
name = match[0];
|
|
102
|
+
index = dollar + 1 + name.length;
|
|
103
|
+
}
|
|
104
|
+
const value = (env && env[name]) || process.env[name] || undefined;
|
|
105
|
+
if (value === undefined) return undefined;
|
|
106
|
+
resolved += value;
|
|
107
|
+
}
|
|
108
|
+
return resolved;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The API key, resolved the way pi itself resolves it for this provider: the
|
|
113
|
+
* stored `hypercharm` credential in ~/.pi/agent/auth.json wins, then
|
|
114
|
+
* the HYPERCHARM_API_KEY environment variable.
|
|
115
|
+
*/
|
|
116
|
+
function resolveApiKey() {
|
|
117
|
+
try {
|
|
118
|
+
const auth = JSON.parse(fs.readFileSync(AUTH_JSON_PATH, 'utf8'));
|
|
119
|
+
const credential = auth?.hypercharm;
|
|
120
|
+
if (credential && credential.type === 'api_key' && typeof credential.key === 'string') {
|
|
121
|
+
const key = resolveConfigValue(credential.key, credential.env);
|
|
122
|
+
if (key) return key;
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Missing or unparseable auth.json: fall through to the env var.
|
|
126
|
+
}
|
|
127
|
+
return process.env.HYPERCHARM_API_KEY || undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const MODELS_API_URL = 'https://hyper.charm.land/v1/provider';
|
|
29
131
|
const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
|
|
30
132
|
const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
|
|
31
133
|
const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
|
|
@@ -76,6 +178,9 @@ function applyPatch(model, patch) {
|
|
|
76
178
|
if (!result.reasoning && result.compat?.thinkingFormat) {
|
|
77
179
|
delete result.compat.thinkingFormat;
|
|
78
180
|
}
|
|
181
|
+
if (!result.reasoning && result.thinkingLevelMap) {
|
|
182
|
+
delete result.thinkingLevelMap;
|
|
183
|
+
}
|
|
79
184
|
if (result.compat && Object.keys(result.compat).length === 0) {
|
|
80
185
|
delete result.compat;
|
|
81
186
|
}
|
|
@@ -102,91 +207,68 @@ function buildModels(baseModels, customModels, patchData) {
|
|
|
102
207
|
|
|
103
208
|
// ─── Model transformation ─────────────────────────────────────────────────────
|
|
104
209
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Update fields from API that may change
|
|
131
|
-
const cost = apiModel.cost?.usd || {};
|
|
132
|
-
const inputCost = convertPricing(cost['1m_in']);
|
|
133
|
-
const outputCost = convertPricing(cost['1m_out']);
|
|
134
|
-
const cacheReadCost = convertPricing(cost['1m_in_cache']);
|
|
135
|
-
const cacheWriteCost = convertPricing(cost['1m_out_cache']);
|
|
136
|
-
|
|
137
|
-
if (inputCost > 0) existing.cost.input = inputCost;
|
|
138
|
-
if (outputCost > 0) existing.cost.output = outputCost;
|
|
139
|
-
if (cacheReadCost > 0) existing.cost.cacheRead = cacheReadCost;
|
|
140
|
-
if (cacheWriteCost > 0) existing.cost.cacheWrite = cacheWriteCost;
|
|
141
|
-
if (apiModel.context_window) existing.contextWindow = apiModel.context_window;
|
|
142
|
-
// Don't override maxTokens from API for DeepSeek — it reports 8000 but the
|
|
143
|
-
// actual max is 384K (set in models.json / patch.json)
|
|
144
|
-
if (apiModel.max_output_tokens && !/^deepseek-v/.test(modelId)) {
|
|
145
|
-
existing.maxTokens = apiModel.max_output_tokens;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return existing;
|
|
210
|
+
const PI_THINKING_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
211
|
+
|
|
212
|
+
// Charm's official extension treats a reasoning-capable model with no levels as
|
|
213
|
+
// a boolean on/off model: Pi's max level selects the single on state.
|
|
214
|
+
const ON_OFF_THINKING_LEVEL_MAP = {
|
|
215
|
+
off: 'off',
|
|
216
|
+
minimal: null,
|
|
217
|
+
low: null,
|
|
218
|
+
medium: null,
|
|
219
|
+
high: null,
|
|
220
|
+
xhigh: null,
|
|
221
|
+
max: 'max',
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function buildThinkingLevelMap(levels) {
|
|
225
|
+
if (levels.length === 0) return undefined;
|
|
226
|
+
const available = new Set(levels);
|
|
227
|
+
const result = {
|
|
228
|
+
// The provider enum uses "none" for the off state on newer deployments;
|
|
229
|
+
// the official extension looked only for the older "off" spelling.
|
|
230
|
+
off: available.has('off') ? 'off' : available.has('none') ? 'none' : null,
|
|
231
|
+
};
|
|
232
|
+
for (const level of PI_THINKING_LEVELS) {
|
|
233
|
+
result[level] = available.has(level) ? level : null;
|
|
149
234
|
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
150
237
|
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
238
|
+
function transformModel(apiModel) {
|
|
239
|
+
const reasoningLevels = Array.isArray(apiModel.reasoning_levels)
|
|
240
|
+
? apiModel.reasoning_levels.filter(level => typeof level === 'string')
|
|
241
|
+
: [];
|
|
242
|
+
const supportsReasoningEffort = reasoningLevels.length > 0;
|
|
243
|
+
const thinkingLevelMap = supportsReasoningEffort
|
|
244
|
+
? buildThinkingLevelMap(reasoningLevels)
|
|
245
|
+
: apiModel.can_reason === true
|
|
246
|
+
? ON_OFF_THINKING_LEVEL_MAP
|
|
247
|
+
: undefined;
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
id: apiModel.id,
|
|
251
|
+
name: apiModel.name,
|
|
252
|
+
reasoning: apiModel.can_reason === true,
|
|
253
|
+
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
|
254
|
+
input: apiModel.supports_attachments === true ? ['text', 'image'] : ['text'],
|
|
162
255
|
cost: {
|
|
163
|
-
input:
|
|
164
|
-
output:
|
|
165
|
-
cacheRead:
|
|
166
|
-
|
|
256
|
+
input: typeof apiModel.cost_per_1m_in === 'number' ? apiModel.cost_per_1m_in : 0,
|
|
257
|
+
output: typeof apiModel.cost_per_1m_out === 'number' ? apiModel.cost_per_1m_out : 0,
|
|
258
|
+
cacheRead: typeof apiModel.cost_per_1m_in_cached === 'number' ? apiModel.cost_per_1m_in_cached : 0,
|
|
259
|
+
// Hyper exposes discounted cached-output pricing, not cache-write pricing;
|
|
260
|
+
// pi's cost.cacheWrite field remains unused. This matches Charm's official extension.
|
|
261
|
+
cacheWrite: 0,
|
|
167
262
|
},
|
|
168
263
|
contextWindow: apiModel.context_window || 0,
|
|
169
|
-
maxTokens: apiModel.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// and add thinkingLevelMap + deepseek compat
|
|
174
|
-
if (isDeepSeek && isReasoning) {
|
|
175
|
-
model.maxTokens = 384000;
|
|
176
|
-
model.thinkingLevelMap = {
|
|
177
|
-
minimal: null, low: null, medium: null, high: 'high', max: 'max',
|
|
178
|
-
};
|
|
179
|
-
model.compat = {
|
|
264
|
+
maxTokens: apiModel.default_max_tokens || apiModel.context_window || 0,
|
|
265
|
+
compat: {
|
|
266
|
+
supportsStore: false,
|
|
267
|
+
supportsReasoningEffort,
|
|
180
268
|
thinkingFormat: 'deepseek',
|
|
181
269
|
maxTokensField: 'max_tokens',
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
supportsReasoningEffort: true,
|
|
185
|
-
requiresReasoningContentOnAssistantMessages: true,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return model;
|
|
270
|
+
},
|
|
271
|
+
};
|
|
190
272
|
}
|
|
191
273
|
|
|
192
274
|
// ─── README generation ────────────────────────────────────────────────────────
|
|
@@ -299,9 +381,9 @@ function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
|
299
381
|
}
|
|
300
382
|
|
|
301
383
|
async function main() {
|
|
302
|
-
const apiKey =
|
|
384
|
+
const apiKey = resolveApiKey();
|
|
303
385
|
if (!apiKey) {
|
|
304
|
-
console.error('Error:
|
|
386
|
+
console.error('Error: No API key found: no `hypercharm` credential resolved from ' + AUTH_JSON_PATH + ' and HYPERCHARM_API_KEY is not set');
|
|
305
387
|
console.error('Usage: HYPERCHARM_API_KEY=your-key node scripts/update-models.js');
|
|
306
388
|
process.exit(1);
|
|
307
389
|
}
|
|
@@ -317,7 +399,9 @@ async function main() {
|
|
|
317
399
|
}
|
|
318
400
|
|
|
319
401
|
const apiResponse = await response.json();
|
|
320
|
-
const apiModels =
|
|
402
|
+
const apiModels = Array.isArray(apiResponse)
|
|
403
|
+
? apiResponse
|
|
404
|
+
: (apiResponse.models || apiResponse.data || []);
|
|
321
405
|
|
|
322
406
|
if (!Array.isArray(apiModels)) {
|
|
323
407
|
throw new Error('API response does not contain an array of models');
|
|
@@ -338,16 +422,12 @@ async function main() {
|
|
|
338
422
|
}
|
|
339
423
|
|
|
340
424
|
// Transform models from API, preserving existing curated data
|
|
341
|
-
let apiTransformed = apiModels.map(m => transformModel(m
|
|
425
|
+
let apiTransformed = apiModels.map(m => transformModel(m));
|
|
342
426
|
apiTransformed.sort((a, b) => a.name.localeCompare(b.name));
|
|
343
427
|
|
|
344
|
-
//
|
|
428
|
+
// Load patch overrides for README rendering. Canonical metadata already
|
|
429
|
+
// comes from /v1/provider, so new models do not require a patch entry.
|
|
345
430
|
const patch = loadJson(PATCH_JSON_PATH);
|
|
346
|
-
for (const m of apiTransformed) {
|
|
347
|
-
if (!patch[m.id]) {
|
|
348
|
-
console.log(` 🆕 New model: ${m.id} (${m.name}) — add to patch.json for compat overrides`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
431
|
|
|
352
432
|
// Update models.json — curated API data
|
|
353
433
|
// Move delisted models to deprecated-models.json BEFORE models.json is overwritten
|