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