@hyav/pi-provider 0.1.7 → 0.2.0
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 +23 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/core/adapter-extensions.ts +9 -2
- package/core/adapter-protocol.ts +4 -2
- package/core/adapter-validation.ts +14 -1
- package/core/catalog-preflight.ts +14 -1
- package/core/host.ts +43 -75
- package/core/model-catalog.ts +289 -0
- package/core/opencode-preflight.ts +6 -0
- package/core/pi-model-metadata.ts +739 -0
- package/core/provider-registration.ts +111 -56
- package/core/public-adapters.ts +16 -1
- package/core/runtime-config.ts +18 -38
- package/core/runtime-entry.ts +11 -6
- package/core/runtime.ts +26 -104
- package/core/status-report.ts +327 -229
- package/core/types.ts +37 -20
- package/index.ts +37 -18
- package/package.json +3 -1
- package/preflight/charm-hyper.ts +6 -2
- package/preflight/deepseek.ts +10 -1
- package/preflight/github-copilot.ts +4 -0
- package/preflight/google.ts +10 -1
- package/preflight/groq.ts +10 -1
- package/preflight/openai-codex.ts +10 -1
- package/preflight/openrouter.ts +11 -2
- package/preflight/vercel-ai-gateway.ts +11 -1
- package/preflight/xai.ts +18 -4
- package/providers/charm-hyper/oauth.ts +17 -11
- package/providers/charm-hyper.ts +106 -236
- package/status/huggingface.ts +2 -1
- package/status/openrouter.ts +8 -2
- package/status/vercel-ai-gateway.ts +1 -1
- package/core/official-pricing.ts +0 -899
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { ModelCatalogSource, ModelCatalogStatus, ProviderModelDraft, ProviderRefreshContext } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export interface ModelCatalogDiagnostics {
|
|
4
|
+
rejectedCount?: number;
|
|
5
|
+
duplicateCount?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ModelCatalogDiscoveryResult {
|
|
9
|
+
models: ProviderModelDraft[];
|
|
10
|
+
diagnostics?: ModelCatalogDiagnostics;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ModelCatalogLifecycleOptions {
|
|
14
|
+
initialModels?: ProviderModelDraft[];
|
|
15
|
+
initialSource?: ModelCatalogSource;
|
|
16
|
+
ttlMs: number;
|
|
17
|
+
failureBackoffMs?: number;
|
|
18
|
+
maxFailureBackoffMs?: number;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
discover(
|
|
21
|
+
context: ProviderRefreshContext,
|
|
22
|
+
): Promise<ProviderModelDraft[] | ModelCatalogDiscoveryResult> | ProviderModelDraft[] | ModelCatalogDiscoveryResult;
|
|
23
|
+
restore(stored: ProviderRefreshContext["stored"]): ProviderModelDraft[] | undefined;
|
|
24
|
+
persist(models: ProviderModelDraft[], checkedAt: number): NonNullable<ProviderRefreshContext["stored"]>;
|
|
25
|
+
onUpdate(models: ProviderModelDraft[]): void;
|
|
26
|
+
errorCode(error: unknown): string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ModelCatalogLifecycle {
|
|
30
|
+
catalog: ModelCatalogStatus;
|
|
31
|
+
getModels(): ProviderModelDraft[];
|
|
32
|
+
refreshModels(context: ProviderRefreshContext): Promise<ProviderModelDraft[]>;
|
|
33
|
+
setModels(models: ProviderModelDraft[], source?: ModelCatalogSource, updatedAt?: number): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isAbortError(error: unknown): boolean {
|
|
37
|
+
return error !== null && typeof error === "object" && "name" in error && error.name === "AbortError";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isValidTimestamp(value: unknown): value is number {
|
|
41
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function publish(
|
|
45
|
+
context: ProviderRefreshContext,
|
|
46
|
+
update: () => void,
|
|
47
|
+
persist?: NonNullable<ProviderRefreshContext["stored"]>,
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
try {
|
|
50
|
+
return await context.publish({ ...(persist ? { persist } : {}), update });
|
|
51
|
+
} catch {
|
|
52
|
+
if (!persist) return false;
|
|
53
|
+
// Persistence is an optimization. Retry the generation-checked update without it.
|
|
54
|
+
try {
|
|
55
|
+
return await context.publish({ update });
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
63
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function waitForCaller<T>(request: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
67
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
68
|
+
return new Promise<T>((resolve, reject) => {
|
|
69
|
+
const onAbort = () => reject(abortReason(signal));
|
|
70
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
void request.then(
|
|
72
|
+
(value) => {
|
|
73
|
+
signal.removeEventListener("abort", onAbort);
|
|
74
|
+
resolve(value);
|
|
75
|
+
},
|
|
76
|
+
(error) => {
|
|
77
|
+
signal.removeEventListener("abort", onAbort);
|
|
78
|
+
reject(error);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DEFAULT_FAILURE_BACKOFF_MS = 30_000;
|
|
85
|
+
const DEFAULT_MAX_FAILURE_BACKOFF_MS = 15 * 60_000;
|
|
86
|
+
|
|
87
|
+
function finiteNonNegative(value: number | undefined, fallback: number, label: string): number {
|
|
88
|
+
const resolved = value ?? fallback;
|
|
89
|
+
if (!Number.isFinite(resolved) || resolved < 0)
|
|
90
|
+
throw new RangeError(`${label} must be a finite non-negative number`);
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface NormalizedCatalogDiscoveryResult {
|
|
95
|
+
models: ProviderModelDraft[];
|
|
96
|
+
diagnostics: Required<ModelCatalogDiagnostics>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function diagnosticCount(value: number | undefined, label: string): number {
|
|
100
|
+
const count = value ?? 0;
|
|
101
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
102
|
+
throw new RangeError(`Model catalog ${label} must be a non-negative safe integer`);
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeDiscoveryResult(
|
|
108
|
+
result: ProviderModelDraft[] | ModelCatalogDiscoveryResult,
|
|
109
|
+
): NormalizedCatalogDiscoveryResult {
|
|
110
|
+
const models = Array.isArray(result) ? result : result.models;
|
|
111
|
+
const diagnostics = Array.isArray(result) ? undefined : result.diagnostics;
|
|
112
|
+
return {
|
|
113
|
+
models,
|
|
114
|
+
diagnostics: {
|
|
115
|
+
rejectedCount: diagnosticCount(diagnostics?.rejectedCount, "rejected count"),
|
|
116
|
+
duplicateCount: diagnosticCount(diagnostics?.duplicateCount, "duplicate count"),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ActiveCatalogRefresh {
|
|
122
|
+
request: Promise<NormalizedCatalogDiscoveryResult>;
|
|
123
|
+
publication: Promise<void>;
|
|
124
|
+
waiters: number;
|
|
125
|
+
settled: boolean;
|
|
126
|
+
applied: boolean;
|
|
127
|
+
failureRecorded: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createModelCatalogLifecycle(options: ModelCatalogLifecycleOptions): ModelCatalogLifecycle {
|
|
131
|
+
const now = options.now ?? Date.now;
|
|
132
|
+
const failureBackoffMs = finiteNonNegative(
|
|
133
|
+
options.failureBackoffMs,
|
|
134
|
+
DEFAULT_FAILURE_BACKOFF_MS,
|
|
135
|
+
"Model catalog failure backoff",
|
|
136
|
+
);
|
|
137
|
+
const maxFailureBackoffMs = finiteNonNegative(
|
|
138
|
+
options.maxFailureBackoffMs,
|
|
139
|
+
DEFAULT_MAX_FAILURE_BACKOFF_MS,
|
|
140
|
+
"Model catalog maximum failure backoff",
|
|
141
|
+
);
|
|
142
|
+
if (maxFailureBackoffMs < failureBackoffMs) {
|
|
143
|
+
throw new RangeError("Model catalog maximum failure backoff must not be less than its initial backoff");
|
|
144
|
+
}
|
|
145
|
+
let models = [...(options.initialModels ?? [])];
|
|
146
|
+
let lastCatalogUpdatedAt: number | undefined;
|
|
147
|
+
let inFlight: ActiveCatalogRefresh | undefined;
|
|
148
|
+
const catalog: ModelCatalogStatus = {
|
|
149
|
+
source: options.initialSource ?? (models.length > 0 ? "static" : "empty"),
|
|
150
|
+
modelCount: models.length,
|
|
151
|
+
consecutiveFailures: 0,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const applyModels = (
|
|
155
|
+
nextModels: ProviderModelDraft[],
|
|
156
|
+
source: ModelCatalogSource,
|
|
157
|
+
updatedAt?: number,
|
|
158
|
+
diagnostics: Required<ModelCatalogDiagnostics> = { rejectedCount: 0, duplicateCount: 0 },
|
|
159
|
+
) => {
|
|
160
|
+
models = [...nextModels];
|
|
161
|
+
options.onUpdate(models);
|
|
162
|
+
catalog.source = source;
|
|
163
|
+
catalog.modelCount = models.length;
|
|
164
|
+
catalog.rejectedCount = diagnostics.rejectedCount;
|
|
165
|
+
catalog.duplicateCount = diagnostics.duplicateCount;
|
|
166
|
+
catalog.lastError = undefined;
|
|
167
|
+
catalog.consecutiveFailures = 0;
|
|
168
|
+
catalog.nextRetryAt = undefined;
|
|
169
|
+
if (updatedAt !== undefined) {
|
|
170
|
+
catalog.updatedAt = updatedAt;
|
|
171
|
+
catalog.lastSuccessfulRefreshAt = updatedAt;
|
|
172
|
+
lastCatalogUpdatedAt = updatedAt;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const restoreStored = async (context: ProviderRefreshContext): Promise<void> => {
|
|
177
|
+
const restored = options.restore(context.stored);
|
|
178
|
+
if (!restored) return;
|
|
179
|
+
const checkedAt = isValidTimestamp(context.stored?.checkedAt) ? context.stored.checkedAt : undefined;
|
|
180
|
+
if (lastCatalogUpdatedAt !== undefined && (checkedAt === undefined || checkedAt <= lastCatalogUpdatedAt)) return;
|
|
181
|
+
await publish(context, () => {
|
|
182
|
+
applyModels(restored, "cached", checkedAt);
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const clearSettledRefresh = (active: ActiveCatalogRefresh) => {
|
|
187
|
+
if (inFlight === active && active.settled && active.waiters === 0) inFlight = undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const recordFailure = (active: ActiveCatalogRefresh, error: unknown) => {
|
|
191
|
+
if (active.failureRecorded || isAbortError(error)) return;
|
|
192
|
+
active.failureRecorded = true;
|
|
193
|
+
const failedAt = now();
|
|
194
|
+
const consecutiveFailures = (catalog.consecutiveFailures ?? 0) + 1;
|
|
195
|
+
const multiplier = 2 ** Math.min(30, consecutiveFailures - 1);
|
|
196
|
+
catalog.consecutiveFailures = consecutiveFailures;
|
|
197
|
+
catalog.nextRetryAt = failedAt + Math.min(maxFailureBackoffMs, failureBackoffMs * multiplier);
|
|
198
|
+
catalog.lastError = options.errorCode(error);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const startRefresh = (context: ProviderRefreshContext): ActiveCatalogRefresh => {
|
|
202
|
+
const active: ActiveCatalogRefresh = {
|
|
203
|
+
request: Promise.resolve({
|
|
204
|
+
models: [],
|
|
205
|
+
diagnostics: { rejectedCount: 0, duplicateCount: 0 },
|
|
206
|
+
}),
|
|
207
|
+
publication: Promise.resolve(),
|
|
208
|
+
waiters: 0,
|
|
209
|
+
settled: false,
|
|
210
|
+
applied: false,
|
|
211
|
+
failureRecorded: false,
|
|
212
|
+
};
|
|
213
|
+
const sharedContext = { ...context, signal: new AbortController().signal };
|
|
214
|
+
catalog.lastAttemptAt = now();
|
|
215
|
+
active.request = Promise.resolve()
|
|
216
|
+
.then(() => options.discover(sharedContext))
|
|
217
|
+
.then(normalizeDiscoveryResult)
|
|
218
|
+
.catch((error: unknown) => {
|
|
219
|
+
recordFailure(active, error);
|
|
220
|
+
throw error;
|
|
221
|
+
})
|
|
222
|
+
.finally(() => {
|
|
223
|
+
active.settled = true;
|
|
224
|
+
clearSettledRefresh(active);
|
|
225
|
+
});
|
|
226
|
+
inFlight = active;
|
|
227
|
+
return active;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const refreshModels = async (context: ProviderRefreshContext): Promise<ProviderModelDraft[]> => {
|
|
231
|
+
await restoreStored(context);
|
|
232
|
+
if (context.allowNetwork !== true || context.signal.aborted) return [...models];
|
|
233
|
+
|
|
234
|
+
const currentTime = now();
|
|
235
|
+
const isFresh =
|
|
236
|
+
catalog.lastSuccessfulRefreshAt !== undefined &&
|
|
237
|
+
Math.max(0, currentTime - catalog.lastSuccessfulRefreshAt) <= Math.max(0, options.ttlMs);
|
|
238
|
+
if (!context.force && catalog.lastError === undefined && isFresh) return [...models];
|
|
239
|
+
if (
|
|
240
|
+
!context.force &&
|
|
241
|
+
inFlight === undefined &&
|
|
242
|
+
catalog.nextRetryAt !== undefined &&
|
|
243
|
+
currentTime < catalog.nextRetryAt
|
|
244
|
+
) {
|
|
245
|
+
return [...models];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const active = inFlight ?? startRefresh(context);
|
|
249
|
+
active.waiters++;
|
|
250
|
+
try {
|
|
251
|
+
const discovered = await waitForCaller(active.request, context.signal);
|
|
252
|
+
const attempt = active.publication.then(async () => {
|
|
253
|
+
if (active.applied || context.signal.aborted) return;
|
|
254
|
+
const updatedAt = now();
|
|
255
|
+
await publish(
|
|
256
|
+
context,
|
|
257
|
+
() => {
|
|
258
|
+
applyModels(discovered.models, "live", updatedAt, discovered.diagnostics);
|
|
259
|
+
active.applied = true;
|
|
260
|
+
},
|
|
261
|
+
options.persist(discovered.models, updatedAt),
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
active.publication = attempt.catch(() => undefined);
|
|
265
|
+
await waitForCaller(attempt, context.signal);
|
|
266
|
+
return [...models];
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!context.signal.aborted) recordFailure(active, error);
|
|
269
|
+
throw error;
|
|
270
|
+
} finally {
|
|
271
|
+
active.waiters = Math.max(0, active.waiters - 1);
|
|
272
|
+
clearSettledRefresh(active);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const setModels = (nextModels: ProviderModelDraft[], source?: ModelCatalogSource, updatedAt?: number): void => {
|
|
277
|
+
applyModels(nextModels, source ?? (nextModels.length > 0 ? "cached" : "empty"), updatedAt);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
options.onUpdate(models);
|
|
281
|
+
const lifecycle: ModelCatalogLifecycle = {
|
|
282
|
+
catalog,
|
|
283
|
+
getModels: () => [...models],
|
|
284
|
+
refreshModels,
|
|
285
|
+
setModels,
|
|
286
|
+
};
|
|
287
|
+
(refreshModels as any).lifecycle = lifecycle;
|
|
288
|
+
return lifecycle;
|
|
289
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MAX_PROVIDER_MODEL_COUNT } from "../core/adapter-validation.ts";
|
|
1
2
|
import { ProviderDataError } from "../core/errors.ts";
|
|
2
3
|
import type { PreflightAdapter } from "../core/preflight-manager.ts";
|
|
3
4
|
import { parseRetryAfter } from "../core/retry-after.ts";
|
|
@@ -25,6 +26,8 @@ export function createOpenCodeCatalogPreflightAdapter(
|
|
|
25
26
|
requestTimeoutMs,
|
|
26
27
|
async fetch(context) {
|
|
27
28
|
const apiKey = await context.getApiKey();
|
|
29
|
+
// OpenCode Zen catalogs are public. The check stays unauthenticated and
|
|
30
|
+
// intentionally omits the "auth" check when no credential is resolved.
|
|
28
31
|
const headers: Record<string, string> = {
|
|
29
32
|
Accept: "application/json",
|
|
30
33
|
"Accept-Encoding": "identity",
|
|
@@ -49,6 +52,9 @@ export function createOpenCodeCatalogPreflightAdapter(
|
|
|
49
52
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
50
53
|
throw new ProviderDataError(`${config.name} preflight returned invalid catalog data`, "badjson");
|
|
51
54
|
}
|
|
55
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
56
|
+
throw new ProviderDataError(`${config.name} preflight catalog exceeds the maximum model count`, "badjson");
|
|
57
|
+
}
|
|
52
58
|
const modelIds = new Set(
|
|
53
59
|
payload.data
|
|
54
60
|
.filter(isRecord)
|