@hyav/pi-provider 0.1.3 → 0.1.4

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  This file is the authoritative user-facing release history for `@hyav/pi-provider`.
4
4
 
5
+ ## 0.1.4 - 2026-08-21
6
+
7
+ - Add Status and Preflight Adapters for the Vercel AI Gateway (auth, model catalog, and credits).
8
+ - Add Status and Preflight Adapters for Pi native providers Moonshot (Kimi) international and China platforms, and Hugging Face router (plan/credits via `whoami-v2`).
9
+ - Report Moonshot balance currency by platform: USD for the international platform and CNY for the China platform.
10
+ - Skip MiniMax Token Plan status: the documented `/coding_plan/remains` endpoint requires a web session cookie, not the API key, and its field semantics are known to be unreliable (see MiniMax-M2 issues #88 and #99).
11
+ - Add first-batch catalog Preflight Adapters for Pi native providers: OpenAI, Anthropic, Mistral, NVIDIA NIM, and Cerebras.
12
+ - Add a shared OpenAI-style catalog preflight helper (`createCatalogPreflightAdapter`).
13
+ - Add Status and Preflight Adapters for Pi native providers: Anthropic (subscription extra usage), OpenRouter (key credits and free tier), Groq (rate-limit headers), xAI (rate-limit headers), and GitHub Copilot (Individual plan quotas).
14
+ - Route Anthropic credentials by type: API keys (`sk-ant-api...`) use `x-api-key` for preflight catalog checks and default status without sending keys to subscription endpoints; OAuth tokens (`sk-ant-oat...`) query subscription usage and send `Authorization: Bearer`.
15
+ - Parse compound duration strings (`2m59.56s`, `7.66s`, `250ms`, `1d`, `1h30m`) and bare seconds in rate-limit reset headers for Groq and xAI status adapters.
16
+ - Expose the stored credential type (`oauth` vs `api_key`) to Status and Preflight Adapters through `getCredentialType()`.
17
+ - Align Charm Hyper status adapter cache TTL with standard status adapters (60s).
18
+
5
19
  ## 0.1.3 - 2026-08-17
6
20
 
7
21
  - Discover user Adapter files under `<agent-dir>/extensions/pi-provider/` in addition to built-ins; user files load last and override same-ID built-ins.
package/README.md CHANGED
@@ -14,6 +14,10 @@ A provider extension toolkit for [Pi](https://pi.dev). It registers LLM provider
14
14
  - Provider-first pricing metadata with optional OpenRouter completion and quality indicators
15
15
  - Explicit diagnostics: cached `/status`, free `/status refresh`, and potentially billable `/status check`
16
16
  - Built-in integrations for Charm Hyper, DeepSeek, Google Gemini, OpenAI Codex, OpenCode Zen, and OpenCode Go
17
+ - Status/preflight adapters for the native Pi providers Anthropic, GitHub Copilot, OpenRouter, Groq, and xAI
18
+ - Status/preflight adapters for Moonshot (Kimi) international and China platforms, and Hugging Face plan/credits
19
+ - Status/preflight adapters for the Vercel AI Gateway (auth, catalog, and credits)
20
+ - Catalog preflight adapters for the native Pi providers OpenAI, Anthropic, Mistral, NVIDIA NIM, and Cerebras
17
21
 
18
22
  ## Install
19
23
 
@@ -46,6 +50,7 @@ Use `/status refresh` for free endpoint, authentication, catalog, and account ch
46
50
  | Name | Required | Default | Effect |
47
51
  |---|---:|---|---|
48
52
  | `HYPER_API_KEY` | For Charm Hyper API-key auth | None | Supplies the built-in `charm-hyper` provider credential; OAuth users may use `/login` |
53
+ | `ANTHROPIC_USAGE_URL` | No | `https://claude.ai/api/usage` | Custom Anthropic usage endpoint; default endpoint is subscription OAuth only |
49
54
  | `PI_CODING_AGENT_DIR` | No | `~/.pi/agent` | Changes Pi's agent directory; public OpenRouter metadata is cached under `<agent-dir>/extensions/pi-provider/` |
50
55
 
51
56
  Programmatic integrations can configure pricing fallback, pricing policies, request timeouts, metadata URLs, and cache paths through `createPiProviderRuntime()` or `createPiProviderHost()`. Programmatic defaults resolve the agent directory from `PI_CODING_AGENT_DIR` (falling back to `~/.pi/agent`) and keep the OpenRouter metadata cache on disk under `<agent-dir>/extensions/pi-provider/`, matching the table above; the Pi entrypoint overrides it with Pi's own resolution. Host packages with a custom capability root can call `createPiProviderExtension({ adapterRoot, dependencies })`. The source definition [`PiProviderDependencies`](core/runtime-config.ts) is authoritative.
package/README.zh-CN.md CHANGED
@@ -14,6 +14,10 @@
14
14
  - 优先采用 Provider 价格元数据,并可由 OpenRouter 补全价格和质量指标
15
15
  - 显式诊断:缓存 `/status`、免费 `/status refresh` 和可能计费的 `/status check`
16
16
  - 内置 Charm Hyper、DeepSeek、Google Gemini、OpenAI Codex、OpenCode Zen 和 OpenCode Go 集成
17
+ - Status/Preflight 适配覆盖 Pi 原生 Provider:Anthropic、GitHub Copilot、OpenRouter、Groq、xAI
18
+ - Status/Preflight 适配覆盖 Moonshot(Kimi)国际/国内平台与 Hugging Face 套餐/额度
19
+ - Status/Preflight 适配覆盖 Vercel AI Gateway(鉴权、模型目录与额度)
20
+ - 目录 Preflight 适配覆盖 Pi 原生 Provider:OpenAI、Anthropic、Mistral、NVIDIA NIM、Cerebras
17
21
 
18
22
  ## 安装
19
23
 
@@ -46,6 +50,7 @@ pi install npm:@hyav/pi-provider
46
50
  | 名称 | 必需 | 默认值 | 作用 |
47
51
  |---|---:|---|---|
48
52
  | `HYPER_API_KEY` | Charm Hyper API Key 鉴权需要 | 无 | 为内置 `charm-hyper` Provider 提供凭据;OAuth 用户可以使用 `/login` |
53
+ | `ANTHROPIC_USAGE_URL` | 否 | `https://claude.ai/api/usage` | 自定义 Anthropic 用量端点;默认端点仅支持订阅 OAuth |
49
54
  | `PI_CODING_AGENT_DIR` | 否 | `~/.pi/agent` | 修改 Pi agent 目录;公开 OpenRouter 元数据缓存在 `<agent-dir>/extensions/pi-provider/` 下 |
50
55
 
51
56
  程序化集成可以通过 `createPiProviderRuntime()` 或 `createPiProviderHost()` 配置价格回退、价格策略、请求超时、元数据 URL 和缓存路径。程序化默认值会从 `PI_CODING_AGENT_DIR`(回退到 `~/.pi/agent`)解析 agent 目录,并保持 OpenRouter 元数据缓存落盘到 `<agent-dir>/extensions/pi-provider/`,与上表一致;Pi 入口会用 Pi 自身的解析覆盖它。使用自定义 capability 根目录的 Host 包可以调用 `createPiProviderExtension({ adapterRoot, dependencies })`。源码定义 [`PiProviderDependencies`](core/runtime-config.ts) 是权威依据。
@@ -0,0 +1,130 @@
1
+ /** Shared helpers for Provider-agnostic, OpenAI-style model-catalog checks. */
2
+
3
+ import { ProviderDataError } from "./errors.ts";
4
+ import type { PreflightAdapter } from "./preflight-manager.ts";
5
+ import { parseRetryAfter } from "./retry-after.ts";
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 interface CatalogPreflightConfig {
12
+ id: string;
13
+ providerId: string;
14
+ name: string;
15
+ modelsUrl: string;
16
+ /**
17
+ * Key header name such as "x-goog-api-key" or "x-api-key". When set, the
18
+ * key-only header is used. Defaults to Authorization Bearer.
19
+ */
20
+ keyHeader?: string;
21
+ /**
22
+ * Provider-specific authentication for dual-mode providers. When set it
23
+ * decides the request auth headers from the resolved credential type.
24
+ */
25
+ authHeaders?: (apiKey: string, credential: string | undefined) => Record<string, string>;
26
+ /** Additional static headers such as {"anthropic-version": "2023-06-01"}. */
27
+ headers?: Record<string, string>;
28
+ /** Filter for usable entries; defaults to accepting every record with a non-empty `id`. */
29
+ include?: (model: Record<string, unknown>) => boolean;
30
+ /** Credentials are required; when false (mock), the check drops "auth". */
31
+ requireAuth?: boolean;
32
+ }
33
+
34
+ export function createCatalogPreflightAdapter(
35
+ config: CatalogPreflightConfig,
36
+ requestTimeoutMs: number,
37
+ ): PreflightAdapter {
38
+ return {
39
+ id: config.id,
40
+ providerId: config.providerId,
41
+ name: config.name,
42
+ cacheTtlMs: 30_000,
43
+ requestTimeoutMs,
44
+ async fetch(context) {
45
+ const apiKey = await context.getApiKey();
46
+ const credential = context.getCredentialType
47
+ ? await context.getCredentialType().catch(() => undefined)
48
+ : undefined;
49
+ const headers: Record<string, string> = {
50
+ Accept: "application/json",
51
+ "Accept-Encoding": "identity",
52
+ ...(config.headers ?? {}),
53
+ };
54
+ if (apiKey && apiKey !== "proxy-managed") {
55
+ if (config.authHeaders) {
56
+ Object.assign(headers, config.authHeaders(apiKey, credential));
57
+ } else if (config.keyHeader) {
58
+ headers[config.keyHeader] = apiKey;
59
+ } else {
60
+ headers.Authorization = `Bearer ${apiKey}`;
61
+ }
62
+ }
63
+ const response = await context.fetch(config.modelsUrl, { headers, signal: context.signal });
64
+ if (!response.ok) {
65
+ throw new ProviderDataError(
66
+ `${config.name} preflight failed: HTTP ${response.status}`,
67
+ `http${response.status}`,
68
+ parseRetryAfter(response.headers.get("retry-after"), context.now()),
69
+ response.status,
70
+ );
71
+ }
72
+ let payload: unknown;
73
+ try {
74
+ payload = await response.json();
75
+ } catch {
76
+ throw new ProviderDataError(`${config.name} preflight returned invalid JSON`, "badjson");
77
+ }
78
+ if (!isRecord(payload) || !Array.isArray(payload.data)) {
79
+ throw new ProviderDataError(`${config.name} preflight returned invalid catalog data`, "badjson");
80
+ }
81
+ const modelIds = new Set(
82
+ payload.data
83
+ .filter(isRecord)
84
+ .filter((model) => config.include?.(model) ?? true)
85
+ .map((model) => (typeof model.id === "string" ? model.id.trim() : undefined))
86
+ .filter((id): id is string => id !== undefined && id !== ""),
87
+ );
88
+ const checks = config.requireAuth === false ? ["endpoint", "catalog"] : ["endpoint", "catalog", "auth"];
89
+ return {
90
+ passed: modelIds.has(context.model.id),
91
+ checks,
92
+ updatedAt: context.now(),
93
+ httpStatus: response.status,
94
+ };
95
+ },
96
+ };
97
+ }
98
+
99
+ /** Recognize the dependencies mock tag "mock" as a placeholder key. */
100
+ export function isUsableApiKey(apiKey: string | undefined): apiKey is string {
101
+ return apiKey !== undefined && apiKey !== "" && apiKey !== "proxy-managed" && apiKey !== "mock";
102
+ }
103
+
104
+ export function collectCatalogIds(
105
+ payload: unknown,
106
+ include?: (model: Record<string, unknown>) => boolean,
107
+ ): Set<string> {
108
+ if (!isRecord(payload) || !Array.isArray(payload.data)) {
109
+ throw new ProviderDataError("Catalog response returned invalid catalog data", "badjson");
110
+ }
111
+ return new Set(
112
+ payload.data
113
+ .filter(isRecord)
114
+ .filter((model) => include?.(model) ?? true)
115
+ .map((model) => (typeof model.id === "string" ? model.id.trim() : undefined))
116
+ .filter((id): id is string => id !== undefined && id !== ""),
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Workspace endpoint selected from the model's own baseUrl. Returns undefined
122
+ * when the convention cannot be resolved (e.g. Anthropic base without /v1).
123
+ */
124
+ export function workspaceModelsUrl(modelBaseUrl: string | undefined): string | undefined {
125
+ if (!modelBaseUrl) return undefined;
126
+ const trimmed = modelBaseUrl.replace(/\/+$/, "");
127
+ if (trimmed.startsWith("https://api.anthropic.com")) return `${trimmed}/v1/models`;
128
+ if (trimmed.includes("/v1")) return `${trimmed}/models`;
129
+ return undefined;
130
+ }
@@ -0,0 +1,13 @@
1
+ /** Non-secret credential type shared across the Status and Preflight managers. */
2
+
3
+ export function deriveCredentialType(metadata: unknown): string | undefined {
4
+ try {
5
+ if (metadata !== null && typeof metadata === "object" && "type" in metadata) {
6
+ const value = (metadata as { type?: unknown }).type;
7
+ return typeof value === "string" ? value : undefined;
8
+ }
9
+ } catch {
10
+ // Credential metadata must never break status or preflight rendering.
11
+ }
12
+ return undefined;
13
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { deriveCredentialType } from "./credential-type.ts";
2
3
  import { isValidTimeoutMs, withDeadline } from "./deadline.ts";
3
4
  import { isProviderDataError, ProviderDataError } from "./errors.ts";
4
5
 
@@ -12,6 +13,10 @@ export interface PreflightContext {
12
13
  signal?: AbortSignal;
13
14
  now: () => number;
14
15
  model: PreflightModel;
16
+ /** Optional non-secret credential metadata for provider-specific account labels. */
17
+ getCredentialMetadata?: () => unknown;
18
+ /** Optional non-secret credential type ("oauth" vs "api_key") for providers with dual auth modes. */
19
+ getCredentialType?: () => Promise<string | undefined>;
15
20
  }
16
21
 
17
22
  export interface PreflightSnapshot {
@@ -33,6 +38,8 @@ export interface PreflightAdapter {
33
38
  export interface PreflightContextLike {
34
39
  model: PreflightModel;
35
40
  modelRegistry: Pick<ModelRegistry, "getApiKeyForProvider">;
41
+ /** Optional non-secret credential metadata for provider-specific account labels. */
42
+ getCredentialMetadata?: () => unknown;
36
43
  }
37
44
 
38
45
  export interface PreflightErrorState {
@@ -182,6 +189,12 @@ export class PreflightManager {
182
189
  now: this.now,
183
190
  signal,
184
191
  model: ctx.model,
192
+ ...(ctx.getCredentialMetadata === undefined
193
+ ? {}
194
+ : {
195
+ getCredentialMetadata: ctx.getCredentialMetadata,
196
+ getCredentialType: async () => deriveCredentialType(ctx.getCredentialMetadata?.()),
197
+ }),
185
198
  }),
186
199
  adapter.requestTimeoutMs,
187
200
  cancellation.signal,
@@ -18,6 +18,7 @@ export {
18
18
  defineStatusExtension,
19
19
  defineTunerExtension,
20
20
  } from "./adapter-extensions.ts";
21
+ export { createCatalogPreflightAdapter } from "./catalog-preflight.ts";
21
22
  export { withDeadline } from "./deadline.ts";
22
23
  export { isProviderDataError, ProviderDataError } from "./errors.ts";
23
24
  export { createOpenCodeCatalogPreflightAdapter } from "./opencode-preflight.ts";
@@ -0,0 +1,72 @@
1
+ /** Shared parsing for OpenAI-style `x-ratelimit-*` response headers. */
2
+
3
+ export interface RateLimitWindow {
4
+ limit: number;
5
+ remaining: number;
6
+ resetAt?: number;
7
+ }
8
+
9
+ function numberValue(value: unknown): number | undefined {
10
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
11
+ if (typeof value === "string" && value.trim() !== "") {
12
+ const parsed = Number(value);
13
+ return Number.isFinite(parsed) ? parsed : undefined;
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ const DURATION_PATTERN = /^((?:\d+(?:\.\d+)?(?:ms|[smhd]))+)$/i;
19
+ const DURATION_PARTS = /(\d+(?:\.\d+)?)(ms|[smhd])/gi;
20
+ const UNIT_SECONDS: Record<string, number> = { ms: 0.001, s: 1, m: 60, h: 3_600, d: 86_400 };
21
+
22
+ export function durationSeconds(value: string): number | undefined {
23
+ const matched = DURATION_PATTERN.exec(value.trim());
24
+ if (!matched?.[1]) return undefined;
25
+ let total = 0;
26
+ for (const match of value.matchAll(DURATION_PARTS)) {
27
+ const amount = Number(match[1]);
28
+ if (!Number.isFinite(amount)) return undefined;
29
+ total += amount * (UNIT_SECONDS[match[2]!.toLowerCase()] ?? Number.NaN);
30
+ if (!Number.isFinite(total)) return undefined;
31
+ }
32
+ return total;
33
+ }
34
+
35
+ /**
36
+ * Reset hint in seconds. Providers use bare numbers, `7.66s`, and compound
37
+ * durations like Groq's `2m59.56s`; reset windows like `1d` also appear.
38
+ */
39
+ export function resetSecondsFromHeader(value: string | null): number | undefined {
40
+ if (value === null || value.trim() === "") return undefined;
41
+ const trimmed = value.trim();
42
+ const duration = durationSeconds(trimmed);
43
+ if (duration === undefined) {
44
+ const bare = numberValue(trimmed);
45
+ if (bare === undefined || bare <= 0) return undefined;
46
+ return bare;
47
+ }
48
+ if (duration <= 0) return undefined;
49
+ return duration;
50
+ }
51
+
52
+ function parseWindow(headers: Headers, kind: "requests" | "tokens", now: number): RateLimitWindow | undefined {
53
+ const limit = numberValue(headers.get(`x-ratelimit-limit-${kind}`));
54
+ const remaining = numberValue(headers.get(`x-ratelimit-remaining-${kind}`));
55
+ if (limit === undefined || remaining === undefined || limit <= 0) return undefined;
56
+ const reset = resetSecondsFromHeader(headers.get(`x-ratelimit-reset-${kind}`) ?? headers.get("x-ratelimit-reset"));
57
+ return {
58
+ limit,
59
+ remaining,
60
+ ...(reset !== undefined ? { resetAt: now + Math.round(reset * 1_000) } : {}),
61
+ };
62
+ }
63
+
64
+ export function parseRateLimitWindows(
65
+ headers: Headers,
66
+ now: number,
67
+ ): { requests?: RateLimitWindow; tokens?: RateLimitWindow } {
68
+ return {
69
+ requests: parseWindow(headers, "requests", now),
70
+ tokens: parseWindow(headers, "tokens", now),
71
+ };
72
+ }
package/core/runtime.ts CHANGED
@@ -46,10 +46,11 @@ function readProviderCredentialMetadata(
46
46
  ): unknown {
47
47
  try {
48
48
  const credential = readStoredCredential(provider);
49
- if (credential?.type !== "oauth") return undefined;
49
+ const type = credential?.type;
50
+ if (type !== "oauth" && type !== "api_key") return undefined;
50
51
  return {
51
- type: "oauth",
52
- ...(typeof credential.teamName === "string" ? { teamName: credential.teamName } : {}),
52
+ type,
53
+ ...(type === "oauth" && typeof credential?.teamName === "string" ? { teamName: credential.teamName } : {}),
53
54
  };
54
55
  } catch {
55
56
  return undefined;
@@ -337,13 +338,19 @@ export function installPiProviderRuntime(
337
338
  const { status, preflight, auth } = getStatusDetails(model, ctx);
338
339
  let liveCheckRequested = false;
339
340
  if ((mode === "refresh" || mode === "check") && auth.configured) {
341
+ const getCredentialMetadata = () =>
342
+ readProviderCredentialMetadata(model.provider, runtime.readStoredCredential);
340
343
  const statusContext: StatusContextLike = {
341
344
  model,
342
345
  modelRegistry: ctx.modelRegistry,
343
346
  getCredentialKey: () => ctx.modelRegistry.getApiKeyForProvider(model.provider),
344
- getCredentialMetadata: () => readProviderCredentialMetadata(model.provider, runtime.readStoredCredential),
347
+ getCredentialMetadata,
348
+ };
349
+ const preflightContext: PreflightContextLike = {
350
+ model,
351
+ modelRegistry: ctx.modelRegistry,
352
+ getCredentialMetadata,
345
353
  };
346
- const preflightContext: PreflightContextLike = { model, modelRegistry: ctx.modelRegistry };
347
354
  const refreshChecks: Array<Promise<unknown>> = [];
348
355
  if (status) refreshChecks.push(statusManager.update(statusContext, { force: true }));
349
356
  if (preflight) refreshChecks.push(preflightManager.update(preflightContext, { force: true }));
@@ -1,3 +1,4 @@
1
+ import { deriveCredentialType } from "./credential-type.ts";
1
2
  import { isValidTimeoutMs, withDeadline } from "./deadline.ts";
2
3
  import { isProviderDataError, ProviderDataError } from "./errors.ts";
3
4
  import type {
@@ -226,7 +227,12 @@ export class StatusManager {
226
227
  adapter.fetch({
227
228
  fetch: this.fetchFn,
228
229
  getApiKey: () => ctx.modelRegistry.getApiKeyForProvider(adapter.providerId),
229
- ...(ctx.getCredentialMetadata ? { getCredentialMetadata: ctx.getCredentialMetadata } : {}),
230
+ ...(ctx.getCredentialMetadata === undefined
231
+ ? {}
232
+ : {
233
+ getCredentialMetadata: ctx.getCredentialMetadata,
234
+ getCredentialType: async () => deriveCredentialType(ctx.getCredentialMetadata?.()),
235
+ }),
230
236
  now: this.now,
231
237
  signal,
232
238
  }),
package/core/types.ts CHANGED
@@ -147,6 +147,8 @@ export interface StatusContext {
147
147
  getApiKey: () => Promise<string | undefined>;
148
148
  /** Optional non-secret credential metadata for provider-specific account labels. */
149
149
  getCredentialMetadata?: () => unknown;
150
+ /** Optional non-secret credential type ("oauth" vs "api_key") for providers with dual auth modes. */
151
+ getCredentialType?: () => Promise<string | undefined>;
150
152
  signal?: AbortSignal;
151
153
  now: () => number;
152
154
  }
package/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  defineStatusExtension,
18
18
  defineTunerExtension,
19
19
  } from "./core/adapter-extensions.ts";
20
+ export { createCatalogPreflightAdapter } from "./core/catalog-preflight.ts";
20
21
  export { withDeadline } from "./core/deadline.ts";
21
22
  export type { ProviderDataErrorLike } from "./core/errors.ts";
22
23
  export { isProviderDataError, ProviderDataError } from "./core/errors.ts";
@@ -74,6 +75,7 @@ export type {
74
75
  export { getPreflightKey, normalizePreflightSnapshot, PreflightManager } from "./core/preflight-manager.ts";
75
76
  export { applyPricingAdjustment, resolvePricingDetails } from "./core/pricing-adjustments.ts";
76
77
  export { normalizeProviderModels } from "./core/provider-registration.ts";
78
+ export type { RateLimitWindow } from "./core/ratelimit-headers.ts";
77
79
  export { parseRetryAfter } from "./core/retry-after.ts";
78
80
  export type { StatusDiagnostics, StatusErrorState } from "./core/status-manager.ts";
79
81
  export { normalizeStatusSnapshot, StatusManager } from "./core/status-manager.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyav/pi-provider",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Provider extension toolkit for Pi to integrate and manage custom LLM providers with dynamic models, request tuners, and account status.",
5
5
  "author": "hyav",
6
6
  "license": "MIT",
@@ -0,0 +1,42 @@
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension } from "@hyav/pi-provider";
3
+ import { createCatalogPreflightAdapter } from "../core/catalog-preflight.ts";
4
+ import { isAnthropicApiKey, isAnthropicOAuthToken } from "../status/anthropic.ts";
5
+
6
+ export const ANTHROPIC_MODELS_URL = "https://api.anthropic.com/v1/models";
7
+ export const ANTHROPIC_API_VERSION = "2023-06-01";
8
+
9
+ /**
10
+ * Anthropic accepts API keys via x-api-key and short-lived OAuth/WIF tokens via
11
+ * Authorization Bearer. Only the user's own default clause receives the key, no
12
+ * unknown/third-party endpoint.
13
+ */
14
+ function anthropicAuthHeaders(apiKey: string, credential: string | undefined): Record<string, string> {
15
+ const isOAuth = credential === "oauth" ? !isAnthropicApiKey(apiKey) : isAnthropicOAuthToken(apiKey);
16
+ if (isOAuth) return { Authorization: `Bearer ${apiKey}` };
17
+ return { "x-api-key": apiKey };
18
+ }
19
+
20
+ export const anthropicPreflightAdapter: PreflightAdapter = createCatalogPreflightAdapter(
21
+ {
22
+ id: "anthropic-preflight",
23
+ providerId: "anthropic",
24
+ name: "Anthropic",
25
+ modelsUrl: ANTHROPIC_MODELS_URL,
26
+ headers: { "anthropic-version": ANTHROPIC_API_VERSION },
27
+ authHeaders: anthropicAuthHeaders,
28
+ },
29
+ 8_000,
30
+ );
31
+
32
+ export function createAnthropicPreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
33
+ return { ...anthropicPreflightAdapter, requestTimeoutMs };
34
+ }
35
+
36
+ const anthropicPreflightExtension = definePreflightExtension({
37
+ id: "anthropic-preflight",
38
+ providerId: "anthropic",
39
+ create: ({ statusRequestTimeoutMs }) => createAnthropicPreflightAdapter(statusRequestTimeoutMs),
40
+ });
41
+
42
+ export default anthropicPreflightExtension;
@@ -0,0 +1,27 @@
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension } from "@hyav/pi-provider";
3
+ import { createCatalogPreflightAdapter } from "../core/catalog-preflight.ts";
4
+
5
+ export const CEREBRAS_MODELS_URL = "https://api.cerebras.ai/v1/models";
6
+
7
+ export const cerebrasPreflightAdapter: PreflightAdapter = createCatalogPreflightAdapter(
8
+ {
9
+ id: "cerebras-preflight",
10
+ providerId: "cerebras",
11
+ name: "Cerebras",
12
+ modelsUrl: CEREBRAS_MODELS_URL,
13
+ },
14
+ 8_000,
15
+ );
16
+
17
+ export function createCerebrasPreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
18
+ return { ...cerebrasPreflightAdapter, requestTimeoutMs };
19
+ }
20
+
21
+ const cerebrasPreflightExtension = definePreflightExtension({
22
+ id: "cerebras-preflight",
23
+ providerId: "cerebras",
24
+ create: ({ statusRequestTimeoutMs }) => createCerebrasPreflightAdapter(statusRequestTimeoutMs),
25
+ });
26
+
27
+ export default cerebrasPreflightExtension;
@@ -0,0 +1,74 @@
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
+
4
+ export const COPILOT_MODELS_URL = "https://api.individual.githubcopilot.com/models";
5
+
6
+ function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+
10
+ export const githubCopilotPreflightAdapter: PreflightAdapter = {
11
+ id: "github-copilot-preflight",
12
+ providerId: "github-copilot",
13
+ name: "GitHub Copilot",
14
+ cacheTtlMs: 30_000,
15
+ requestTimeoutMs: 8_000,
16
+ async fetch(context) {
17
+ const apiKey = await context.getApiKey();
18
+ if (!apiKey || apiKey === "proxy-managed") {
19
+ return { passed: false, checks: ["auth"], updatedAt: context.now() };
20
+ }
21
+ const response = await context.fetch(COPILOT_MODELS_URL, {
22
+ headers: {
23
+ Accept: "application/json",
24
+ "Accept-Encoding": "identity",
25
+ Authorization: `Bearer ${apiKey}`,
26
+ "User-Agent": "@hyav/pi-provider",
27
+ },
28
+ signal: context.signal,
29
+ });
30
+ if (!response.ok) {
31
+ throw new ProviderDataError(
32
+ `GitHub Copilot preflight failed: HTTP ${response.status}`,
33
+ `http${response.status}`,
34
+ parseRetryAfter(response.headers.get("retry-after"), context.now()),
35
+ response.status,
36
+ );
37
+ }
38
+ let payload: unknown;
39
+ try {
40
+ payload = await response.json();
41
+ } catch {
42
+ throw new ProviderDataError("GitHub Copilot preflight returned invalid JSON", "badjson");
43
+ }
44
+ if (!isRecord(payload) || !Array.isArray(payload.data)) {
45
+ throw new ProviderDataError("GitHub Copilot preflight returned invalid catalog data", "badjson");
46
+ }
47
+ const modelIds = new Set(
48
+ payload.data.flatMap((model) => {
49
+ if (!isRecord(model) || typeof model.id !== "string") return [];
50
+ const id = model.id.trim();
51
+ if (id === "") return [];
52
+ return [id];
53
+ }),
54
+ );
55
+ return {
56
+ passed: modelIds.has(context.model.id),
57
+ checks: ["endpoint", "catalog", "auth"],
58
+ updatedAt: context.now(),
59
+ httpStatus: response.status,
60
+ };
61
+ },
62
+ };
63
+
64
+ export function createGithubCopilotPreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
65
+ return { ...githubCopilotPreflightAdapter, requestTimeoutMs };
66
+ }
67
+
68
+ const githubCopilotPreflightExtension = definePreflightExtension({
69
+ id: "github-copilot-preflight",
70
+ providerId: "github-copilot",
71
+ create: ({ statusRequestTimeoutMs }) => createGithubCopilotPreflightAdapter(statusRequestTimeoutMs),
72
+ });
73
+
74
+ export default githubCopilotPreflightExtension;
@@ -0,0 +1,72 @@
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
3
+ import { GROQ_MODELS_URL } from "../status/groq.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 groqPreflightAdapter: PreflightAdapter = {
10
+ id: "groq-preflight",
11
+ providerId: "groq",
12
+ name: "Groq",
13
+ cacheTtlMs: 30_000,
14
+ requestTimeoutMs: 8_000,
15
+ async fetch(context) {
16
+ const apiKey = await context.getApiKey();
17
+ if (!apiKey || apiKey === "proxy-managed") {
18
+ return { passed: false, checks: ["auth"], updatedAt: context.now() };
19
+ }
20
+ const response = await context.fetch(GROQ_MODELS_URL, {
21
+ headers: {
22
+ Accept: "application/json",
23
+ "Accept-Encoding": "identity",
24
+ Authorization: `Bearer ${apiKey}`,
25
+ },
26
+ signal: context.signal,
27
+ });
28
+ if (!response.ok) {
29
+ throw new ProviderDataError(
30
+ `Groq 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("Groq preflight returned invalid JSON", "badjson");
41
+ }
42
+ if (!isRecord(payload) || !Array.isArray(payload.data)) {
43
+ throw new ProviderDataError("Groq preflight returned invalid catalog data", "badjson");
44
+ }
45
+ const activeIds = new Set(
46
+ payload.data.flatMap((model) => {
47
+ if (!isRecord(model) || typeof model.id !== "string") return [];
48
+ const id = model.id.trim();
49
+ if (id === "" || model.active === false) return [];
50
+ return [id];
51
+ }),
52
+ );
53
+ return {
54
+ passed: activeIds.has(context.model.id),
55
+ checks: ["endpoint", "auth", "catalog"],
56
+ updatedAt: context.now(),
57
+ httpStatus: response.status,
58
+ };
59
+ },
60
+ };
61
+
62
+ export function createGroqPreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
63
+ return { ...groqPreflightAdapter, requestTimeoutMs };
64
+ }
65
+
66
+ const groqPreflightExtension = definePreflightExtension({
67
+ id: "groq-preflight",
68
+ providerId: "groq",
69
+ create: ({ statusRequestTimeoutMs }) => createGroqPreflightAdapter(statusRequestTimeoutMs),
70
+ });
71
+
72
+ export default groqPreflightExtension;
@@ -0,0 +1,27 @@
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension } from "@hyav/pi-provider";
3
+ import { createCatalogPreflightAdapter } from "../core/catalog-preflight.ts";
4
+
5
+ export const HF_ROUTER_MODELS_URL = "https://router.huggingface.co/v1/models";
6
+
7
+ export const huggingFacePreflightAdapter: PreflightAdapter = createCatalogPreflightAdapter(
8
+ {
9
+ id: "huggingface-preflight",
10
+ providerId: "huggingface",
11
+ name: "Hugging Face",
12
+ modelsUrl: HF_ROUTER_MODELS_URL,
13
+ },
14
+ 8_000,
15
+ );
16
+
17
+ export function createHuggingFacePreflightAdapter(requestTimeoutMs: number): PreflightAdapter {
18
+ return { ...huggingFacePreflightAdapter, requestTimeoutMs };
19
+ }
20
+
21
+ const huggingFacePreflightExtension = definePreflightExtension({
22
+ id: "huggingface-preflight",
23
+ providerId: "huggingface",
24
+ create: ({ statusRequestTimeoutMs }) => createHuggingFacePreflightAdapter(statusRequestTimeoutMs),
25
+ });
26
+
27
+ export default huggingFacePreflightExtension;