@hyav/pi-provider 0.1.4 → 0.1.5

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,4 +1,5 @@
1
1
  import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
2
+ import { validateProviderModelDrafts } from "./adapter-validation.ts";
2
3
  import { applyOfficialModelCosts, findOfficialMeta, type OfficialModelMeta } from "./official-pricing.ts";
3
4
  import { resolvePricingDetails } from "./pricing-adjustments.ts";
4
5
  import type { PiProviderDependencies } from "./runtime-config.ts";
@@ -83,6 +84,7 @@ export function normalizeProviderModel(model: ProviderModelDraft): ProviderModel
83
84
  }
84
85
 
85
86
  export function normalizeProviderModels(models: ProviderModelDraft[]): ProviderModel[] {
87
+ validateProviderModelDrafts(models);
86
88
  const seen = new Set<string>();
87
89
  return models.map((model) => {
88
90
  const normalized = normalizeProviderModel(model);
@@ -113,6 +115,7 @@ function resolveModelRegistration(
113
115
  modelDrafts: ProviderModelDraft[],
114
116
  officialPricing: Record<string, OfficialModelMeta>,
115
117
  ): { models: ProviderModel[]; modelMetadata: Record<string, ProviderModelMetadata> } {
118
+ validateProviderModelDrafts(modelDrafts, `Provider ${adapter.id}`);
116
119
  const enrichedDrafts = applyOfficialModelCosts(modelDrafts, officialPricing);
117
120
  const pricingPolicy = runtime.pricingPolicies?.[adapter.id] ?? adapter.pricing;
118
121
  const metadata: Record<string, ProviderModelMetadata> = {};
@@ -177,6 +180,65 @@ function getErrorCode(error: unknown): string {
177
180
  return "fetch";
178
181
  }
179
182
 
183
+ const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
184
+ const ENVIRONMENT_NAME_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*/;
185
+
186
+ /** Match Pi's `$NAME` / `${NAME}` interpolation without executing API-key commands. */
187
+ function getEnvironmentReferences(value: string): string[] {
188
+ if (value.startsWith("!")) return [];
189
+ const names = new Set<string>();
190
+ let index = 0;
191
+ while (index < value.length) {
192
+ const dollarIndex = value.indexOf("$", index);
193
+ if (dollarIndex < 0) break;
194
+ const next = value[dollarIndex + 1];
195
+ if (next === "$" || next === "!") {
196
+ index = dollarIndex + 2;
197
+ continue;
198
+ }
199
+ if (next === "{") {
200
+ const endIndex = value.indexOf("}", dollarIndex + 2);
201
+ if (endIndex < 0) {
202
+ index = dollarIndex + 1;
203
+ continue;
204
+ }
205
+ const name = value.slice(dollarIndex + 2, endIndex);
206
+ if (ENVIRONMENT_NAME.test(name)) names.add(name);
207
+ index = endIndex + 1;
208
+ continue;
209
+ }
210
+ const match = value.slice(dollarIndex + 1).match(ENVIRONMENT_NAME_PREFIX);
211
+ if (match) {
212
+ names.add(match[0]);
213
+ index = dollarIndex + 1 + match[0].length;
214
+ } else {
215
+ index = dollarIndex + 1;
216
+ }
217
+ }
218
+ return [...names];
219
+ }
220
+
221
+ function getRegistrationState(adapter: ProviderAdapter): NonNullable<ProviderAdapter["registration"]> | undefined {
222
+ const registration = adapter.registration;
223
+ if (!registration) return undefined;
224
+ registration.normalizedModels ??= [];
225
+ registration.modelMetadata ??= {};
226
+ registration.officialPricing ??= {};
227
+ registration.activeRefreshes ??= 0;
228
+ return registration;
229
+ }
230
+
231
+ function replaceModels(target: ProviderModel[], source: ProviderModel[]): ProviderModel[] {
232
+ target.splice(0, target.length, ...source);
233
+ return target;
234
+ }
235
+
236
+ function lacksCatalogRefreshCredential(apiKey: string, context: ProviderRefreshContext): boolean {
237
+ if (context.allowNetwork !== true || context.credential !== undefined) return false;
238
+ const environmentNames = getEnvironmentReferences(apiKey);
239
+ return environmentNames.length > 0 && environmentNames.some((name) => !process.env[name]);
240
+ }
241
+
180
242
  /**
181
243
  * Register a normalized Provider before the Host has assembled its final
182
244
  * registry. The original drafts remain attached to the adapter so a Host in a
@@ -194,9 +256,19 @@ export function prepareProviderRegistration(
194
256
  ? adapter.registration.modelDrafts
195
257
  : adapter.provider.models);
196
258
  const resolved = resolveModelRegistration(adapter, runtime, drafts, officialPricing);
197
- const models = resolved.models;
259
+ const existingRegistration = getRegistrationState(adapter);
260
+ const registration: NonNullable<ProviderAdapter["registration"]> = existingRegistration ?? {
261
+ modelDrafts: drafts,
262
+ normalizedModels: [],
263
+ modelMetadata: {},
264
+ officialPricing,
265
+ activeRefreshes: 0,
266
+ };
267
+ registration.modelDrafts = drafts;
268
+ registration.modelMetadata = resolved.modelMetadata;
269
+ registration.officialPricing = officialPricing;
270
+ const models = replaceModels(registration.normalizedModels, resolved.models);
198
271
  const adapterOwnsCatalog = adapter.catalog !== undefined;
199
- const registration = { modelDrafts: drafts, normalizedModels: models, modelMetadata: resolved.modelMetadata };
200
272
  adapter.registration = registration;
201
273
  adapter.provider.models = models;
202
274
  adapter.catalog ??= { source: "static", modelCount: models.length };
@@ -206,12 +278,18 @@ export function prepareProviderRegistration(
206
278
  const registeredProvider: ProviderConfig = { ...providerMetadata, models };
207
279
  if (originalRefresh) {
208
280
  registeredProvider.refreshModels = async (options: ProviderRefreshContext) => {
281
+ // Pi may ask every dynamic Provider to refresh. Keep the current catalog
282
+ // when this Provider's environment-backed key is absent instead of
283
+ // attempting an unauthenticated request that becomes a global refresh error.
284
+ if (lacksCatalogRefreshCredential(adapter.provider.apiKey, options)) {
285
+ return [...registration.normalizedModels];
286
+ }
287
+ registration.activeRefreshes++;
209
288
  try {
210
289
  const refreshedModels = await originalRefresh(options);
211
- const resolved = resolveModelRegistration(adapter, runtime, refreshedModels, officialPricing);
212
- const normalizedModels = resolved.models;
290
+ const resolved = resolveModelRegistration(adapter, runtime, refreshedModels, registration.officialPricing);
291
+ const normalizedModels = replaceModels(registration.normalizedModels, resolved.models);
213
292
  registration.modelDrafts = refreshedModels;
214
- registration.normalizedModels = normalizedModels;
215
293
  registration.modelMetadata = resolved.modelMetadata;
216
294
  adapter.provider.models = normalizedModels;
217
295
  registeredProvider.models = normalizedModels;
@@ -226,16 +304,30 @@ export function prepareProviderRegistration(
226
304
  lastError: undefined,
227
305
  };
228
306
  }
229
- return normalizedModels;
307
+ return [...normalizedModels];
230
308
  } catch (error) {
231
309
  if (adapter.catalog && !isAbortError(error)) adapter.catalog.lastError = getErrorCode(error);
232
310
  throw error;
311
+ } finally {
312
+ registration.activeRefreshes = Math.max(0, registration.activeRefreshes - 1);
313
+ if (registration.activeRefreshes === 0 && registration.deferredRegistration) {
314
+ const deferred = registration.deferredRegistration;
315
+ registration.deferredRegistration = undefined;
316
+ queueMicrotask(deferred);
317
+ }
233
318
  }
234
319
  };
235
320
  }
236
321
  return registeredProvider;
237
322
  }
238
323
 
324
+ export function cancelDeferredProviderRegistrations(providers: readonly ProviderAdapter[]): void {
325
+ for (const adapter of providers) {
326
+ const registration = getRegistrationState(adapter);
327
+ if (registration) registration.deferredRegistration = undefined;
328
+ }
329
+ }
330
+
239
331
  export function refreshProviderRegistrations(
240
332
  pi: Pick<ExtensionAPI, "registerProvider">,
241
333
  providers: readonly ProviderAdapter[],
@@ -244,6 +336,13 @@ export function refreshProviderRegistrations(
244
336
  providerDrafts?: ReadonlyMap<ProviderAdapter, ProviderModelDraft[]>,
245
337
  ): void {
246
338
  for (const adapter of providers) {
339
+ const registration = getRegistrationState(adapter);
340
+ if (registration) registration.officialPricing = officialPricing;
341
+ if (registration && registration.activeRefreshes > 0) {
342
+ registration.deferredRegistration = () =>
343
+ registerProviderAdapter(pi, adapter, runtime, officialPricing, providerDrafts?.get(adapter));
344
+ continue;
345
+ }
247
346
  registerProviderAdapter(pi, adapter, runtime, officialPricing, providerDrafts?.get(adapter));
248
347
  }
249
348
  }
@@ -18,8 +18,16 @@ export {
18
18
  defineStatusExtension,
19
19
  defineTunerExtension,
20
20
  } from "./adapter-extensions.ts";
21
+ export { MAX_PROVIDER_MODEL_COUNT } from "./adapter-validation.ts";
21
22
  export { createCatalogPreflightAdapter } from "./catalog-preflight.ts";
22
23
  export { withDeadline } from "./deadline.ts";
24
+ export {
25
+ appendBaseUrlPath,
26
+ authDefinesHeader,
27
+ getContextAuth,
28
+ hasBaseUrlOrigin,
29
+ mergeDiagnosticHeaders,
30
+ } from "./diagnostic-auth.ts";
23
31
  export { isProviderDataError, ProviderDataError } from "./errors.ts";
24
32
  export { createOpenCodeCatalogPreflightAdapter } from "./opencode-preflight.ts";
25
33
  export type {
@@ -38,6 +46,7 @@ export type {
38
46
  ProviderModel,
39
47
  ProviderModelDraft,
40
48
  ProviderRefreshContext,
49
+ ProviderRequestAuth,
41
50
  StatusAdapter,
42
51
  StatusContext,
43
52
  StatusEntry,
@@ -173,6 +173,9 @@ export function resolvePiProviderDependencies(
173
173
  dependencies: Partial<PiProviderDependencies> = {},
174
174
  ): PiProviderDependencies {
175
175
  const runtime = { ...getDefaultPiProviderDependencies(), ...dependencies };
176
+ if (!Object.hasOwn(dependencies, "openRouterMetadataCachePath")) {
177
+ runtime.openRouterMetadataCachePath = getDefaultOpenRouterMetadataCachePath(runtime.agentDir);
178
+ }
176
179
  if (runtime.pricingPolicies === undefined) runtime.pricingPolicies = {};
177
180
  validatePiProviderDependencies(runtime);
178
181
  return runtime;
package/core/runtime.ts CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  } from "./official-pricing.ts";
12
12
  import type { PreflightContextLike } from "./preflight-manager.ts";
13
13
  import { PreflightManager } from "./preflight-manager.ts";
14
- import { refreshProviderRegistrations, registerProviderAdapter } from "./provider-registration.ts";
14
+ import {
15
+ cancelDeferredProviderRegistrations,
16
+ refreshProviderRegistrations,
17
+ registerProviderAdapter,
18
+ } from "./provider-registration.ts";
15
19
  import type { PiProviderDependencies, PiProviderLoader } from "./runtime-config.ts";
16
20
  import { resolvePiProviderDependencies } from "./runtime-config.ts";
17
21
  import type { StatusContextLike } from "./status-manager.ts";
@@ -392,6 +396,7 @@ export function installPiProviderRuntime(
392
396
  lifecycleGeneration++;
393
397
  statusPresentationGeneration++;
394
398
  statusPresentationVisible = false;
399
+ cancelDeferredProviderRegistrations(providers);
395
400
  statusManager.cancelAll();
396
401
  statusManager.clear();
397
402
  preflightManager.cancelAll();
@@ -444,43 +449,52 @@ export function createPiProviderRuntime(
444
449
  ): (pi: ExtensionAPI) => Promise<void> {
445
450
  const runtime = resolvePiProviderDependencies(dependencies);
446
451
  return async (pi) => {
447
- let latestBackgroundPricing: Record<string, OfficialModelMeta> | undefined;
448
- let installedDefinition: PiProviderDefinition | undefined;
449
452
  let installedController: PiProviderRuntimeController | undefined;
450
453
  let disposed = false;
451
- const onBackgroundRefresh = (snapshot: Record<string, OfficialModelMeta>): void => {
452
- latestBackgroundPricing = snapshot;
453
- if (disposed) return;
454
- installedController?.updateOfficialPricing?.(snapshot);
455
- if (installedDefinition === undefined) return;
456
- refreshProviderRegistrations(pi, installedDefinition.providers, runtime, snapshot);
457
- };
454
+ let pricingRefreshController: AbortController | undefined;
455
+ const cachePath =
456
+ runtime.officialPricingUrl === OPENROUTER_MODELS_URL ? runtime.openRouterMetadataCachePath : undefined;
457
+ const fetchPricing = (options: { allowNetwork?: boolean; signal?: AbortSignal } = {}) =>
458
+ fetchOfficialPricing(
459
+ runtime.fetch,
460
+ runtime.officialPricingUrl,
461
+ runtime.officialPricingTimeoutMs,
462
+ runtime.officialPricingCacheTtlMs,
463
+ runtime.officialPricingMaxStaleMs,
464
+ runtime.now,
465
+ { cachePath, ...options },
466
+ );
458
467
  const officialPricingPromise = runtime.enableOfficialPricingFallback
459
- ? fetchOfficialPricing(
460
- runtime.fetch,
461
- runtime.officialPricingUrl,
462
- runtime.officialPricingTimeoutMs,
463
- runtime.officialPricingCacheTtlMs,
464
- runtime.officialPricingMaxStaleMs,
465
- runtime.now,
466
- {
467
- cachePath:
468
- runtime.officialPricingUrl === OPENROUTER_MODELS_URL
469
- ? runtime.openRouterMetadataCachePath
470
- : undefined,
471
- background: runtime.officialPricingUrl === OPENROUTER_MODELS_URL,
472
- onBackgroundRefresh: onBackgroundRefresh,
473
- },
474
- )
468
+ ? fetchPricing({ allowNetwork: false })
475
469
  : Promise.resolve({});
476
470
  const definitionPromise = loadDefinition(runtime);
477
471
  const [officialPricing, definition] = await Promise.all([officialPricingPromise, definitionPromise]);
478
472
  validatePiProviderDefinition(definition);
479
- installedController = installPiProviderRuntime(pi, runtime, definition, officialPricing);
480
- installedDefinition = definition;
481
- if (latestBackgroundPricing !== undefined) onBackgroundRefresh(latestBackgroundPricing);
473
+
474
+ pi.on("session_start", () => {
475
+ pricingRefreshController?.abort();
476
+ pricingRefreshController = undefined;
477
+ if (!runtime.enableOfficialPricingFallback || disposed) {
478
+ return;
479
+ }
480
+ const controller = new AbortController();
481
+ pricingRefreshController = controller;
482
+ void fetchPricing({ signal: controller.signal })
483
+ .then((snapshot) => {
484
+ if (disposed || controller.signal.aborted) return;
485
+ installedController?.updateOfficialPricing?.(snapshot);
486
+ refreshProviderRegistrations(pi, definition.providers, runtime, snapshot);
487
+ })
488
+ .catch(() => undefined)
489
+ .finally(() => {
490
+ if (pricingRefreshController === controller) pricingRefreshController = undefined;
491
+ });
492
+ });
482
493
  pi.on("session_shutdown", () => {
483
494
  disposed = true;
495
+ pricingRefreshController?.abort();
496
+ pricingRefreshController = undefined;
484
497
  });
498
+ installedController = installPiProviderRuntime(pi, runtime, definition, officialPricing);
485
499
  };
486
500
  }
@@ -1,5 +1,7 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
1
2
  import { deriveCredentialType } from "./credential-type.ts";
2
3
  import { isValidTimeoutMs, withDeadline } from "./deadline.ts";
4
+ import { applyDiagnosticBaseUrl, type DiagnosticModelRegistry, resolveDiagnosticAuth } from "./diagnostic-auth.ts";
3
5
  import { isProviderDataError, ProviderDataError } from "./errors.ts";
4
6
  import type {
5
7
  StatusAdapter,
@@ -10,11 +12,11 @@ import type {
10
12
  StatusWindowEntry,
11
13
  } from "./types.ts";
12
14
 
15
+ type StatusModel = NonNullable<ExtensionContext["model"]>;
16
+
13
17
  export interface StatusContextLike {
14
- model?: { provider?: string };
15
- modelRegistry: {
16
- getApiKeyForProvider(provider: string): Promise<string | undefined>;
17
- };
18
+ model?: { provider?: string; id?: string; baseUrl?: string };
19
+ modelRegistry: DiagnosticModelRegistry;
18
20
  /** Optional credential identity used to isolate cached account data. */
19
21
  getCredentialKey?: () => Promise<string | undefined>;
20
22
  /** Optional non-secret credential metadata for provider-specific account labels. */
@@ -223,10 +225,21 @@ export class StatusManager {
223
225
  const cancellation = new AbortController();
224
226
  const generation = ++state.generation;
225
227
  const promise = withDeadline(
226
- (signal) =>
227
- adapter.fetch({
228
+ async (signal) => {
229
+ const sourceModel = ctx.model as StatusModel;
230
+ const auth = await resolveDiagnosticAuth(sourceModel, ctx.modelRegistry);
231
+ const model = applyDiagnosticBaseUrl(sourceModel, auth);
232
+ if (adapter.supportsModel && !adapter.supportsModel(model)) {
233
+ throw new ProviderDataError(
234
+ "Status endpoint is unavailable for the effective model URL",
235
+ "unsupported",
236
+ );
237
+ }
238
+ return await adapter.fetch({
228
239
  fetch: this.fetchFn,
229
- getApiKey: () => ctx.modelRegistry.getApiKeyForProvider(adapter.providerId),
240
+ getApiKey: async () => auth.apiKey,
241
+ getAuth: async () => auth,
242
+ model,
230
243
  ...(ctx.getCredentialMetadata === undefined
231
244
  ? {}
232
245
  : {
@@ -235,7 +248,8 @@ export class StatusManager {
235
248
  }),
236
249
  now: this.now,
237
250
  signal,
238
- }),
251
+ });
252
+ },
239
253
  adapter.requestTimeoutMs,
240
254
  cancellation.signal,
241
255
  );
@@ -262,6 +276,11 @@ export class StatusManager {
262
276
  } catch (error) {
263
277
  if (state.generation !== generation || isErrorNamed(error, "AbortError")) return "skipped";
264
278
  const dataError = isProviderDataError(error) ? error : undefined;
279
+ if (dataError?.code === "unsupported") {
280
+ state.snapshot = undefined;
281
+ state.lastError = undefined;
282
+ return "skipped";
283
+ }
265
284
  const code = dataError?.code ?? (isErrorNamed(error, "TimeoutError") ? "timeout" : "fetch");
266
285
  const retryAt =
267
286
  dataError?.retryAt !== undefined && Number.isFinite(dataError.retryAt) ? dataError.retryAt : undefined;
package/core/types.ts CHANGED
@@ -4,6 +4,7 @@ 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
 
8
9
  export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
9
10
 
@@ -32,6 +33,14 @@ export interface StoredCredentialLike {
32
33
  readonly teamName?: string;
33
34
  }
34
35
 
36
+ /** Model-scoped authentication resolved by Pi for one diagnostic request. */
37
+ export interface ProviderRequestAuth {
38
+ apiKey?: string;
39
+ headers?: Record<string, string | null>;
40
+ baseUrl?: string;
41
+ env?: Record<string, string>;
42
+ }
43
+
35
44
  export interface ModelFieldSources {
36
45
  contextWindow?: ModelFieldSource;
37
46
  maxTokens?: ModelFieldSource;
@@ -145,6 +154,10 @@ export interface StatusSnapshot {
145
154
  export interface StatusContext {
146
155
  fetch: typeof globalThis.fetch;
147
156
  getApiKey: () => Promise<string | undefined>;
157
+ /** Complete model-scoped request authentication resolved by Pi. */
158
+ getAuth?: () => Promise<ProviderRequestAuth>;
159
+ /** Effective model, including any credential-specific base URL. */
160
+ model?: ActiveModel;
148
161
  /** Optional non-secret credential metadata for provider-specific account labels. */
149
162
  getCredentialMetadata?: () => unknown;
150
163
  /** Optional non-secret credential type ("oauth" vs "api_key") for providers with dual auth modes. */
@@ -159,6 +172,8 @@ export interface StatusAdapter {
159
172
  name: string;
160
173
  cacheTtlMs: number;
161
174
  requestTimeoutMs: number;
175
+ /** Return false when this account endpoint cannot safely serve the effective model URL. */
176
+ supportsModel?: (model: ActiveModel) => boolean;
162
177
  fetch(context: StatusContext): Promise<StatusSnapshot>;
163
178
  }
164
179
 
@@ -179,8 +194,11 @@ export interface ProviderAdapter {
179
194
  /** @internal Draft state shared across isolated Adapter and Host contexts. */
180
195
  registration?: {
181
196
  modelDrafts: ProviderModelDraft[];
182
- normalizedModels?: ProviderModel[];
183
- modelMetadata?: Record<string, ProviderModelMetadata>;
197
+ normalizedModels: ProviderModel[];
198
+ modelMetadata: Record<string, ProviderModelMetadata>;
199
+ officialPricing: Record<string, OfficialModelMeta>;
200
+ activeRefreshes: number;
201
+ deferredRegistration?: () => void;
184
202
  };
185
203
  }
186
204
 
package/index.ts CHANGED
@@ -17,8 +17,16 @@ export {
17
17
  defineStatusExtension,
18
18
  defineTunerExtension,
19
19
  } from "./core/adapter-extensions.ts";
20
+ export { MAX_PROVIDER_MODEL_COUNT } from "./core/adapter-validation.ts";
20
21
  export { createCatalogPreflightAdapter } from "./core/catalog-preflight.ts";
21
22
  export { withDeadline } from "./core/deadline.ts";
23
+ export {
24
+ appendBaseUrlPath,
25
+ authDefinesHeader,
26
+ getContextAuth,
27
+ hasBaseUrlOrigin,
28
+ mergeDiagnosticHeaders,
29
+ } from "./core/diagnostic-auth.ts";
22
30
  export type { ProviderDataErrorLike } from "./core/errors.ts";
23
31
  export { isProviderDataError, ProviderDataError } from "./core/errors.ts";
24
32
  export type {
@@ -103,6 +111,7 @@ export type {
103
111
  ProviderPricingPolicy,
104
112
  ProviderPricingSource,
105
113
  ProviderRefreshContext,
114
+ ProviderRequestAuth,
106
115
  StatusAdapter,
107
116
  StatusAmountEntry,
108
117
  StatusContext,
@@ -116,7 +125,7 @@ export type {
116
125
  } from "./core/types.ts";
117
126
 
118
127
  export interface PiProviderExtensionOptions {
119
- /** User adapter root; replaces the default `<agentDir>/pi-provider` directory. */
128
+ /** User adapter root; replaces the default `<agentDir>/extensions/pi-provider` directory. */
120
129
  adapterRoot?: string;
121
130
  /** Host runtime dependency overrides. */
122
131
  dependencies?: Partial<PiProviderDependencies>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyav/pi-provider",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
  import { hyperJsonHeaders } from "../providers/charm-hyper/constants.ts";
4
4
  import { HYPER_MODELS_URL, HYPER_PROVIDER_URL, parseHyperModels } from "../providers/charm-hyper.ts";
5
5
 
@@ -10,6 +10,7 @@ export function createCharmHyperPreflightAdapter(requestTimeoutMs: number): Pref
10
10
  name: "Charm Hyper",
11
11
  cacheTtlMs: 30_000,
12
12
  requestTimeoutMs,
13
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, HYPER_PROVIDER_URL),
13
14
  async fetch(context) {
14
15
  const apiKey = await context.getApiKey();
15
16
  if (!apiKey) return { passed: false, checks: ["auth"], updatedAt: context.now() };
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
 
4
4
  export const DEEPSEEK_MODELS_URL = "https://api.deepseek.com/models";
5
5
 
@@ -13,6 +13,7 @@ export const deepSeekPreflightAdapter: PreflightAdapter = {
13
13
  name: "DeepSeek",
14
14
  cacheTtlMs: 30_000,
15
15
  requestTimeoutMs: 8_000,
16
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, DEEPSEEK_MODELS_URL),
16
17
  async fetch(context) {
17
18
  const apiKey = await context.getApiKey();
18
19
  if (!apiKey || apiKey === "proxy-managed") {
@@ -1,7 +1,16 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import {
3
+ appendBaseUrlPath,
4
+ authDefinesHeader,
5
+ definePreflightExtension,
6
+ getContextAuth,
7
+ mergeDiagnosticHeaders,
8
+ ProviderDataError,
9
+ parseRetryAfter,
10
+ } from "@hyav/pi-provider";
3
11
 
4
- export const COPILOT_MODELS_URL = "https://api.individual.githubcopilot.com/models";
12
+ export const COPILOT_BASE_URL = "https://api.individual.githubcopilot.com";
13
+ export const COPILOT_MODELS_URL = `${COPILOT_BASE_URL}/models`;
5
14
 
6
15
  function isRecord(value: unknown): value is Record<string, unknown> {
7
16
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -14,17 +23,21 @@ export const githubCopilotPreflightAdapter: PreflightAdapter = {
14
23
  cacheTtlMs: 30_000,
15
24
  requestTimeoutMs: 8_000,
16
25
  async fetch(context) {
17
- const apiKey = await context.getApiKey();
26
+ const auth = await getContextAuth(context);
27
+ const apiKey = auth.apiKey;
18
28
  if (!apiKey || apiKey === "proxy-managed") {
19
29
  return { passed: false, checks: ["auth"], updatedAt: context.now() };
20
30
  }
21
- const response = await context.fetch(COPILOT_MODELS_URL, {
22
- headers: {
23
- Accept: "application/json",
24
- "Accept-Encoding": "identity",
25
- Authorization: `Bearer ${apiKey}`,
26
- "User-Agent": "@hyav/pi-provider",
27
- },
31
+ const url = appendBaseUrlPath(context.model.baseUrl ?? auth.baseUrl, "models", COPILOT_BASE_URL);
32
+ if (!url) throw new ProviderDataError("GitHub Copilot model endpoint is unavailable", "unsupported");
33
+ const headers = mergeDiagnosticHeaders(auth, {
34
+ Accept: "application/json",
35
+ "Accept-Encoding": "identity",
36
+ "User-Agent": "@hyav/pi-provider",
37
+ });
38
+ if (!authDefinesHeader(auth, "Authorization")) headers.set("Authorization", `Bearer ${apiKey}`);
39
+ const response = await context.fetch(url, {
40
+ headers,
28
41
  signal: context.signal,
29
42
  });
30
43
  if (!response.ok) {
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
 
4
4
  export const GOOGLE_MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
5
5
 
@@ -28,6 +28,7 @@ export const googlePreflightAdapter: PreflightAdapter = {
28
28
  name: "Google Gemini",
29
29
  cacheTtlMs: 30_000,
30
30
  requestTimeoutMs: 8_000,
31
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, GOOGLE_MODELS_URL),
31
32
  async fetch(context) {
32
33
  const apiKey = await context.getApiKey();
33
34
  if (!apiKey || apiKey === "proxy-managed") {
package/preflight/groq.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
  import { GROQ_MODELS_URL } from "../status/groq.ts";
4
4
 
5
5
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -12,6 +12,7 @@ export const groqPreflightAdapter: PreflightAdapter = {
12
12
  name: "Groq",
13
13
  cacheTtlMs: 30_000,
14
14
  requestTimeoutMs: 8_000,
15
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, GROQ_MODELS_URL),
15
16
  async fetch(context) {
16
17
  const apiKey = await context.getApiKey();
17
18
  if (!apiKey || apiKey === "proxy-managed") {
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
  import { extractCodexAccountId } from "../status/openai-codex.ts";
4
4
 
5
5
  export const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
@@ -28,6 +28,7 @@ export const openAICodexPreflightAdapter: PreflightAdapter = {
28
28
  name: "OpenAI Codex",
29
29
  cacheTtlMs: 30_000,
30
30
  requestTimeoutMs: 8_000,
31
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, CODEX_MODELS_URL),
31
32
  async fetch(context) {
32
33
  const apiKey = await context.getApiKey();
33
34
  if (!apiKey || apiKey === "proxy-managed") {
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
  import { OPENROUTER_KEY_URL } from "../status/openrouter.ts";
4
4
 
5
5
  export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
@@ -79,6 +79,7 @@ export const openRouterPreflightAdapter: PreflightAdapter = {
79
79
  name: "OpenRouter",
80
80
  cacheTtlMs: 30_000,
81
81
  requestTimeoutMs: 8_000,
82
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, OPENROUTER_MODELS_URL),
82
83
  async fetch(context) {
83
84
  const modelIds = await collectModelIds(context);
84
85
  const apiKey = await context.getApiKey();
@@ -37,11 +37,6 @@ export function createVercelAIGatewayPreflightAdapter(requestTimeoutMs: number):
37
37
  cacheTtlMs: 30_000,
38
38
  requestTimeoutMs,
39
39
  async fetch(context): Promise<PreflightSnapshot> {
40
- const key = await context.getApiKey();
41
- if (!key || key === "proxy-managed") {
42
- return { passed: false, checks: ["auth"], updatedAt: context.now() };
43
- }
44
-
45
40
  const response = await context.fetch(VERCEL_MODELS_URL, {
46
41
  headers: {
47
42
  Accept: "application/json",
@@ -67,7 +62,7 @@ export function createVercelAIGatewayPreflightAdapter(requestTimeoutMs: number):
67
62
 
68
63
  return {
69
64
  passed: parseVercelModelIds(payload).has(context.model.id),
70
- checks: ["endpoint", "auth", "catalog"],
65
+ checks: ["endpoint", "catalog"],
71
66
  updatedAt: context.now(),
72
67
  httpStatus: response.status,
73
68
  };
package/preflight/xai.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PreflightAdapter } from "@hyav/pi-provider";
2
- import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
3
  import { XAI_MODELS_URL } from "../status/xai.ts";
4
4
 
5
5
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -12,6 +12,7 @@ export const xaiPreflightAdapter: PreflightAdapter = {
12
12
  name: "xAI",
13
13
  cacheTtlMs: 30_000,
14
14
  requestTimeoutMs: 8_000,
15
+ supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, XAI_MODELS_URL),
15
16
  async fetch(context) {
16
17
  const apiKey = await context.getApiKey();
17
18
  const authHeaders: Record<string, string> = {