@hyav/pi-provider 0.1.0-oidc-bootstrap.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 +11 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/README.zh-CN.md +61 -0
- package/SECURITY.md +36 -0
- package/SUPPORT.md +25 -0
- package/core/adapter-extensions.ts +175 -0
- package/core/adapter-protocol.ts +120 -0
- package/core/adapter-validation.ts +241 -0
- package/core/deadline.ts +78 -0
- package/core/definition.ts +64 -0
- package/core/errors.ts +38 -0
- package/core/extension.ts +20 -0
- package/core/host.ts +462 -0
- package/core/live-check-manager.ts +263 -0
- package/core/official-pricing.ts +881 -0
- package/core/opencode-preflight.ts +66 -0
- package/core/preflight-manager.ts +251 -0
- package/core/pricing-adjustments.ts +118 -0
- package/core/provider-registration.ts +261 -0
- package/core/retry-after.ts +24 -0
- package/core/runtime-config.ts +95 -0
- package/core/runtime.ts +473 -0
- package/core/status-manager.ts +332 -0
- package/core/status-report.ts +592 -0
- package/core/tuner-manager.ts +34 -0
- package/core/types.ts +175 -0
- package/index.ts +108 -0
- package/package.json +81 -0
- package/preflight/charm-hyper.ts +62 -0
- package/preflight/deepseek.ts +73 -0
- package/preflight/google.ts +89 -0
- package/preflight/openai-codex.ts +88 -0
- package/preflight/opencode-go.ts +27 -0
- package/preflight/opencode.ts +27 -0
- package/providers/charm-hyper/constants.ts +31 -0
- package/providers/charm-hyper/oauth.ts +360 -0
- package/providers/charm-hyper.ts +536 -0
- package/status/charm-hyper.ts +76 -0
- package/status/deepseek.ts +102 -0
- package/status/openai-codex.ts +224 -0
- package/status/opencode-go.ts +133 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { isValidTimeoutMs } from "./deadline.ts";
|
|
2
|
+
import type { PreflightAdapter } from "./preflight-manager.ts";
|
|
3
|
+
import { validatePricingAdjustment, validatePricingPolicy } from "./pricing-adjustments.ts";
|
|
4
|
+
import type { ProviderAdapter, StatusAdapter, TunerAdapter } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type AdapterValue = ProviderAdapter | StatusAdapter | PreflightAdapter | TunerAdapter;
|
|
7
|
+
|
|
8
|
+
const MAX_STABLE_ID_LENGTH = 128;
|
|
9
|
+
const MAX_TEXT_LENGTH = 1_024;
|
|
10
|
+
const MAX_MODEL_ID_LENGTH = 512;
|
|
11
|
+
|
|
12
|
+
export function isStableAdapterId(value: unknown): value is string {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === "string" &&
|
|
15
|
+
value.length > 0 &&
|
|
16
|
+
value.length <= MAX_STABLE_ID_LENGTH &&
|
|
17
|
+
value.trim() === value &&
|
|
18
|
+
!/\s/.test(value) &&
|
|
19
|
+
!/[\u0000-\u001f\u007f]/.test(value)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
24
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isSafeText(value: unknown, maxLength = MAX_TEXT_LENGTH): value is string {
|
|
28
|
+
return (
|
|
29
|
+
typeof value === "string" &&
|
|
30
|
+
value.trim() !== "" &&
|
|
31
|
+
value.length <= maxLength &&
|
|
32
|
+
!/[\u0000-\u001f\u007f]/.test(value)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertStableId(value: unknown, label: string): asserts value is string {
|
|
37
|
+
if (!isStableAdapterId(value)) {
|
|
38
|
+
throw new Error(`${label} must be a non-empty ID without whitespace`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertAdapterObject(value: unknown, label: string): asserts value is Record<string, unknown> {
|
|
43
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertFiniteNonNegative(value: unknown, label: string): void {
|
|
47
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
48
|
+
throw new Error(`${label} must be a finite non-negative number`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertPositiveInteger(value: unknown, label: string): void {
|
|
53
|
+
if (typeof value !== "number" || !Number.isInteger(value) || !Number.isFinite(value) || value <= 0) {
|
|
54
|
+
throw new Error(`${label} must be a positive integer`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateProviderCost(value: unknown, label: string): void {
|
|
59
|
+
assertAdapterObject(value, label);
|
|
60
|
+
for (const field of ["input", "output", "cacheRead", "cacheWrite"] as const) {
|
|
61
|
+
if (value[field] !== undefined) assertFiniteNonNegative(value[field], `${label}.${field}`);
|
|
62
|
+
}
|
|
63
|
+
if (value.tiers === undefined) return;
|
|
64
|
+
if (!Array.isArray(value.tiers)) throw new Error(`${label}.tiers must be an array`);
|
|
65
|
+
for (const [index, tier] of value.tiers.entries()) {
|
|
66
|
+
assertAdapterObject(tier, `${label}.tiers[${index}]`);
|
|
67
|
+
assertPositiveInteger(tier.inputTokensAbove, `${label}.tiers[${index}].inputTokensAbove`);
|
|
68
|
+
for (const field of ["input", "output", "cacheRead", "cacheWrite"] as const) {
|
|
69
|
+
if (tier[field] !== undefined) {
|
|
70
|
+
assertFiniteNonNegative(tier[field], `${label}.tiers[${index}].${field}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateProviderModelDraft(value: unknown, label: string): void {
|
|
77
|
+
assertAdapterObject(value, label);
|
|
78
|
+
if (!isSafeText(value.id, MAX_MODEL_ID_LENGTH)) throw new Error(`${label}.id must be a non-empty model ID`);
|
|
79
|
+
if (value.name !== undefined && !isSafeText(value.name)) throw new Error(`${label}.name must be safe text`);
|
|
80
|
+
if (value.reasoning !== undefined && typeof value.reasoning !== "boolean") {
|
|
81
|
+
throw new Error(`${label}.reasoning must be a boolean`);
|
|
82
|
+
}
|
|
83
|
+
if (value.input !== undefined) {
|
|
84
|
+
if (!Array.isArray(value.input) || !value.input.every((item) => item === "text" || item === "image")) {
|
|
85
|
+
throw new Error(`${label}.input must contain only text or image`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (value.contextWindow !== undefined) assertPositiveInteger(value.contextWindow, `${label}.contextWindow`);
|
|
89
|
+
if (value.maxTokens !== undefined) assertPositiveInteger(value.maxTokens, `${label}.maxTokens`);
|
|
90
|
+
if (value.cost !== undefined) validateProviderCost(value.cost, `${label}.cost`);
|
|
91
|
+
if (value.pricingAdjustment !== undefined) {
|
|
92
|
+
validatePricingAdjustment(value.pricingAdjustment, `${label}.pricingAdjustment`);
|
|
93
|
+
}
|
|
94
|
+
if (value.compat !== undefined) assertAdapterObject(value.compat, `${label}.compat`);
|
|
95
|
+
if (value.thinkingLevelMap !== undefined) assertAdapterObject(value.thinkingLevelMap, `${label}.thinkingLevelMap`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function validateProviderAdapter(adapter: unknown): asserts adapter is ProviderAdapter {
|
|
99
|
+
assertAdapterObject(adapter, "Provider adapter");
|
|
100
|
+
assertStableId(adapter.id, "Provider adapter ID");
|
|
101
|
+
if (adapter.pricing !== undefined) validatePricingPolicy(adapter.pricing, `Provider ${adapter.id} pricing`);
|
|
102
|
+
if (!isRecord(adapter.provider)) throw new Error(`Provider ${adapter.id} must define provider metadata`);
|
|
103
|
+
const provider = adapter.provider;
|
|
104
|
+
if (!isSafeText(provider.name)) throw new Error(`Provider ${adapter.id} must define a name`);
|
|
105
|
+
if (!isSafeText(provider.baseUrl)) throw new Error(`Provider ${adapter.id} must define a base URL`);
|
|
106
|
+
if (!isSafeText(provider.apiKey)) throw new Error(`Provider ${adapter.id} must define an API key reference`);
|
|
107
|
+
if (!isSafeText(provider.api)) throw new Error(`Provider ${adapter.id} must define an API type`);
|
|
108
|
+
if (provider.authHeader !== undefined && typeof provider.authHeader !== "boolean") {
|
|
109
|
+
throw new Error(`Provider ${adapter.id} has invalid authHeader`);
|
|
110
|
+
}
|
|
111
|
+
if (provider.headers !== undefined) {
|
|
112
|
+
if (!isRecord(provider.headers)) throw new Error(`Provider ${adapter.id} has invalid headers`);
|
|
113
|
+
for (const [key, value] of Object.entries(provider.headers)) {
|
|
114
|
+
if (!isSafeText(key) || typeof value !== "string")
|
|
115
|
+
throw new Error(`Provider ${adapter.id} has invalid headers`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (provider.oauth !== undefined) {
|
|
119
|
+
if (!isRecord(provider.oauth)) throw new Error(`Provider ${adapter.id} has invalid OAuth configuration`);
|
|
120
|
+
if (!isSafeText(provider.oauth.name)) throw new Error(`Provider ${adapter.id} has invalid OAuth name`);
|
|
121
|
+
for (const field of ["login", "refreshToken", "getApiKey"] as const) {
|
|
122
|
+
if (typeof provider.oauth[field] !== "function") {
|
|
123
|
+
throw new Error(`Provider ${adapter.id} has invalid OAuth ${field}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(provider.models)) throw new Error(`Provider ${adapter.id} must define a model list`);
|
|
128
|
+
for (const [index, model] of provider.models.entries()) {
|
|
129
|
+
validateProviderModelDraft(model, `Provider ${adapter.id} model ${index}`);
|
|
130
|
+
}
|
|
131
|
+
if (provider.refreshModels !== undefined && typeof provider.refreshModels !== "function") {
|
|
132
|
+
throw new Error(`Provider ${adapter.id} has invalid refreshModels`);
|
|
133
|
+
}
|
|
134
|
+
if (adapter.catalog !== undefined) {
|
|
135
|
+
if (!isRecord(adapter.catalog)) throw new Error(`Provider ${adapter.id} has invalid catalog metadata`);
|
|
136
|
+
if (
|
|
137
|
+
adapter.catalog.source !== "static" &&
|
|
138
|
+
adapter.catalog.source !== "live" &&
|
|
139
|
+
adapter.catalog.source !== "fallback"
|
|
140
|
+
) {
|
|
141
|
+
throw new Error(`Provider ${adapter.id} has invalid catalog source`);
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
typeof adapter.catalog.modelCount !== "number" ||
|
|
145
|
+
!Number.isInteger(adapter.catalog.modelCount) ||
|
|
146
|
+
adapter.catalog.modelCount < 0
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(`Provider ${adapter.id} has invalid catalog model count`);
|
|
149
|
+
}
|
|
150
|
+
if (adapter.catalog.updatedAt !== undefined)
|
|
151
|
+
assertFiniteNonNegative(adapter.catalog.updatedAt, "Catalog updatedAt");
|
|
152
|
+
if (adapter.catalog.lastError !== undefined && !isSafeText(adapter.catalog.lastError)) {
|
|
153
|
+
throw new Error(`Provider ${adapter.id} has invalid catalog error`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function validateStatusAdapter(adapter: unknown): asserts adapter is StatusAdapter {
|
|
159
|
+
assertAdapterObject(adapter, "Status adapter");
|
|
160
|
+
assertStableId(adapter.id, "Status adapter ID");
|
|
161
|
+
assertStableId(adapter.providerId, "Status adapter provider ID");
|
|
162
|
+
if (!isSafeText(adapter.name)) throw new Error(`Status ${adapter.id} must define a name`);
|
|
163
|
+
if (typeof adapter.fetch !== "function") throw new Error(`Status ${adapter.id} must define fetch()`);
|
|
164
|
+
if (typeof adapter.cacheTtlMs !== "number" || !Number.isFinite(adapter.cacheTtlMs) || adapter.cacheTtlMs < 0) {
|
|
165
|
+
throw new Error(`Status ${adapter.id} has invalid cache TTL`);
|
|
166
|
+
}
|
|
167
|
+
if (!isValidTimeoutMs(adapter.requestTimeoutMs)) throw new Error(`Status ${adapter.id} has invalid timing settings`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function validatePreflightAdapter(adapter: unknown): asserts adapter is PreflightAdapter {
|
|
171
|
+
assertAdapterObject(adapter, "Preflight adapter");
|
|
172
|
+
assertStableId(adapter.id, "Preflight adapter ID");
|
|
173
|
+
assertStableId(adapter.providerId, "Preflight adapter provider ID");
|
|
174
|
+
if (!isSafeText(adapter.name)) throw new Error(`Preflight ${adapter.id} must define a name`);
|
|
175
|
+
if (typeof adapter.fetch !== "function") throw new Error(`Preflight ${adapter.id} must define fetch()`);
|
|
176
|
+
if (typeof adapter.cacheTtlMs !== "number" || !Number.isFinite(adapter.cacheTtlMs) || adapter.cacheTtlMs < 0) {
|
|
177
|
+
throw new Error(`Preflight ${adapter.id} has invalid cache TTL`);
|
|
178
|
+
}
|
|
179
|
+
if (!isValidTimeoutMs(adapter.requestTimeoutMs)) {
|
|
180
|
+
throw new Error(`Preflight ${adapter.id} has invalid timing settings`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function validateTunerAdapter(adapter: unknown): asserts adapter is TunerAdapter {
|
|
185
|
+
assertAdapterObject(adapter, "Tuner adapter");
|
|
186
|
+
assertStableId(adapter.id, "Tuner adapter ID");
|
|
187
|
+
if (typeof adapter.matches !== "function") throw new Error(`Tuner ${adapter.id} must define matches()`);
|
|
188
|
+
if (typeof adapter.transform !== "function") throw new Error(`Tuner ${adapter.id} must define transform()`);
|
|
189
|
+
if (adapter.priority !== undefined && (!Number.isFinite(adapter.priority) || !Number.isInteger(adapter.priority))) {
|
|
190
|
+
throw new Error(`Tuner ${adapter.id} has invalid priority`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function validateAdapter(kind: "provider", adapter: unknown): asserts adapter is ProviderAdapter;
|
|
195
|
+
export function validateAdapter(kind: "status", adapter: unknown): asserts adapter is StatusAdapter;
|
|
196
|
+
export function validateAdapter(kind: "preflight", adapter: unknown): asserts adapter is PreflightAdapter;
|
|
197
|
+
export function validateAdapter(kind: "tuner", adapter: unknown): asserts adapter is TunerAdapter;
|
|
198
|
+
export function validateAdapter(kind: "provider" | "status" | "preflight" | "tuner", adapter: unknown): void {
|
|
199
|
+
switch (kind) {
|
|
200
|
+
case "provider":
|
|
201
|
+
validateProviderAdapter(adapter);
|
|
202
|
+
return;
|
|
203
|
+
case "status":
|
|
204
|
+
validateStatusAdapter(adapter);
|
|
205
|
+
return;
|
|
206
|
+
case "preflight":
|
|
207
|
+
validatePreflightAdapter(adapter);
|
|
208
|
+
return;
|
|
209
|
+
case "tuner":
|
|
210
|
+
validateTunerAdapter(adapter);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function validateAdapterIdentity(
|
|
216
|
+
kind: "provider" | "tuner",
|
|
217
|
+
staticId: unknown,
|
|
218
|
+
adapter: { id: unknown },
|
|
219
|
+
): asserts adapter is { id: string };
|
|
220
|
+
export function validateAdapterIdentity(
|
|
221
|
+
kind: "status" | "preflight",
|
|
222
|
+
staticId: unknown,
|
|
223
|
+
staticProviderId: unknown,
|
|
224
|
+
adapter: { id: unknown; providerId: unknown },
|
|
225
|
+
): asserts adapter is { id: string; providerId: string };
|
|
226
|
+
export function validateAdapterIdentity(
|
|
227
|
+
kind: "provider" | "status" | "preflight" | "tuner",
|
|
228
|
+
staticId: unknown,
|
|
229
|
+
staticProviderIdOrAdapter: unknown,
|
|
230
|
+
maybeAdapter?: { id: unknown; providerId?: unknown },
|
|
231
|
+
): void {
|
|
232
|
+
const adapter = maybeAdapter === undefined ? staticProviderIdOrAdapter : maybeAdapter;
|
|
233
|
+
assertStableId(staticId, `${kind} static ID`);
|
|
234
|
+
if (!isRecord(adapter)) throw new Error(`${kind} adapter must be an object`);
|
|
235
|
+
if (adapter.id !== staticId) throw new Error(`${kind} adapter ID does not match its static ID`);
|
|
236
|
+
if (kind !== "status" && kind !== "preflight") return;
|
|
237
|
+
assertStableId(staticProviderIdOrAdapter, `${kind} static provider ID`);
|
|
238
|
+
if (adapter.providerId !== staticProviderIdOrAdapter) {
|
|
239
|
+
throw new Error(`${kind} adapter provider ID does not match its static provider ID`);
|
|
240
|
+
}
|
|
241
|
+
}
|
package/core/deadline.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
2
|
+
|
|
3
|
+
export function isValidTimeoutMs(value: unknown): value is number {
|
|
4
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMEOUT_MS;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function createAbortError(): DOMException {
|
|
8
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createTimeoutError(): DOMException {
|
|
12
|
+
return new DOMException("The operation timed out", "TimeoutError");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function signalReason(signal: AbortSignal): unknown {
|
|
16
|
+
return signal.reason ?? createAbortError();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Races the complete operation, including response processing, against a finite deadline.
|
|
21
|
+
* The underlying operation receives a signal, but its late settlement is intentionally ignored.
|
|
22
|
+
*/
|
|
23
|
+
export function withDeadline<T>(
|
|
24
|
+
operation: (signal: AbortSignal) => Promise<T> | T,
|
|
25
|
+
timeoutMs: number,
|
|
26
|
+
externalSignal?: AbortSignal,
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
if (!isValidTimeoutMs(timeoutMs)) {
|
|
29
|
+
return Promise.reject(new RangeError(`Timeout must be an integer from 1 to ${MAX_TIMEOUT_MS} ms`));
|
|
30
|
+
}
|
|
31
|
+
if (externalSignal?.aborted) return Promise.reject(signalReason(externalSignal));
|
|
32
|
+
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let rejectCancellation: (reason: unknown) => void = () => {};
|
|
36
|
+
let timeoutReason: DOMException | undefined;
|
|
37
|
+
let cancellationReason: unknown;
|
|
38
|
+
let timedOut = false;
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
|
|
41
|
+
const cancellationPromise = new Promise<never>((_, reject) => {
|
|
42
|
+
rejectCancellation = reject;
|
|
43
|
+
});
|
|
44
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
45
|
+
timeoutId = setTimeout(() => {
|
|
46
|
+
timedOut = true;
|
|
47
|
+
timeoutReason = createTimeoutError();
|
|
48
|
+
controller.abort(timeoutReason);
|
|
49
|
+
reject(timeoutReason);
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
});
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
if (cancelled || timedOut) return;
|
|
54
|
+
cancelled = true;
|
|
55
|
+
cancellationReason = signalReason(externalSignal!);
|
|
56
|
+
controller.abort(cancellationReason);
|
|
57
|
+
rejectCancellation(cancellationReason);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (externalSignal) {
|
|
61
|
+
externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
if (externalSignal.aborted) onAbort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const operationPromise = Promise.resolve().then(() => operation(controller.signal));
|
|
66
|
+
const raced = Promise.race([operationPromise, timeoutPromise, cancellationPromise]);
|
|
67
|
+
const result = raced.catch((error: unknown) => {
|
|
68
|
+
if (timedOut) throw timeoutReason;
|
|
69
|
+
if (cancelled) throw cancellationReason;
|
|
70
|
+
throw error;
|
|
71
|
+
});
|
|
72
|
+
const cleanup = () => {
|
|
73
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
74
|
+
externalSignal?.removeEventListener("abort", onAbort);
|
|
75
|
+
};
|
|
76
|
+
result.then(cleanup, cleanup);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { validateAdapter } from "./adapter-validation.ts";
|
|
2
|
+
import type { PreflightAdapter } from "./preflight-manager.ts";
|
|
3
|
+
import type { ProviderAdapter, StatusAdapter, TunerAdapter } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export interface ProviderKitDefinition {
|
|
6
|
+
providers: ProviderAdapter[];
|
|
7
|
+
statuses?: StatusAdapter[];
|
|
8
|
+
preflights?: PreflightAdapter[];
|
|
9
|
+
tuners?: TunerAdapter[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function assertArray(value: unknown, label: string): asserts value is unknown[] {
|
|
13
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rejectDuplicate(ids: Set<string>, id: string, label: string): void {
|
|
17
|
+
if (ids.has(id)) throw new Error(`Duplicate ${label}: ${id}`);
|
|
18
|
+
ids.add(id);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate a complete, programmatically assembled definition.
|
|
23
|
+
*
|
|
24
|
+
* Dynamic Hosts use the same adapter validators while resolving conflicts, but
|
|
25
|
+
* keep invalid entries isolated instead of failing the whole registry.
|
|
26
|
+
*/
|
|
27
|
+
export function validateProviderKitDefinition(value: unknown): asserts value is ProviderKitDefinition {
|
|
28
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
29
|
+
throw new Error("Provider kit definition must be an object");
|
|
30
|
+
}
|
|
31
|
+
const definition = value as Partial<ProviderKitDefinition>;
|
|
32
|
+
assertArray(definition.providers, "Provider kit definition providers");
|
|
33
|
+
if (definition.statuses !== undefined) assertArray(definition.statuses, "Provider kit definition statuses");
|
|
34
|
+
if (definition.preflights !== undefined) assertArray(definition.preflights, "Provider kit definition preflights");
|
|
35
|
+
if (definition.tuners !== undefined) assertArray(definition.tuners, "Provider kit definition tuners");
|
|
36
|
+
|
|
37
|
+
const providerIds = new Set<string>();
|
|
38
|
+
for (const adapter of definition.providers) {
|
|
39
|
+
validateAdapter("provider", adapter);
|
|
40
|
+
rejectDuplicate(providerIds, adapter.id, "provider adapter ID");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const statusIds = new Set<string>();
|
|
44
|
+
const statusProviders = new Set<string>();
|
|
45
|
+
for (const adapter of definition.statuses ?? []) {
|
|
46
|
+
validateAdapter("status", adapter);
|
|
47
|
+
rejectDuplicate(statusIds, adapter.id, "status adapter ID");
|
|
48
|
+
rejectDuplicate(statusProviders, adapter.providerId, "status adapter provider ID");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const preflightIds = new Set<string>();
|
|
52
|
+
const preflightProviders = new Set<string>();
|
|
53
|
+
for (const adapter of definition.preflights ?? []) {
|
|
54
|
+
validateAdapter("preflight", adapter);
|
|
55
|
+
rejectDuplicate(preflightIds, adapter.id, "preflight adapter ID");
|
|
56
|
+
rejectDuplicate(preflightProviders, adapter.providerId, "preflight adapter provider ID");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tunerIds = new Set<string>();
|
|
60
|
+
for (const adapter of definition.tuners ?? []) {
|
|
61
|
+
validateAdapter("tuner", adapter);
|
|
62
|
+
rejectDuplicate(tunerIds, adapter.id, "tuner adapter ID");
|
|
63
|
+
}
|
|
64
|
+
}
|
package/core/errors.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ProviderDataError extends Error {
|
|
2
|
+
override readonly name = "ProviderDataError";
|
|
3
|
+
|
|
4
|
+
constructor(
|
|
5
|
+
message: string,
|
|
6
|
+
readonly code: string,
|
|
7
|
+
readonly retryAt?: number,
|
|
8
|
+
readonly httpStatus?: number,
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error shape shared across Pi's isolated extension module contexts.
|
|
16
|
+
* `instanceof` is intentionally not part of this boundary contract.
|
|
17
|
+
*/
|
|
18
|
+
export interface ProviderDataErrorLike {
|
|
19
|
+
readonly name: "ProviderDataError";
|
|
20
|
+
readonly code: string;
|
|
21
|
+
readonly retryAt?: number;
|
|
22
|
+
readonly httpStatus?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isProviderDataError(error: unknown): error is ProviderDataErrorLike {
|
|
26
|
+
if (error === null || typeof error !== "object") return false;
|
|
27
|
+
const candidate = error as Partial<ProviderDataErrorLike>;
|
|
28
|
+
return (
|
|
29
|
+
candidate.name === "ProviderDataError" &&
|
|
30
|
+
typeof candidate.code === "string" &&
|
|
31
|
+
(candidate.retryAt === undefined || typeof candidate.retryAt === "number") &&
|
|
32
|
+
(candidate.httpStatus === undefined ||
|
|
33
|
+
(typeof candidate.httpStatus === "number" &&
|
|
34
|
+
Number.isInteger(candidate.httpStatus) &&
|
|
35
|
+
candidate.httpStatus >= 100 &&
|
|
36
|
+
candidate.httpStatus <= 599))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type { ProviderKitDefinition } from "./definition.ts";
|
|
2
|
+
export { validateProviderKitDefinition } from "./definition.ts";
|
|
3
|
+
export {
|
|
4
|
+
normalizeProviderModel,
|
|
5
|
+
normalizeProviderModels,
|
|
6
|
+
prepareProviderRegistration,
|
|
7
|
+
registerProviderAdapter,
|
|
8
|
+
} from "./provider-registration.ts";
|
|
9
|
+
export {
|
|
10
|
+
createProviderKitRuntime,
|
|
11
|
+
installProviderKitRuntime,
|
|
12
|
+
type ProviderKitRuntimeController,
|
|
13
|
+
} from "./runtime.ts";
|
|
14
|
+
export type { ProviderKitDependencies, ProviderKitLoader } from "./runtime-config.ts";
|
|
15
|
+
export {
|
|
16
|
+
getDefaultProviderKitDependencies,
|
|
17
|
+
resolveProviderKitDependencies,
|
|
18
|
+
validateProviderKitDependencies,
|
|
19
|
+
} from "./runtime-config.ts";
|
|
20
|
+
export { getStatusModeCompletions } from "./status-report.ts";
|