@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/core/types.ts CHANGED
@@ -4,7 +4,10 @@ import type {
4
4
  ProviderConfig,
5
5
  ProviderModelConfig,
6
6
  } from "@earendil-works/pi-coding-agent";
7
- import type { OfficialModelMeta } from "./official-pricing.ts";
7
+
8
+ import type { ModelCatalogLifecycle } from "./model-catalog.ts";
9
+ import type { PiCatalogModelMeta, PiCatalogSnapshot } from "./pi-model-metadata.ts";
10
+ export type { PiCatalogModelMeta, PiCatalogSnapshot };
8
11
 
9
12
  export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
10
13
 
@@ -12,9 +15,17 @@ export type ProviderCost = ProviderModelConfig["cost"];
12
15
  export type ProviderModel = ProviderModelConfig;
13
16
  export type PricingSku = "input" | "output" | "cacheRead" | "cacheWrite";
14
17
  /** Pricing provenance used by Pi Provider sidecars; not added to Pi model objects. */
15
- export type ProviderPricingSource = "provider" | "fallback" | "official";
18
+ export type ProviderPricingSource = "provider" | "fallback" | "official" | "pi" | "mixed";
16
19
  export type ModelPricingSource = ProviderPricingSource | "native";
17
- export type ModelFieldSource = ProviderPricingSource | "native" | "default";
20
+ export type ModelFieldSource =
21
+ | "provider"
22
+ | "pi"
23
+ | "native"
24
+ | "default"
25
+ | "normalized"
26
+ | "mixed"
27
+ | "official"
28
+ | "fallback";
18
29
  export type ModelMetadataState = "fresh" | "stale" | "checking" | "unavailable";
19
30
 
