@hyav/pi-provider 0.1.4 → 0.1.6

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
- import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
1
+ import type { 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";
@@ -16,6 +17,11 @@ import type {
16
17
  const DEFAULT_CONTEXT_WINDOW = 128_000;
17
18
  const DEFAULT_MAX_TOKENS = 16_384;
18
19
 
20
+ type ProviderRegistrationApi = {
21
+ registerProvider(name: string, config: ProviderConfig): void;
22
+ unregisterProvider?(name: string): void;
23
+ };
24
+
19
25
  function finiteNonNegative(value: unknown): number {
20
26
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
21
27
  }
@@ -83,6 +89,7 @@ export function normalizeProviderModel(model: ProviderModelDraft): ProviderModel
83
89
  }
84
90
 
85
91
  export function normalizeProviderModels(models: ProviderModelDraft[]): ProviderModel[] {
92
+ validateProviderModelDrafts(models);
86
93
  const seen = new Set<string>();
87
94
  return models.map((model) => {
88
95
  const normalized = normalizeProviderModel(model);
@@ -113,6 +120,7 @@ function resolveModelRegistration(
113
120
  modelDrafts: ProviderModelDraft[],
114
121
  officialPricing: Record<string, OfficialModelMeta>,
115
122
  ): { models: ProviderModel[]; modelMetadata: Record<string, ProviderModelMetadata> } {
123
+ validateProviderModelDrafts(modelDrafts, `Provider ${adapter.id}`);
116
124
  const enrichedDrafts = applyOfficialModelCosts(modelDrafts, officialPricing);
117
125
  const pricingPolicy = runtime.pricingPolicies?.[adapter.id] ?? adapter.pricing;
118
126
  const metadata: Record<string, ProviderModelMetadata> = {};
@@ -177,6 +185,76 @@ function getErrorCode(error: unknown): string {
177
185
  return "fetch";
178
186
  }
179
187
 
188
+ const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
189
+ const ENVIRONMENT_NAME_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*/;
190
+
191
+ /** Match Pi's `$NAME` / `${NAME}` interpolation without executing API-key commands. */
192
+ function getEnvironmentReferences(value: string): string[] {
193
+ if (value.startsWith("!")) return [];
194
+ const names = new Set<string>();
195
+ let index = 0;
196
+ while (index < value.length) {
197
+ const dollarIndex = value.indexOf("$", index);
198
+ if (dollarIndex < 0) break;
199
+ const next = value[dollarIndex + 1];
200
+ if (next === "$" || next === "!") {
201
+ index = dollarIndex + 2;
202
+ continue;
203
+ }
204
+ if (next === "{") {
205
+ const endIndex = value.indexOf("}", dollarIndex + 2);
206
+ if (endIndex < 0) {
207
+ index = dollarIndex + 1;
208
+ continue;
209
+ }
210
+ const name = value.slice(dollarIndex + 2, endIndex);
211
+ if (ENVIRONMENT_NAME.test(name)) names.add(name);
212
+ index = endIndex + 1;
213
+ continue;
214
+ }
215
+ const match = value.slice(dollarIndex + 1).match(ENVIRONMENT_NAME_PREFIX);
216
+ if (match) {
217
+ names.add(match[0]);
218
+ index = dollarIndex + 1 + match[0].length;
219
+ } else {
220
+ index = dollarIndex + 1;
221
+ }
222
+ }
223
+ return [...names];
224
+ }
225
+
226
+ function getRegistrationState(adapter: ProviderAdapter): NonNullable<ProviderAdapter["registration"]> | undefined {
227
+ const registration = adapter.registration;
228
+ if (!registration) return undefined;
229
+ registration.normalizedModels ??= [];
230
+ registration.modelMetadata ??= {};
231
+ registration.officialPricing ??= {};
232
+ registration.activeRefreshes ??= 0;
233
+ return registration;
234
+ }
235
+
236
+ function replaceModels(target: ProviderModel[], source: ProviderModel[]): ProviderModel[] {
237
+ target.splice(0, target.length, ...source);
238
+ return target;
239
+ }
240
+
241
+ function hasMissingEnvironmentReference(apiKey: string): boolean {
242
+ const environmentNames = getEnvironmentReferences(apiKey);
243
+ return environmentNames.length > 0 && environmentNames.some((name) => !process.env[name]);
244
+ }
245
+
246
+ function lacksCatalogRefreshCredential(apiKey: string, context: ProviderRefreshContext): boolean {
247
+ return context.allowNetwork === true && context.credential === undefined && hasMissingEnvironmentReference(apiKey);
248
+ }
249
+
250
+ function shouldOmitOptionalApiKey(adapter: ProviderAdapter, runtime: PiProviderDependencies): boolean {
251
+ return (
252
+ adapter.provider.oauth !== undefined &&
253
+ hasMissingEnvironmentReference(adapter.provider.apiKey) &&
254
+ runtime.readStoredCredential(adapter.id)?.type !== "api_key"
255
+ );
256
+ }
257
+
180
258
  /**
181
259
  * Register a normalized Provider before the Host has assembled its final
182
260
  * registry. The original drafts remain attached to the adapter so a Host in a
@@ -194,9 +272,19 @@ export function prepareProviderRegistration(
194
272
  ? adapter.registration.modelDrafts
195
273
  : adapter.provider.models);
196
274
  const resolved = resolveModelRegistration(adapter, runtime, drafts, officialPricing);
197
- const models = resolved.models;
275
+ const existingRegistration = getRegistrationState(adapter);
276
+ const registration: NonNullable<ProviderAdapter["registration"]> = existingRegistration ?? {
277
+ modelDrafts: drafts,
278
+ normalizedModels: [],
279
+ modelMetadata: {},
280
+ officialPricing,
281
+ activeRefreshes: 0,
282
+ };
283
+ registration.modelDrafts = drafts;
284
+ registration.modelMetadata = resolved.modelMetadata;
285
+ registration.officialPricing = officialPricing;
286
+ const models = replaceModels(registration.normalizedModels, resolved.models);
198
287
  const adapterOwnsCatalog = adapter.catalog !== undefined;
199
- const registration = { modelDrafts: drafts, normalizedModels: models, modelMetadata: resolved.modelMetadata };
200
288
  adapter.registration = registration;
201
289
  adapter.provider.models = models;
202
290
  adapter.catalog ??= { source: "static", modelCount: models.length };
@@ -204,14 +292,24 @@ export function prepareProviderRegistration(
204
292
 
205
293
  const { models: _draftModels, refreshModels: originalRefresh, ...providerMetadata } = adapter.provider;
206
294
  const registeredProvider: ProviderConfig = { ...providerMetadata, models };
295
+ // Pi 0.84.2 resolves a declared environment API key before it can skip an
296
+ // unauthenticated catalog refresh. Register OAuth-only until the optional
297
+ // key exists, while preserving API-key credentials already stored by Pi.
298
+ if (shouldOmitOptionalApiKey(adapter, runtime)) delete registeredProvider.apiKey;
207
299
  if (originalRefresh) {
208
300
  registeredProvider.refreshModels = async (options: ProviderRefreshContext) => {
301
+ // Pi may ask every dynamic Provider to refresh. Keep the current catalog
302
+ // when this Provider's environment-backed key is absent instead of
303
+ // attempting an unauthenticated request that becomes a global refresh error.
304
+ if (lacksCatalogRefreshCredential(adapter.provider.apiKey, options)) {
305
+ return [...registration.normalizedModels];
306
+ }
307
+ registration.activeRefreshes++;
209
308
  try {
210
309
  const refreshedModels = await originalRefresh(options);
211
- const resolved = resolveModelRegistration(adapter, runtime, refreshedModels, officialPricing);
212
- const normalizedModels = resolved.models;
310
+ const resolved = resolveModelRegistration(adapter, runtime, refreshedModels, registration.officialPricing);
311
+ const normalizedModels = replaceModels(registration.normalizedModels, resolved.models);
213
312
  registration.modelDrafts = refreshedModels;
214
- registration.normalizedModels = normalizedModels;
215
313
  registration.modelMetadata = resolved.modelMetadata;
216
314
  adapter.provider.models = normalizedModels;
217
315
  registeredProvider.models = normalizedModels;
@@ -226,36 +324,60 @@ export function prepareProviderRegistration(
226
324
  lastError: undefined,
227
325
  };
228
326
  }
229
- return normalizedModels;
327
+ return [...normalizedModels];
230
328
  } catch (error) {
231
329
  if (adapter.catalog && !isAbortError(error)) adapter.catalog.lastError = getErrorCode(error);
232
330
  throw error;
331
+ } finally {
332
+ registration.activeRefreshes = Math.max(0, registration.activeRefreshes - 1);
333
+ if (registration.activeRefreshes === 0 && registration.deferredRegistration) {
334
+ const deferred = registration.deferredRegistration;
335
+ registration.deferredRegistration = undefined;
336
+ queueMicrotask(deferred);
337
+ }
233
338
  }
234
339
  };
235
340
  }
236
341
  return registeredProvider;
237
342
  }
238
343
 
344
+ export function cancelDeferredProviderRegistrations(providers: readonly ProviderAdapter[]): void {
345
+ for (const adapter of providers) {
346
+ const registration = getRegistrationState(adapter);
347
+ if (registration) registration.deferredRegistration = undefined;
348
+ }
349
+ }
350
+
239
351
  export function refreshProviderRegistrations(
240
- pi: Pick<ExtensionAPI, "registerProvider">,
352
+ pi: ProviderRegistrationApi,
241
353
  providers: readonly ProviderAdapter[],
242
354
  runtime: PiProviderDependencies,
243
355
  officialPricing: Record<string, OfficialModelMeta>,
244
356
  providerDrafts?: ReadonlyMap<ProviderAdapter, ProviderModelDraft[]>,
245
357
  ): void {
246
358
  for (const adapter of providers) {
359
+ const registration = getRegistrationState(adapter);
360
+ if (registration) registration.officialPricing = officialPricing;
361
+ if (registration && registration.activeRefreshes > 0) {
362
+ registration.deferredRegistration = () =>
363
+ registerProviderAdapter(pi, adapter, runtime, officialPricing, providerDrafts?.get(adapter));
364
+ continue;
365
+ }
247
366
  registerProviderAdapter(pi, adapter, runtime, officialPricing, providerDrafts?.get(adapter));
248
367
  }
249
368
  }
250
369
 
251
370
  export function registerProviderAdapter(
252
- pi: Pick<ExtensionAPI, "registerProvider">,
371
+ pi: ProviderRegistrationApi,
253
372
  adapter: ProviderAdapter,
254
373
  runtime: PiProviderDependencies,
255
374
  officialPricing: Record<string, OfficialModelMeta> = {},
256
375
  modelDrafts?: ProviderModelDraft[],
257
376
  ): ProviderConfig {
258
377
  const registeredProvider = prepareProviderRegistration(adapter, runtime, officialPricing, modelDrafts);
378
+ // Pi merges re-registrations, so omission alone cannot clear a raw API key
379
+ // left by the previous extension instance during /reload.
380
+ if (registeredProvider.apiKey === undefined) pi.unregisterProvider?.(adapter.id);
259
381
  pi.registerProvider(adapter.id, registeredProvider);
260
382
  return registeredProvider;
261
383
  }
@@ -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.6",
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") {