@hyav/pi-provider 0.1.3 → 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 +24 -0
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/core/adapter-validation.ts +23 -6
- package/core/catalog-preflight.ts +142 -0
- package/core/credential-type.ts +13 -0
- 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 +40 -8
- package/core/provider-registration.ts +105 -6
- package/core/public-adapters.ts +10 -0
- package/core/ratelimit-headers.ts +72 -0
- package/core/runtime-config.ts +3 -0
- package/core/runtime.ts +55 -34
- package/core/status-manager.ts +34 -9
- package/core/types.ts +22 -2
- package/index.ts +12 -1
- package/package.json +1 -1
- package/preflight/anthropic.ts +42 -0
- package/preflight/cerebras.ts +27 -0
- package/preflight/charm-hyper.ts +2 -1
- package/preflight/deepseek.ts +2 -1
- package/preflight/github-copilot.ts +87 -0
- package/preflight/google.ts +2 -1
- package/preflight/groq.ts +73 -0
- package/preflight/huggingface.ts +27 -0
- package/preflight/mistral.ts +27 -0
- package/preflight/moonshotai-cn.ts +27 -0
- package/preflight/moonshotai.ts +37 -0
- package/preflight/nvidia.ts +27 -0
- package/preflight/openai-codex.ts +2 -1
- package/preflight/openai.ts +27 -0
- package/preflight/openrouter.ts +112 -0
- package/preflight/vercel-ai-gateway.ts +81 -0
- package/preflight/xai.ts +71 -0
- package/providers/charm-hyper.ts +4 -1
- package/status/anthropic.ts +262 -0
- package/status/charm-hyper.ts +3 -2
- package/status/deepseek.ts +2 -1
- package/status/github-copilot.ts +189 -0
- package/status/groq.ts +89 -0
- package/status/huggingface.ts +95 -0
- package/status/moonshotai-cn.ts +26 -0
- package/status/moonshotai.ts +151 -0
- package/status/openai-codex.ts +2 -1
- package/status/opencode-go.ts +2 -1
- package/status/openrouter.ts +173 -0
- package/status/vercel-ai-gateway/constants.ts +3 -0
- package/status/vercel-ai-gateway.ts +95 -0
- package/status/xai.ts +74 -0
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
6
|
+
import type { ProviderRequestAuth } from "./types.ts";
|
|
4
7
|
|
|
5
8
|
export type PreflightModel = NonNullable<ExtensionContext["model"]>;
|
|
6
9
|
|
|
7
|
-
type ModelRegistry = ExtensionContext["modelRegistry"];
|
|
8
|
-
|
|
9
10
|
export interface PreflightContext {
|
|
10
11
|
fetch: typeof globalThis.fetch;
|
|
11
12
|
getApiKey: () => Promise<string | undefined>;
|
|
13
|
+
/** Complete model-scoped request authentication resolved by Pi. */
|
|
14
|
+
getAuth?: () => Promise<ProviderRequestAuth>;
|
|
12
15
|
signal?: AbortSignal;
|
|
13
16
|
now: () => number;
|
|
14
17
|
model: PreflightModel;
|
|
18
|
+
/** Optional non-secret credential metadata for provider-specific account labels. */
|
|
19
|
+
getCredentialMetadata?: () => unknown;
|
|
20
|
+
/** Optional non-secret credential type ("oauth" vs "api_key") for providers with dual auth modes. */
|
|
21
|
+
getCredentialType?: () => Promise<string | undefined>;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export interface PreflightSnapshot {
|
|
@@ -27,12 +34,16 @@ export interface PreflightAdapter {
|
|
|
27
34
|
name: string;
|
|
28
35
|
cacheTtlMs: number;
|
|
29
36
|
requestTimeoutMs: number;
|
|
37
|
+
/** Return false when this endpoint cannot safely serve the effective model URL. */
|
|
38
|
+
supportsModel?: (model: PreflightModel) => boolean;
|
|
30
39
|
fetch(context: PreflightContext): Promise<PreflightSnapshot>;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export interface PreflightContextLike {
|
|
34
43
|
model: PreflightModel;
|
|
35
|
-
modelRegistry:
|
|
44
|
+
modelRegistry: DiagnosticModelRegistry;
|
|
45
|
+
/** Optional non-secret credential metadata for provider-specific account labels. */
|
|
46
|
+
getCredentialMetadata?: () => unknown;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
export interface PreflightErrorState {
|
|
@@ -175,14 +186,30 @@ export class PreflightManager {
|
|
|
175
186
|
const cancellation = new AbortController();
|
|
176
187
|
const generation = ++state.generation;
|
|
177
188
|
const promise = withDeadline(
|
|
178
|
-
(signal) =>
|
|
179
|
-
|
|
189
|
+
async (signal) => {
|
|
190
|
+
const auth = await resolveDiagnosticAuth(ctx.model, ctx.modelRegistry);
|
|
191
|
+
const model = applyDiagnosticBaseUrl(ctx.model, auth);
|
|
192
|
+
if (adapter.supportsModel && !adapter.supportsModel(model)) {
|
|
193
|
+
throw new ProviderDataError(
|
|
194
|
+
"Preflight endpoint is unavailable for the effective model URL",
|
|
195
|
+
"unsupported",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return await adapter.fetch({
|
|
180
199
|
fetch: this.fetchFn,
|
|
181
|
-
getApiKey: () =>
|
|
200
|
+
getApiKey: async () => auth.apiKey,
|
|
201
|
+
getAuth: async () => auth,
|
|
182
202
|
now: this.now,
|
|
183
203
|
signal,
|
|
184
|
-
model
|
|
185
|
-
|
|
204
|
+
model,
|
|
205
|
+
...(ctx.getCredentialMetadata === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: {
|
|
208
|
+
getCredentialMetadata: ctx.getCredentialMetadata,
|
|
209
|
+
getCredentialType: async () => deriveCredentialType(ctx.getCredentialMetadata?.()),
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
},
|
|
186
213
|
adapter.requestTimeoutMs,
|
|
187
214
|
cancellation.signal,
|
|
188
215
|
);
|
|
@@ -204,6 +231,11 @@ export class PreflightManager {
|
|
|
204
231
|
return "refreshed";
|
|
205
232
|
} catch (error) {
|
|
206
233
|
if (state.generation !== generation || isErrorNamed(error, "AbortError")) return "skipped";
|
|
234
|
+
if (isProviderDataError(error) && error.code === "unsupported") {
|
|
235
|
+
state.snapshot = undefined;
|
|
236
|
+
state.lastError = undefined;
|
|
237
|
+
return "skipped";
|
|
238
|
+
}
|
|
207
239
|
state.lastError = errorState(error);
|
|
208
240
|
if (state.lastError.code === "timeout") {
|
|
209
241
|
state.generation++;
|
|
@@ -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,7 +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";
|
|
22
|
+
export { createCatalogPreflightAdapter } from "./catalog-preflight.ts";
|
|
21
23
|
export { withDeadline } from "./deadline.ts";
|
|
24
|
+
export {
|
|
25
|
+
appendBaseUrlPath,
|
|
26
|
+
authDefinesHeader,
|
|
27
|
+
getContextAuth,
|
|
28
|
+
hasBaseUrlOrigin,
|
|
29
|
+
mergeDiagnosticHeaders,
|
|
30
|
+
} from "./diagnostic-auth.ts";
|
|
22
31
|
export { isProviderDataError, ProviderDataError } from "./errors.ts";
|
|
23
32
|
export { createOpenCodeCatalogPreflightAdapter } from "./opencode-preflight.ts";
|
|
24
33
|
export type {
|
|
@@ -37,6 +46,7 @@ export type {
|
|
|
37
46
|
ProviderModel,
|
|
38
47
|
ProviderModelDraft,
|
|
39
48
|
ProviderRefreshContext,
|
|
49
|
+
ProviderRequestAuth,
|
|
40
50
|
StatusAdapter,
|
|
41
51
|
StatusContext,
|
|
42
52
|
StatusEntry,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Shared parsing for OpenAI-style `x-ratelimit-*` response headers. */
|
|
2
|
+
|
|
3
|
+
export interface RateLimitWindow {
|
|
4
|
+
limit: number;
|
|
5
|
+
remaining: number;
|
|
6
|
+
resetAt?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function numberValue(value: unknown): number | undefined {
|
|
10
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
|
|
11
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DURATION_PATTERN = /^((?:\d+(?:\.\d+)?(?:ms|[smhd]))+)$/i;
|
|
19
|
+
const DURATION_PARTS = /(\d+(?:\.\d+)?)(ms|[smhd])/gi;
|
|
20
|
+
const UNIT_SECONDS: Record<string, number> = { ms: 0.001, s: 1, m: 60, h: 3_600, d: 86_400 };
|
|
21
|
+
|
|
22
|
+
export function durationSeconds(value: string): number | undefined {
|
|
23
|
+
const matched = DURATION_PATTERN.exec(value.trim());
|
|
24
|
+
if (!matched?.[1]) return undefined;
|
|
25
|
+
let total = 0;
|
|
26
|
+
for (const match of value.matchAll(DURATION_PARTS)) {
|
|
27
|
+
const amount = Number(match[1]);
|
|
28
|
+
if (!Number.isFinite(amount)) return undefined;
|
|
29
|
+
total += amount * (UNIT_SECONDS[match[2]!.toLowerCase()] ?? Number.NaN);
|
|
30
|
+
if (!Number.isFinite(total)) return undefined;
|
|
31
|
+
}
|
|
32
|
+
return total;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reset hint in seconds. Providers use bare numbers, `7.66s`, and compound
|
|
37
|
+
* durations like Groq's `2m59.56s`; reset windows like `1d` also appear.
|
|
38
|
+
*/
|
|
39
|
+
export function resetSecondsFromHeader(value: string | null): number | undefined {
|
|
40
|
+
if (value === null || value.trim() === "") return undefined;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
const duration = durationSeconds(trimmed);
|
|
43
|
+
if (duration === undefined) {
|
|
44
|
+
const bare = numberValue(trimmed);
|
|
45
|
+
if (bare === undefined || bare <= 0) return undefined;
|
|
46
|
+
return bare;
|
|
47
|
+
}
|
|
48
|
+
if (duration <= 0) return undefined;
|
|
49
|
+
return duration;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseWindow(headers: Headers, kind: "requests" | "tokens", now: number): RateLimitWindow | undefined {
|
|
53
|
+
const limit = numberValue(headers.get(`x-ratelimit-limit-${kind}`));
|
|
54
|
+
const remaining = numberValue(headers.get(`x-ratelimit-remaining-${kind}`));
|
|
55
|
+
if (limit === undefined || remaining === undefined || limit <= 0) return undefined;
|
|
56
|
+
const reset = resetSecondsFromHeader(headers.get(`x-ratelimit-reset-${kind}`) ?? headers.get("x-ratelimit-reset"));
|
|
57
|
+
return {
|
|
58
|
+
limit,
|
|
59
|
+
remaining,
|
|
60
|
+
...(reset !== undefined ? { resetAt: now + Math.round(reset * 1_000) } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseRateLimitWindows(
|
|
65
|
+
headers: Headers,
|
|
66
|
+
now: number,
|
|
67
|
+
): { requests?: RateLimitWindow; tokens?: RateLimitWindow } {
|
|
68
|
+
return {
|
|
69
|
+
requests: parseWindow(headers, "requests", now),
|
|
70
|
+
tokens: parseWindow(headers, "tokens", now),
|
|
71
|
+
};
|
|
72
|
+
}
|
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";
|
|
@@ -46,10 +50,11 @@ function readProviderCredentialMetadata(
|
|
|
46
50
|
): unknown {
|
|
47
51
|
try {
|
|
48
52
|
const credential = readStoredCredential(provider);
|
|
49
|
-
|
|
53
|
+
const type = credential?.type;
|
|
54
|
+
if (type !== "oauth" && type !== "api_key") return undefined;
|
|
50
55
|
return {
|
|
51
|
-
type
|
|
52
|
-
...(typeof credential
|
|
56
|
+
type,
|
|
57
|
+
...(type === "oauth" && typeof credential?.teamName === "string" ? { teamName: credential.teamName } : {}),
|
|
53
58
|
};
|
|
54
59
|
} catch {
|
|
55
60
|
return undefined;
|
|
@@ -337,13 +342,19 @@ export function installPiProviderRuntime(
|
|
|
337
342
|
const { status, preflight, auth } = getStatusDetails(model, ctx);
|
|
338
343
|
let liveCheckRequested = false;
|
|
339
344
|
if ((mode === "refresh" || mode === "check") && auth.configured) {
|
|
345
|
+
const getCredentialMetadata = () =>
|
|
346
|
+
readProviderCredentialMetadata(model.provider, runtime.readStoredCredential);
|
|
340
347
|
const statusContext: StatusContextLike = {
|
|
341
348
|
model,
|
|
342
349
|
modelRegistry: ctx.modelRegistry,
|
|
343
350
|
getCredentialKey: () => ctx.modelRegistry.getApiKeyForProvider(model.provider),
|
|
344
|
-
getCredentialMetadata
|
|
351
|
+
getCredentialMetadata,
|
|
352
|
+
};
|
|
353
|
+
const preflightContext: PreflightContextLike = {
|
|
354
|
+
model,
|
|
355
|
+
modelRegistry: ctx.modelRegistry,
|
|
356
|
+
getCredentialMetadata,
|
|
345
357
|
};
|
|
346
|
-
const preflightContext: PreflightContextLike = { model, modelRegistry: ctx.modelRegistry };
|
|
347
358
|
const refreshChecks: Array<Promise<unknown>> = [];
|
|
348
359
|
if (status) refreshChecks.push(statusManager.update(statusContext, { force: true }));
|
|
349
360
|
if (preflight) refreshChecks.push(preflightManager.update(preflightContext, { force: true }));
|
|
@@ -385,6 +396,7 @@ export function installPiProviderRuntime(
|
|
|
385
396
|
lifecycleGeneration++;
|
|
386
397
|
statusPresentationGeneration++;
|
|
387
398
|
statusPresentationVisible = false;
|
|
399
|
+
cancelDeferredProviderRegistrations(providers);
|
|
388
400
|
statusManager.cancelAll();
|
|
389
401
|
statusManager.clear();
|
|
390
402
|
preflightManager.cancelAll();
|
|
@@ -437,43 +449,52 @@ export function createPiProviderRuntime(
|
|
|
437
449
|
): (pi: ExtensionAPI) => Promise<void> {
|
|
438
450
|
const runtime = resolvePiProviderDependencies(dependencies);
|
|
439
451
|
return async (pi) => {
|
|
440
|
-
let latestBackgroundPricing: Record<string, OfficialModelMeta> | undefined;
|
|
441
|
-
let installedDefinition: PiProviderDefinition | undefined;
|
|
442
452
|
let installedController: PiProviderRuntimeController | undefined;
|
|
443
453
|
let disposed = false;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
);
|
|
451
467
|
const officialPricingPromise = runtime.enableOfficialPricingFallback
|
|
452
|
-
?
|
|
453
|
-
runtime.fetch,
|
|
454
|
-
runtime.officialPricingUrl,
|
|
455
|
-
runtime.officialPricingTimeoutMs,
|
|
456
|
-
runtime.officialPricingCacheTtlMs,
|
|
457
|
-
runtime.officialPricingMaxStaleMs,
|
|
458
|
-
runtime.now,
|
|
459
|
-
{
|
|
460
|
-
cachePath:
|
|
461
|
-
runtime.officialPricingUrl === OPENROUTER_MODELS_URL
|
|
462
|
-
? runtime.openRouterMetadataCachePath
|
|
463
|
-
: undefined,
|
|
464
|
-
background: runtime.officialPricingUrl === OPENROUTER_MODELS_URL,
|
|
465
|
-
onBackgroundRefresh: onBackgroundRefresh,
|
|
466
|
-
},
|
|
467
|
-
)
|
|
468
|
+
? fetchPricing({ allowNetwork: false })
|
|
468
469
|
: Promise.resolve({});
|
|
469
470
|
const definitionPromise = loadDefinition(runtime);
|
|
470
471
|
const [officialPricing, definition] = await Promise.all([officialPricingPromise, definitionPromise]);
|
|
471
472
|
validatePiProviderDefinition(definition);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
+
});
|
|
475
493
|
pi.on("session_shutdown", () => {
|
|
476
494
|
disposed = true;
|
|
495
|
+
pricingRefreshController?.abort();
|
|
496
|
+
pricingRefreshController = undefined;
|
|
477
497
|
});
|
|
498
|
+
installedController = installPiProviderRuntime(pi, runtime, definition, officialPricing);
|
|
478
499
|
};
|
|
479
500
|
}
|
package/core/status-manager.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { deriveCredentialType } from "./credential-type.ts";
|
|
1
3
|
import { isValidTimeoutMs, withDeadline } from "./deadline.ts";
|
|
4
|
+
import { applyDiagnosticBaseUrl, type DiagnosticModelRegistry, resolveDiagnosticAuth } from "./diagnostic-auth.ts";
|
|
2
5
|
import { isProviderDataError, ProviderDataError } from "./errors.ts";
|
|
3
6
|
import type {
|
|
4
7
|
StatusAdapter,
|
|
@@ -9,11 +12,11 @@ import type {
|
|
|
9
12
|
StatusWindowEntry,
|
|
10
13
|
} from "./types.ts";
|
|
11
14
|
|
|
15
|
+
type StatusModel = NonNullable<ExtensionContext["model"]>;
|
|
16
|
+
|
|
12
17
|
export interface StatusContextLike {
|
|
13
|
-
model?: { provider?: string };
|
|
14
|
-
modelRegistry:
|
|
15
|
-
getApiKeyForProvider(provider: string): Promise<string | undefined>;
|
|
16
|
-
};
|
|
18
|
+
model?: { provider?: string; id?: string; baseUrl?: string };
|
|
19
|
+
modelRegistry: DiagnosticModelRegistry;
|
|
17
20
|
/** Optional credential identity used to isolate cached account data. */
|
|
18
21
|
getCredentialKey?: () => Promise<string | undefined>;
|
|
19
22
|
/** Optional non-secret credential metadata for provider-specific account labels. */
|
|
@@ -222,14 +225,31 @@ export class StatusManager {
|
|
|
222
225
|
const cancellation = new AbortController();
|
|
223
226
|
const generation = ++state.generation;
|
|
224
227
|
const promise = withDeadline(
|
|
225
|
-
(signal) =>
|
|
226
|
-
|
|
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({
|
|
227
239
|
fetch: this.fetchFn,
|
|
228
|
-
getApiKey: () =>
|
|
229
|
-
|
|
240
|
+
getApiKey: async () => auth.apiKey,
|
|
241
|
+
getAuth: async () => auth,
|
|
242
|
+
model,
|
|
243
|
+
...(ctx.getCredentialMetadata === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: {
|
|
246
|
+
getCredentialMetadata: ctx.getCredentialMetadata,
|
|
247
|
+
getCredentialType: async () => deriveCredentialType(ctx.getCredentialMetadata?.()),
|
|
248
|
+
}),
|
|
230
249
|
now: this.now,
|
|
231
250
|
signal,
|
|
232
|
-
})
|
|
251
|
+
});
|
|
252
|
+
},
|
|
233
253
|
adapter.requestTimeoutMs,
|
|
234
254
|
cancellation.signal,
|
|
235
255
|
);
|
|
@@ -256,6 +276,11 @@ export class StatusManager {
|
|
|
256
276
|
} catch (error) {
|
|
257
277
|
if (state.generation !== generation || isErrorNamed(error, "AbortError")) return "skipped";
|
|
258
278
|
const dataError = isProviderDataError(error) ? error : undefined;
|
|
279
|
+
if (dataError?.code === "unsupported") {
|
|
280
|
+
state.snapshot = undefined;
|
|
281
|
+
state.lastError = undefined;
|
|
282
|
+
return "skipped";
|
|
283
|
+
}
|
|
259
284
|
const code = dataError?.code ?? (isErrorNamed(error, "TimeoutError") ? "timeout" : "fetch");
|
|
260
285
|
const retryAt =
|
|
261
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,8 +154,14 @@ 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;
|
|
163
|
+
/** Optional non-secret credential type ("oauth" vs "api_key") for providers with dual auth modes. */
|
|
164
|
+
getCredentialType?: () => Promise<string | undefined>;
|
|
150
165
|
signal?: AbortSignal;
|
|
151
166
|
now: () => number;
|
|
152
167
|
}
|
|
@@ -157,6 +172,8 @@ export interface StatusAdapter {
|
|
|
157
172
|
name: string;
|
|
158
173
|
cacheTtlMs: number;
|
|
159
174
|
requestTimeoutMs: number;
|
|
175
|
+
/** Return false when this account endpoint cannot safely serve the effective model URL. */
|
|
176
|
+
supportsModel?: (model: ActiveModel) => boolean;
|
|
160
177
|
fetch(context: StatusContext): Promise<StatusSnapshot>;
|
|
161
178
|
}
|
|
162
179
|
|
|
@@ -177,8 +194,11 @@ export interface ProviderAdapter {
|
|
|
177
194
|
/** @internal Draft state shared across isolated Adapter and Host contexts. */
|
|
178
195
|
registration?: {
|
|
179
196
|
modelDrafts: ProviderModelDraft[];
|
|
180
|
-
normalizedModels
|
|
181
|
-
modelMetadata
|
|
197
|
+
normalizedModels: ProviderModel[];
|
|
198
|
+
modelMetadata: Record<string, ProviderModelMetadata>;
|
|
199
|
+
officialPricing: Record<string, OfficialModelMeta>;
|
|
200
|
+
activeRefreshes: number;
|
|
201
|
+
deferredRegistration?: () => void;
|
|
182
202
|
};
|
|
183
203
|
}
|
|
184
204
|
|