@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.
@@ -1,5 +1,5 @@
1
1
  import type {
2
- ModelCatalogStatus,
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
- if (!isFiniteNonNegative(value.cost_per_1m_in_cached)) return undefined;
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
- cacheRead: value.cost_per_1m_in_cached,
233
- cacheWrite: 0,
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 applyModelOverride(mapped);
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>): ProviderModelDraft[] | undefined {
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) return [];
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) return [];
175
+ if (model === undefined) {
176
+ rejectedCount++;
177
+ continue;
178
+ }
252
179
  const normalizedId = model.id.toLowerCase();
253
- if (seenIds.has(normalizedId)) continue;
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>): ProviderModelDraft[] {
261
- if (!Array.isArray(payload.data) || payload.data.length > MAX_PROVIDER_MODEL_COUNT) return [];
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() === "") continue;
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 (seenIds.has(normalizedId)) continue;
270
- if (value.context_window !== undefined && !isPositiveInteger(value.context_window)) continue;
271
- if (value.max_output_tokens !== undefined && !isPositiveInteger(value.max_output_tokens)) continue;
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) continue;
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: cost ?? costFallbackFor(id),
314
- pricingSource: cost ? "provider" : "fallback",
265
+ ...(cost ? { cost, pricingSource: "provider" as const } : {}),
315
266
  compat: { ...baseHyperCompat, supportsReasoningEffort },
316
267
  };
317
- models.push(applyModelOverride(mapped));
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
- if (!isRecord(payload)) return [];
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<ProviderModelDraft[]> {
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 models = parseHyperModels(payload);
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
- normalizeProviderModels(models);
362
- return models;
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 = NonNullable<HyperModelsStoreEntry>["models"][number] & {
384
- pricingSource?: ProviderModelDraft["pricingSource"];
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
- normalizeProviderModels(drafts);
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
- return normalizeProviderModels(models).map((model) => {
404
- const source = models.find(({ id }) => id === model.id)?.pricingSource;
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
- const publishModels = (
446
- nextModels: ProviderModelDraft[],
447
- source: ModelCatalogStatus["source"],
448
- updatedAt?: number,
449
- ) => {
450
- models = nextModels;
451
- provider.models = models;
452
- catalog.source = source;
453
- catalog.modelCount = models.length;
454
- catalog.lastError = undefined;
455
- if (updatedAt !== undefined) {
456
- catalog.updatedAt = updatedAt;
457
- lastCatalogUpdatedAt = updatedAt;
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({
@@ -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
- ...(finiteNumber(payload.credits) !== undefined ? { credits: finiteNumber(payload.credits) } : {}),
38
+ ...(credits !== undefined ? { credits } : {}),
38
39
  };
39
40
  }
40
41
 
@@ -149,8 +149,14 @@ export const openRouterStatusAdapter: StatusAdapter = {
149
149
  unit: "USD",
150
150
  };
151
151
  } catch (error) {
152
- // Safe fallback: key credits, free-tier, and key-level limit still display.
153
- if (!(error instanceof ProviderDataError)) throw error;
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
  );