@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { PreflightAdapter, PreflightSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
import { VERCEL_PROVIDER_ID } from "../status/vercel-ai-gateway/constants.ts";
|
|
4
|
+
|
|
5
|
+
export const VERCEL_MODELS_URL = "https://ai-gateway.vercel.sh/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
|
+
export function parseVercelModelIds(payload: unknown): Set<string> {
|
|
12
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
13
|
+
throw new ProviderDataError("Vercel AI Gateway preflight returned invalid catalog data", "badjson");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const modelIds = new Set<string>();
|
|
17
|
+
for (const value of payload.data) {
|
|
18
|
+
if (!isRecord(value)) continue;
|
|
19
|
+
// Mixed-type gateway catalogs can include non-language entries.
|
|
20
|
+
if (value.type !== undefined && value.type !== "language") continue;
|
|
21
|
+
if (typeof value.id !== "string") continue;
|
|
22
|
+
const id = value.id.trim();
|
|
23
|
+
if (id !== "") modelIds.add(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (modelIds.size === 0) {
|
|
27
|
+
throw new ProviderDataError("Vercel AI Gateway preflight returned an empty catalog", "badjson");
|
|
28
|
+
}
|
|
29
|
+
return modelIds;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createVercelAIGatewayPreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
|
|
33
|
+
return {
|
|
34
|
+
id: "vercel-ai-gateway-preflight",
|
|
35
|
+
providerId: VERCEL_PROVIDER_ID,
|
|
36
|
+
name: "Vercel AI Gateway",
|
|
37
|
+
cacheTtlMs: 30_000,
|
|
38
|
+
requestTimeoutMs,
|
|
39
|
+
async fetch(context): Promise<PreflightSnapshot> {
|
|
40
|
+
const response = await context.fetch(VERCEL_MODELS_URL, {
|
|
41
|
+
headers: {
|
|
42
|
+
Accept: "application/json",
|
|
43
|
+
"Accept-Encoding": "identity",
|
|
44
|
+
},
|
|
45
|
+
signal: context.signal,
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new ProviderDataError(
|
|
49
|
+
`Vercel AI Gateway preflight failed: HTTP ${response.status}`,
|
|
50
|
+
`http${response.status}`,
|
|
51
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
52
|
+
response.status,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let payload: unknown;
|
|
57
|
+
try {
|
|
58
|
+
payload = await response.json();
|
|
59
|
+
} catch {
|
|
60
|
+
throw new ProviderDataError("Vercel AI Gateway preflight returned invalid JSON", "badjson");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
passed: parseVercelModelIds(payload).has(context.model.id),
|
|
65
|
+
checks: ["endpoint", "catalog"],
|
|
66
|
+
updatedAt: context.now(),
|
|
67
|
+
httpStatus: response.status,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const vercelAIGatewayPreflightAdapter = createVercelAIGatewayPreflightAdapter(8_000);
|
|
74
|
+
|
|
75
|
+
const vercelAIGatewayPreflightExtension = definePreflightExtension({
|
|
76
|
+
id: "vercel-ai-gateway-preflight",
|
|
77
|
+
providerId: VERCEL_PROVIDER_ID,
|
|
78
|
+
create: ({ statusRequestTimeoutMs }) => createVercelAIGatewayPreflightAdapter(statusRequestTimeoutMs),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export default vercelAIGatewayPreflightExtension;
|
package/preflight/xai.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
+
import { definePreflightExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
import { XAI_MODELS_URL } from "../status/xai.ts";
|
|
4
|
+
|
|
5
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const xaiPreflightAdapter: PreflightAdapter = {
|
|
10
|
+
id: "xai-preflight",
|
|
11
|
+
providerId: "xai",
|
|
12
|
+
name: "xAI",
|
|
13
|
+
cacheTtlMs: 30_000,
|
|
14
|
+
requestTimeoutMs: 8_000,
|
|
15
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, XAI_MODELS_URL),
|
|
16
|
+
async fetch(context) {
|
|
17
|
+
const apiKey = await context.getApiKey();
|
|
18
|
+
const authHeaders: Record<string, string> = {
|
|
19
|
+
Accept: "application/json",
|
|
20
|
+
"Accept-Encoding": "identity",
|
|
21
|
+
"User-Agent": "@hyav/pi-provider",
|
|
22
|
+
};
|
|
23
|
+
if (apiKey && apiKey !== "proxy-managed") authHeaders.Authorization = `Bearer ${apiKey}`;
|
|
24
|
+
const response = await context.fetch(XAI_MODELS_URL, {
|
|
25
|
+
headers: authHeaders,
|
|
26
|
+
signal: context.signal,
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new ProviderDataError(
|
|
30
|
+
`xAI preflight failed: HTTP ${response.status}`,
|
|
31
|
+
`http${response.status}`,
|
|
32
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
33
|
+
response.status,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
let payload: unknown;
|
|
37
|
+
try {
|
|
38
|
+
payload = await response.json();
|
|
39
|
+
} catch {
|
|
40
|
+
throw new ProviderDataError("xAI preflight returned invalid JSON", "badjson");
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
43
|
+
throw new ProviderDataError("xAI preflight returned invalid catalog data", "badjson");
|
|
44
|
+
}
|
|
45
|
+
const modelIds = new Set(
|
|
46
|
+
payload.data
|
|
47
|
+
.filter(isRecord)
|
|
48
|
+
.map((model) => (typeof model.id === "string" ? model.id.trim() : undefined))
|
|
49
|
+
.filter((id): id is string => id !== undefined && id !== ""),
|
|
50
|
+
);
|
|
51
|
+
const checks = apiKey && apiKey !== "proxy-managed" ? ["endpoint", "catalog", "auth"] : ["endpoint", "catalog"];
|
|
52
|
+
return {
|
|
53
|
+
passed: modelIds.has(context.model.id),
|
|
54
|
+
checks,
|
|
55
|
+
updatedAt: context.now(),
|
|
56
|
+
httpStatus: response.status,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function createXaiPreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
|
|
62
|
+
return { ...xaiPreflightAdapter, requestTimeoutMs };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const xaiPreflightExtension = definePreflightExtension({
|
|
66
|
+
id: "xai-preflight",
|
|
67
|
+
providerId: "xai",
|
|
68
|
+
create: ({ statusRequestTimeoutMs }) => createXaiPreflightAdapter(statusRequestTimeoutMs),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export default xaiPreflightExtension;
|
package/providers/charm-hyper.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import {
|
|
10
10
|
defineProviderExtension,
|
|
11
11
|
isProviderDataError,
|
|
12
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
12
13
|
normalizeProviderModels,
|
|
13
14
|
ProviderDataError,
|
|
14
15
|
withDeadline,
|
|
@@ -242,6 +243,7 @@ function parseCurrentHyperModel(value: unknown): ProviderModelDraft | undefined
|
|
|
242
243
|
|
|
243
244
|
function parseCurrentHyperModels(payload: Record<string, unknown>): ProviderModelDraft[] | undefined {
|
|
244
245
|
if (!Array.isArray(payload.models)) return undefined;
|
|
246
|
+
if (payload.models.length > MAX_PROVIDER_MODEL_COUNT) return [];
|
|
245
247
|
const models: ProviderModelDraft[] = [];
|
|
246
248
|
const seenIds = new Set<string>();
|
|
247
249
|
for (const value of payload.models) {
|
|
@@ -256,7 +258,7 @@ function parseCurrentHyperModels(payload: Record<string, unknown>): ProviderMode
|
|
|
256
258
|
}
|
|
257
259
|
|
|
258
260
|
function parseLegacyHyperModels(payload: Record<string, unknown>): ProviderModelDraft[] {
|
|
259
|
-
if (!Array.isArray(payload.data)) return [];
|
|
261
|
+
if (!Array.isArray(payload.data) || payload.data.length > MAX_PROVIDER_MODEL_COUNT) return [];
|
|
260
262
|
const models: ProviderModelDraft[] = [];
|
|
261
263
|
const seenIds = new Set<string>();
|
|
262
264
|
|
|
@@ -356,6 +358,7 @@ async function discoverHyperModels(
|
|
|
356
358
|
if (models.length === 0) {
|
|
357
359
|
throw new ProviderDataError("Charm Hyper model discovery returned no valid models", "badjson");
|
|
358
360
|
}
|
|
361
|
+
normalizeProviderModels(models);
|
|
359
362
|
return models;
|
|
360
363
|
},
|
|
361
364
|
timeoutMs,
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import { defineStatusExtension, hasBaseUrlOrigin, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Anthropic usage endpoint for subscription quotas (Claude Pro/Max) and extra
|
|
6
|
+
* usage credit. Not part of the public platform API contract, so the URL is
|
|
7
|
+
* overridable: ANTHROPIC_USAGE_URL, or set it to an empty string to disable.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_ANTHROPIC_USAGE_URL = "https://claude.ai/api/usage";
|
|
10
|
+
export const ANTHROPIC_USAGE_URL =
|
|
11
|
+
typeof process !== "undefined" && process.env.ANTHROPIC_USAGE_URL !== undefined
|
|
12
|
+
? process.env.ANTHROPIC_USAGE_URL
|
|
13
|
+
: DEFAULT_ANTHROPIC_USAGE_URL;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Anthropic API keys are `sk-ant-api...`; subscription OAuth access tokens
|
|
17
|
+
* (used by Claude web / Claude Code) contain `sk-ant-oat...`. Only subscription
|
|
18
|
+
* tokens may be sent to the Claude web usage endpoint. This keeps API keys
|
|
19
|
+
* resolved from environment variables, models.json, or a runtime from ever
|
|
20
|
+
* reaching it.
|
|
21
|
+
*/
|
|
22
|
+
export function isAnthropicOAuthToken(key: string): boolean {
|
|
23
|
+
return key.includes("sk-ant-oat");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isAnthropicApiKey(key: string): boolean {
|
|
27
|
+
return (key.startsWith("sk-ant-api") || key.startsWith("sk-ant-")) && !isAnthropicOAuthToken(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
35
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MAX_LABEL_LENGTH = 64;
|
|
39
|
+
|
|
40
|
+
function safeLabel(value: unknown): string | undefined {
|
|
41
|
+
if (typeof value !== "string") return undefined;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (trimmed === "" || trimmed.length > MAX_LABEL_LENGTH || /[\u0000-\u001f\u007f]/.test(trimmed)) return undefined;
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AnthropicUsageWindow {
|
|
48
|
+
id: string;
|
|
49
|
+
label: string;
|
|
50
|
+
used: number;
|
|
51
|
+
limit: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseAnthropicUsage(payload: unknown): {
|
|
55
|
+
plan?: string;
|
|
56
|
+
resetAt?: number;
|
|
57
|
+
windows: AnthropicUsageWindow[];
|
|
58
|
+
extraUsageBalanceUsd?: number;
|
|
59
|
+
} {
|
|
60
|
+
if (!isRecord(payload)) {
|
|
61
|
+
throw new ProviderDataError("Anthropic status returned an invalid usage response", "badjson");
|
|
62
|
+
}
|
|
63
|
+
const subscribed = isRecord(payload.subscribedUsage) ? payload.subscribedUsage : payload;
|
|
64
|
+
|
|
65
|
+
/** History arrays use their latest entry as the current value. */
|
|
66
|
+
const read = (field: string): number | undefined => {
|
|
67
|
+
const value = subscribed[field];
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
const latest = value[value.length - 1];
|
|
70
|
+
return typeof latest === "number" && Number.isFinite(latest) ? latest : undefined;
|
|
71
|
+
}
|
|
72
|
+
return finiteNumber(value);
|
|
73
|
+
};
|
|
74
|
+
const readLimit = (field: string): number | undefined => {
|
|
75
|
+
const limitValue = finiteNumber(subscribed[`${field}Limit`]);
|
|
76
|
+
if (limitValue !== undefined && limitValue > 0) return limitValue;
|
|
77
|
+
const entriesValue = subscribed[field];
|
|
78
|
+
if (isRecord(entriesValue)) {
|
|
79
|
+
const nested = finiteNumber(entriesValue.limit);
|
|
80
|
+
if (nested !== undefined && nested > 0) return nested;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const windows: AnthropicUsageWindow[] = [];
|
|
86
|
+
for (const [field, id, label] of [
|
|
87
|
+
["session", "session-usage", "Session"],
|
|
88
|
+
["daily", "daily-usage", "Daily"],
|
|
89
|
+
["weekly", "weekly-usage", "Weekly"],
|
|
90
|
+
["monthly", "monthly-usage", "Monthly"],
|
|
91
|
+
] as const) {
|
|
92
|
+
const used = read(field);
|
|
93
|
+
const limit = readLimit(field);
|
|
94
|
+
if (used !== undefined && limit !== undefined) {
|
|
95
|
+
windows.push({ id, label, used, limit });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resetAtValue = finiteNumber(payload.weeklyResetAt) ?? finiteNumber(subscribed.weeklyResetAt);
|
|
100
|
+
const resetAt = resetAtValue !== undefined && resetAtValue > 0 ? resetAtValue * 1_000 : undefined;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
plan: safeLabel(payload.plan) ?? safeLabel(payload.subscriptionPlan),
|
|
104
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
105
|
+
windows,
|
|
106
|
+
extraUsageBalanceUsd: finiteNumber(payload.extraUsageBalanceUsd) ?? finiteNumber(subscribed.extraUsageBalanceUsd),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function windowEntry(id: string, label: string, used: number, limit: number, resetAt: number | undefined): StatusEntry {
|
|
111
|
+
const percent = (used / limit) * 100;
|
|
112
|
+
return {
|
|
113
|
+
kind: "window",
|
|
114
|
+
id,
|
|
115
|
+
label,
|
|
116
|
+
remainingPercent: Math.max(0, Math.min(100, 100 - percent)),
|
|
117
|
+
...(resetAt !== undefined ? { resetAt } : {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function usageEntries(payload: unknown): StatusEntry[] {
|
|
122
|
+
const parsed = parseAnthropicUsage(payload);
|
|
123
|
+
const entries: StatusEntry[] = [{ kind: "text", id: "plan", label: "Plan", value: parsed.plan ?? "Unknown" }];
|
|
124
|
+
for (const window of parsed.windows) {
|
|
125
|
+
entries.push(windowEntry(window.id, window.label, window.used, window.limit, parsed.resetAt));
|
|
126
|
+
}
|
|
127
|
+
if (parsed.extraUsageBalanceUsd !== undefined) {
|
|
128
|
+
entries.push({
|
|
129
|
+
kind: "amount",
|
|
130
|
+
id: "extra-usage-balance",
|
|
131
|
+
label: "Extra usage balance",
|
|
132
|
+
value: parsed.extraUsageBalanceUsd,
|
|
133
|
+
unit: "USD",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (entries.length === 1) {
|
|
137
|
+
entries.push({ kind: "text", id: "limits", label: "Limits", value: "not available" });
|
|
138
|
+
}
|
|
139
|
+
return entries;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function credentialType(context: Parameters<StatusAdapter["fetch"]>[0]): Promise<string | undefined> {
|
|
143
|
+
try {
|
|
144
|
+
return await context.getCredentialType?.();
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function apiKeyEntries(): StatusEntry[] {
|
|
151
|
+
return [
|
|
152
|
+
{ kind: "text", id: "auth", label: "Auth", value: "API key" },
|
|
153
|
+
{ kind: "text", id: "usage", label: "Usage", value: "not available from the subscription endpoint" },
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface AnthropicStatusOptions {
|
|
158
|
+
/** Usage endpoint override; defaults to ANTHROPIC_USAGE_URL. */
|
|
159
|
+
usageUrl?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createAnthropicStatusAdapter(
|
|
163
|
+
requestTimeoutMs: number,
|
|
164
|
+
options: AnthropicStatusOptions = {},
|
|
165
|
+
): StatusAdapter {
|
|
166
|
+
const usageUrl = (options.usageUrl ?? ANTHROPIC_USAGE_URL).trim();
|
|
167
|
+
return {
|
|
168
|
+
id: "anthropic-status",
|
|
169
|
+
providerId: "anthropic",
|
|
170
|
+
name: "Anthropic",
|
|
171
|
+
cacheTtlMs: 60_000,
|
|
172
|
+
requestTimeoutMs,
|
|
173
|
+
supportsModel: (model) =>
|
|
174
|
+
usageUrl === "" ||
|
|
175
|
+
usageUrl !== DEFAULT_ANTHROPIC_USAGE_URL ||
|
|
176
|
+
hasBaseUrlOrigin(model.baseUrl, "https://api.anthropic.com"),
|
|
177
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
178
|
+
const key = await context.getApiKey();
|
|
179
|
+
if (!key || key === "proxy-managed") {
|
|
180
|
+
throw new ProviderDataError("Anthropic status requires authentication", "auth");
|
|
181
|
+
}
|
|
182
|
+
const credential = await credentialType(context);
|
|
183
|
+
const isOAuth = credential === "oauth" ? !isAnthropicApiKey(key) : isAnthropicOAuthToken(key);
|
|
184
|
+
if (usageUrl === "") {
|
|
185
|
+
return {
|
|
186
|
+
entries: [{ kind: "text", id: "usage", label: "Usage", value: "disabled" }],
|
|
187
|
+
updatedAt: context.now(),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!isOAuth) {
|
|
192
|
+
if (usageUrl === DEFAULT_ANTHROPIC_USAGE_URL) {
|
|
193
|
+
// The default endpoint is subscription-only. Never send an
|
|
194
|
+
// API key there; the user can configure a custom endpoint
|
|
195
|
+
// via ANTHROPIC_USAGE_URL if they run their own queries.
|
|
196
|
+
return { entries: apiKeyEntries(), updatedAt: context.now() };
|
|
197
|
+
}
|
|
198
|
+
// Explicitly configured custom endpoint: send as x-api-key only.
|
|
199
|
+
const response = await context.fetch(usageUrl, {
|
|
200
|
+
headers: {
|
|
201
|
+
Accept: "application/json",
|
|
202
|
+
"Accept-Encoding": "identity",
|
|
203
|
+
"x-api-key": key,
|
|
204
|
+
"User-Agent": "@hyav/pi-provider",
|
|
205
|
+
},
|
|
206
|
+
signal: context.signal,
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
throw new ProviderDataError(
|
|
210
|
+
`Anthropic status failed: HTTP ${response.status}`,
|
|
211
|
+
`http${response.status}`,
|
|
212
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
213
|
+
response.status,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
let payload: unknown;
|
|
217
|
+
try {
|
|
218
|
+
payload = await response.json();
|
|
219
|
+
} catch {
|
|
220
|
+
throw new ProviderDataError("Anthropic status returned invalid JSON", "badjson");
|
|
221
|
+
}
|
|
222
|
+
return { entries: usageEntries(payload), updatedAt: context.now() };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// OAuth (Claude Pro/Max) uses the usage endpoint with the subscription Bearer token.
|
|
226
|
+
const response = await context.fetch(usageUrl, {
|
|
227
|
+
headers: {
|
|
228
|
+
Accept: "application/json",
|
|
229
|
+
"Accept-Encoding": "identity",
|
|
230
|
+
Authorization: `Bearer ${key}`,
|
|
231
|
+
"User-Agent": "@hyav/pi-provider",
|
|
232
|
+
},
|
|
233
|
+
signal: context.signal,
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new ProviderDataError(
|
|
237
|
+
`Anthropic status failed: HTTP ${response.status}`,
|
|
238
|
+
`http${response.status}`,
|
|
239
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
240
|
+
response.status,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
let payload: unknown;
|
|
244
|
+
try {
|
|
245
|
+
payload = await response.json();
|
|
246
|
+
} catch {
|
|
247
|
+
throw new ProviderDataError("Anthropic status returned invalid JSON", "badjson");
|
|
248
|
+
}
|
|
249
|
+
return { entries: usageEntries(payload), updatedAt: context.now() };
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const anthropicStatusAdapter = createAnthropicStatusAdapter(8_000);
|
|
255
|
+
|
|
256
|
+
const anthropicStatusExtension = defineStatusExtension({
|
|
257
|
+
id: "anthropic-status",
|
|
258
|
+
providerId: "anthropic",
|
|
259
|
+
create: ({ statusRequestTimeoutMs }) => createAnthropicStatusAdapter(statusRequestTimeoutMs),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
export default anthropicStatusExtension;
|
package/status/charm-hyper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StatusAdapter, 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
|
import { hyperJsonHeaders } from "../providers/charm-hyper/constants.ts";
|
|
4
4
|
|
|
5
5
|
const CREDITS_URL = "https://hyper.charm.land/v1/credits";
|
|
@@ -21,8 +21,9 @@ export const hyperStatusAdapter: StatusAdapter = {
|
|
|
21
21
|
id: "charm-hyper-status",
|
|
22
22
|
providerId: "charm-hyper",
|
|
23
23
|
name: "Charm Hyper",
|
|
24
|
-
cacheTtlMs:
|
|
24
|
+
cacheTtlMs: 60_000,
|
|
25
25
|
requestTimeoutMs: 8_000,
|
|
26
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, CREDITS_URL),
|
|
26
27
|
async fetch(context): Promise<StatusSnapshot> {
|
|
27
28
|
const key = await context.getApiKey();
|
|
28
29
|
const headers = new Headers(hyperJsonHeaders({ "Accept-Encoding": "identity" }));
|
package/status/deepseek.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StatusAdapter, 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 DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
|
|
5
5
|
|
|
@@ -41,6 +41,7 @@ export const deepSeekStatusAdapter: StatusAdapter = {
|
|
|
41
41
|
name: "DeepSeek",
|
|
42
42
|
cacheTtlMs: 30_000,
|
|
43
43
|
requestTimeoutMs: 8_000,
|
|
44
|
+
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, DEEPSEEK_BALANCE_URL),
|
|
44
45
|
async fetch(context): Promise<StatusSnapshot> {
|
|
45
46
|
const key = await context.getApiKey();
|
|
46
47
|
if (!key || key === "proxy-managed") {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
|
|
2
|
+
import {
|
|
3
|
+
appendBaseUrlPath,
|
|
4
|
+
authDefinesHeader,
|
|
5
|
+
defineStatusExtension,
|
|
6
|
+
getContextAuth,
|
|
7
|
+
mergeDiagnosticHeaders,
|
|
8
|
+
ProviderDataError,
|
|
9
|
+
parseRetryAfter,
|
|
10
|
+
} from "@hyav/pi-provider";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GitHub Copilot Individual usage. These endpoints are not part of public
|
|
14
|
+
* GitHub documentation, so payload shapes are parsed defensively and a 404
|
|
15
|
+
* degrades to a single explanatory entry instead of an error state.
|
|
16
|
+
*/
|
|
17
|
+
export const COPILOT_BASE_URL = "https://api.individual.githubcopilot.com";
|
|
18
|
+
export const COPILOT_USAGE_URL = `${COPILOT_BASE_URL}/usage`;
|
|
19
|
+
|
|
20
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MAX_LABEL_LENGTH = 64;
|
|
29
|
+
|
|
30
|
+
function safeText(value: unknown): string | undefined {
|
|
31
|
+
if (typeof value !== "string") return undefined;
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (trimmed === "" || trimmed.length > MAX_LABEL_LENGTH || /[\u0000-\u001f\u007f]/.test(trimmed)) return undefined;
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CopilotQuota {
|
|
38
|
+
id: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
used: number;
|
|
41
|
+
limit: number;
|
|
42
|
+
resetAt?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function labelValue(value: unknown): string | undefined {
|
|
46
|
+
if (typeof value === "string") return safeText(value);
|
|
47
|
+
if (isRecord(value)) return safeText(value.value) ?? safeText(value.label);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function planName(value: unknown): string | undefined {
|
|
52
|
+
return labelValue(isRecord(value) ? value.planName : undefined);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Preferred shape: `modelCatalog.usage.modelQuotas`. */
|
|
56
|
+
function parseModelCatalog(catalog: unknown): CopilotQuota[] {
|
|
57
|
+
if (!isRecord(catalog) || !isRecord(catalog.usage) || !isRecord(catalog.usage.modelQuotas)) return [];
|
|
58
|
+
const quotas: CopilotQuota[] = [];
|
|
59
|
+
for (const [key, value] of Object.entries(catalog.usage.modelQuotas)) {
|
|
60
|
+
if (!isRecord(value)) continue;
|
|
61
|
+
const used =
|
|
62
|
+
finiteNumber(value.usedRequestsQuantity) ?? finiteNumber(value.usedRequests) ?? finiteNumber(value.used);
|
|
63
|
+
const limit = finiteNumber(value.allowedRequestsQuantity);
|
|
64
|
+
if (used === undefined || limit === undefined || limit <= 0) continue;
|
|
65
|
+
const resetAt = finiteNumber(value.resetAt);
|
|
66
|
+
quotas.push({
|
|
67
|
+
id: key,
|
|
68
|
+
name: safeText(value.modelCopilotName),
|
|
69
|
+
used,
|
|
70
|
+
limit,
|
|
71
|
+
...(resetAt !== undefined ? { resetAt: resetAt * 1_000 } : {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return quotas;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Fallback shape: `modelQuotaByFeature[]` with nested payload. */
|
|
78
|
+
function parseQuotaByFeature(payload: unknown): CopilotQuota[] {
|
|
79
|
+
if (!isRecord(payload) || !Array.isArray(payload.modelQuotaByFeature)) return [];
|
|
80
|
+
const quotas: CopilotQuota[] = [];
|
|
81
|
+
for (const [index, entry] of payload.modelQuotaByFeature.entries()) {
|
|
82
|
+
if (!isRecord(entry)) continue;
|
|
83
|
+
const quota =
|
|
84
|
+
(isRecord(entry.modelQuotaPayload) && entry.modelQuotaPayload) ||
|
|
85
|
+
(isRecord(entry.modelQuotaForFeature) && entry.modelQuotaForFeature);
|
|
86
|
+
if (!quota) continue;
|
|
87
|
+
const used = finiteNumber(quota.usedRequestsQuantity) ?? finiteNumber(quota.usedRequests);
|
|
88
|
+
const limit = finiteNumber(quota.allowedRequestsQuantity);
|
|
89
|
+
if (used === undefined || limit === undefined) continue;
|
|
90
|
+
const resetAt = finiteNumber(quota.resetAt);
|
|
91
|
+
const name = safeText(quota.modelCopilotName);
|
|
92
|
+
quotas.push({
|
|
93
|
+
id: typeof quota.quotaId === "string" && quota.quotaId.trim() !== "" ? quota.quotaId.trim() : `quota-${index}`,
|
|
94
|
+
...(name !== undefined ? { name } : {}),
|
|
95
|
+
used,
|
|
96
|
+
limit,
|
|
97
|
+
...(resetAt !== undefined ? { resetAt: resetAt * 1_000 } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return quotas;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function windowEntry(quota: CopilotQuota): StatusEntry {
|
|
104
|
+
const percent = (quota.used / quota.limit) * 100;
|
|
105
|
+
return {
|
|
106
|
+
kind: "window",
|
|
107
|
+
id: `quota-${quota.id}`,
|
|
108
|
+
label: quota.name ?? quota.id,
|
|
109
|
+
remainingPercent: Math.max(0, Math.min(100, 100 - percent)),
|
|
110
|
+
...(quota.resetAt !== undefined ? { resetAt: quota.resetAt } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const githubCopilotStatusAdapter: StatusAdapter = {
|
|
115
|
+
id: "github-copilot-status",
|
|
116
|
+
providerId: "github-copilot",
|
|
117
|
+
name: "GitHub Copilot",
|
|
118
|
+
cacheTtlMs: 60_000,
|
|
119
|
+
requestTimeoutMs: 8_000,
|
|
120
|
+
async fetch(context): Promise<StatusSnapshot> {
|
|
121
|
+
const auth = await getContextAuth(context);
|
|
122
|
+
const key = auth.apiKey;
|
|
123
|
+
if (!key || key === "proxy-managed") {
|
|
124
|
+
throw new ProviderDataError("GitHub Copilot status requires Copilot OAuth", "auth");
|
|
125
|
+
}
|
|
126
|
+
const url = appendBaseUrlPath(context.model?.baseUrl ?? auth.baseUrl, "usage", COPILOT_BASE_URL);
|
|
127
|
+
if (!url) throw new ProviderDataError("GitHub Copilot usage endpoint is unavailable", "unsupported");
|
|
128
|
+
const headers = mergeDiagnosticHeaders(auth, {
|
|
129
|
+
Accept: "application/json",
|
|
130
|
+
"Accept-Encoding": "identity",
|
|
131
|
+
"User-Agent": "@hyav/pi-provider",
|
|
132
|
+
});
|
|
133
|
+
if (!authDefinesHeader(auth, "Authorization")) headers.set("Authorization", `Bearer ${key}`);
|
|
134
|
+
const response = await context.fetch(url, {
|
|
135
|
+
headers,
|
|
136
|
+
signal: context.signal,
|
|
137
|
+
});
|
|
138
|
+
if (response.status === 404) {
|
|
139
|
+
return {
|
|
140
|
+
entries: [{ kind: "text", id: "usage", label: "Usage", value: "unavailable for this plan" }],
|
|
141
|
+
updatedAt: context.now(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new ProviderDataError(
|
|
146
|
+
`GitHub Copilot status failed: HTTP ${response.status}`,
|
|
147
|
+
`http${response.status}`,
|
|
148
|
+
parseRetryAfter(response.headers.get("retry-after"), context.now()),
|
|
149
|
+
response.status,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
let payload: unknown;
|
|
153
|
+
try {
|
|
154
|
+
payload = await response.json();
|
|
155
|
+
} catch {
|
|
156
|
+
throw new ProviderDataError("GitHub Copilot status returned invalid JSON", "badjson");
|
|
157
|
+
}
|
|
158
|
+
if (!isRecord(payload)) {
|
|
159
|
+
throw new ProviderDataError("GitHub Copilot status returned an invalid usage response", "badjson");
|
|
160
|
+
}
|
|
161
|
+
const quotas = parseModelCatalog(isRecord(payload.modelCatalog) ? payload.modelCatalog : payload);
|
|
162
|
+
const modelQuotas = quotas.length > 0 ? quotas : parseQuotaByFeature(payload);
|
|
163
|
+
const entries: StatusEntry[] = [
|
|
164
|
+
{
|
|
165
|
+
kind: "text",
|
|
166
|
+
id: "plan",
|
|
167
|
+
label: "Plan",
|
|
168
|
+
value: isRecord(payload.modelCatalog) ? (planName(payload.modelCatalog) ?? "Unknown") : "Unknown",
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
for (const quota of modelQuotas) entries.push(windowEntry(quota));
|
|
172
|
+
if (entries.length === 1) {
|
|
173
|
+
entries.push({ kind: "text", id: "limits", label: "Limits", value: "not available" });
|
|
174
|
+
}
|
|
175
|
+
return { entries, updatedAt: context.now() };
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export function createGithubCopilotStatusAdapter(requestTimeoutMs: number): StatusAdapter {
|
|
180
|
+
return { ...githubCopilotStatusAdapter, requestTimeoutMs };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const githubCopilotStatusExtension = defineStatusExtension({
|
|
184
|
+
id: "github-copilot-status",
|
|
185
|
+
providerId: "github-copilot",
|
|
186
|
+
create: ({ statusRequestTimeoutMs }) => createGithubCopilotStatusAdapter(statusRequestTimeoutMs),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export default githubCopilotStatusExtension;
|