@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,66 @@
|
|
|
1
|
+
import { ProviderDataError } from "../core/errors.ts";
|
|
2
|
+
import type { PreflightAdapter } from "../core/preflight-manager.ts";
|
|
3
|
+
import { parseRetryAfter } from "../core/retry-after.ts";
|
|
4
|
+
|
|
5
|
+
interface OpenCodePreflightConfig {
|
|
6
|
+
id: string;
|
|
7
|
+
providerId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
modelsUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createOpenCodeCatalogPreflightAdapter(
|
|
17
|
+
config: OpenCodePreflightConfig,
|
|
18
|
+
requestTimeoutMs: number,
|
|
19
|
+
): PreflightAdapter {
|
|
20
|
+
return {
|
|
21
|
+
id: config.id,
|
|
22
|
+
providerId: config.providerId,
|
|
23
|
+
name: config.name,
|
|
24
|
+
cacheTtlMs: 30_000,
|
|
25
|
+
requestTimeoutMs,
|
|
26
|
+
async fetch(context) {
|
|
27
|
+
const apiKey = await context.getApiKey();
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
"Accept-Encoding": "identity",
|
|
31
|
+
};
|
|
32
|
+
if (apiKey && apiKey !== "proxy-managed") headers.Authorization = `Bearer ${apiKey}`;
|
|
33
|
+
|
|
34
|
+
const response = await context.fetch(config.modelsUrl, { headers, signal: context.signal });
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new ProviderDataError(
|
|
37
|
+
`${config.name} preflight failed: HTTP ${response.status}`,
|
|
38
|
+
`http${response.status}`,
|
|
39
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
40
|
+
response.status,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
let payload: unknown;
|
|
44
|
+
try {
|
|
45
|
+
payload = await response.json();
|
|
46
|
+
} catch {
|
|
47
|
+
throw new ProviderDataError(`${config.name} preflight returned invalid JSON`, "badjson");
|
|
48
|
+
}
|
|
49
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
50
|
+
throw new ProviderDataError(`${config.name} preflight returned invalid catalog data`, "badjson");
|
|
51
|
+
}
|
|
52
|
+
const modelIds = new Set(
|
|
53
|
+
payload.data
|
|
54
|
+
.filter(isRecord)
|
|
55
|
+
.map((model) => (typeof model.id === "string" ? model.id.trim() : undefined))
|
|
56
|
+
.filter((id): id is string => id !== undefined && id !== ""),
|
|
57
|
+
);
|
|
58
|
+
return {
|
|
59
|
+
passed: modelIds.has(context.model.id),
|
|
60
|
+
checks: ["endpoint", "catalog"],
|
|
61
|
+
updatedAt: context.now(),
|
|
62
|
+
httpStatus: response.status,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isValidTimeoutMs, withDeadline } from "./deadline.ts";
|
|
3
|
+
import { isProviderDataError, ProviderDataError } from "./errors.ts";
|
|
4
|
+
|
|
5
|
+
export type PreflightModel = NonNullable<ExtensionContext["model"]>;
|
|
6
|
+
|
|
7
|
+
type ModelRegistry = ExtensionContext["modelRegistry"];
|
|
8
|
+
|
|
9
|
+
export interface PreflightContext {
|
|
10
|
+
fetch: typeof globalThis.fetch;
|
|
11
|
+
getApiKey: () => Promise<string | undefined>;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
now: () => number;
|
|
14
|
+
model: PreflightModel;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PreflightSnapshot {
|
|
18
|
+
passed: boolean;
|
|
19
|
+
checks: string[];
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
httpStatus?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PreflightAdapter {
|
|
25
|
+
id: string;
|
|
26
|
+
providerId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
cacheTtlMs: number;
|
|
29
|
+
requestTimeoutMs: number;
|
|
30
|
+
fetch(context: PreflightContext): Promise<PreflightSnapshot>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PreflightContextLike {
|
|
34
|
+
model: PreflightModel;
|
|
35
|
+
modelRegistry: Pick<ModelRegistry, "getApiKeyForProvider">;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreflightErrorState {
|
|
39
|
+
code: string;
|
|
40
|
+
retryAt?: number;
|
|
41
|
+
httpStatus?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PreflightDiagnostics {
|
|
45
|
+
snapshot?: PreflightSnapshot;
|
|
46
|
+
pending: boolean;
|
|
47
|
+
lastError?: PreflightErrorState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type PreflightUpdateResult = "cached" | "refreshed" | "failed" | "skipped";
|
|
51
|
+
|
|
52
|
+
interface PendingPreflight {
|
|
53
|
+
promise: Promise<PreflightSnapshot>;
|
|
54
|
+
generation: number;
|
|
55
|
+
cancel: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface PreflightState {
|
|
59
|
+
snapshot?: PreflightSnapshot;
|
|
60
|
+
pending?: PendingPreflight;
|
|
61
|
+
lastError?: PreflightErrorState;
|
|
62
|
+
generation: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isErrorNamed(error: unknown, name: string): boolean {
|
|
70
|
+
return error !== null && typeof error === "object" && "name" in error && error.name === name;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const MAX_PREFLIGHT_CHECKS = 32;
|
|
74
|
+
const MAX_PREFLIGHT_TEXT_LENGTH = 256;
|
|
75
|
+
|
|
76
|
+
function hasSafeText(value: unknown): value is string {
|
|
77
|
+
return (
|
|
78
|
+
typeof value === "string" &&
|
|
79
|
+
value.trim() !== "" &&
|
|
80
|
+
value.length <= MAX_PREFLIGHT_TEXT_LENGTH &&
|
|
81
|
+
!/[\u0000-\u001f\u007f]/.test(value)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizePreflightSnapshot(value: unknown): PreflightSnapshot {
|
|
86
|
+
if (!isRecord(value) || typeof value.passed !== "boolean" || !Array.isArray(value.checks)) {
|
|
87
|
+
throw new ProviderDataError("Preflight adapter returned an invalid snapshot", "badjson");
|
|
88
|
+
}
|
|
89
|
+
if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt)) {
|
|
90
|
+
throw new ProviderDataError("Preflight adapter returned an invalid snapshot", "badjson");
|
|
91
|
+
}
|
|
92
|
+
if (value.checks.length > MAX_PREFLIGHT_CHECKS) {
|
|
93
|
+
throw new ProviderDataError("Preflight adapter returned too many checks", "badjson");
|
|
94
|
+
}
|
|
95
|
+
const checks = value.checks.map((check) => {
|
|
96
|
+
if (!hasSafeText(check)) throw new ProviderDataError("Preflight adapter returned an invalid check", "badjson");
|
|
97
|
+
return check.trim();
|
|
98
|
+
});
|
|
99
|
+
if (new Set(checks).size !== checks.length) {
|
|
100
|
+
throw new ProviderDataError("Preflight adapter returned duplicate checks", "badjson");
|
|
101
|
+
}
|
|
102
|
+
if (value.httpStatus !== undefined) {
|
|
103
|
+
if (
|
|
104
|
+
typeof value.httpStatus !== "number" ||
|
|
105
|
+
!Number.isInteger(value.httpStatus) ||
|
|
106
|
+
value.httpStatus < 100 ||
|
|
107
|
+
value.httpStatus > 599
|
|
108
|
+
) {
|
|
109
|
+
throw new ProviderDataError("Preflight adapter returned an invalid HTTP status", "badjson");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
passed: value.passed,
|
|
114
|
+
checks,
|
|
115
|
+
updatedAt: value.updatedAt,
|
|
116
|
+
...(value.httpStatus !== undefined ? { httpStatus: value.httpStatus } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cloneSnapshot(snapshot: PreflightSnapshot): PreflightSnapshot {
|
|
121
|
+
return { ...snapshot, checks: [...snapshot.checks] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function errorState(error: unknown): PreflightErrorState {
|
|
125
|
+
const dataError = isProviderDataError(error) ? error : undefined;
|
|
126
|
+
if (dataError) {
|
|
127
|
+
return {
|
|
128
|
+
code: dataError.code,
|
|
129
|
+
...(dataError.httpStatus !== undefined ? { httpStatus: dataError.httpStatus } : {}),
|
|
130
|
+
...(dataError.retryAt !== undefined && Number.isFinite(dataError.retryAt)
|
|
131
|
+
? { retryAt: dataError.retryAt }
|
|
132
|
+
: {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (isErrorNamed(error, "TimeoutError")) return { code: "timeout" };
|
|
136
|
+
if (isErrorNamed(error, "AbortError")) return { code: "cancelled" };
|
|
137
|
+
if (error !== null && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
138
|
+
return { code: error.code };
|
|
139
|
+
}
|
|
140
|
+
return { code: "fetch" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getPreflightKey(provider: string, model: string): string {
|
|
144
|
+
return JSON.stringify([provider, model]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class PreflightManager {
|
|
148
|
+
private readonly states = new Map<string, PreflightState>();
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
private readonly adapters: PreflightAdapter[],
|
|
152
|
+
private readonly fetchFn: typeof globalThis.fetch,
|
|
153
|
+
private readonly now: () => number,
|
|
154
|
+
) {}
|
|
155
|
+
|
|
156
|
+
async update(ctx: PreflightContextLike, options: { force?: boolean } = {}): Promise<PreflightUpdateResult> {
|
|
157
|
+
const adapter = this.adapters.find(({ providerId }) => providerId === ctx.model.provider);
|
|
158
|
+
if (!adapter) return "skipped";
|
|
159
|
+
|
|
160
|
+
const state = this.getState(getPreflightKey(adapter.providerId, ctx.model.id));
|
|
161
|
+
const now = this.now();
|
|
162
|
+
const snapshotAge = state.snapshot ? now - state.snapshot.updatedAt : undefined;
|
|
163
|
+
if (!options.force && snapshotAge !== undefined && snapshotAge >= 0 && snapshotAge < adapter.cacheTtlMs) {
|
|
164
|
+
return "cached";
|
|
165
|
+
}
|
|
166
|
+
const retryAt = state.lastError?.retryAt;
|
|
167
|
+
if (retryAt !== undefined && Number.isFinite(retryAt) && now < retryAt) return "skipped";
|
|
168
|
+
if (!isValidTimeoutMs(adapter.requestTimeoutMs)) {
|
|
169
|
+
state.lastError = { code: "config" };
|
|
170
|
+
return "failed";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let pending = state.pending;
|
|
174
|
+
if (!pending) {
|
|
175
|
+
const cancellation = new AbortController();
|
|
176
|
+
const generation = ++state.generation;
|
|
177
|
+
const promise = withDeadline(
|
|
178
|
+
(signal) =>
|
|
179
|
+
adapter.fetch({
|
|
180
|
+
fetch: this.fetchFn,
|
|
181
|
+
getApiKey: () => ctx.modelRegistry.getApiKeyForProvider(adapter.providerId),
|
|
182
|
+
now: this.now,
|
|
183
|
+
signal,
|
|
184
|
+
model: ctx.model,
|
|
185
|
+
}),
|
|
186
|
+
adapter.requestTimeoutMs,
|
|
187
|
+
cancellation.signal,
|
|
188
|
+
);
|
|
189
|
+
pending = {
|
|
190
|
+
promise,
|
|
191
|
+
generation,
|
|
192
|
+
cancel: () => cancellation.abort(),
|
|
193
|
+
};
|
|
194
|
+
state.pending = pending;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const activePending = pending;
|
|
198
|
+
const { generation } = activePending;
|
|
199
|
+
try {
|
|
200
|
+
const snapshot = normalizePreflightSnapshot(await activePending.promise);
|
|
201
|
+
if (state.generation !== generation) return "skipped";
|
|
202
|
+
state.snapshot = snapshot;
|
|
203
|
+
state.lastError = undefined;
|
|
204
|
+
return "refreshed";
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (state.generation !== generation || isErrorNamed(error, "AbortError")) return "skipped";
|
|
207
|
+
state.lastError = errorState(error);
|
|
208
|
+
if (state.lastError.code === "timeout") {
|
|
209
|
+
state.generation++;
|
|
210
|
+
if (state.pending === activePending) state.pending = undefined;
|
|
211
|
+
}
|
|
212
|
+
return "failed";
|
|
213
|
+
} finally {
|
|
214
|
+
if (state.generation === generation && state.pending === activePending) state.pending = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async refresh(ctx: PreflightContextLike): Promise<PreflightUpdateResult> {
|
|
219
|
+
return await this.update(ctx, { force: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
cancelAll(): void {
|
|
223
|
+
for (const state of this.states.values()) {
|
|
224
|
+
state.generation++;
|
|
225
|
+
state.pending?.cancel();
|
|
226
|
+
state.pending = undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
clear(): void {
|
|
231
|
+
this.cancelAll();
|
|
232
|
+
this.states.clear();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getDiagnostics(provider: string, model: string): PreflightDiagnostics {
|
|
236
|
+
const state = this.states.get(getPreflightKey(provider, model));
|
|
237
|
+
return {
|
|
238
|
+
snapshot: state?.snapshot ? cloneSnapshot(state.snapshot) : undefined,
|
|
239
|
+
pending: state?.pending !== undefined,
|
|
240
|
+
lastError: state?.lastError ? { ...state.lastError } : undefined,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private getState(key: string): PreflightState {
|
|
245
|
+
const existing = this.states.get(key);
|
|
246
|
+
if (existing) return existing;
|
|
247
|
+
const state: PreflightState = { generation: 0 };
|
|
248
|
+
this.states.set(key, state);
|
|
249
|
+
return state;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModelPricingDetails,
|
|
3
|
+
PricingSku,
|
|
4
|
+
ProviderCost,
|
|
5
|
+
ProviderPricingAdjustment,
|
|
6
|
+
ProviderPricingPolicy,
|
|
7
|
+
ProviderPricingSource,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
const PRICING_SKUS: readonly PricingSku[] = ["input", "output", "cacheRead", "cacheWrite"];
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validatePricingAdjustment(
|
|
17
|
+
value: unknown,
|
|
18
|
+
label = "Pricing adjustment",
|
|
19
|
+
): asserts value is ProviderPricingAdjustment {
|
|
20
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
21
|
+
if (typeof value.multiplier !== "number" || !Number.isFinite(value.multiplier) || value.multiplier < 0) {
|
|
22
|
+
throw new Error(`${label}.multiplier must be a finite non-negative number`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof value.label !== "string" || value.label.trim() === "") {
|
|
25
|
+
throw new Error(`${label}.label must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
if (value.source !== undefined && (typeof value.source !== "string" || value.source.trim() === "")) {
|
|
28
|
+
throw new Error(`${label}.source must be a non-empty string`);
|
|
29
|
+
}
|
|
30
|
+
if (value.appliesToReference !== undefined && typeof value.appliesToReference !== "boolean") {
|
|
31
|
+
throw new Error(`${label}.appliesToReference must be a boolean`);
|
|
32
|
+
}
|
|
33
|
+
if (value.appliesTo !== undefined) {
|
|
34
|
+
if (
|
|
35
|
+
!Array.isArray(value.appliesTo) ||
|
|
36
|
+
value.appliesTo.length === 0 ||
|
|
37
|
+
value.appliesTo.some((sku) => !PRICING_SKUS.includes(sku as PricingSku))
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`${label}.appliesTo must contain only known pricing SKUs`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validatePricingPolicy(
|
|
45
|
+
value: unknown,
|
|
46
|
+
label = "Provider pricing policy",
|
|
47
|
+
): asserts value is ProviderPricingPolicy {
|
|
48
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
49
|
+
if (value.defaultAdjustment !== undefined)
|
|
50
|
+
validatePricingAdjustment(value.defaultAdjustment, `${label}.defaultAdjustment`);
|
|
51
|
+
if (value.models !== undefined) {
|
|
52
|
+
if (!isRecord(value.models)) throw new Error(`${label}.models must be an object`);
|
|
53
|
+
for (const [modelId, adjustment] of Object.entries(value.models)) {
|
|
54
|
+
if (modelId.trim() === "") throw new Error(`${label}.models has an empty model ID`);
|
|
55
|
+
validatePricingAdjustment(adjustment, `${label}.models.${modelId}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function scaleRate(rate: number, sku: PricingSku, adjustment: ProviderPricingAdjustment): number {
|
|
61
|
+
const appliesTo = adjustment.appliesTo ?? PRICING_SKUS;
|
|
62
|
+
return appliesTo.includes(sku) ? rate * adjustment.multiplier : rate;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cloneCost(cost: ProviderCost): ProviderCost {
|
|
66
|
+
return {
|
|
67
|
+
...cost,
|
|
68
|
+
...(cost.tiers ? { tiers: cost.tiers.map((tier) => ({ ...tier })) } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolvePricingDetails(
|
|
73
|
+
baseCost: ProviderCost | undefined,
|
|
74
|
+
source: ProviderPricingSource | "none",
|
|
75
|
+
adjustment?: ProviderPricingAdjustment,
|
|
76
|
+
): ModelPricingDetails {
|
|
77
|
+
if (baseCost === undefined) {
|
|
78
|
+
return {
|
|
79
|
+
known: false,
|
|
80
|
+
source,
|
|
81
|
+
...(adjustment ? { adjustment: { ...adjustment } } : {}),
|
|
82
|
+
...(adjustment ? { note: "discount configured, base price unavailable" } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const canAdjustReference = source !== "official" || adjustment?.appliesToReference !== false;
|
|
87
|
+
const effectiveCost =
|
|
88
|
+
adjustment && canAdjustReference ? applyPricingAdjustment(baseCost, adjustment) : cloneCost(baseCost);
|
|
89
|
+
return {
|
|
90
|
+
known: true,
|
|
91
|
+
source,
|
|
92
|
+
baseCost: cloneCost(baseCost),
|
|
93
|
+
effectiveCost,
|
|
94
|
+
...(adjustment ? { adjustment: { ...adjustment } } : {}),
|
|
95
|
+
...(adjustment && !canAdjustReference ? { note: "discount not applied to reference price" } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Apply one explicit static pricing adjustment without changing token thresholds. */
|
|
100
|
+
export function applyPricingAdjustment(cost: ProviderCost, adjustment: ProviderPricingAdjustment): ProviderCost {
|
|
101
|
+
return {
|
|
102
|
+
input: scaleRate(cost.input, "input", adjustment),
|
|
103
|
+
output: scaleRate(cost.output, "output", adjustment),
|
|
104
|
+
cacheRead: scaleRate(cost.cacheRead, "cacheRead", adjustment),
|
|
105
|
+
cacheWrite: scaleRate(cost.cacheWrite, "cacheWrite", adjustment),
|
|
106
|
+
...(cost.tiers
|
|
107
|
+
? {
|
|
108
|
+
tiers: cost.tiers.map((tier) => ({
|
|
109
|
+
inputTokensAbove: tier.inputTokensAbove,
|
|
110
|
+
input: scaleRate(tier.input, "input", adjustment),
|
|
111
|
+
output: scaleRate(tier.output, "output", adjustment),
|
|
112
|
+
cacheRead: scaleRate(tier.cacheRead, "cacheRead", adjustment),
|
|
113
|
+
cacheWrite: scaleRate(tier.cacheWrite, "cacheWrite", adjustment),
|
|
114
|
+
})),
|
|
115
|
+
}
|
|
116
|
+
: {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { applyOfficialModelCosts, findOfficialMeta, type OfficialModelMeta } from "./official-pricing.ts";
|
|
3
|
+
import { resolvePricingDetails } from "./pricing-adjustments.ts";
|
|
4
|
+
import type { ProviderKitDependencies } from "./runtime-config.ts";
|
|
5
|
+
import type {
|
|
6
|
+
ProviderAdapter,
|
|
7
|
+
ProviderCost,
|
|
8
|
+
ProviderModel,
|
|
9
|
+
ProviderModelDraft,
|
|
10
|
+
ProviderModelMetadata,
|
|
11
|
+
ProviderPricingAdjustment,
|
|
12
|
+
ProviderPricingSource,
|
|
13
|
+
ProviderRefreshContext,
|
|
14
|
+
} from "./types.ts";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
17
|
+
const DEFAULT_MAX_TOKENS = 16_384;
|
|
18
|
+
|
|
19
|
+
function finiteNonNegative(value: unknown): number {
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function positiveInteger(value: unknown, fallback: number): number {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value > 0
|
|
25
|
+
? value
|
|
26
|
+
: fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeCost(cost: ProviderModelDraft["cost"]): ProviderCost {
|
|
30
|
+
const candidate = cost as Partial<ProviderCost> | undefined;
|
|
31
|
+
const tiers = Array.isArray(candidate?.tiers)
|
|
32
|
+
? candidate.tiers
|
|
33
|
+
.filter(
|
|
34
|
+
(tier) =>
|
|
35
|
+
tier &&
|
|
36
|
+
typeof tier.inputTokensAbove === "number" &&
|
|
37
|
+
Number.isFinite(tier.inputTokensAbove) &&
|
|
38
|
+
tier.inputTokensAbove > 0,
|
|
39
|
+
)
|
|
40
|
+
.map((tier) => ({
|
|
41
|
+
inputTokensAbove: tier.inputTokensAbove,
|
|
42
|
+
input: finiteNonNegative(tier.input),
|
|
43
|
+
output: finiteNonNegative(tier.output),
|
|
44
|
+
cacheRead: finiteNonNegative(tier.cacheRead),
|
|
45
|
+
cacheWrite: finiteNonNegative(tier.cacheWrite),
|
|
46
|
+
}))
|
|
47
|
+
: undefined;
|
|
48
|
+
return {
|
|
49
|
+
input: finiteNonNegative(candidate?.input),
|
|
50
|
+
output: finiteNonNegative(candidate?.output),
|
|
51
|
+
cacheRead: finiteNonNegative(candidate?.cacheRead),
|
|
52
|
+
cacheWrite: finiteNonNegative(candidate?.cacheWrite),
|
|
53
|
+
...(tiers && tiers.length > 0 ? { tiers } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeInput(input: ProviderModelDraft["input"]): ("text" | "image")[] {
|
|
58
|
+
const normalized = Array.isArray(input)
|
|
59
|
+
? input.filter((value): value is "text" | "image" => value === "text" || value === "image")
|
|
60
|
+
: [];
|
|
61
|
+
return normalized.length > 0 ? [...new Set(normalized)] : ["text"];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeProviderModel(model: ProviderModelDraft): ProviderModel {
|
|
65
|
+
const { pricingSource: _pricingSource, pricingAdjustment: _pricingAdjustment, ...modelConfig } = model;
|
|
66
|
+
void _pricingSource;
|
|
67
|
+
void _pricingAdjustment;
|
|
68
|
+
if (typeof model.id !== "string") throw new Error("Provider model ID must be a string");
|
|
69
|
+
const id = model.id.trim();
|
|
70
|
+
if (id === "") throw new Error("Provider model ID must not be empty");
|
|
71
|
+
const contextWindow = positiveInteger(model.contextWindow, DEFAULT_CONTEXT_WINDOW);
|
|
72
|
+
const maxTokens = Math.min(positiveInteger(model.maxTokens, DEFAULT_MAX_TOKENS), contextWindow);
|
|
73
|
+
return {
|
|
74
|
+
...modelConfig,
|
|
75
|
+
id,
|
|
76
|
+
name: typeof model.name === "string" && model.name.trim() !== "" ? model.name.trim() : id,
|
|
77
|
+
reasoning: model.reasoning ?? false,
|
|
78
|
+
input: normalizeInput(model.input),
|
|
79
|
+
cost: normalizeCost(model.cost),
|
|
80
|
+
contextWindow,
|
|
81
|
+
maxTokens,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeProviderModels(models: ProviderModelDraft[]): ProviderModel[] {
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
return models.map((model) => {
|
|
88
|
+
const normalized = normalizeProviderModel(model);
|
|
89
|
+
if (seen.has(normalized.id)) throw new Error(`Duplicate model ID: ${normalized.id}`);
|
|
90
|
+
seen.add(normalized.id);
|
|
91
|
+
return normalized;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cloneQuality(quality: NonNullable<OfficialModelMeta["quality"]>): NonNullable<OfficialModelMeta["quality"]> {
|
|
96
|
+
return quality.map((score) => ({
|
|
97
|
+
...score,
|
|
98
|
+
...(score.confidenceInterval ? { confidenceInterval: { ...score.confidenceInterval } } : {}),
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function selectPricingAdjustment(
|
|
103
|
+
adapter: ProviderAdapter,
|
|
104
|
+
model: ProviderModelDraft,
|
|
105
|
+
policy = adapter.pricing,
|
|
106
|
+
): ProviderPricingAdjustment | undefined {
|
|
107
|
+
return model.pricingAdjustment ?? policy?.models?.[model.id.trim()] ?? policy?.defaultAdjustment;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveModelRegistration(
|
|
111
|
+
adapter: ProviderAdapter,
|
|
112
|
+
runtime: ProviderKitDependencies,
|
|
113
|
+
modelDrafts: ProviderModelDraft[],
|
|
114
|
+
officialPricing: Record<string, OfficialModelMeta>,
|
|
115
|
+
): { models: ProviderModel[]; modelMetadata: Record<string, ProviderModelMetadata> } {
|
|
116
|
+
const enrichedDrafts = applyOfficialModelCosts(modelDrafts, officialPricing);
|
|
117
|
+
const pricingPolicy = runtime.pricingPolicies?.[adapter.id] ?? adapter.pricing;
|
|
118
|
+
const metadata: Record<string, ProviderModelMetadata> = {};
|
|
119
|
+
const adjustedDrafts = enrichedDrafts.map((model, index) => {
|
|
120
|
+
const modelId = model.id.trim();
|
|
121
|
+
const originalDraft = modelDrafts[index];
|
|
122
|
+
const officialMeta = findOfficialMeta(modelId, officialPricing);
|
|
123
|
+
const fieldSources = {
|
|
124
|
+
contextWindow:
|
|
125
|
+
originalDraft?.contextWindow !== undefined
|
|
126
|
+
? ("provider" as const)
|
|
127
|
+
: officialMeta?.contextWindow !== undefined
|
|
128
|
+
? ("official" as const)
|
|
129
|
+
: ("default" as const),
|
|
130
|
+
maxTokens:
|
|
131
|
+
originalDraft?.maxTokens !== undefined
|
|
132
|
+
? ("provider" as const)
|
|
133
|
+
: officialMeta?.maxTokens !== undefined
|
|
134
|
+
? ("official" as const)
|
|
135
|
+
: ("default" as const),
|
|
136
|
+
input:
|
|
137
|
+
originalDraft?.input !== undefined
|
|
138
|
+
? ("provider" as const)
|
|
139
|
+
: officialMeta?.input !== undefined
|
|
140
|
+
? ("official" as const)
|
|
141
|
+
: ("default" as const),
|
|
142
|
+
reasoning:
|
|
143
|
+
originalDraft?.reasoning !== undefined
|
|
144
|
+
? ("provider" as const)
|
|
145
|
+
: officialMeta?.reasoning !== undefined
|
|
146
|
+
? ("official" as const)
|
|
147
|
+
: ("default" as const),
|
|
148
|
+
};
|
|
149
|
+
const source: ProviderPricingSource | "none" =
|
|
150
|
+
model.cost === undefined ? "none" : (model.pricingSource ?? "provider");
|
|
151
|
+
const pricing = resolvePricingDetails(model.cost, source, selectPricingAdjustment(adapter, model, pricingPolicy));
|
|
152
|
+
metadata[modelId] = {
|
|
153
|
+
pricing,
|
|
154
|
+
fieldSources,
|
|
155
|
+
...(officialMeta?.quality ? { quality: cloneQuality(officialMeta.quality) } : {}),
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
...model,
|
|
159
|
+
...(pricing.effectiveCost ? { cost: pricing.effectiveCost } : {}),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
return { models: normalizeProviderModels(adjustedDrafts), modelMetadata: metadata };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isAbortError(error: unknown): boolean {
|
|
166
|
+
return error !== null && typeof error === "object" && "name" in error && error.name === "AbortError";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getErrorCode(error: unknown): string {
|
|
170
|
+
if (isAbortError(error)) return "cancelled";
|
|
171
|
+
if (error !== null && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
172
|
+
return error.code;
|
|
173
|
+
}
|
|
174
|
+
if (error !== null && typeof error === "object" && "name" in error && error.name === "TimeoutError") {
|
|
175
|
+
return "timeout";
|
|
176
|
+
}
|
|
177
|
+
return "fetch";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Register a normalized Provider before the Host has assembled its final
|
|
182
|
+
* registry. The original drafts remain attached to the adapter so a Host in a
|
|
183
|
+
* different module context can apply official metadata later.
|
|
184
|
+
*/
|
|
185
|
+
export function prepareProviderRegistration(
|
|
186
|
+
adapter: ProviderAdapter,
|
|
187
|
+
runtime: ProviderKitDependencies,
|
|
188
|
+
officialPricing: Record<string, OfficialModelMeta> = {},
|
|
189
|
+
modelDrafts?: ProviderModelDraft[],
|
|
190
|
+
): ProviderConfig {
|
|
191
|
+
const drafts =
|
|
192
|
+
modelDrafts ??
|
|
193
|
+
(adapter.registration?.normalizedModels === adapter.provider.models
|
|
194
|
+
? adapter.registration.modelDrafts
|
|
195
|
+
: adapter.provider.models);
|
|
196
|
+
const resolved = resolveModelRegistration(adapter, runtime, drafts, officialPricing);
|
|
197
|
+
const models = resolved.models;
|
|
198
|
+
const adapterOwnsCatalog = adapter.catalog !== undefined;
|
|
199
|
+
const registration = { modelDrafts: drafts, normalizedModels: models, modelMetadata: resolved.modelMetadata };
|
|
200
|
+
adapter.registration = registration;
|
|
201
|
+
adapter.provider.models = models;
|
|
202
|
+
adapter.catalog ??= { source: "static", modelCount: models.length };
|
|
203
|
+
adapter.catalog.modelCount = models.length;
|
|
204
|
+
|
|
205
|
+
const { models: _draftModels, refreshModels: originalRefresh, ...providerMetadata } = adapter.provider;
|
|
206
|
+
const registeredProvider: ProviderConfig = { ...providerMetadata, models };
|
|
207
|
+
if (originalRefresh) {
|
|
208
|
+
registeredProvider.refreshModels = async (options: ProviderRefreshContext) => {
|
|
209
|
+
try {
|
|
210
|
+
const refreshedModels = await originalRefresh(options);
|
|
211
|
+
const resolved = resolveModelRegistration(adapter, runtime, refreshedModels, officialPricing);
|
|
212
|
+
const normalizedModels = resolved.models;
|
|
213
|
+
registration.modelDrafts = refreshedModels;
|
|
214
|
+
registration.normalizedModels = normalizedModels;
|
|
215
|
+
registration.modelMetadata = resolved.modelMetadata;
|
|
216
|
+
adapter.provider.models = normalizedModels;
|
|
217
|
+
registeredProvider.models = normalizedModels;
|
|
218
|
+
if (adapterOwnsCatalog && adapter.catalog) {
|
|
219
|
+
adapter.catalog.modelCount = normalizedModels.length;
|
|
220
|
+
} else {
|
|
221
|
+
adapter.catalog = {
|
|
222
|
+
...(adapter.catalog ?? { source: "live" }),
|
|
223
|
+
source: "live",
|
|
224
|
+
modelCount: normalizedModels.length,
|
|
225
|
+
updatedAt: runtime.now(),
|
|
226
|
+
lastError: undefined,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return normalizedModels;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (adapter.catalog && !isAbortError(error)) adapter.catalog.lastError = getErrorCode(error);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return registeredProvider;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function refreshProviderRegistrations(
|
|
240
|
+
pi: Pick<ExtensionAPI, "registerProvider">,
|
|
241
|
+
providers: readonly ProviderAdapter[],
|
|
242
|
+
runtime: ProviderKitDependencies,
|
|
243
|
+
officialPricing: Record<string, OfficialModelMeta>,
|
|
244
|
+
providerDrafts?: ReadonlyMap<ProviderAdapter, ProviderModelDraft[]>,
|
|
245
|
+
): void {
|
|
246
|
+
for (const adapter of providers) {
|
|
247
|
+
registerProviderAdapter(pi, adapter, runtime, officialPricing, providerDrafts?.get(adapter));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function registerProviderAdapter(
|
|
252
|
+
pi: Pick<ExtensionAPI, "registerProvider">,
|
|
253
|
+
adapter: ProviderAdapter,
|
|
254
|
+
runtime: ProviderKitDependencies,
|
|
255
|
+
officialPricing: Record<string, OfficialModelMeta> = {},
|
|
256
|
+
modelDrafts?: ProviderModelDraft[],
|
|
257
|
+
): ProviderConfig {
|
|
258
|
+
const registeredProvider = prepareProviderRegistration(adapter, runtime, officialPricing, modelDrafts);
|
|
259
|
+
pi.registerProvider(adapter.id, registeredProvider);
|
|
260
|
+
return registeredProvider;
|
|
261
|
+
}
|