@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.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/core/adapter-validation.ts +23 -6
- package/core/catalog-preflight.ts +20 -8
- package/core/diagnostic-auth.ts +103 -0
- package/core/host.ts +26 -4
- package/core/live-check-manager.ts +2 -1
- package/core/official-pricing.ts +39 -17
- package/core/preflight-manager.ts +27 -8
- package/core/provider-registration.ts +105 -6
- package/core/public-adapters.ts +9 -0
- package/core/runtime-config.ts +3 -0
- package/core/runtime.ts +43 -29
- package/core/status-manager.ts +27 -8
- package/core/types.ts +20 -2
- package/index.ts +10 -1
- package/package.json +1 -1
- package/preflight/charm-hyper.ts +2 -1
- package/preflight/deepseek.ts +2 -1
- package/preflight/github-copilot.ts +23 -10
- package/preflight/google.ts +2 -1
- package/preflight/groq.ts +2 -1
- package/preflight/openai-codex.ts +2 -1
- package/preflight/openrouter.ts +2 -1
- package/preflight/vercel-ai-gateway.ts +1 -6
- package/preflight/xai.ts +2 -1
- package/providers/charm-hyper.ts +4 -1
- package/status/anthropic.ts +5 -1
- package/status/charm-hyper.ts +2 -1
- package/status/deepseek.ts +2 -1
- package/status/github-copilot.ts +23 -10
- package/status/groq.ts +2 -1
- package/status/huggingface.ts +2 -1
- package/status/moonshotai.ts +2 -1
- package/status/openai-codex.ts +2 -1
- package/status/opencode-go.ts +2 -1
- package/status/openrouter.ts +2 -1
- package/status/vercel-ai-gateway.ts +2 -1
- package/status/xai.ts +2 -1
|
@@ -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
|
|
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
|
}
|
package/core/public-adapters.ts
CHANGED
|
@@ -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,
|
package/core/runtime-config.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
}
|
package/core/status-manager.ts
CHANGED
|
@@ -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
|
-
|
|
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: () =>
|
|
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
|
|
183
|
-
modelMetadata
|
|
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.
|
|
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",
|
package/preflight/charm-hyper.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 { 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() };
|
package/preflight/deepseek.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
|
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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) {
|
package/preflight/google.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
|
|
|
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") {
|
package/preflight/openrouter.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 { 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", "
|
|
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> = {
|