@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CONTRIBUTING.md +63 -0
  3. package/LICENSE +21 -0
  4. package/README.md +61 -0
  5. package/README.zh-CN.md +61 -0
  6. package/SECURITY.md +36 -0
  7. package/SUPPORT.md +25 -0
  8. package/core/adapter-extensions.ts +175 -0
  9. package/core/adapter-protocol.ts +120 -0
  10. package/core/adapter-validation.ts +241 -0
  11. package/core/deadline.ts +78 -0
  12. package/core/definition.ts +64 -0
  13. package/core/errors.ts +38 -0
  14. package/core/extension.ts +20 -0
  15. package/core/host.ts +462 -0
  16. package/core/live-check-manager.ts +263 -0
  17. package/core/official-pricing.ts +881 -0
  18. package/core/opencode-preflight.ts +66 -0
  19. package/core/preflight-manager.ts +251 -0
  20. package/core/pricing-adjustments.ts +118 -0
  21. package/core/provider-registration.ts +261 -0
  22. package/core/retry-after.ts +24 -0
  23. package/core/runtime-config.ts +95 -0
  24. package/core/runtime.ts +473 -0
  25. package/core/status-manager.ts +332 -0
  26. package/core/status-report.ts +592 -0
  27. package/core/tuner-manager.ts +34 -0
  28. package/core/types.ts +175 -0
  29. package/index.ts +108 -0
  30. package/package.json +81 -0
  31. package/preflight/charm-hyper.ts +62 -0
  32. package/preflight/deepseek.ts +73 -0
  33. package/preflight/google.ts +89 -0
  34. package/preflight/openai-codex.ts +88 -0
  35. package/preflight/opencode-go.ts +27 -0
  36. package/preflight/opencode.ts +27 -0
  37. package/providers/charm-hyper/constants.ts +31 -0
  38. package/providers/charm-hyper/oauth.ts +360 -0
  39. package/providers/charm-hyper.ts +536 -0
  40. package/status/charm-hyper.ts +76 -0
  41. package/status/deepseek.ts +102 -0
  42. package/status/openai-codex.ts +224 -0
  43. package/status/opencode-go.ts +133 -0
