@hyav/pi-provider 0.1.3 → 0.1.5
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 +24 -0
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/core/adapter-validation.ts +23 -6
- package/core/catalog-preflight.ts +142 -0
- package/core/credential-type.ts +13 -0
- package/core/diagnostic-auth.ts +103 -0
- package/core/host.ts +26 -4
- package/core/live-check-manager.ts +2 -1
- package/core/official-pricing.ts +39 -17
- package/core/preflight-manager.ts +40 -8
- package/core/provider-registration.ts +105 -6
- package/core/public-adapters.ts +10 -0
- package/core/ratelimit-headers.ts +72 -0
- package/core/runtime-config.ts +3 -0
- package/core/runtime.ts +55 -34
- package/core/status-manager.ts +34 -9
- package/core/types.ts +22 -2
- package/index.ts +12 -1
- package/package.json +1 -1
- package/preflight/anthropic.ts +42 -0
- package/preflight/cerebras.ts +27 -0
- package/preflight/charm-hyper.ts +2 -1
- package/preflight/deepseek.ts +2 -1
- package/preflight/github-copilot.ts +87 -0
- package/preflight/google.ts +2 -1
- package/preflight/groq.ts +73 -0
- package/preflight/huggingface.ts +27 -0
- package/preflight/mistral.ts +27 -0
- package/preflight/moonshotai-cn.ts +27 -0
- package/preflight/moonshotai.ts +37 -0
- package/preflight/nvidia.ts +27 -0
- package/preflight/openai-codex.ts +2 -1
- package/preflight/openai.ts +27 -0
- package/preflight/openrouter.ts +112 -0
- package/preflight/vercel-ai-gateway.ts +81 -0
- package/preflight/xai.ts +71 -0
- package/providers/charm-hyper.ts +4 -1
- package/status/anthropic.ts +262 -0
- package/status/charm-hyper.ts +3 -2
- package/status/deepseek.ts +2 -1
- package/status/github-copilot.ts +189 -0
- package/status/groq.ts +89 -0
- package/status/huggingface.ts +95 -0
- package/status/moonshotai-cn.ts +26 -0
- package/status/moonshotai.ts +151 -0
- package/status/openai-codex.ts +2 -1
- package/status/opencode-go.ts +2 -1
- package/status/openrouter.ts +173 -0
- package/status/vercel-ai-gateway/constants.ts +3 -0
- package/status/vercel-ai-gateway.ts +95 -0
- package/status/xai.ts +74 -0
package/status/groq.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
import { parseRateLimitWindows } from "../core/ratelimit-headers.ts";
|
|
4
|
+
|
|
5
|
+
export const GROQ_MODELS_URL = "https://api.groq.com/openai/v1/models";
|
|
6
|
+
|
|
7
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function windowEntry(
|
|
12
|
+
id: string,
|
|
13
|
+
label: string,
|
|
14
|
+
window: { limit: number; remaining: number; resetAt?: number },
|
|
15
|
+
): StatusEntry {
|
|
16
|
+
const percent = (window.remaining / window.limit) * 100;
|
|
17
|
+
return {
|
|
18
|
+
kind: "window",
|
|
19
|
+
id,
|
|
20
|
+
label,
|
|
21
|
+
remainingPercent: Math.max(0, Math.min(100, percent)),
|
|
22
|
+
...(window.resetAt !== undefined ? { resetAt: window.resetAt } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const groqStatusAdapter: StatusAdapter = {
|
|
27
|
+
id: "groq-status",
|
|
28
|
+
providerId: "groq",
|
|
29
|
+
name: "Groq",
|
|
30
|
+
cacheTtlMs: 60_000,
|
|
31
|
+
requestTimeoutMs: 8_000,
|
|
32
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, GROQ_MODELS_URL),
|
|
33
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
34
|
+
const key = await context.getApiKey();
|
|
35
|
+
if (!key || key === "proxy-managed") {
|
|
36
|
+
throw new ProviderDataError("Groq status requires an API key", "auth");
|
|
37
|
+
}
|
|
38
|
+
const response = await context.fetch(GROQ_MODELS_URL, {
|
|
39
|
+
headers: {
|
|
40
|
+
Accept: "application/json",
|
|
41
|
+
"Accept-Encoding": "identity",
|
|
42
|
+
Authorization: `Bearer ${key}`,
|
|
43
|
+
"User-Agent": "@hyav/pi-provider",
|
|
44
|
+
},
|
|
45
|
+
signal: context.signal,
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new ProviderDataError(
|
|
49
|
+
`Groq status failed: HTTP ${response.status}`,
|
|
50
|
+
`http${response.status}`,
|
|
51
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
52
|
+
response.status,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
let modelCount: string | undefined;
|
|
56
|
+
try {
|
|
57
|
+
const payload: unknown = await response.json();
|
|
58
|
+
modelCount = isRecord(payload) && Array.isArray(payload.data) ? `${payload.data.length} available` : undefined;
|
|
59
|
+
} catch {
|
|
60
|
+
modelCount = undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const now = context.now();
|
|
64
|
+
const { requests, tokens } = parseRateLimitWindows(response.headers, now);
|
|
65
|
+
const entries: StatusEntry[] = [
|
|
66
|
+
modelCount === undefined
|
|
67
|
+
? { kind: "text", id: "models", label: "Models", value: "unavailable" }
|
|
68
|
+
: { kind: "text", id: "models", label: "Models", value: modelCount },
|
|
69
|
+
];
|
|
70
|
+
if (requests) entries.push(windowEntry("requests-per-day", "Requests per day", requests));
|
|
71
|
+
if (tokens) entries.push(windowEntry("tokens-per-minute", "Tokens per minute", tokens));
|
|
72
|
+
if (entries.length === 1) {
|
|
73
|
+
entries.push({ kind: "text", id: "limits", label: "Limits", value: "not available" });
|
|
74
|
+
}
|
|
75
|
+
return { entries, updatedAt: now };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function createGroqStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
80
|
+
return { ...groqStatusAdapter, requestTimeoutMs };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const groqStatusExtension = defineStatusExtension({
|
|
84
|
+
id: "groq-status",
|
|
85
|
+
providerId: "groq",
|
|
86
|
+
create: ({ statusRequestTimeoutMs }) => createGroqStatusAdapter(statusRequestTimeoutMs),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export default groqStatusExtension;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
|
|
4
|
+
export const HF_WHOAMI_URL = "https://huggingface.co/api/whoami-v2";
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
11
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MAX_LABEL_LENGTH = 64;
|
|
15
|
+
|
|
16
|
+
function safeText(value: unknown): string | undefined {
|
|
17
|
+
if (typeof value !== "string") return undefined;
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
if (trimmed === "" || trimmed.length > MAX_LABEL_LENGTH || /[\u0000-\u001f\u007f]/.test(trimmed)) return undefined;
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HuggingFaceAccount {
|
|
24
|
+
plan?: string;
|
|
25
|
+
credits?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseHuggingFaceAccount(payload: unknown): HuggingFaceAccount {
|
|
29
|
+
if (!isRecord(payload)) {
|
|
30
|
+
throw new ProviderDataError("Hugging Face status returned an invalid account response", "badjson");
|
|
31
|
+
}
|
|
32
|
+
// Response envelope: { type, id, name, emailVerified, canPay, isPro, plan, periodEnd, credits, ... }
|
|
33
|
+
const plan = safeText(payload.plan);
|
|
34
|
+
// Older token generations omit the envelope fields entirely.
|
|
35
|
+
return {
|
|
36
|
+
...(plan !== undefined ? { plan } : {}),
|
|
37
|
+
...(finiteNumber(payload.credits) !== undefined ? { credits: finiteNumber(payload.credits) } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const huggingFaceStatusAdapter: StatusAdapter = {
|
|
42
|
+
id: "huggingface-status",
|
|
43
|
+
providerId: "huggingface",
|
|
44
|
+
name: "Hugging Face",
|
|
45
|
+
cacheTtlMs: 60_000,
|
|
46
|
+
requestTimeoutMs: 8_000,
|
|
47
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, "https://router.huggingface.co"),
|
|
48
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
49
|
+
const key = await context.getApiKey();
|
|
50
|
+
if (!key || key === "proxy-managed") {
|
|
51
|
+
throw new ProviderDataError("Hugging Face status requires a token", "auth");
|
|
52
|
+
}
|
|
53
|
+
const response = await context.fetch(HF_WHOAMI_URL, {
|
|
54
|
+
headers: {
|
|
55
|
+
Accept: "application/json",
|
|
56
|
+
"Accept-Encoding": "identity",
|
|
57
|
+
Authorization: `Bearer ${key}`,
|
|
58
|
+
"User-Agent": "@hyav/pi-provider",
|
|
59
|
+
},
|
|
60
|
+
signal: context.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new ProviderDataError(
|
|
64
|
+
`Hugging Face status failed: HTTP ${response.status}`,
|
|
65
|
+
`http${response.status}`,
|
|
66
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
67
|
+
response.status,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
let payload: unknown;
|
|
71
|
+
try {
|
|
72
|
+
payload = await response.json();
|
|
73
|
+
} catch {
|
|
74
|
+
throw new ProviderDataError("Hugging Face status returned invalid JSON", "badjson");
|
|
75
|
+
}
|
|
76
|
+
const account = parseHuggingFaceAccount(payload);
|
|
77
|
+
const entries: StatusEntry[] = [{ kind: "text", id: "plan", label: "Plan", value: account.plan ?? "Unknown" }];
|
|
78
|
+
if (account.credits !== undefined) {
|
|
79
|
+
entries.push({ kind: "amount", id: "credits", label: "Credits", value: account.credits, unit: "USD" });
|
|
80
|
+
}
|
|
81
|
+
return { entries, updatedAt: context.now() };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export function createHuggingFaceStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
86
|
+
return { ...huggingFaceStatusAdapter, requestTimeoutMs };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const huggingFaceStatusExtension = defineStatusExtension({
|
|
90
|
+
id: "huggingface-status",
|
|
91
|
+
providerId: "huggingface",
|
|
92
|
+
create: ({ statusRequestTimeoutMs }) => createHuggingFaceStatusAdapter(statusRequestTimeoutMs),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export default huggingFaceStatusExtension;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StatusAdapter } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension } from "@hyav/pi-provider";
|
|
3
|
+
import { createMoonshotStatusAdapter, MOONSHOT_CN_BALANCE_URL } from "./moonshotai.ts";
|
|
4
|
+
|
|
5
|
+
export const moonshotaiCnStatusAdapter: StatusAdapter = createMoonshotStatusAdapter(
|
|
6
|
+
{
|
|
7
|
+
id: "moonshotai-cn-status",
|
|
8
|
+
providerId: "moonshotai-cn",
|
|
9
|
+
name: "Moonshot CN (Kimi)",
|
|
10
|
+
balanceUrl: MOONSHOT_CN_BALANCE_URL,
|
|
11
|
+
unit: "CNY",
|
|
12
|
+
},
|
|
13
|
+
8_000,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export function createMoonshotaiCnStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
17
|
+
return { ...moonshotaiCnStatusAdapter, requestTimeoutMs };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const moonshotaiCnStatusExtension = defineStatusExtension({
|
|
21
|
+
id: "moonshotai-cn-status",
|
|
22
|
+
providerId: "moonshotai-cn",
|
|
23
|
+
create: ({ statusRequestTimeoutMs }) => createMoonshotaiCnStatusAdapter(statusRequestTimeoutMs),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default moonshotaiCnStatusExtension;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Moonshot (Kimi) balance checks. International and China platforms keep
|
|
6
|
+
* fully independent API keys; the same response shape is shared by both
|
|
7
|
+
* endpoints, so the adapter body is factored once.
|
|
8
|
+
*/
|
|
9
|
+
export const MOONSHOT_BALANCE_URL = "https://api.moonshot.ai/v1/users/me/balance";
|
|
10
|
+
export const MOONSHOT_CN_BALANCE_URL = "https://api.moonshot.cn/v1/users/me/balance";
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MoonshotBalance {
|
|
21
|
+
available: number;
|
|
22
|
+
voucher?: number;
|
|
23
|
+
cash?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseMoonshotBalance(payload: unknown): MoonshotBalance {
|
|
27
|
+
if (!isRecord(payload) || !isRecord(payload.data)) {
|
|
28
|
+
throw new ProviderDataError("Moonshot status returned an invalid balance response", "badjson");
|
|
29
|
+
}
|
|
30
|
+
const code = payload.code;
|
|
31
|
+
if (code !== 0 && code !== undefined) {
|
|
32
|
+
throw new ProviderDataError("Moonshot status returned an unsuccessful balance response", "provider");
|
|
33
|
+
}
|
|
34
|
+
const available = finiteNumber(payload.data.available_balance);
|
|
35
|
+
if (available === undefined) {
|
|
36
|
+
throw new ProviderDataError("Moonshot status returned an invalid balance response", "badjson");
|
|
37
|
+
}
|
|
38
|
+
const voucher = finiteNumber(payload.data.voucher_balance);
|
|
39
|
+
const cash = finiteNumber(payload.data.cash_balance);
|
|
40
|
+
// Voucher balances are documented as non-negative; cash may be negative.
|
|
41
|
+
if (voucher !== undefined && voucher < 0) {
|
|
42
|
+
throw new ProviderDataError("Moonshot status returned an invalid balance response", "badjson");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
available,
|
|
46
|
+
...(voucher !== undefined ? { voucher } : {}),
|
|
47
|
+
...(cash !== undefined ? { cash } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface MoonshotStatusConfig {
|
|
52
|
+
id: string;
|
|
53
|
+
providerId: string;
|
|
54
|
+
name: string;
|
|
55
|
+
balanceUrl: string;
|
|
56
|
+
/** Official docs: international balances are USD; China balances are CNY. */
|
|
57
|
+
unit: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createMoonshotStatusAdapter(config: MoonshotStatusConfig, requestTimeoutMs: number): StatusAdapter {
|
|
61
|
+
const unit = config.unit;
|
|
62
|
+
return {
|
|
63
|
+
id: config.id,
|
|
64
|
+
providerId: config.providerId,
|
|
65
|
+
name: config.name,
|
|
66
|
+
cacheTtlMs: 60_000,
|
|
67
|
+
requestTimeoutMs,
|
|
68
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, config.balanceUrl),
|
|
69
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
70
|
+
const key = await context.getApiKey();
|
|
71
|
+
if (!key || key === "proxy-managed") {
|
|
72
|
+
throw new ProviderDataError(`${config.name} status requires an API key`, "auth");
|
|
73
|
+
}
|
|
74
|
+
const response = await context.fetch(config.balanceUrl, {
|
|
75
|
+
headers: {
|
|
76
|
+
Accept: "application/json",
|
|
77
|
+
"Accept-Encoding": "identity",
|
|
78
|
+
Authorization: `Bearer ${key}`,
|
|
79
|
+
"User-Agent": "@hyav/pi-provider",
|
|
80
|
+
},
|
|
81
|
+
signal: context.signal,
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new ProviderDataError(
|
|
85
|
+
`${config.name} status failed: HTTP ${response.status}`,
|
|
86
|
+
`http${response.status}`,
|
|
87
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
88
|
+
response.status,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
let payload: unknown;
|
|
92
|
+
try {
|
|
93
|
+
payload = await response.json();
|
|
94
|
+
} catch {
|
|
95
|
+
throw new ProviderDataError(`${config.name} status returned invalid JSON`, "badjson");
|
|
96
|
+
}
|
|
97
|
+
const balance = parseMoonshotBalance(payload);
|
|
98
|
+
const entries: StatusEntry[] = [
|
|
99
|
+
{
|
|
100
|
+
kind: "amount",
|
|
101
|
+
id: "available-balance",
|
|
102
|
+
label: "Available balance",
|
|
103
|
+
value: balance.available,
|
|
104
|
+
unit,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
if (balance.voucher !== undefined) {
|
|
108
|
+
entries.push({
|
|
109
|
+
kind: "amount",
|
|
110
|
+
id: "voucher-balance",
|
|
111
|
+
label: "Voucher balance",
|
|
112
|
+
value: balance.voucher,
|
|
113
|
+
unit,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (balance.cash !== undefined) {
|
|
117
|
+
entries.push({
|
|
118
|
+
kind: "amount",
|
|
119
|
+
id: "cash-balance",
|
|
120
|
+
label: "Cash balance",
|
|
121
|
+
value: balance.cash,
|
|
122
|
+
unit,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return { entries, updatedAt: context.now() };
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const moonshotaiStatusAdapter = createMoonshotStatusAdapter(
|
|
131
|
+
{
|
|
132
|
+
id: "moonshotai-status",
|
|
133
|
+
providerId: "moonshotai",
|
|
134
|
+
name: "Moonshot (Kimi)",
|
|
135
|
+
balanceUrl: MOONSHOT_BALANCE_URL,
|
|
136
|
+
unit: "USD",
|
|
137
|
+
},
|
|
138
|
+
8_000,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export function createMoonshotaiStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
142
|
+
return { ...moonshotaiStatusAdapter, requestTimeoutMs };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const moonshotaiStatusExtension = defineStatusExtension({
|
|
146
|
+
id: "moonshotai-status",
|
|
147
|
+
providerId: "moonshotai",
|
|
148
|
+
create: ({ statusRequestTimeoutMs }) => createMoonshotaiStatusAdapter(statusRequestTimeoutMs),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export default moonshotaiStatusExtension;
|
package/status/openai-codex.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
-
import { defineStatusExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
3
|
|
|
4
4
|
export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
5
5
|
const ACCOUNT_ID_CLAIM = "https://api.openai.com/auth";
|
|
@@ -170,6 +170,7 @@ export const openAICodexStatusAdapter: StatusAdapter = {
|
|
|
170
170
|
name: "OpenAI Codex",
|
|
171
171
|
cacheTtlMs: 60_000,
|
|
172
172
|
requestTimeoutMs: 8_000,
|
|
173
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, CODEX_USAGE_URL),
|
|
173
174
|
async fetch(context): Promise<StatusSnapshot> {
|
|
174
175
|
const key = await context.getApiKey();
|
|
175
176
|
if (!key || key === "proxy-managed") {
|
package/status/opencode-go.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
-
import { defineStatusExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
3
|
|
|
4
4
|
export const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
5
5
|
|
|
@@ -70,6 +70,7 @@ export const openCodeGoStatusAdapter: StatusAdapter = {
|
|
|
70
70
|
name: "OpenCode Go",
|
|
71
71
|
cacheTtlMs: 60_000,
|
|
72
72
|
requestTimeoutMs: 8_000,
|
|
73
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, OPENCODE_GO_USAGE_URL),
|
|
73
74
|
async fetch(context): Promise<StatusSnapshot> {
|
|
74
75
|
const key = await context.getApiKey();
|
|
75
76
|
if (!key || key === "proxy-managed") {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
|
|
4
|
+
export const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/auth/key";
|
|
5
|
+
export const OPENROUTER_CREDITS_URL = "https://openrouter.ai/api/v1/credits";
|
|
6
|
+
|
|
7
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function safeNumber(value: unknown): number | undefined {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isErrorPayload(value: Record<string, unknown>): boolean {
|
|
16
|
+
return isRecord(value.error) && (typeof value.error.code === "number" || typeof value.error.message === "string");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OpenRouterKey {
|
|
20
|
+
label: string;
|
|
21
|
+
usage: number;
|
|
22
|
+
limit: number | null;
|
|
23
|
+
isFreeTier: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseKeyPayload(value: unknown): OpenRouterKey {
|
|
27
|
+
if (!isRecord(value) || !isRecord(value.data)) {
|
|
28
|
+
throw new ProviderDataError("OpenRouter status returned an invalid key response", "badjson");
|
|
29
|
+
}
|
|
30
|
+
const data = value.data;
|
|
31
|
+
const usage = safeNumber(data.usage);
|
|
32
|
+
if (
|
|
33
|
+
typeof data.label !== "string" ||
|
|
34
|
+
typeof data.is_free_tier !== "boolean" ||
|
|
35
|
+
usage === undefined ||
|
|
36
|
+
(data.limit !== null && safeNumber(data.limit) === undefined)
|
|
37
|
+
) {
|
|
38
|
+
throw new ProviderDataError("OpenRouter status returned an invalid key response", "badjson");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
label: data.label.replace(/[\u0000-\u001f\u007f]/g, "").trim() || "API key",
|
|
42
|
+
usage,
|
|
43
|
+
limit: data.limit === null ? null : (safeNumber(data.limit) as number),
|
|
44
|
+
isFreeTier: data.is_free_tier,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readJson(response: Response, providerName: string): Promise<unknown> {
|
|
49
|
+
let payload: unknown;
|
|
50
|
+
try {
|
|
51
|
+
payload = await response.json();
|
|
52
|
+
} catch {
|
|
53
|
+
throw new ProviderDataError(`${providerName} status returned invalid JSON`, "badjson");
|
|
54
|
+
}
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetchWithAuth(
|
|
59
|
+
context: Parameters<StatusAdapter["fetch"]>[0],
|
|
60
|
+
key: string,
|
|
61
|
+
url: string,
|
|
62
|
+
providerName: string,
|
|
63
|
+
): Promise<{ response: Response; payload: unknown }> {
|
|
64
|
+
const response = await context.fetch(url, {
|
|
65
|
+
headers: {
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
"Accept-Encoding": "identity",
|
|
68
|
+
Authorization: `Bearer ${key}`,
|
|
69
|
+
"User-Agent": "@hyav/pi-provider",
|
|
70
|
+
},
|
|
71
|
+
signal: context.signal,
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new ProviderDataError(
|
|
75
|
+
`${providerName} status failed: HTTP ${response.status}`,
|
|
76
|
+
`http${response.status}`,
|
|
77
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
78
|
+
response.status,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return { response, payload: await readJson(response, providerName) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function keyEntries(
|
|
85
|
+
key: OpenRouterKey,
|
|
86
|
+
limitEntry: StatusEntry | undefined,
|
|
87
|
+
freeTierEntry: StatusEntry,
|
|
88
|
+
): StatusEntry[] {
|
|
89
|
+
const usageEntry: StatusEntry = {
|
|
90
|
+
kind: "amount",
|
|
91
|
+
id: "credits-used",
|
|
92
|
+
label: "Credits used",
|
|
93
|
+
value: key.usage,
|
|
94
|
+
unit: "USD",
|
|
95
|
+
};
|
|
96
|
+
const entries: StatusEntry[] = [{ kind: "text", id: "key", label: "Key", value: key.label }, usageEntry];
|
|
97
|
+
if (limitEntry) entries.push(limitEntry);
|
|
98
|
+
entries.push(freeTierEntry);
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function freeTierEntry(value: boolean): StatusEntry {
|
|
103
|
+
return { kind: "text", id: "account-tier", label: "Account", value: value ? "Free tier" : "Paid" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const openRouterStatusAdapter: StatusAdapter = {
|
|
107
|
+
id: "openrouter-status",
|
|
108
|
+
providerId: "openrouter",
|
|
109
|
+
name: "OpenRouter",
|
|
110
|
+
cacheTtlMs: 60_000,
|
|
111
|
+
requestTimeoutMs: 8_000,
|
|
112
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, OPENROUTER_KEY_URL),
|
|
113
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
114
|
+
const key = await context.getApiKey();
|
|
115
|
+
if (!key || key === "proxy-managed") {
|
|
116
|
+
throw new ProviderDataError("OpenRouter status requires an API key", "auth");
|
|
117
|
+
}
|
|
118
|
+
const keyResult = await fetchWithAuth(context, key, OPENROUTER_KEY_URL, "OpenRouter");
|
|
119
|
+
const keyPayload = keyResult.payload;
|
|
120
|
+
if (isRecord(keyPayload) && isErrorPayload(keyPayload)) {
|
|
121
|
+
throw new ProviderDataError(
|
|
122
|
+
"OpenRouter status failed: invalid API key response",
|
|
123
|
+
"auth",
|
|
124
|
+
undefined,
|
|
125
|
+
keyResult.response.status,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const openRouterKey = parseKeyPayload(keyPayload);
|
|
129
|
+
|
|
130
|
+
// /credits requires a management key; only show the limit when resolved.
|
|
131
|
+
let limitEntry: StatusEntry | undefined;
|
|
132
|
+
try {
|
|
133
|
+
const creditsResult = await fetchWithAuth(context, key, OPENROUTER_CREDITS_URL, "OpenRouter");
|
|
134
|
+
const payload = creditsResult.payload;
|
|
135
|
+
if (!isRecord(payload) || !isRecord(payload.data)) {
|
|
136
|
+
throw new ProviderDataError("OpenRouter status returned an invalid credits response", "badjson");
|
|
137
|
+
}
|
|
138
|
+
const totalCredits = safeNumber(payload.data.total_credits);
|
|
139
|
+
const totalUsage = safeNumber(payload.data.total_usage);
|
|
140
|
+
if (totalCredits === undefined || totalUsage === undefined) {
|
|
141
|
+
throw new ProviderDataError("OpenRouter status returned an invalid credits response", "badjson");
|
|
142
|
+
}
|
|
143
|
+
const remaining = Math.max(0, totalCredits - totalUsage);
|
|
144
|
+
limitEntry = {
|
|
145
|
+
kind: "amount",
|
|
146
|
+
id: "credits-remaining",
|
|
147
|
+
label: "Credits remaining",
|
|
148
|
+
value: remaining,
|
|
149
|
+
unit: "USD",
|
|
150
|
+
};
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// Safe fallback: key credits, free-tier, and key-level limit still display.
|
|
153
|
+
if (!(error instanceof ProviderDataError)) throw error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
entries: keyEntries(openRouterKey, limitEntry, freeTierEntry(openRouterKey.isFreeTier)),
|
|
158
|
+
updatedAt: context.now(),
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export function createOpenRouterStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
164
|
+
return { ...openRouterStatusAdapter, requestTimeoutMs };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const openRouterStatusExtension = defineStatusExtension({
|
|
168
|
+
id: "openrouter-status",
|
|
169
|
+
providerId: "openrouter",
|
|
170
|
+
create: ({ statusRequestTimeoutMs }) => createOpenRouterStatusAdapter(statusRequestTimeoutMs),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export default openRouterStatusExtension;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
import { VERCEL_PROVIDER_ID } from "./vercel-ai-gateway/constants.ts";
|
|
4
|
+
|
|
5
|
+
export const VERCEL_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
|
|
6
|
+
|
|
7
|
+
interface VercelCredits {
|
|
8
|
+
balance: number;
|
|
9
|
+
totalUsed: number;
|
|
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
|
+
function parseFiniteAmount(value: unknown): number | undefined {
|
|
17
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
|
|
18
|
+
if (typeof value !== "string" || value.trim() === "") return undefined;
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseVercelCredits(payload: unknown): VercelCredits {
|
|
24
|
+
if (!isRecord(payload)) {
|
|
25
|
+
throw new ProviderDataError("Vercel AI Gateway status returned an invalid credits response", "badjson");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const balance = parseFiniteAmount(payload.balance);
|
|
29
|
+
const totalUsed = parseFiniteAmount(payload.total_used);
|
|
30
|
+
if (balance === undefined || totalUsed === undefined) {
|
|
31
|
+
throw new ProviderDataError("Vercel AI Gateway status returned an invalid credits response", "badjson");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { balance, totalUsed };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createVercelAIGatewayStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
38
|
+
return {
|
|
39
|
+
id: "vercel-ai-gateway-status",
|
|
40
|
+
providerId: VERCEL_PROVIDER_ID,
|
|
41
|
+
name: "Vercel AI Gateway",
|
|
42
|
+
cacheTtlMs: 30_000,
|
|
43
|
+
requestTimeoutMs,
|
|
44
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, VERCEL_CREDITS_URL),
|
|
45
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
46
|
+
const key = await context.getApiKey();
|
|
47
|
+
if (!key || key === "proxy-managed") {
|
|
48
|
+
throw new ProviderDataError("Vercel AI Gateway status requires an API key", "auth");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const response = await context.fetch(VERCEL_CREDITS_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
|
+
`Vercel AI Gateway status failed: HTTP ${response.status}`,
|
|
62
|
+
response.status === 401 ? "auth" : `http${response.status}`,
|
|
63
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
64
|
+
response.status,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let payload: unknown;
|
|
69
|
+
try {
|
|
70
|
+
payload = await response.json();
|
|
71
|
+
} catch {
|
|
72
|
+
throw new ProviderDataError("Vercel AI Gateway status returned invalid JSON", "badjson");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const credits = parseVercelCredits(payload);
|
|
76
|
+
return {
|
|
77
|
+
entries: [
|
|
78
|
+
{ kind: "amount", id: "balance", label: "Balance", value: credits.balance, unit: "USD" },
|
|
79
|
+
{ kind: "amount", id: "total-used", label: "Total Used", value: credits.totalUsed, unit: "USD" },
|
|
80
|
+
],
|
|
81
|
+
updatedAt: context.now(),
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const vercelAIGatewayStatusAdapter = createVercelAIGatewayStatusAdapter(8_000);
|
|
88
|
+
|
|
89
|
+
const vercelAIGatewayStatusExtension = defineStatusExtension({
|
|
90
|
+
id: "vercel-ai-gateway-status",
|
|
91
|
+
providerId: VERCEL_PROVIDER_ID,
|
|
92
|
+
create: ({ statusRequestTimeoutMs }) => createVercelAIGatewayStatusAdapter(statusRequestTimeoutMs),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export default vercelAIGatewayStatusExtension;
|