20
31
  export interface ModelMetadataStatus {
@@ -41,11 +52,21 @@ export interface ProviderRequestAuth {
41
52
  env?: Record<string, string>;
42
53
  }
43
54
 
55
+ export interface ModelCostBySkuSources {
56
+ input?: ModelFieldSource;
57
+ output?: ModelFieldSource;
58
+ cacheRead?: ModelFieldSource;
59
+ cacheWrite?: ModelFieldSource;
60
+ }
61
+
44
62
  export interface ModelFieldSources {
63
+ cost?: ModelFieldSource;
64
+ costBySku?: ModelCostBySkuSources;
45
65
  contextWindow?: ModelFieldSource;
46
66
  maxTokens?: ModelFieldSource;
47
67
  input?: ModelFieldSource;
48
68
  reasoning?: ModelFieldSource;
69
+ thinkingLevelMap?: ModelFieldSource;
49
70
  }
50
71
 
51
72
  export interface ProviderPricingAdjustment {
@@ -70,26 +91,12 @@ export interface ModelPricingDetails {
70
91
  effectiveCost?: ProviderCost;
71
92
  adjustment?: ProviderPricingAdjustment;
72
93
  note?: string;
73
- }
74
-
75
- export interface ModelQualityScore {
76
- source: string;
77
- benchmark: string;
78
- category: string;
79
- metric: "elo" | "rating" | "score" | "ips";
80
- value: number;
81
- rank?: number;
82
- winRate?: number;
83
- confidenceInterval?: {
84
- lower: number;
85
- upper: number;
86
- };
94
+ costBySku?: ModelCostBySkuSources;
87
95
  }
88
96
 
89
97
  export interface ProviderModelMetadata {
90
98
  pricing: ModelPricingDetails;
91
99
  fieldSources?: ModelFieldSources;
92
- quality?: ModelQualityScore[];
93
100
  }
94
101
 
95
102
  export type ProviderModelDraft = Partial<ProviderModel> &
@@ -112,12 +119,18 @@ export type ProviderDefinition = Omit<ProviderConfig, "models" | "refreshModels"
112
119
  refreshModels?: (context: ProviderRefreshContext) => Promise<ProviderModelDraft[]>;
113
120
  };
114
121
 
115
- export type ModelCatalogSource = "static" | "live" | "fallback";
122
+ export type ModelCatalogSource = "static" | "live" | "cached" | "fallback" | "empty";
116
123
 
117
124
  export interface ModelCatalogStatus {
118
125
  source: ModelCatalogSource;
119
126
  modelCount: number;
120
127
  updatedAt?: number;
128
+ lastSuccessfulRefreshAt?: number;
129
+ lastAttemptAt?: number;
130
+ consecutiveFailures?: number;
131
+ nextRetryAt?: number;
132
+ rejectedCount?: number;
133
+ duplicateCount?: number;
121
134
  lastError?: string;
122
135
  }
123
136
 
@@ -191,12 +204,16 @@ export interface ProviderAdapter {
191
204
  /** Optional explicit price adjustments owned by this Provider. */
192
205
  pricing?: ProviderPricingPolicy;
193
206
  catalog?: ModelCatalogStatus;
207
+ lifecycle?: ModelCatalogLifecycle;
208
+ /** Whether to fallback to Pi's builtin catalog when fields are missing. Defaults to true. */
209
+ usePiModelMetaFallback?: boolean;
194
210
  /** @internal Draft state shared across isolated Adapter and Host contexts. */
195
211
  registration?: {
196
212
  modelDrafts: ProviderModelDraft[];
197
213
  normalizedModels: ProviderModel[];
198
214
  modelMetadata: Record<string, ProviderModelMetadata>;
199
- officialPricing: Record<string, OfficialModelMeta>;
215
+ piCatalog?: PiCatalogSnapshot;
216
+ officialPricing?: Record<string, any>;
200
217
  activeRefreshes: number;
201
218
  deferredRegistration?: () => void;
202
219
  };
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as piBuiltinCatalog from "@earendil-works/pi-ai/providers/all";
1
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
  import { getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
3
4
  import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
@@ -17,7 +18,7 @@ export {
17
18
  defineStatusExtension,
18
19
  defineTunerExtension,
19
20
  } from "./core/adapter-extensions.ts";
20
- export { MAX_PROVIDER_MODEL_COUNT } from "./core/adapter-validation.ts";
21
+ export { MAX_PROVIDER_MODEL_COUNT, validateProviderModelDrafts } from "./core/adapter-validation.ts";
21
22
  export { createCatalogPreflightAdapter } from "./core/catalog-preflight.ts";
22
23
  export { withDeadline } from "./core/deadline.ts";
23
24
  export {
@@ -54,23 +55,25 @@ export type {
54
55
  LiveCheckSnapshot,
55
56
  } from "./core/live-check-manager.ts";
56
57
  export { getLiveCheckKey, LIVE_CHECK_SCOPE, LiveCheckManager } from "./core/live-check-manager.ts";
57
- export {
58
- applyOfficialModelCosts,
59
- clearPricingCache,
60
- fetchOfficialPricing,
61
- findOfficialCost,
62
- findOfficialMeta,
63
- getDefaultOpenRouterMetadataCachePath,
64
- getPricingCache,
65
- getPricingCacheAge,
66
- type OfficialModelMeta,
67
- type OfficialPricingFetchOptions,
68
- OPENROUTER_MODELS_URL,
69
- parseOpenRouterModels,
70
- parseOpenRouterPricing,
71
- setPricingCache,
72
- } from "./core/official-pricing.ts";
58
+ export type {
59
+ ModelCatalogDiagnostics,
60
+ ModelCatalogDiscoveryResult,
61
+ ModelCatalogLifecycle,
62
+ ModelCatalogLifecycleOptions,
63
+ } from "./core/model-catalog.ts";
64
+ export { createModelCatalogLifecycle } from "./core/model-catalog.ts";
73
65
  export { createOpenCodeCatalogPreflightAdapter } from "./core/opencode-preflight.ts";
66
+ export type { PiCatalogModelMeta, PiCatalogSnapshot } from "./core/pi-model-metadata.ts";
67
+ export {
68
+ findPiCatalogModel,
69
+ isLegacyNormalizedModel,
70
+ isLegacyNormalizedSnapshot,
71
+ loadPiCatalog,
72
+ mergeModelWithPiCatalog,
73
+ ORIGINAL_PI_PROVIDER_ALLOWLIST,
74
+ parsePiCatalogFromProviders,
75
+ toPiCatalogSnapshot,
76
+ } from "./core/pi-model-metadata.ts";
74
77
  export type {
75
78
  PreflightAdapter,
76
79
  PreflightContext,
@@ -87,18 +90,23 @@ export type { RateLimitWindow } from "./core/ratelimit-headers.ts";
87
90
  export { parseRetryAfter } from "./core/retry-after.ts";
88
91
  export type { StatusDiagnostics, StatusErrorState } from "./core/status-manager.ts";
89
92
  export { normalizeStatusSnapshot, StatusManager } from "./core/status-manager.ts";
93
+ export {
94
+ formatProviderStatus,
95
+ getStatusModeCompletions,
96
+ parseStatusMode,
97
+ } from "./core/status-report.ts";
90
98
  export { applyTunerAdapters, sortTunerAdapters } from "./core/tuner-manager.ts";
91
99
  export type {
92
100
  ActiveModel,
93
101
  ModelCatalogSource,
94
102
  ModelCatalogStatus,
103
+ ModelCostBySkuSources,
95
104
  ModelFieldSource,
96
105
  ModelFieldSources,
97
106
  ModelMetadataState,
98
107
  ModelMetadataStatus,
99
108
  ModelPricingDetails,
100
109
  ModelPricingSource,
101
- ModelQualityScore,
102
110
  PiApi,
103
111
  PricingSku,
104
112
  ProviderAdapter,
@@ -146,6 +154,11 @@ export function createPiProviderExtension(
146
154
  wrapTextWithAnsi: typeof wrapTextWithAnsi;
147
155
  adapterRoot?: string;
148
156
  dependencies?: Partial<PiProviderDependencies>;
157
+ piCatalogSource: {
158
+ getBuiltinProviders: () => string[];
159
+ getBuiltinModels: (provider: string) => unknown[];
160
+ getBuiltinModelDataGeneratedAt: () => number | undefined;
161
+ };
149
162
  },
150
163
  ) => Promise<void>;
151
164
  };
@@ -155,6 +168,12 @@ export function createPiProviderExtension(
155
168
  wrapTextWithAnsi,
156
169
  adapterRoot: options.adapterRoot,
157
170
  dependencies: options.dependencies,
171
+ piCatalogSource: {
172
+ getBuiltinProviders: () => piBuiltinCatalog.getBuiltinProviders(),
173
+ getBuiltinModels: (provider) =>
174
+ piBuiltinCatalog.getBuiltinModels(provider as Parameters<typeof piBuiltinCatalog.getBuiltinModels>[0]),
175
+ getBuiltinModelDataGeneratedAt: () => piBuiltinCatalog.getBuiltinModelDataGeneratedAt?.(),
176
+ },
158
177
  });
159
178
  };
160
179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyav/pi-provider",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Provider extension toolkit for Pi to integrate and manage custom LLM providers with dynamic models, request tuners, and account status.",
5
5
  "author": "hyav",
6
6
  "license": "MIT",
@@ -44,11 +44,13 @@
44
44
  "node": ">=22.19.0"
45
45
  },
46
46
  "peerDependencies": {
47
+ "@earendil-works/pi-ai": "*",
47
48
  "@earendil-works/pi-coding-agent": "*",
48
49
  "@earendil-works/pi-tui": "*"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@biomejs/biome": "2.3.5",
53
+ "@earendil-works/pi-ai": "0.84.1",
52
54
  "@earendil-works/pi-coding-agent": "0.84.1",
53
55
  "@earendil-works/pi-tui": "0.84.1",
54
56
  "@types/node": "22.20.1",
@@ -13,9 +13,13 @@ export function createCharmHyperPreflightAdapter(requestTimeoutMs: number): Pref
13
13
  supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, HYPER_PROVIDER_URL),
14
14
  async fetch(context) {
15
15
  const apiKey = await context.getApiKey();
16
- if (!apiKey) return { passed: false, checks: ["auth"], updatedAt: context.now() };
16
+ // "proxy-managed" is not a usable credential for the official endpoints,
17
+ // so it fails closed together with a missing key.
18
+ if (!apiKey || apiKey === "proxy-managed") {
19
+ return { passed: false, checks: ["auth"], updatedAt: context.now() };
20
+ }
17
21
  const headers = new Headers(hyperJsonHeaders());
18
- if (apiKey !== "proxy-managed") headers.set("Authorization", `Bearer ${apiKey}`);
22
+ headers.set("Authorization", `Bearer ${apiKey}`);
19
23
  let endpoint = HYPER_PROVIDER_URL;
20
24
  let response = await context.fetch(endpoint, { headers, signal: context.signal });
21
25
  if (response.status === 404) {
@@ -1,5 +1,11 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ hasBaseUrlOrigin,
5
+ MAX_PROVIDER_MODEL_COUNT,
6
+ ProviderDataError,
7
+ parseRetryAfter,
8
+ } from "@hyav/pi-provider";
3
9
 
4
10
  export const DEEPSEEK_MODELS_URL = "https://api.deepseek.com/models";
5
11
 
@@ -44,6 +50,9 @@ export const deepSeekPreflightAdapter: PreflightAdapter = {
44
50
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
45
51
  throw new ProviderDataError("DeepSeek preflight returned invalid catalog data", "badjson");
46
52
  }
53
+ if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
54
+ throw new ProviderDataError("DeepSeek preflight catalog exceeds the maximum model count", "badjson");
55
+ }
47
56
  const modelIds = new Set(
48
57
  payload.data
49
58
  .filter(isRecord)
@@ -4,6 +4,7 @@ import {
4
4
  authDefinesHeader,
5
5
  definePreflightExtension,
6
6
  getContextAuth,
7
+ MAX_PROVIDER_MODEL_COUNT,
7
8
  mergeDiagnosticHeaders,
8
9
  ProviderDataError,
9
10
  parseRetryAfter,
@@ -57,6 +58,9 @@ export const githubCopilotPreflightAdapter: PreflightAdapter = {
57
58
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
58
59
  throw new ProviderDataError("GitHub Copilot preflight returned invalid catalog data", "badjson");
59
60
  }
61
+ if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
62
+ throw new ProviderDataError("GitHub Copilot preflight catalog exceeds the maximum model count", "badjson");
63
+ }
60
64
  const modelIds = new Set(
61
65
  payload.data.flatMap((model) => {
62
66
  if (!isRecord(model) || typeof model.id !== "string") return [];
@@ -1,5 +1,11 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ hasBaseUrlOrigin,
5
+ MAX_PROVIDER_MODEL_COUNT,
6
+ ProviderDataError,
7
+ parseRetryAfter,
8
+ } from "@hyav/pi-provider";
3
9
 
4
10
  export const GOOGLE_MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
5
11
 
@@ -59,6 +65,9 @@ export const googlePreflightAdapter: PreflightAdapter = {
59
65
  if (!isRecord(payload) || !Array.isArray(payload.models)) {
60
66
  throw new ProviderDataError("Google preflight returned invalid catalog data", "badjson");
61
67
  }
68
+ if (payload.models.length > MAX_PROVIDER_MODEL_COUNT) {
69
+ throw new ProviderDataError("Google preflight catalog exceeds the maximum model count", "badjson");
70
+ }
62
71
  const modelIds = new Set(
63
72
  payload.models.flatMap((value) => {
64
73
  if (!isRecord(value)) return [];
package/preflight/groq.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ hasBaseUrlOrigin,
5
+ MAX_PROVIDER_MODEL_COUNT,
6
+ ProviderDataError,
7
+ parseRetryAfter,
8
+ } from "@hyav/pi-provider";
3
9
  import { GROQ_MODELS_URL } from "../status/groq.ts";
4
10
 
5
11
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -43,6 +49,9 @@ export const groqPreflightAdapter: PreflightAdapter = {
43
49
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
44
50
  throw new ProviderDataError("Groq preflight returned invalid catalog data", "badjson");
45
51
  }
52
+ if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
53
+ throw new ProviderDataError("Groq preflight catalog exceeds the maximum model count", "badjson");
54
+ }
46
55
  const activeIds = new Set(
47
56
  payload.data.flatMap((model) => {
48
57
  if (!isRecord(model) || typeof model.id !== "string") return [];
@@ -1,5 +1,11 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ hasBaseUrlOrigin,
5
+ MAX_PROVIDER_MODEL_COUNT,
6
+ ProviderDataError,
7
+ parseRetryAfter,
8
+ } from "@hyav/pi-provider";
3
9
  import { extractCodexAccountId } from "../status/openai-codex.ts";
4
10
 
5
11
  export const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
@@ -13,6 +19,9 @@ function codexModelIds(payload: unknown): Set<string> {
13
19
  if (!isRecord(payload) || !Array.isArray(payload.models)) {
14
20
  throw new ProviderDataError("OpenAI Codex preflight returned invalid catalog data", "badjson");
15
21
  }
22
+ if (payload.models.length > MAX_PROVIDER_MODEL_COUNT) {
23
+ throw new ProviderDataError("OpenAI Codex preflight catalog exceeds the maximum model count", "badjson");
24
+ }
16
25
  return new Set(
17
26
  payload.models
18
27
  .filter(isRecord)
@@ -1,5 +1,11 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ hasBaseUrlOrigin,
5
+ MAX_PROVIDER_MODEL_COUNT,
6
+ ProviderDataError,
7
+ parseRetryAfter,
8
+ } from "@hyav/pi-provider";
3
9
  import { OPENROUTER_KEY_URL } from "../status/openrouter.ts";
4
10
 
5
11
  export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
@@ -34,6 +40,9 @@ async function collectModelIds(context: Parameters<PreflightAdapter["fetch"]>[0]
34
40
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
35
41
  throw new ProviderDataError("OpenRouter preflight returned invalid catalog data", "badjson");
36
42
  }
43
+ if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
44
+ throw new ProviderDataError("OpenRouter preflight catalog exceeds the maximum model count", "badjson");
45
+ }
37
46
  return new Set(
38
47
  payload.data
39
48
  .filter(isRecord)
@@ -85,7 +94,7 @@ export const openRouterPreflightAdapter: PreflightAdapter = {
85
94
  const apiKey = await context.getApiKey();
86
95
  const checks: string[] = ["endpoint", "catalog"];
87
96
  if (!apiKey || apiKey === "proxy-managed") {
88
- return { passed: modelIds.has(context.model.id), checks: [...checks, "auth"], updatedAt: context.now() };
97
+ return { passed: false, checks: [...checks, "auth"], updatedAt: context.now() };
89
98
  }
90
99
  // Management keys use /api/v1/credits as the only key endpoint and get 404 here.
91
100
  const authStatus = await checkCredential(context, apiKey);
@@ -1,5 +1,10 @@
1
1
  import type { PreflightAdapter, PreflightSnapshot } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ MAX_PROVIDER_MODEL_COUNT,
5
+ ProviderDataError,
6
+ parseRetryAfter,
7
+ } from "@hyav/pi-provider";
3
8
  import { VERCEL_PROVIDER_ID } from "../status/vercel-ai-gateway/constants.ts";
4
9
 
5
10
  export const VERCEL_MODELS_URL = "https://ai-gateway.vercel.sh/v1/models";
@@ -12,6 +17,9 @@ export function parseVercelModelIds(payload: unknown): Set<string> {
12
17
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
13
18
  throw new ProviderDataError("Vercel AI Gateway preflight returned invalid catalog data", "badjson");
14
19
  }
20
+ if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
21
+ throw new ProviderDataError("Vercel AI Gateway preflight catalog exceeds the maximum model count", "badjson");
22
+ }
15
23
 
16
24
  const modelIds = new Set<string>();
17
25
  for (const value of payload.data) {
@@ -37,6 +45,8 @@ export function createVercelAIGatewayPreflightAdapter(requestTimeoutMs: number):
37
45
  cacheTtlMs: 30_000,
38
46
  requestTimeoutMs,
39
47
  async fetch(context): Promise<PreflightSnapshot> {
48
+ // Vercel's gateway catalog is public and needs no credential; the
49
+ // check intentionally reports no "auth" check.
40
50
  const response = await context.fetch(VERCEL_MODELS_URL, {
41
51
  headers: {
42
52
  Accept: "application/json",
package/preflight/xai.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ definePreflightExtension,
4
+ hasBaseUrlOrigin,
5
+ MAX_PROVIDER_MODEL_COUNT,
6
+ ProviderDataError,
7
+ parseRetryAfter,
8
+ } from "@hyav/pi-provider";
3
9
  import { XAI_MODELS_URL } from "../status/xai.ts";
4
10
 
5
11
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -15,12 +21,18 @@ export const xaiPreflightAdapter: PreflightAdapter = {
15
21
  supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, XAI_MODELS_URL),
16
22
  async fetch(context) {
17
23
  const apiKey = await context.getApiKey();
24
+ if (!apiKey || apiKey === "proxy-managed") {
25
+ // xAI authenticates every route, including /v1/models, so without a
26
+ // credential the catalog request cannot succeed. Fail closed instead
27
+ // of reporting a catalog match as a usable model.
28
+ return { passed: false, checks: ["auth"], updatedAt: context.now() };
29
+ }
18
30
  const authHeaders: Record<string, string> = {
19
31
  Accept: "application/json",
20
32
  "Accept-Encoding": "identity",
21
33
  "User-Agent": "@hyav/pi-provider",
34
+ Authorization: `Bearer ${apiKey}`,
22
35
  };
23
- if (apiKey && apiKey !== "proxy-managed") authHeaders.Authorization = `Bearer ${apiKey}`;
24
36
  const response = await context.fetch(XAI_MODELS_URL, {
25
37
  headers: authHeaders,
26
38
  signal: context.signal,
@@ -42,16 +54,18 @@ export const xaiPreflightAdapter: PreflightAdapter = {
42
54
  if (!isRecord(payload) || !Array.isArray(payload.data)) {
43
55
  throw new ProviderDataError("xAI preflight returned invalid catalog data", "badjson");
44
56
  }
57
+ if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
58
+ throw new ProviderDataError("xAI preflight catalog exceeds the maximum model count", "badjson");
59
+ }
45
60
  const modelIds = new Set(
46
61
  payload.data
47
62
  .filter(isRecord)
48
63
  .map((model) => (typeof model.id === "string" ? model.id.trim() : undefined))
49
64
  .filter((id): id is string => id !== undefined && id !== ""),
50
65
  );
51
- const checks = apiKey && apiKey !== "proxy-managed" ? ["endpoint", "catalog", "auth"] : ["endpoint", "catalog"];
52
66
  return {
53
67
  passed: modelIds.has(context.model.id),
54
- checks,
68
+ checks: ["endpoint", "catalog", "auth"],
55
69
  updatedAt: context.now(),
56
70
  httpStatus: response.status,
57
71
  };
@@ -156,21 +156,20 @@ function parseDevicePollResponse(payload: unknown): DevicePollResult {
156
156
  }
157
157
 
158
158
  function parseTokenExchangeResponse(payload: unknown): TokenExchangeResponse {
159
+ const allowedKeys = ["access_token", "token_type", "refresh_token", "expiry", "expires_in", "expires_at"] as const;
159
160
  if (
160
161
  !isRecord(payload) ||
162
+ !hasOnlyKeys(payload, allowedKeys) ||
161
163
  !nonEmptyString(payload.access_token) ||
162
164
  !nonEmptyString(payload.token_type) ||
163
165
  !nonEmptyString(payload.refresh_token) ||
164
- !nonEmptyString(payload.expiry)
166
+ typeof payload.expiry !== "string"
165
167
  ) {
166
168
  throw new Error("Charm Hyper token exchange response is invalid");
167
169
  }
168
170
  if (Object.hasOwn(payload, "expires_in")) {
169
- if (
170
- !hasOnlyKeys(payload, ["access_token", "token_type", "refresh_token", "expiry", "expires_in"]) ||
171
- !positiveInteger(payload.expires_in) ||
172
- Object.hasOwn(payload, "expires_at")
173
- ) {
171
+ // expires_in is authoritative when other absolute forms are also present.
172
+ if (!positiveInteger(payload.expires_in)) {
174
173
  throw new Error("Charm Hyper token exchange response has an invalid expiry");
175
174
  }
176
175
  return {
@@ -180,10 +179,7 @@ function parseTokenExchangeResponse(payload: unknown): TokenExchangeResponse {
180
179
  };
181
180
  }
182
181
  if (Object.hasOwn(payload, "expires_at")) {
183
- if (
184
- !hasOnlyKeys(payload, ["access_token", "token_type", "refresh_token", "expiry", "expires_at"]) ||
185
- !positiveInteger(payload.expires_at)
186
- ) {
182
+ if (!positiveInteger(payload.expires_at)) {
187
183
  throw new Error("Charm Hyper token exchange response has an invalid expiry");
188
184
  }
189
185
  return {
@@ -192,7 +188,17 @@ function parseTokenExchangeResponse(payload: unknown): TokenExchangeResponse {
192
188
  expiresAtSeconds: payload.expires_at,
193
189
  };
194
190
  }
195
- throw new Error("Charm Hyper token exchange response has an invalid expiry");
191
+ // Some responses only carry the ISO `expiry` timestamp; derive the
192
+ // absolute expiry from it when no numeric form is present.
193
+ const expiryMs = typeof payload.expiry === "string" ? Date.parse(payload.expiry) : NaN;
194
+ if (!Number.isFinite(expiryMs)) {
195
+ throw new Error("Charm Hyper token exchange response has an invalid expiry");
196
+ }
197
+ return {
198
+ accessToken: payload.access_token,
199
+ refreshToken: payload.refresh_token,
200
+ expiresAtSeconds: Math.floor(expiryMs / 1_000),
201
+ };
196
202
  }
197
203
 
198
204
  async function initiateDeviceAuth(