@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,102 @@
1
+ import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
+ import { ProviderDataError } from "../core/errors.ts";
3
+ import { parseRetryAfter } from "../core/retry-after.ts";
4
+ import type { StatusAdapter, StatusSnapshot } from "../core/types.ts";
5
+
6
+ export const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
7
+
8
+ interface DeepSeekBalanceInfo {
9
+ currency: string;
10
+ totalBalance: number;
11
+ }
12
+
13
+ function isRecord(value: unknown): value is Record<string, unknown> {
14
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+
17
+ function parseFiniteAmount(value: unknown): number | undefined {
18
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
19
+ if (typeof value !== "string" || value.trim() === "") return undefined;
20
+ const parsed = Number(value);
21
+ return Number.isFinite(parsed) ? parsed : undefined;
22
+ }
23
+
24
+ export function parseDeepSeekBalance(payload: unknown): DeepSeekBalanceInfo[] {
25
+ if (!isRecord(payload) || !Array.isArray(payload.balance_infos)) {
26
+ throw new ProviderDataError("DeepSeek status returned an invalid balance response", "badjson");
27
+ }
28
+ return payload.balance_infos.map((value) => {
29
+ if (!isRecord(value) || typeof value.currency !== "string" || value.currency.trim() === "") {
30
+ throw new ProviderDataError("DeepSeek status returned an invalid balance response", "badjson");
31
+ }
32
+ const totalBalance = parseFiniteAmount(value.total_balance);
33
+ if (totalBalance === undefined || /[\u0000-\u001f\u007f]/.test(value.currency)) {
34
+ throw new ProviderDataError("DeepSeek status returned an invalid balance response", "badjson");
35
+ }
36
+ return { currency: value.currency.trim(), totalBalance };
37
+ });
38
+ }
39
+
40
+ export const deepSeekStatusAdapter: StatusAdapter = {
41
+ id: "deepseek-status",
42
+ providerId: "deepseek",
43
+ name: "DeepSeek",
44
+ cacheTtlMs: 30_000,
45
+ requestTimeoutMs: 8_000,
46
+ async fetch(context): Promise<StatusSnapshot> {
47
+ const key = await context.getApiKey();
48
+ if (!key || key === "proxy-managed") {
49
+ throw new ProviderDataError("DeepSeek status requires an API key", "auth");
50
+ }
51
+ const response = await context.fetch(DEEPSEEK_BALANCE_URL, {
52
+ headers: {
53
+ Accept: "application/json",
54
+ "Accept-Encoding": "identity",
55
+ Authorization: `Bearer ${key}`,
56
+ },
57
+ signal: context.signal,
58
+ });
59
+ if (!response.ok) {
60
+ throw new ProviderDataError(
61
+ `DeepSeek status failed: HTTP ${response.status}`,
62
+ `http${response.status}`,
63
+ parseRetryAfter(response.headers.get("retry-after"), context.now()),
64
+ response.status,
65
+ );
66
+ }
67
+ let payload: unknown;
68
+ try {
69
+ payload = await response.json();
70
+ } catch {
71
+ throw new ProviderDataError("DeepSeek status returned invalid JSON", "badjson");
72
+ }
73
+ const balances = parseDeepSeekBalance(payload);
74
+ const balance = balances.find(({ currency }) => currency === "USD") ?? balances[0];
75
+ return {
76
+ entries: balance
77
+ ? [
78
+ {
79
+ kind: "amount",
80
+ id: "balance",
81
+ label: "Balance",
82
+ value: balance.totalBalance,
83
+ unit: balance.currency,
84
+ },
85
+ ]
86
+ : [{ kind: "text", id: "balance", label: "Balance", value: "not available" }],
87
+ updatedAt: context.now(),
88
+ };
89
+ },
90
+ };
91
+
92
+ export function createDeepSeekStatusAdapter(requestTimeoutMs: number): StatusAdapter {
93
+ return { ...deepSeekStatusAdapter, requestTimeoutMs };
94
+ }
95
+
96
+ const deepSeekStatusExtension = defineStatusExtension({
97
+ id: "deepseek-status",
98
+ providerId: "deepseek",
99
+ create: ({ statusRequestTimeoutMs }) => createDeepSeekStatusAdapter(statusRequestTimeoutMs),
100
+ });
101
+
102
+ export default deepSeekStatusExtension;
@@ -0,0 +1,224 @@
1
+ import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
+ import { ProviderDataError } from "../core/errors.ts";
3
+ import { parseRetryAfter } from "../core/retry-after.ts";
4
+ import type { StatusAdapter, StatusEntry, StatusSnapshot } from "../core/types.ts";
5
+
6
+ export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
7
+ const ACCOUNT_ID_CLAIM = "https://api.openai.com/auth";
8
+
9
+ interface CodexUsageWindow {
10
+ usedPercent: number;
11
+ windowSeconds: number;
12
+ resetAt: number;
13
+ }
14
+
15
+ interface CodexUsagePayload {
16
+ planType?: unknown;
17
+ primaryWindow?: CodexUsageWindow;
18
+ secondaryWindow?: CodexUsageWindow;
19
+ }
20
+
21
+ function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return value !== null && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+
25
+ function finiteNumber(value: unknown): number | undefined {
26
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
27
+ }
28
+
29
+ function safePlanLabel(value: unknown): string {
30
+ if (typeof value !== "string" || value.trim() === "" || /[\u0000-\u001f\u007f]/.test(value)) return "Unknown";
31
+ const raw = value.trim().toLowerCase();
32
+ const known: Record<string, string> = {
33
+ free: "Free",
34
+ go: "Go",
35
+ plus: "Plus",
36
+ pro: "Pro",
37
+ prolite: "Pro Lite",
38
+ free_workspace: "Free Workspace",
39
+ team: "Business",
40
+ self_serve_business_prolite: "Business",
41
+ self_serve_business_usage_based: "Business",
42
+ business: "Enterprise",
43
+ ent26: "Enterprise",
44
+ enterprise_cbp_automation: "Enterprise (Automation)",
45
+ enterprise_cbp_usage_based: "Enterprise",
46
+ education: "Education",
47
+ quorum: "Quorum",
48
+ k12: "K-12",
49
+ enterprise: "Enterprise",
50
+ edu: "Edu",
51
+ guest: "Guest",
52
+ unknown: "Unknown",
53
+ };
54
+ if (known[raw]) return known[raw];
55
+ return (
56
+ raw
57
+ .split(/[_-]+/)
58
+ .filter(Boolean)
59
+ .map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
60
+ .join(" ") || "Unknown"
61
+ );
62
+ }
63
+
64
+ function parseWindow(value: unknown): CodexUsageWindow | undefined {
65
+ if (!isRecord(value)) return undefined;
66
+ const usedPercent = finiteNumber(value.used_percent);
67
+ const windowSeconds = finiteNumber(value.limit_window_seconds);
68
+ const resetAt = finiteNumber(value.reset_at);
69
+ if (
70
+ usedPercent === undefined ||
71
+ windowSeconds === undefined ||
72
+ resetAt === undefined ||
73
+ usedPercent < 0 ||
74
+ usedPercent > 100 ||
75
+ windowSeconds <= 0 ||
76
+ resetAt < 0
77
+ ) {
78
+ return undefined;
79
+ }
80
+ return { usedPercent, windowSeconds, resetAt };
81
+ }
82
+
83
+ function isApproximate(value: number, expected: number): boolean {
84
+ return value >= expected * 0.95 && value <= expected * 1.05;
85
+ }
86
+
87
+ interface CodexWindowDisplay {
88
+ id: string;
89
+ label: string;
90
+ }
91
+
92
+ function describeWindow(seconds: number, secondary: boolean): CodexWindowDisplay {
93
+ const minutes = seconds / 60;
94
+ if (isApproximate(minutes, 5 * 60)) return { id: "primary-window", label: "5h" };
95
+ if (isApproximate(minutes, 24 * 60)) return { id: "daily-window", label: "Daily" };
96
+ if (isApproximate(minutes, 7 * 24 * 60)) return { id: "weekly-window", label: "Weekly" };
97
+ if (isApproximate(minutes, 30 * 24 * 60)) return { id: "monthly-window", label: "Monthly" };
98
+ if (isApproximate(minutes, 365 * 24 * 60)) return { id: "annual-window", label: "Annual" };
99
+ return secondary ? { id: "secondary-window", label: "Secondary usage" } : { id: "primary-window", label: "Usage" };
100
+ }
101
+
102
+ export function parseCodexUsage(payload: unknown): CodexUsagePayload {
103
+ if (!isRecord(payload)) {
104
+ throw new ProviderDataError("OpenAI Codex status returned an invalid response", "badjson");
105
+ }
106
+ const rateLimit = isRecord(payload.rate_limit) ? payload.rate_limit : undefined;
107
+ return {
108
+ planType: payload.plan_type,
109
+ primaryWindow: parseWindow(rateLimit?.primary_window),
110
+ secondaryWindow: parseWindow(rateLimit?.secondary_window),
111
+ };
112
+ }
113
+
114
+ function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
115
+ try {
116
+ const segment = token.split(".")[1];
117
+ if (!segment) return undefined;
118
+ const base64 = segment
119
+ .replace(/-/g, "+")
120
+ .replace(/_/g, "/")
121
+ .padEnd(Math.ceil(segment.length / 4) * 4, "=");
122
+ const binary = atob(base64);
123
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
124
+ const payload = JSON.parse(new TextDecoder().decode(bytes));
125
+ return isRecord(payload) ? payload : undefined;
126
+ } catch {
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ export function extractCodexAccountId(token: string): string | undefined {
132
+ const payload = decodeJwtPayload(token);
133
+ const auth = payload?.[ACCOUNT_ID_CLAIM];
134
+ if (!isRecord(auth) || typeof auth.chatgpt_account_id !== "string" || auth.chatgpt_account_id.trim() === "") {
135
+ return undefined;
136
+ }
137
+ return auth.chatgpt_account_id.trim();
138
+ }
139
+
140
+ function statusEntries(payload: CodexUsagePayload): StatusEntry[] {
141
+ const entries: StatusEntry[] = [{ kind: "text", id: "plan", label: "Plan", value: safePlanLabel(payload.planType) }];
142
+ const usedIds = new Set(entries.map(({ id }) => id));
143
+ for (const [window, secondary] of [
144
+ [payload.primaryWindow, false],
145
+ [payload.secondaryWindow, true],
146
+ ] as const) {
147
+ if (!window) continue;
148
+ const display = describeWindow(window.windowSeconds, secondary);
149
+ let id = display.id;
150
+ if (usedIds.has(id)) {
151
+ const role = secondary ? "secondary" : "primary";
152
+ id = `${id}-${role}`;
153
+ }
154
+ usedIds.add(id);
155
+ entries.push({
156
+ kind: "window",
157
+ id,
158
+ label: display.label,
159
+ remainingPercent: Math.max(0, Math.min(100, 100 - window.usedPercent)),
160
+ resetAt: window.resetAt * 1_000,
161
+ });
162
+ }
163
+ if (entries.length === 1) {
164
+ entries.push({ kind: "text", id: "limits", label: "Limits", value: "not available" });
165
+ }
166
+ return entries;
167
+ }
168
+
169
+ export const openAICodexStatusAdapter: StatusAdapter = {
170
+ id: "openai-codex-status",
171
+ providerId: "openai-codex",
172
+ name: "OpenAI Codex",
173
+ cacheTtlMs: 60_000,
174
+ requestTimeoutMs: 8_000,
175
+ async fetch(context): Promise<StatusSnapshot> {
176
+ const key = await context.getApiKey();
177
+ if (!key || key === "proxy-managed") {
178
+ throw new ProviderDataError("OpenAI Codex status requires ChatGPT OAuth", "auth");
179
+ }
180
+ const accountId = extractCodexAccountId(key);
181
+ if (!accountId) {
182
+ throw new ProviderDataError("OpenAI Codex OAuth token has no account ID", "auth");
183
+ }
184
+ const response = await context.fetch(CODEX_USAGE_URL, {
185
+ headers: {
186
+ Accept: "application/json",
187
+ "Accept-Encoding": "identity",
188
+ Authorization: `Bearer ${key}`,
189
+ "chatgpt-account-id": accountId,
190
+ originator: "pi",
191
+ "User-Agent": "@hyav/pi-provider",
192
+ },
193
+ signal: context.signal,
194
+ });
195
+ if (!response.ok) {
196
+ throw new ProviderDataError(
197
+ `OpenAI Codex status failed: HTTP ${response.status}`,
198
+ `http${response.status}`,
199
+ parseRetryAfter(response.headers.get("retry-after"), context.now()),
200
+ response.status,
201
+ );
202
+ }
203
+ let body: unknown;
204
+ try {
205
+ body = await response.json();
206
+ } catch {
207
+ throw new ProviderDataError("OpenAI Codex status returned invalid JSON", "badjson");
208
+ }
209
+ const payload = parseCodexUsage(body);
210
+ return { entries: statusEntries(payload), updatedAt: context.now() };
211
+ },
212
+ };
213
+
214
+ export function createOpenAICodexStatusAdapter(requestTimeoutMs: number): StatusAdapter {
215
+ return { ...openAICodexStatusAdapter, requestTimeoutMs };
216
+ }
217
+
218
+ const openAICodexStatusExtension = defineStatusExtension({
219
+ id: "openai-codex-status",
220
+ providerId: "openai-codex",
221
+ create: ({ statusRequestTimeoutMs }) => createOpenAICodexStatusAdapter(statusRequestTimeoutMs),
222
+ });
223
+
224
+ export default openAICodexStatusExtension;
@@ -0,0 +1,133 @@
1
+ import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
+ import { ProviderDataError } from "../core/errors.ts";
3
+ import { parseRetryAfter } from "../core/retry-after.ts";
4
+ import type { StatusAdapter, StatusEntry, StatusSnapshot } from "../core/types.ts";
5
+
6
+ export const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
7
+
8
+ export interface OpenCodeGoUsageWindow {
9
+ status: "ok" | "rate-limited";
10
+ resetInSec: number;
11
+ usagePercent: number;
12
+ }
13
+
14
+ export interface OpenCodeGoUsagePayload {
15
+ useBalance: boolean;
16
+ rollingUsage: OpenCodeGoUsageWindow;
17
+ weeklyUsage: OpenCodeGoUsageWindow;
18
+ monthlyUsage: OpenCodeGoUsageWindow;
19
+ }
20
+
21
+ function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return value !== null && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+
25
+ function parseUsageWindow(value: unknown): OpenCodeGoUsageWindow {
26
+ if (!isRecord(value)) {
27
+ throw new ProviderDataError("OpenCode Go status returned an invalid usage window", "badjson");
28
+ }
29
+ const status = value.status;
30
+ const resetInSec = value.resetInSec;
31
+ const usagePercent = value.usagePercent;
32
+ if (
33
+ (status !== "ok" && status !== "rate-limited") ||
34
+ typeof resetInSec !== "number" ||
35
+ !Number.isInteger(resetInSec) ||
36
+ resetInSec < 0 ||
37
+ typeof usagePercent !== "number" ||
38
+ !Number.isInteger(usagePercent) ||
39
+ usagePercent < 0 ||
40
+ usagePercent > 100
41
+ ) {
42
+ throw new ProviderDataError("OpenCode Go status returned an invalid usage window", "badjson");
43
+ }
44
+ return { status, resetInSec, usagePercent };
45
+ }
46
+
47
+ export function parseOpenCodeGoUsage(value: unknown): OpenCodeGoUsagePayload {
48
+ if (!isRecord(value) || typeof value.useBalance !== "boolean") {
49
+ throw new ProviderDataError("OpenCode Go status returned an invalid usage response", "badjson");
50
+ }
51
+ return {
52
+ useBalance: value.useBalance,
53
+ rollingUsage: parseUsageWindow(value.rollingUsage),
54
+ weeklyUsage: parseUsageWindow(value.weeklyUsage),
55
+ monthlyUsage: parseUsageWindow(value.monthlyUsage),
56
+ };
57
+ }
58
+
59
+ function usageEntry(id: string, label: string, usage: OpenCodeGoUsageWindow, now: number): StatusEntry {
60
+ return {
61
+ kind: "window",
62
+ id,
63
+ label,
64
+ remainingPercent: usage.status === "rate-limited" ? 0 : 100 - usage.usagePercent,
65
+ resetAt: now + usage.resetInSec * 1_000,
66
+ };
67
+ }
68
+
69
+ export const openCodeGoStatusAdapter: StatusAdapter = {
70
+ id: "opencode-go-status",
71
+ providerId: "opencode-go",
72
+ name: "OpenCode Go",
73
+ cacheTtlMs: 60_000,
74
+ requestTimeoutMs: 8_000,
75
+ async fetch(context): Promise<StatusSnapshot> {
76
+ const key = await context.getApiKey();
77
+ if (!key || key === "proxy-managed") {
78
+ throw new ProviderDataError("OpenCode Go status requires an API key", "auth");
79
+ }
80
+ const response = await context.fetch(OPENCODE_GO_USAGE_URL, {
81
+ headers: {
82
+ Accept: "application/json",
83
+ "Accept-Encoding": "identity",
84
+ Authorization: `Bearer ${key}`,
85
+ "User-Agent": "@hyav/pi-provider",
86
+ },
87
+ signal: context.signal,
88
+ });
89
+ if (!response.ok) {
90
+ throw new ProviderDataError(
91
+ `OpenCode Go status failed: HTTP ${response.status}`,
92
+ `http${response.status}`,
93
+ parseRetryAfter(response.headers.get("retry-after"), context.now()),
94
+ response.status,
95
+ );
96
+ }
97
+ let body: unknown;
98
+ try {
99
+ body = await response.json();
100
+ } catch {
101
+ throw new ProviderDataError("OpenCode Go status returned invalid JSON", "badjson");
102
+ }
103
+ const payload = parseOpenCodeGoUsage(body);
104
+ const now = context.now();
105
+ return {
106
+ entries: [
107
+ { kind: "text", id: "plan", label: "Plan", value: "Go" },
108
+ usageEntry("rolling-window", "5h", payload.rollingUsage, now),
109
+ usageEntry("weekly-window", "Weekly", payload.weeklyUsage, now),
110
+ usageEntry("monthly-window", "Monthly", payload.monthlyUsage, now),
111
+ {
112
+ kind: "text",
113
+ id: "zen-balance-fallback",
114
+ label: "Zen balance fallback",
115
+ value: payload.useBalance ? "enabled" : "disabled",
116
+ },
117
+ ],
118
+ updatedAt: now,
119
+ };
120
+ },
121
+ };
122
+
123
+ export function createOpenCodeGoStatusAdapter(requestTimeoutMs: number): StatusAdapter {
124
+ return { ...openCodeGoStatusAdapter, requestTimeoutMs };
125
+ }
126
+
127
+ const openCodeGoStatusExtension = defineStatusExtension({
128
+ id: "opencode-go-status",
129
+ providerId: "opencode-go",
130
+ create: ({ statusRequestTimeoutMs }) => createOpenCodeGoStatusAdapter(statusRequestTimeoutMs),
131
+ });
132
+
133
+ export default openCodeGoStatusExtension;