@@ -0,0 +1,263 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { isValidTimeoutMs, withDeadline } from "./deadline.ts";
3
+ import { parseRetryAfter } from "./retry-after.ts";
4
+
5
+ type LiveCheckModel = NonNullable<ExtensionContext["model"]>;
6
+ type ModelRegistry = ExtensionContext["modelRegistry"];
7
+ type LiveCheckProvider = NonNullable<ReturnType<ModelRegistry["getProvider"]>>;
8
+ type LiveCheckContext = Parameters<LiveCheckProvider["streamSimple"]>[1];
9
+ type LiveCheckOptions = NonNullable<Parameters<LiveCheckProvider["streamSimple"]>[2]>;
10
+ type LiveCheckPayloadTransform = NonNullable<LiveCheckOptions["onPayload"]>;
11
+
12
+ export interface LiveCheckContextLike {
13
+ model: LiveCheckModel;
14
+ modelRegistry: Pick<ModelRegistry, "getApiKeyAndHeaders" | "getProvider">;
15
+ onPayload?: LiveCheckPayloadTransform;
16
+ }
17
+
18
+ export const LIVE_CHECK_SCOPE = "provider-stream" as const;
19
+
20
+ export interface LiveCheckSnapshot {
21
+ /** Scope of the built-in live check; optional for compatibility with older snapshots. */
22
+ scope?: typeof LIVE_CHECK_SCOPE;
23
+ provider: string;
24
+ model: string;
25
+ checkedAt: number;
26
+ latencyMs: number;
27
+ httpStatus?: number;
28
+ }
29
+
30
+ export interface LiveCheckErrorState {
31
+ code: string;
32
+ httpStatus?: number;
33
+ retryAt?: number;
34
+ }
35
+
36
+ export interface LiveCheckDiagnostics {
37
+ snapshot?: LiveCheckSnapshot;
38
+ pending: boolean;
39
+ lastError?: LiveCheckErrorState;
40
+ }
41
+
42
+ export type LiveCheckResult = "verified" | "failed" | "skipped";
43
+
44
+ interface PendingLiveCheck {
45
+ promise: Promise<LiveCheckSnapshot>;
46
+ generation: number;
47
+ cancel: () => void;
48
+ }
49
+
50
+ interface LiveCheckState {
51
+ snapshot?: LiveCheckSnapshot;
52
+ pending?: PendingLiveCheck;
53
+ lastError?: LiveCheckErrorState;
54
+ generation: number;
55
+ }
56
+
57
+ class LiveCheckFailure extends Error {
58
+ override readonly name = "LiveCheckFailure";
59
+
60
+ constructor(
61
+ readonly code: string,
62
+ message: string,
63
+ readonly httpStatus?: number,
64
+ readonly retryAt?: number,
65
+ ) {
66
+ super(message);
67
+ }
68
+ }
69
+
70
+ function isErrorNamed(error: unknown, name: string): boolean {
71
+ return error !== null && typeof error === "object" && "name" in error && error.name === name;
72
+ }
73
+
74
+ function throwIfAborted(signal: AbortSignal): void {
75
+ if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
76
+ }
77
+
78
+ function errorCode(error: unknown, httpStatus: number | undefined, retryAt: number | undefined): LiveCheckErrorState {
79
+ if (error instanceof LiveCheckFailure) {
80
+ const status = error.httpStatus ?? httpStatus;
81
+ return {
82
+ code: error.code,
83
+ ...(status !== undefined ? { httpStatus: status } : {}),
84
+ ...((error.retryAt ?? retryAt) ? { retryAt: error.retryAt ?? retryAt } : {}),
85
+ };
86
+ }
87
+ if (isErrorNamed(error, "TimeoutError")) {
88
+ return { code: "timeout", ...(httpStatus !== undefined ? { httpStatus } : {}), ...(retryAt ? { retryAt } : {}) };
89
+ }
90
+ if (isErrorNamed(error, "AbortError")) {
91
+ return {
92
+ code: "cancelled",
93
+ ...(httpStatus !== undefined ? { httpStatus } : {}),
94
+ ...(retryAt ? { retryAt } : {}),
95
+ };
96
+ }
97
+ if (error !== null && typeof error === "object" && "code" in error && typeof error.code === "string") {
98
+ return { code: error.code, ...(httpStatus !== undefined ? { httpStatus } : {}), ...(retryAt ? { retryAt } : {}) };
99
+ }
100
+ return { code: httpStatus !== undefined ? `http${httpStatus}` : "upstream", ...(retryAt ? { retryAt } : {}) };
101
+ }
102
+
103
+ function cloneSnapshot(snapshot: LiveCheckSnapshot): LiveCheckSnapshot {
104
+ return { ...snapshot };
105
+ }
106
+
107
+ export function getLiveCheckKey(provider: string, model: string): string {
108
+ return JSON.stringify([provider, model]);
109
+ }
110
+
111
+ export class LiveCheckManager {
112
+ private readonly states = new Map<string, LiveCheckState>();
113
+ private readonly requestTimeoutMs: number;
114
+ private readonly fetchFn: typeof globalThis.fetch;
115
+ private readonly now: () => number;
116
+
117
+ constructor(requestTimeoutMs: number, now: () => number);
118
+ constructor(requestTimeoutMs: number, fetchFn: typeof globalThis.fetch, now: () => number);
119
+ constructor(requestTimeoutMs: number, fetchOrNow: typeof globalThis.fetch | (() => number), now?: () => number) {
120
+ this.requestTimeoutMs = requestTimeoutMs;
121
+ if (now) {
122
+ this.fetchFn = fetchOrNow as typeof globalThis.fetch;
123
+ this.now = now;
124
+ } else {
125
+ this.fetchFn = globalThis.fetch;
126
+ this.now = fetchOrNow as () => number;
127
+ }
128
+ }
129
+
130
+ async check(ctx: LiveCheckContextLike): Promise<LiveCheckResult> {
131
+ const key = getLiveCheckKey(ctx.model.provider, ctx.model.id);
132
+ const state = this.getState(key);
133
+ if (!isValidTimeoutMs(this.requestTimeoutMs)) {
134
+ state.lastError = { code: "config" };
135
+ return "failed";
136
+ }
137
+ const retryAt = state.lastError?.retryAt;
138
+ if (retryAt !== undefined && Number.isFinite(retryAt) && this.now() < retryAt) return "skipped";
139
+
140
+ let pending = state.pending;
141
+ if (!pending) {
142
+ const cancellation = new AbortController();
143
+ const generation = ++state.generation;
144
+ const promise = withDeadline(
145
+ (signal) => this.execute(ctx, signal),
146
+ this.requestTimeoutMs,
147
+ cancellation.signal,
148
+ );
149
+ pending = {
150
+ promise,
151
+ generation,
152
+ cancel: () => cancellation.abort(),
153
+ };
154
+ state.pending = pending;
155
+ }
156
+
157
+ const activePending = pending;
158
+ const { generation } = activePending;
159
+ try {
160
+ const snapshot = await activePending.promise;
161
+ if (state.generation !== generation) return "skipped";
162
+ state.snapshot = snapshot;
163
+ state.lastError = undefined;
164
+ return "verified";
165
+ } catch (error) {
166
+ if (state.generation !== generation || isErrorNamed(error, "AbortError")) return "skipped";
167
+ state.lastError = errorCode(error, undefined, undefined);
168
+ return "failed";
169
+ } finally {
170
+ if (state.generation === generation && state.pending === activePending) state.pending = undefined;
171
+ }
172
+ }
173
+
174
+ cancelAll(): void {
175
+ for (const state of this.states.values()) {
176
+ state.generation++;
177
+ state.pending?.cancel();
178
+ state.pending = undefined;
179
+ }
180
+ }
181
+
182
+ clear(): void {
183
+ this.cancelAll();
184
+ this.states.clear();
185
+ }
186
+
187
+ getDiagnostics(provider: string, model: string): LiveCheckDiagnostics {
188
+ const state = this.states.get(getLiveCheckKey(provider, model));
189
+ return {
190
+ snapshot: state?.snapshot ? cloneSnapshot(state.snapshot) : undefined,
191
+ pending: state?.pending !== undefined,
192
+ lastError: state?.lastError ? { ...state.lastError } : undefined,
193
+ };
194
+ }
195
+
196
+ private async execute(ctx: LiveCheckContextLike, signal: AbortSignal): Promise<LiveCheckSnapshot> {
197
+ throwIfAborted(signal);
198
+ const startedAt = this.now();
199
+ const provider = ctx.modelRegistry.getProvider(ctx.model.provider);
200
+ if (!provider) throw new LiveCheckFailure("unsupported", "Provider is not available in Pi");
201
+
202
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
203
+ throwIfAborted(signal);
204
+ if (!auth.ok) throw new LiveCheckFailure("auth", auth.error);
205
+
206
+ let httpStatus: number | undefined;
207
+ let retryAt: number | undefined;
208
+ const context: LiveCheckContext = {
209
+ systemPrompt: "",
210
+ messages: [{ role: "user", content: "Reply with OK.", timestamp: this.now() }],
211
+ };
212
+ const options: LiveCheckOptions = {
213
+ signal,
214
+ maxTokens: 1,
215
+ maxRetries: 0,
216
+ reasoning: undefined,
217
+ fetch: this.fetchFn,
218
+ ...(auth.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
219
+ ...(auth.headers !== undefined ? { headers: auth.headers } : {}),
220
+ ...(auth.env !== undefined ? { env: auth.env } : {}),
221
+ ...(ctx.onPayload !== undefined ? { onPayload: ctx.onPayload } : {}),
222
+ onResponse: (response) => {
223
+ httpStatus = response.status;
224
+ const retryAfter = Object.entries(response.headers).find(
225
+ ([name]) => name.toLowerCase() === "retry-after",
226
+ )?.[1];
227
+ retryAt = parseRetryAfter(retryAfter ?? null, this.now());
228
+ },
229
+ };
230
+ throwIfAborted(signal);
231
+ const stream = provider.streamSimple(ctx.model, context, options);
232
+ let completed = false;
233
+ for await (const event of stream) {
234
+ if (event.type === "error") {
235
+ throw new LiveCheckFailure(
236
+ "upstream",
237
+ event.error.errorMessage ?? "Live check stream failed",
238
+ httpStatus,
239
+ retryAt,
240
+ );
241
+ }
242
+ if (event.type === "done") completed = true;
243
+ }
244
+ if (!completed) throw new LiveCheckFailure("parse", "Live check stream ended without a result", httpStatus);
245
+
246
+ return {
247
+ scope: LIVE_CHECK_SCOPE,
248
+ provider: ctx.model.provider,
249
+ model: ctx.model.id,
250
+ checkedAt: this.now(),
251
+ latencyMs: Math.max(0, this.now() - startedAt),
252
+ ...(httpStatus !== undefined ? { httpStatus } : {}),
253
+ };
254
+ }
255
+
256
+ private getState(key: string): LiveCheckState {
257
+ const existing = this.states.get(key);
258
+ if (existing) return existing;
259
+ const state: LiveCheckState = { generation: 0 };
260
+ this.states.set(key, state);
261
+ return state;
262
+ }
263
+ }