@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.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/core/adapter-extensions.ts +22 -12
- 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 +131 -9
- 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
|
-
import type {
|
|
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
|
|
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:
|
|
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:
|
|
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
|
}
|
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.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",
|
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") {
|