@hyav/pi-provider 0.1.7 → 0.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/CHANGELOG.md +23 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/core/adapter-extensions.ts +9 -2
- package/core/adapter-protocol.ts +4 -2
- package/core/adapter-validation.ts +14 -1
- package/core/catalog-preflight.ts +14 -1
- package/core/host.ts +43 -75
- package/core/model-catalog.ts +289 -0
- package/core/opencode-preflight.ts +6 -0
- package/core/pi-model-metadata.ts +739 -0
- package/core/provider-registration.ts +111 -56
- package/core/public-adapters.ts +16 -1
- package/core/runtime-config.ts +18 -38
- package/core/runtime-entry.ts +11 -6
- package/core/runtime.ts +26 -104
- package/core/status-report.ts +327 -229
- package/core/types.ts +37 -20
- package/index.ts +37 -18
- package/package.json +3 -1
- package/preflight/charm-hyper.ts +6 -2
- package/preflight/deepseek.ts +10 -1
- package/preflight/github-copilot.ts +4 -0
- package/preflight/google.ts +10 -1
- package/preflight/groq.ts +10 -1
- package/preflight/openai-codex.ts +10 -1
- package/preflight/openrouter.ts +11 -2
- package/preflight/vercel-ai-gateway.ts +11 -1
- package/preflight/xai.ts +18 -4
- package/providers/charm-hyper/oauth.ts +17 -11
- package/providers/charm-hyper.ts +106 -236
- package/status/huggingface.ts +2 -1
- package/status/openrouter.ts +8 -2
- package/status/vercel-ai-gateway.ts +1 -1
- package/core/official-pricing.ts +0 -899
package/providers/charm-hyper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
ModelCatalogDiscoveryResult,
|
|
3
3
|
ProviderAdapter,
|
|
4
4
|
ProviderModel,
|
|
5
5
|
ProviderModelDraft,
|
|
@@ -7,11 +7,13 @@ import type {
|
|
|
7
7
|
ThinkingLevel,
|
|
8
8
|
} from "@hyav/pi-provider";
|
|
9
9
|
import {
|
|
10
|
+
createModelCatalogLifecycle,
|
|
10
11
|
defineProviderExtension,
|
|
12
|
+
isLegacyNormalizedSnapshot,
|
|
11
13
|
isProviderDataError,
|
|
12
14
|
MAX_PROVIDER_MODEL_COUNT,
|
|
13
|
-
normalizeProviderModels,
|
|
14
15
|
ProviderDataError,
|
|
16
|
+
validateProviderModelDrafts,
|
|
15
17
|
withDeadline,
|
|
16
18
|
} from "@hyav/pi-provider";
|
|
17
19
|
import { HYPER_BASE_URL, HYPER_USER_AGENT, hyperJsonHeaders } from "./charm-hyper/constants.ts";
|
|
@@ -21,17 +23,6 @@ export { HYPER_BASE_URL, HYPER_USER_AGENT } from "./charm-hyper/constants.ts";
|
|
|
21
23
|
export const HYPER_PROVIDER_URL = "https://hyper.charm.land/v1/provider";
|
|
22
24
|
export const HYPER_MODELS_URL = "https://hyper.charm.land/v1/models";
|
|
23
25
|
export const HYPER_MODEL_CATALOG_TTL_MS = 4 * 60 * 60 * 1_000;
|
|
24
|
-
const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
25
|
-
|
|
26
|
-
const officialCostFallbacks: Partial<Record<string, ProviderModel["cost"]>> = {
|
|
27
|
-
"deepseek-v4-flash": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
|
|
28
|
-
"deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625, cacheWrite: 0 },
|
|
29
|
-
"glm-5": { input: 1, output: 3.2, cacheRead: 0, cacheWrite: 0 },
|
|
30
|
-
"glm-5.1": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
|
31
|
-
"kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0 },
|
|
32
|
-
"kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
|
|
33
|
-
"mistral-large-instruct-2411": { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 },
|
|
34
|
-
};
|
|
35
26
|
|
|
36
27
|
const thinkingLevels = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
37
28
|
const baseHyperCompat: NonNullable<ProviderModel["compat"]> = {
|
|
@@ -50,21 +41,6 @@ const onOffThinkingLevelMap: NonNullable<ProviderModel["thinkingLevelMap"]> = {
|
|
|
50
41
|
max: "max",
|
|
51
42
|
};
|
|
52
43
|
|
|
53
|
-
const modelOverrides: Record<string, Partial<ProviderModelDraft>> = {
|
|
54
|
-
"qwen3-coder-480b-a35b-instruct-int4-mixed-ar": { reasoning: false },
|
|
55
|
-
"qwen3-next-80b-a3b-instruct": { reasoning: false },
|
|
56
|
-
"gpt-oss-120b": {
|
|
57
|
-
thinkingLevelMap: {
|
|
58
|
-
minimal: null,
|
|
59
|
-
low: "low",
|
|
60
|
-
medium: "medium",
|
|
61
|
-
high: "high",
|
|
62
|
-
xhigh: "high",
|
|
63
|
-
},
|
|
64
|
-
compat: { supportsReasoningEffort: true },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
44
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
69
45
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
46
|
}
|
|
@@ -77,63 +53,6 @@ function isFiniteNonNegative(value: unknown): value is number {
|
|
|
77
53
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
78
54
|
}
|
|
79
55
|
|
|
80
|
-
function costFallbackFor(id: string): ProviderModel["cost"] {
|
|
81
|
-
return { ...(officialCostFallbacks[id] ?? zeroCost) };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function fallbackModel(
|
|
85
|
-
id: string,
|
|
86
|
-
name: string,
|
|
87
|
-
contextWindow: number,
|
|
88
|
-
overrides: Partial<ProviderModelDraft> = {},
|
|
89
|
-
): ProviderModelDraft {
|
|
90
|
-
const { compat: overrideCompat, ...rest } = overrides;
|
|
91
|
-
return {
|
|
92
|
-
id,
|
|
93
|
-
name,
|
|
94
|
-
reasoning: true,
|
|
95
|
-
input: ["text", "image"],
|
|
96
|
-
contextWindow,
|
|
97
|
-
maxTokens: Math.floor(contextWindow / 10),
|
|
98
|
-
cost: costFallbackFor(id),
|
|
99
|
-
pricingSource: "fallback",
|
|
100
|
-
headers: { ...hyperModelHeaders },
|
|
101
|
-
compat: { ...baseHyperCompat, ...overrideCompat },
|
|
102
|
-
...rest,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function getHyperFallbackModels(): ProviderModelDraft[] {
|
|
107
|
-
return [
|
|
108
|
-
fallbackModel("deepseek-v4-flash", "DeepSeek V4 Flash", 1_048_576),
|
|
109
|
-
fallbackModel("deepseek-v4-pro", "DeepSeek V4 Pro", 1_048_576),
|
|
110
|
-
fallbackModel("gemma-4-26b-a4b-it", "Gemma 4 26B A4B", 32_768, { input: ["text"] }),
|
|
111
|
-
fallbackModel("glm-5", "GLM-5", 202_752),
|
|
112
|
-
fallbackModel("glm-5.1", "GLM-5.1", 202_752),
|
|
113
|
-
fallbackModel("gpt-oss-120b", "GPT-OSS-120B", 131_072, {
|
|
114
|
-
input: ["text"],
|
|
115
|
-
thinkingLevelMap: { minimal: null, low: "low", medium: "medium", high: "high", xhigh: "high" },
|
|
116
|
-
compat: { supportsReasoningEffort: true },
|
|
117
|
-
}),
|
|
118
|
-
fallbackModel("kimi-k2.5", "Kimi K2.5", 262_144),
|
|
119
|
-
fallbackModel("kimi-k2.6", "Kimi K2.6", 32_768),
|
|
120
|
-
fallbackModel("llama-3.3-70b-instruct", "Llama 3.3 70B Instruct", 128_000, { input: ["text"] }),
|
|
121
|
-
fallbackModel("llama-4-maverick-17b-128e-instruct-fp8", "Llama 4 Maverick 17B 128E", 430_000),
|
|
122
|
-
fallbackModel("mistral-large-instruct-2411", "Mistral Large Instruct 2411", 128_000, {
|
|
123
|
-
reasoning: false,
|
|
124
|
-
input: ["text"],
|
|
125
|
-
}),
|
|
126
|
-
fallbackModel("qwen3-coder-480b-a35b-instruct-int4-mixed-ar", "Qwen3 Coder 480B INT4", 106_000, {
|
|
127
|
-
reasoning: false,
|
|
128
|
-
input: ["text"],
|
|
129
|
-
}),
|
|
130
|
-
fallbackModel("qwen3-next-80b-a3b-instruct", "Qwen3 Next 80B A3B", 262_144, {
|
|
131
|
-
reasoning: false,
|
|
132
|
-
input: ["text"],
|
|
133
|
-
}),
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
56
|
function mapHyperPricing(value: unknown): ProviderModel["cost"] | undefined {
|
|
138
57
|
if (!isRecord(value)) return undefined;
|
|
139
58
|
if (!isFiniteNonNegative(value.input) || !isFiniteNonNegative(value.output)) return undefined;
|
|
@@ -163,16 +82,6 @@ function buildThinkingLevelMap(levels: readonly string[]): NonNullable<ProviderM
|
|
|
163
82
|
};
|
|
164
83
|
}
|
|
165
84
|
|
|
166
|
-
function applyModelOverride(model: ProviderModelDraft): ProviderModelDraft {
|
|
167
|
-
const override = modelOverrides[model.id];
|
|
168
|
-
if (override === undefined) return model;
|
|
169
|
-
return {
|
|
170
|
-
...model,
|
|
171
|
-
...override,
|
|
172
|
-
...(override.compat ? { compat: { ...(model.compat ?? {}), ...override.compat } } : {}),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
85
|
function readEffortLevels(model: Record<string, unknown>): ThinkingLevel[] {
|
|
177
86
|
const reasoning = isRecord(model.reasoning) ? model.reasoning : undefined;
|
|
178
87
|
const currentLevels = Array.isArray(reasoning?.effort_levels)
|
|
@@ -190,7 +99,8 @@ function parseCurrentHyperModel(value: unknown): ProviderModelDraft | undefined
|
|
|
190
99
|
if (typeof value.name !== "string" || value.name.trim() === "") return undefined;
|
|
191
100
|
if (!isFiniteNonNegative(value.cost_per_1m_in)) return undefined;
|
|
192
101
|
if (!isFiniteNonNegative(value.cost_per_1m_out)) return undefined;
|
|
193
|
-
|
|
102
|
+
// Cached prices are optional; the manifest documents 0/missing as no discount.
|
|
103
|
+
if (value.cost_per_1m_in_cached !== undefined && !isFiniteNonNegative(value.cost_per_1m_in_cached)) return undefined;
|
|
194
104
|
if (value.cost_per_1m_out_cached !== undefined && !isFiniteNonNegative(value.cost_per_1m_out_cached))
|
|
195
105
|
return undefined;
|
|
196
106
|
if (!isPositiveInteger(value.context_window) || !isPositiveInteger(value.default_max_tokens)) return undefined;
|
|
@@ -229,8 +139,10 @@ function parseCurrentHyperModel(value: unknown): ProviderModelDraft | undefined
|
|
|
229
139
|
cost: {
|
|
230
140
|
input: value.cost_per_1m_in,
|
|
231
141
|
output: value.cost_per_1m_out,
|
|
232
|
-
|
|
233
|
-
|
|
142
|
+
// Official Charm mapping: the cached-output price is the cache-read
|
|
143
|
+
// price and the cached-input price is the cache-write price.
|
|
144
|
+
cacheRead: value.cost_per_1m_out_cached ?? 0,
|
|
145
|
+
cacheWrite: value.cost_per_1m_in_cached ?? 0,
|
|
234
146
|
},
|
|
235
147
|
contextWindow: value.context_window,
|
|
236
148
|
maxTokens: value.default_max_tokens,
|
|
@@ -238,41 +150,81 @@ function parseCurrentHyperModel(value: unknown): ProviderModelDraft | undefined
|
|
|
238
150
|
compat: { ...baseHyperCompat, supportsReasoningEffort: reportedReasoningLevels.length > 0 },
|
|
239
151
|
...(thinkingLevelMap ? { thinkingLevelMap } : {}),
|
|
240
152
|
};
|
|
241
|
-
return
|
|
153
|
+
return mapped;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface ParsedHyperCatalog {
|
|
157
|
+
models: ProviderModelDraft[];
|
|
158
|
+
diagnostics: {
|
|
159
|
+
rejectedCount: number;
|
|
160
|
+
duplicateCount: number;
|
|
161
|
+
};
|
|
242
162
|
}
|
|
243
163
|
|
|
244
|
-
function parseCurrentHyperModels(payload: Record<string, unknown>):
|
|
164
|
+
function parseCurrentHyperModels(payload: Record<string, unknown>): ParsedHyperCatalog | undefined {
|
|
245
165
|
if (!Array.isArray(payload.models)) return undefined;
|
|
246
|
-
if (payload.models.length > MAX_PROVIDER_MODEL_COUNT)
|
|
166
|
+
if (payload.models.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
167
|
+
return { models: [], diagnostics: { rejectedCount: payload.models.length, duplicateCount: 0 } };
|
|
168
|
+
}
|
|
247
169
|
const models: ProviderModelDraft[] = [];
|
|
248
170
|
const seenIds = new Set<string>();
|
|
171
|
+
let rejectedCount = 0;
|
|
172
|
+
let duplicateCount = 0;
|
|
249
173
|
for (const value of payload.models) {
|
|
250
174
|
const model = parseCurrentHyperModel(value);
|
|
251
|
-
if (model === undefined)
|
|
175
|
+
if (model === undefined) {
|
|
176
|
+
rejectedCount++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
252
179
|
const normalizedId = model.id.toLowerCase();
|
|
253
|
-
if (seenIds.has(normalizedId))
|
|
180
|
+
if (seenIds.has(normalizedId)) {
|
|
181
|
+
duplicateCount++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
254
184
|
seenIds.add(normalizedId);
|
|
255
185
|
models.push(model);
|
|
256
186
|
}
|
|
257
|
-
return models;
|
|
187
|
+
return { models, diagnostics: { rejectedCount, duplicateCount } };
|
|
258
188
|
}
|
|
259
189
|
|
|
260
|
-
function parseLegacyHyperModels(payload: Record<string, unknown>):
|
|
261
|
-
if (!Array.isArray(payload.data)
|
|
190
|
+
function parseLegacyHyperModels(payload: Record<string, unknown>): ParsedHyperCatalog {
|
|
191
|
+
if (!Array.isArray(payload.data)) {
|
|
192
|
+
return { models: [], diagnostics: { rejectedCount: 0, duplicateCount: 0 } };
|
|
193
|
+
}
|
|
194
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
195
|
+
return { models: [], diagnostics: { rejectedCount: payload.data.length, duplicateCount: 0 } };
|
|
196
|
+
}
|
|
262
197
|
const models: ProviderModelDraft[] = [];
|
|
263
198
|
const seenIds = new Set<string>();
|
|
199
|
+
let rejectedCount = 0;
|
|
200
|
+
let duplicateCount = 0;
|
|
264
201
|
|
|
265
202
|
for (const value of payload.data) {
|
|
266
|
-
if (!isRecord(value) || typeof value.id !== "string" || value.id.trim() === "")
|
|
203
|
+
if (!isRecord(value) || typeof value.id !== "string" || value.id.trim() === "") {
|
|
204
|
+
rejectedCount++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
267
207
|
const id = value.id.trim();
|
|
268
208
|
const normalizedId = id.toLowerCase();
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
209
|
+
if (value.context_window !== undefined && !isPositiveInteger(value.context_window)) {
|
|
210
|
+
rejectedCount++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (value.max_output_tokens !== undefined && !isPositiveInteger(value.max_output_tokens)) {
|
|
214
|
+
rejectedCount++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
272
217
|
|
|
273
218
|
const contextWindow = isPositiveInteger(value.context_window) ? value.context_window : undefined;
|
|
274
219
|
const maxTokens = isPositiveInteger(value.max_output_tokens) ? value.max_output_tokens : undefined;
|
|
275
|
-
if (contextWindow !== undefined && maxTokens !== undefined && maxTokens > contextWindow)
|
|
220
|
+
if (contextWindow !== undefined && maxTokens !== undefined && maxTokens > contextWindow) {
|
|
221
|
+
rejectedCount++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (seenIds.has(normalizedId)) {
|
|
225
|
+
duplicateCount++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
276
228
|
const reasoning = isRecord(value.reasoning) ? value.reasoning : undefined;
|
|
277
229
|
const capabilities = isRecord(value.capabilities) ? value.capabilities : undefined;
|
|
278
230
|
const reasoningEffortLevels = readEffortLevels(value);
|
|
@@ -310,27 +262,29 @@ function parseLegacyHyperModels(payload: Record<string, unknown>): ProviderModel
|
|
|
310
262
|
headers: { ...hyperModelHeaders },
|
|
311
263
|
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
312
264
|
...(maxTokens !== undefined ? { maxTokens } : {}),
|
|
313
|
-
cost
|
|
314
|
-
pricingSource: cost ? "provider" : "fallback",
|
|
265
|
+
...(cost ? { cost, pricingSource: "provider" as const } : {}),
|
|
315
266
|
compat: { ...baseHyperCompat, supportsReasoningEffort },
|
|
316
267
|
};
|
|
317
|
-
models.push(
|
|
268
|
+
models.push(mapped);
|
|
318
269
|
seenIds.add(normalizedId);
|
|
319
270
|
}
|
|
320
|
-
return models;
|
|
271
|
+
return { models, diagnostics: { rejectedCount, duplicateCount } };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseHyperCatalog(payload: unknown): ParsedHyperCatalog {
|
|
275
|
+
if (!isRecord(payload)) return { models: [], diagnostics: { rejectedCount: 0, duplicateCount: 0 } };
|
|
276
|
+
return parseCurrentHyperModels(payload) ?? parseLegacyHyperModels(payload);
|
|
321
277
|
}
|
|
322
278
|
|
|
323
279
|
export function parseHyperModels(payload: unknown): ProviderModelDraft[] {
|
|
324
|
-
|
|
325
|
-
const currentModels = parseCurrentHyperModels(payload);
|
|
326
|
-
return currentModels ?? parseLegacyHyperModels(payload);
|
|
280
|
+
return parseHyperCatalog(payload).models;
|
|
327
281
|
}
|
|
328
282
|
|
|
329
283
|
async function discoverHyperModels(
|
|
330
284
|
fetchFn: typeof globalThis.fetch,
|
|
331
285
|
timeoutMs: number,
|
|
332
286
|
externalSignal?: AbortSignal,
|
|
333
|
-
): Promise<
|
|
287
|
+
): Promise<ModelCatalogDiscoveryResult> {
|
|
334
288
|
return withDeadline(
|
|
335
289
|
async (signal) => {
|
|
336
290
|
let endpoint = HYPER_PROVIDER_URL;
|
|
@@ -354,12 +308,12 @@ async function discoverHyperModels(
|
|
|
354
308
|
"badjson",
|
|
355
309
|
);
|
|
356
310
|
}
|
|
357
|
-
const
|
|
358
|
-
if (models.length === 0) {
|
|
311
|
+
const parsed = parseHyperCatalog(payload);
|
|
312
|
+
if (parsed.models.length === 0) {
|
|
359
313
|
throw new ProviderDataError("Charm Hyper model discovery returned no valid models", "badjson");
|
|
360
314
|
}
|
|
361
|
-
|
|
362
|
-
return
|
|
315
|
+
validateProviderModelDrafts(parsed.models);
|
|
316
|
+
return parsed;
|
|
363
317
|
},
|
|
364
318
|
timeoutMs,
|
|
365
319
|
externalSignal,
|
|
@@ -380,19 +334,18 @@ function catalogErrorCode(error: unknown): string {
|
|
|
380
334
|
}
|
|
381
335
|
|
|
382
336
|
type HyperModelsStoreEntry = ProviderRefreshContext["stored"];
|
|
383
|
-
type HyperStoredModel =
|
|
384
|
-
|
|
337
|
+
type HyperStoredModel = ProviderModelDraft & {
|
|
338
|
+
provider: string;
|
|
339
|
+
baseUrl: string;
|
|
340
|
+
api: ProviderModelDraft["api"];
|
|
385
341
|
};
|
|
386
342
|
|
|
387
|
-
function isValidTimestamp(value: unknown): value is number {
|
|
388
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
343
|
function draftsFromStoredModels(entry: HyperModelsStoreEntry): ProviderModelDraft[] | undefined {
|
|
392
344
|
if (!entry || !Array.isArray(entry.models) || entry.models.length === 0) return undefined;
|
|
345
|
+
if (isLegacyNormalizedSnapshot(entry.models)) return undefined;
|
|
393
346
|
try {
|
|
394
347
|
const drafts: ProviderModelDraft[] = entry.models.map(({ provider: _provider, ...model }) => model);
|
|
395
|
-
|
|
348
|
+
validateProviderModelDrafts(drafts);
|
|
396
349
|
return drafts;
|
|
397
350
|
} catch {
|
|
398
351
|
return undefined;
|
|
@@ -400,10 +353,12 @@ function draftsFromStoredModels(entry: HyperModelsStoreEntry): ProviderModelDraf
|
|
|
400
353
|
}
|
|
401
354
|
|
|
402
355
|
function storedModelsFromDrafts(models: ProviderModelDraft[]): HyperStoredModel[] {
|
|
403
|
-
|
|
404
|
-
|
|
356
|
+
validateProviderModelDrafts(models);
|
|
357
|
+
return models.map((model) => {
|
|
358
|
+
const source = model.pricingSource;
|
|
405
359
|
return {
|
|
406
360
|
...model,
|
|
361
|
+
name: typeof model.name === "string" && model.name.trim() !== "" ? model.name.trim() : model.id,
|
|
407
362
|
...(source ? { pricingSource: source } : {}),
|
|
408
363
|
api: model.api ?? "openai-completions",
|
|
409
364
|
provider: "charm-hyper",
|
|
@@ -412,113 +367,28 @@ function storedModelsFromDrafts(models: ProviderModelDraft[]): HyperStoredModel[
|
|
|
412
367
|
});
|
|
413
368
|
}
|
|
414
369
|
|
|
415
|
-
async function publishCatalog(
|
|
416
|
-
context: ProviderRefreshContext,
|
|
417
|
-
models: ProviderModelDraft[],
|
|
418
|
-
checkedAt: number,
|
|
419
|
-
update: () => void,
|
|
420
|
-
): Promise<boolean> {
|
|
421
|
-
try {
|
|
422
|
-
return await context.publish({ persist: { models: storedModelsFromDrafts(models), checkedAt }, update });
|
|
423
|
-
} catch {
|
|
424
|
-
// Persistence is an optimization. Retry the generation-checked in-memory update without it.
|
|
425
|
-
try {
|
|
426
|
-
return await context.publish({ update });
|
|
427
|
-
} catch {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
370
|
export function createCharmHyperAdapter(
|
|
434
371
|
fetchFn: typeof globalThis.fetch,
|
|
435
372
|
discoveryTimeoutMs: number,
|
|
436
373
|
now: () => number = Date.now,
|
|
374
|
+
initialModels?: ProviderModelDraft[],
|
|
437
375
|
): ProviderAdapter {
|
|
438
|
-
let models = getHyperFallbackModels();
|
|
439
|
-
let lastRefreshAt: number | undefined;
|
|
440
|
-
let lastCatalogUpdatedAt: number | undefined;
|
|
441
|
-
let inFlightRefresh: { signal: AbortSignal; request: Promise<ProviderModelDraft[]> } | undefined;
|
|
442
|
-
const catalog: ModelCatalogStatus = { source: "fallback", modelCount: models.length };
|
|
443
376
|
let provider: ProviderAdapter["provider"];
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
models
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const restoreStoredModels = async (context: ProviderRefreshContext, entry: HyperModelsStoreEntry): Promise<void> => {
|
|
462
|
-
const restoredModels = draftsFromStoredModels(entry);
|
|
463
|
-
if (!restoredModels) return;
|
|
464
|
-
const checkedAt = isValidTimestamp(entry?.checkedAt) ? entry.checkedAt : undefined;
|
|
465
|
-
if (lastCatalogUpdatedAt !== undefined && checkedAt !== undefined && checkedAt <= lastCatalogUpdatedAt) return;
|
|
466
|
-
if (lastCatalogUpdatedAt !== undefined && checkedAt === undefined) return;
|
|
467
|
-
try {
|
|
468
|
-
await context.publish({
|
|
469
|
-
update: () => {
|
|
470
|
-
publishModels(restoredModels, "live", checkedAt);
|
|
471
|
-
if (checkedAt !== undefined) lastRefreshAt = checkedAt;
|
|
472
|
-
},
|
|
473
|
-
});
|
|
474
|
-
} catch {
|
|
475
|
-
// A stale or cancelled refresh must not replace the current in-memory catalog.
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
const isFresh = (timestamp: number | undefined, currentTime: number): boolean =>
|
|
480
|
-
timestamp !== undefined && Math.max(0, currentTime - timestamp) <= HYPER_MODEL_CATALOG_TTL_MS;
|
|
481
|
-
|
|
482
|
-
const refreshModels = async (context: ProviderRefreshContext): Promise<ProviderModelDraft[]> => {
|
|
483
|
-
await restoreStoredModels(context, context.stored);
|
|
484
|
-
if (context?.allowNetwork !== true || context.signal?.aborted) return [...models];
|
|
485
|
-
|
|
486
|
-
const currentTime = now();
|
|
487
|
-
if (!context.force && isFresh(lastRefreshAt, currentTime)) return [...models];
|
|
488
|
-
if (inFlightRefresh?.signal === context.signal) return inFlightRefresh.request;
|
|
489
|
-
|
|
490
|
-
const request = (async (): Promise<ProviderModelDraft[]> => {
|
|
491
|
-
try {
|
|
492
|
-
const refreshedModels = await discoverHyperModels(fetchFn, discoveryTimeoutMs, context.signal);
|
|
493
|
-
if (context.signal?.aborted) {
|
|
494
|
-
throw context.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
495
|
-
}
|
|
496
|
-
const updatedAt = now();
|
|
497
|
-
await publishCatalog(context, refreshedModels, updatedAt, () => {
|
|
498
|
-
publishModels(refreshedModels, "live", updatedAt);
|
|
499
|
-
lastRefreshAt = updatedAt;
|
|
500
|
-
});
|
|
501
|
-
return [...models];
|
|
502
|
-
} catch (error) {
|
|
503
|
-
if (!context.signal.aborted && !isAbortError(error)) {
|
|
504
|
-
lastRefreshAt = now();
|
|
505
|
-
catalog.lastError = catalogErrorCode(error);
|
|
506
|
-
}
|
|
507
|
-
throw error;
|
|
508
|
-
}
|
|
509
|
-
})();
|
|
510
|
-
const activeRefresh = { signal: context.signal, request };
|
|
511
|
-
inFlightRefresh = activeRefresh;
|
|
512
|
-
void request.then(
|
|
513
|
-
() => {
|
|
514
|
-
if (inFlightRefresh === activeRefresh) inFlightRefresh = undefined;
|
|
515
|
-
},
|
|
516
|
-
() => {
|
|
517
|
-
if (inFlightRefresh === activeRefresh) inFlightRefresh = undefined;
|
|
518
|
-
},
|
|
519
|
-
);
|
|
520
|
-
return request;
|
|
521
|
-
};
|
|
377
|
+
const lifecycle = createModelCatalogLifecycle({
|
|
378
|
+
initialModels,
|
|
379
|
+
ttlMs: HYPER_MODEL_CATALOG_TTL_MS,
|
|
380
|
+
now,
|
|
381
|
+
discover: (context) => discoverHyperModels(fetchFn, discoveryTimeoutMs, context.signal),
|
|
382
|
+
restore: draftsFromStoredModels,
|
|
383
|
+
persist: (models, checkedAt) => ({
|
|
384
|
+
models: storedModelsFromDrafts(models) as unknown as NonNullable<HyperModelsStoreEntry>["models"],
|
|
385
|
+
checkedAt,
|
|
386
|
+
}),
|
|
387
|
+
onUpdate: (models) => {
|
|
388
|
+
if (provider) provider.models = models;
|
|
389
|
+
},
|
|
390
|
+
errorCode: catalogErrorCode,
|
|
391
|
+
});
|
|
522
392
|
|
|
523
393
|
provider = {
|
|
524
394
|
name: "Charm Hyper",
|
|
@@ -526,12 +396,12 @@ export function createCharmHyperAdapter(
|
|
|
526
396
|
apiKey: "$HYPER_API_KEY",
|
|
527
397
|
authHeader: true,
|
|
528
398
|
api: "openai-completions",
|
|
529
|
-
models,
|
|
530
|
-
refreshModels,
|
|
399
|
+
models: lifecycle.getModels(),
|
|
400
|
+
refreshModels: lifecycle.refreshModels,
|
|
531
401
|
oauth: createCharmHyperOAuth(fetchFn, now),
|
|
532
402
|
};
|
|
533
403
|
|
|
534
|
-
return { id: "charm-hyper", catalog, provider };
|
|
404
|
+
return { id: "charm-hyper", catalog: lifecycle.catalog, lifecycle, provider };
|
|
535
405
|
}
|
|
536
406
|
|
|
537
407
|
const charmHyperProviderExtension = defineProviderExtension({
|
package/status/huggingface.ts
CHANGED
|
@@ -31,10 +31,11 @@ export function parseHuggingFaceAccount(payload: unknown): HuggingFaceAccount {
|
|
|
31
31
|
}
|
|
32
32
|
// Response envelope: { type, id, name, emailVerified, canPay, isPro, plan, periodEnd, credits, ... }
|
|
33
33
|
const plan = safeText(payload.plan);
|
|
34
|
+
const credits = finiteNumber(payload.credits);
|
|
34
35
|
// Older token generations omit the envelope fields entirely.
|
|
35
36
|
return {
|
|
36
37
|
...(plan !== undefined ? { plan } : {}),
|
|
37
|
-
...(
|
|
38
|
+
...(credits !== undefined ? { credits } : {}),
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
package/status/openrouter.ts
CHANGED
|
@@ -149,8 +149,14 @@ export const openRouterStatusAdapter: StatusAdapter = {
|
|
|
149
149
|
unit: "USD",
|
|
150
150
|
};
|
|
151
151
|
} catch (error) {
|
|
152
|
-
//
|
|
153
|
-
|
|
152
|
+
// /credits requires a management key, so permission-shaped failures
|
|
153
|
+
// degrade to the key payload; real failures must still surface.
|
|
154
|
+
if (
|
|
155
|
+
!(error instanceof ProviderDataError) ||
|
|
156
|
+
(error.httpStatus !== 401 && error.httpStatus !== 403 && error.httpStatus !== 404)
|
|
157
|
+
) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
return {
|
|
@@ -59,7 +59,7 @@ export function createVercelAIGatewayStatusAdapter(requestTimeoutMs: number): St
|
|
|
59
59
|
if (!response.ok) {
|
|
60
60
|
throw new ProviderDataError(
|
|
61
61
|
`Vercel AI Gateway status failed: HTTP ${response.status}`,
|
|
62
|
-
response.status === 401 ? "auth" : `http${response.status}`,
|
|
62
|
+
response.status === 401 || response.status === 403 ? "auth" : `http${response.status}`,
|
|
63
63
|
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
64
64
|
response.status,
|
|
65
65
|
);
|