@hyav/pi-provider 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +5 -5
- package/README.zh-CN.md +5 -5
- package/core/adapter-extensions.ts +9 -2
- package/core/adapter-protocol.ts +4 -2
- package/core/catalog-preflight.ts +14 -1
- package/core/host.ts +43 -75
- package/core/model-catalog.ts +13 -1
- package/core/opencode-preflight.ts +6 -0
- package/core/pi-model-metadata.ts +739 -0
- package/core/provider-registration.ts +75 -78
- package/core/public-adapters.ts +9 -1
- package/core/runtime-config.ts +18 -38
- package/core/runtime-entry.ts +11 -6
- package/core/runtime.ts +24 -104
- package/core/status-report.ts +316 -236
- package/core/types.ts +28 -19
- package/index.ts +30 -21
- package/package.json +3 -1
- package/preflight/charm-hyper.ts +6 -2
- package/preflight/deepseek.ts +10 -1
- package/preflight/github-copilot.ts +4 -0
- package/preflight/google.ts +10 -1
- package/preflight/groq.ts +10 -1
- package/preflight/openai-codex.ts +10 -1
- package/preflight/openrouter.ts +11 -2
- package/preflight/vercel-ai-gateway.ts +11 -1
- package/preflight/xai.ts +18 -4
- package/providers/charm-hyper/oauth.ts +17 -11
- package/providers/charm-hyper.ts +26 -12
- package/status/huggingface.ts +2 -1
- package/status/openrouter.ts +8 -2
- package/status/vercel-ai-gateway.ts +1 -1
- package/core/official-pricing.ts +0 -923
package/core/types.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type {
|
|
|
4
4
|
ProviderConfig,
|
|
5
5
|
ProviderModelConfig,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
import type { ModelCatalogLifecycle } from "./model-catalog.ts";
|
|
9
|
+
import type { PiCatalogModelMeta, PiCatalogSnapshot } from "./pi-model-metadata.ts";
|
|
10
|
+
export type { PiCatalogModelMeta, PiCatalogSnapshot };
|
|
8
11
|
|
|
9
12
|
export type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
10
13
|
|
|
@@ -12,9 +15,17 @@ export type ProviderCost = ProviderModelConfig["cost"];
|
|
|
12
15
|
export type ProviderModel = ProviderModelConfig;
|
|
13
16
|
export type PricingSku = "input" | "output" | "cacheRead" | "cacheWrite";
|
|
14
17
|
/** Pricing provenance used by Pi Provider sidecars; not added to Pi model objects. */
|
|
15
|
-
export type ProviderPricingSource = "provider" | "fallback" | "official";
|
|
18
|
+
export type ProviderPricingSource = "provider" | "fallback" | "official" | "pi" | "mixed";
|
|
16
19
|
export type ModelPricingSource = ProviderPricingSource | "native";
|
|
17
|
-
export type ModelFieldSource =
|
|
20
|
+
export type ModelFieldSource =
|
|
21
|
+
| "provider"
|
|
22
|
+
| "pi"
|
|
23
|
+
| "native"
|
|
24
|
+
| "default"
|
|
25
|
+
| "normalized"
|
|
26
|
+
| "mixed"
|
|
27
|
+
| "official"
|
|
28
|
+
| "fallback";
|
|
18
29
|
export type ModelMetadataState = "fresh" | "stale" | "checking" | "unavailable";
|
|
19
30
|
|
|
20
31
|
export interface ModelMetadataStatus {
|
|
@@ -41,8 +52,16 @@ export interface ProviderRequestAuth {
|
|
|
41
52
|
env?: Record<string, string>;
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
export interface ModelCostBySkuSources {
|
|
56
|
+
input?: ModelFieldSource;
|
|
57
|
+
output?: ModelFieldSource;
|
|
58
|
+
cacheRead?: ModelFieldSource;
|
|
59
|
+
cacheWrite?: ModelFieldSource;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
export interface ModelFieldSources {
|
|
45
63
|
cost?: ModelFieldSource;
|
|
64
|
+
costBySku?: ModelCostBySkuSources;
|
|
46
65
|
contextWindow?: ModelFieldSource;
|
|
47
66
|
maxTokens?: ModelFieldSource;
|
|
48
67
|
input?: ModelFieldSource;
|
|
@@ -72,26 +91,12 @@ export interface ModelPricingDetails {
|
|
|
72
91
|
effectiveCost?: ProviderCost;
|
|
73
92
|
adjustment?: ProviderPricingAdjustment;
|
|
74
93
|
note?: string;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
export interface ModelQualityScore {
|
|
78
|
-
source: string;
|
|
79
|
-
benchmark: string;
|
|
80
|
-
category: string;
|
|
81
|
-
metric: "elo" | "rating" | "score" | "ips";
|
|
82
|
-
value: number;
|
|
83
|
-
rank?: number;
|
|
84
|
-
winRate?: number;
|
|
85
|
-
confidenceInterval?: {
|
|
86
|
-
lower: number;
|
|
87
|
-
upper: number;
|
|
88
|
-
};
|
|
94
|
+
costBySku?: ModelCostBySkuSources;
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
export interface ProviderModelMetadata {
|
|
92
98
|
pricing: ModelPricingDetails;
|
|
93
99
|
fieldSources?: ModelFieldSources;
|
|
94
|
-
quality?: ModelQualityScore[];
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
export type ProviderModelDraft = Partial<ProviderModel> &
|
|
@@ -199,12 +204,16 @@ export interface ProviderAdapter {
|
|
|
199
204
|
/** Optional explicit price adjustments owned by this Provider. */
|
|
200
205
|
pricing?: ProviderPricingPolicy;
|
|
201
206
|
catalog?: ModelCatalogStatus;
|
|
207
|
+
lifecycle?: ModelCatalogLifecycle;
|
|
208
|
+
/** Whether to fallback to Pi's builtin catalog when fields are missing. Defaults to true. */
|
|
209
|
+
usePiModelMetaFallback?: boolean;
|
|
202
210
|
/** @internal Draft state shared across isolated Adapter and Host contexts. */
|
|
203
211
|
registration?: {
|
|
204
212
|
modelDrafts: ProviderModelDraft[];
|
|
205
213
|
normalizedModels: ProviderModel[];
|
|
206
214
|
modelMetadata: Record<string, ProviderModelMetadata>;
|
|
207
|
-
|
|
215
|
+
piCatalog?: PiCatalogSnapshot;
|
|
216
|
+
officialPricing?: Record<string, any>;
|
|
208
217
|
activeRefreshes: number;
|
|
209
218
|
deferredRegistration?: () => void;
|
|
210
219
|
};
|
package/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as piBuiltinCatalog from "@earendil-works/pi-ai/providers/all";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
@@ -17,7 +18,7 @@ export {
|
|
|
17
18
|
defineStatusExtension,
|
|
18
19
|
defineTunerExtension,
|
|
19
20
|
} from "./core/adapter-extensions.ts";
|
|
20
|
-
export { MAX_PROVIDER_MODEL_COUNT } from "./core/adapter-validation.ts";
|
|
21
|
+
export { MAX_PROVIDER_MODEL_COUNT, validateProviderModelDrafts } from "./core/adapter-validation.ts";
|
|
21
22
|
export { createCatalogPreflightAdapter } from "./core/catalog-preflight.ts";
|
|
22
23
|
export { withDeadline } from "./core/deadline.ts";
|
|
23
24
|
export {
|
|
@@ -61,26 +62,18 @@ export type {
|
|
|
61
62
|
ModelCatalogLifecycleOptions,
|
|
62
63
|
} from "./core/model-catalog.ts";
|
|
63
64
|
export { createModelCatalogLifecycle } from "./core/model-catalog.ts";
|
|
64
|
-
export {
|
|
65
|
-
applyOfficialModelCosts,
|
|
66
|
-
applyOfficialModelMetadata,
|
|
67
|
-
clearPricingCache,
|
|
68
|
-
fetchOfficialModelMetadata,
|
|
69
|
-
fetchOfficialPricing,
|
|
70
|
-
findOfficialCost,
|
|
71
|
-
findOfficialMeta,
|
|
72
|
-
getDefaultOpenRouterMetadataCachePath,
|
|
73
|
-
getPricingCache,
|
|
74
|
-
getPricingCacheAge,
|
|
75
|
-
type OfficialModelMeta,
|
|
76
|
-
type OfficialModelMetadataFetchOptions,
|
|
77
|
-
type OfficialPricingFetchOptions,
|
|
78
|
-
OPENROUTER_MODELS_URL,
|
|
79
|
-
parseOpenRouterModels,
|
|
80
|
-
parseOpenRouterPricing,
|
|
81
|
-
setPricingCache,
|
|
82
|
-
} from "./core/official-pricing.ts";
|
|
83
65
|
export { createOpenCodeCatalogPreflightAdapter } from "./core/opencode-preflight.ts";
|
|
66
|
+
export type { PiCatalogModelMeta, PiCatalogSnapshot } from "./core/pi-model-metadata.ts";
|
|
67
|
+
export {
|
|
68
|
+
findPiCatalogModel,
|
|
69
|
+
isLegacyNormalizedModel,
|
|
70
|
+
isLegacyNormalizedSnapshot,
|
|
71
|
+
loadPiCatalog,
|
|
72
|
+
mergeModelWithPiCatalog,
|
|
73
|
+
ORIGINAL_PI_PROVIDER_ALLOWLIST,
|
|
74
|
+
parsePiCatalogFromProviders,
|
|
75
|
+
toPiCatalogSnapshot,
|
|
76
|
+
} from "./core/pi-model-metadata.ts";
|
|
84
77
|
export type {
|
|
85
78
|
PreflightAdapter,
|
|
86
79
|
PreflightContext,
|
|
@@ -97,18 +90,23 @@ export type { RateLimitWindow } from "./core/ratelimit-headers.ts";
|
|
|
97
90
|
export { parseRetryAfter } from "./core/retry-after.ts";
|
|
98
91
|
export type { StatusDiagnostics, StatusErrorState } from "./core/status-manager.ts";
|
|
99
92
|
export { normalizeStatusSnapshot, StatusManager } from "./core/status-manager.ts";
|
|
93
|
+
export {
|
|
94
|
+
formatProviderStatus,
|
|
95
|
+
getStatusModeCompletions,
|
|
96
|
+
parseStatusMode,
|
|
97
|
+
} from "./core/status-report.ts";
|
|
100
98
|
export { applyTunerAdapters, sortTunerAdapters } from "./core/tuner-manager.ts";
|
|
101
99
|
export type {
|
|
102
100
|
ActiveModel,
|
|
103
101
|
ModelCatalogSource,
|
|
104
102
|
ModelCatalogStatus,
|
|
103
|
+
ModelCostBySkuSources,
|
|
105
104
|
ModelFieldSource,
|
|
106
105
|
ModelFieldSources,
|
|
107
106
|
ModelMetadataState,
|
|
108
107
|
ModelMetadataStatus,
|
|
109
108
|
ModelPricingDetails,
|
|
110
109
|
ModelPricingSource,
|
|
111
|
-
ModelQualityScore,
|
|
112
110
|
PiApi,
|
|
113
111
|
PricingSku,
|
|
114
112
|
ProviderAdapter,
|
|
@@ -156,6 +154,11 @@ export function createPiProviderExtension(
|
|
|
156
154
|
wrapTextWithAnsi: typeof wrapTextWithAnsi;
|
|
157
155
|
adapterRoot?: string;
|
|
158
156
|
dependencies?: Partial<PiProviderDependencies>;
|
|
157
|
+
piCatalogSource: {
|
|
158
|
+
getBuiltinProviders: () => string[];
|
|
159
|
+
getBuiltinModels: (provider: string) => unknown[];
|
|
160
|
+
getBuiltinModelDataGeneratedAt: () => number | undefined;
|
|
161
|
+
};
|
|
159
162
|
},
|
|
160
163
|
) => Promise<void>;
|
|
161
164
|
};
|
|
@@ -165,6 +168,12 @@ export function createPiProviderExtension(
|
|
|
165
168
|
wrapTextWithAnsi,
|
|
166
169
|
adapterRoot: options.adapterRoot,
|
|
167
170
|
dependencies: options.dependencies,
|
|
171
|
+
piCatalogSource: {
|
|
172
|
+
getBuiltinProviders: () => piBuiltinCatalog.getBuiltinProviders(),
|
|
173
|
+
getBuiltinModels: (provider) =>
|
|
174
|
+
piBuiltinCatalog.getBuiltinModels(provider as Parameters<typeof piBuiltinCatalog.getBuiltinModels>[0]),
|
|
175
|
+
getBuiltinModelDataGeneratedAt: () => piBuiltinCatalog.getBuiltinModelDataGeneratedAt?.(),
|
|
176
|
+
},
|
|
168
177
|
});
|
|
169
178
|
};
|
|
170
179
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyav/pi-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -44,11 +44,13 @@
|
|
|
44
44
|
"node": ">=22.19.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-ai": "*",
|
|
47
48
|
"@earendil-works/pi-coding-agent": "*",
|
|
48
49
|
"@earendil-works/pi-tui": "*"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@biomejs/biome": "2.3.5",
|
|
53
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
52
54
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
53
55
|
"@earendil-works/pi-tui": "0.84.1",
|
|
54
56
|
"@types/node": "22.20.1",
|
package/preflight/charm-hyper.ts
CHANGED
|
@@ -13,9 +13,13 @@ export function createCharmHyperPreflightAdapter(requestTimeoutMs: number): Pref
|
|
|
13
13
|
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, HYPER_PROVIDER_URL),
|
|
14
14
|
async fetch(context) {
|
|
15
15
|
const apiKey = await context.getApiKey();
|
|
16
|
-
|
|
16
|
+
// "proxy-managed" is not a usable credential for the official endpoints,
|
|
17
|
+
// so it fails closed together with a missing key.
|
|
18
|
+
if (!apiKey || apiKey === "proxy-managed") {
|
|
19
|
+
return { passed: false, checks: ["auth"], updatedAt: context.now() };
|
|
20
|
+
}
|
|
17
21
|
const headers = new Headers(hyperJsonHeaders());
|
|
18
|
-
|
|
22
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
19
23
|
let endpoint = HYPER_PROVIDER_URL;
|
|
20
24
|
let response = await context.fetch(endpoint, { headers, signal: context.signal });
|
|
21
25
|
if (response.status === 404) {
|
package/preflight/deepseek.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
hasBaseUrlOrigin,
|
|
5
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
6
|
+
ProviderDataError,
|
|
7
|
+
parseRetryAfter,
|
|
8
|
+
} from "@hyav/pi-provider";
|
|
3
9
|
|
|
4
10
|
export const DEEPSEEK_MODELS_URL = "https://api.deepseek.com/models";
|
|
5
11
|
|
|
@@ -44,6 +50,9 @@ export const deepSeekPreflightAdapter: PreflightAdapter = {
|
|
|
44
50
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
45
51
|
throw new ProviderDataError("DeepSeek preflight returned invalid catalog data", "badjson");
|
|
46
52
|
}
|
|
53
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
54
|
+
throw new ProviderDataError("DeepSeek preflight catalog exceeds the maximum model count", "badjson");
|
|
55
|
+
}
|
|
47
56
|
const modelIds = new Set(
|
|
48
57
|
payload.data
|
|
49
58
|
.filter(isRecord)
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
authDefinesHeader,
|
|
5
5
|
definePreflightExtension,
|
|
6
6
|
getContextAuth,
|
|
7
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
7
8
|
mergeDiagnosticHeaders,
|
|
8
9
|
ProviderDataError,
|
|
9
10
|
parseRetryAfter,
|
|
@@ -57,6 +58,9 @@ export const githubCopilotPreflightAdapter: PreflightAdapter = {
|
|
|
57
58
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
58
59
|
throw new ProviderDataError("GitHub Copilot preflight returned invalid catalog data", "badjson");
|
|
59
60
|
}
|
|
61
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
62
|
+
throw new ProviderDataError("GitHub Copilot preflight catalog exceeds the maximum model count", "badjson");
|
|
63
|
+
}
|
|
60
64
|
const modelIds = new Set(
|
|
61
65
|
payload.data.flatMap((model) => {
|
|
62
66
|
if (!isRecord(model) || typeof model.id !== "string") return [];
|
package/preflight/google.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
hasBaseUrlOrigin,
|
|
5
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
6
|
+
ProviderDataError,
|
|
7
|
+
parseRetryAfter,
|
|
8
|
+
} from "@hyav/pi-provider";
|
|
3
9
|
|
|
4
10
|
export const GOOGLE_MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
5
11
|
|
|
@@ -59,6 +65,9 @@ export const googlePreflightAdapter: PreflightAdapter = {
|
|
|
59
65
|
if (!isRecord(payload) || !Array.isArray(payload.models)) {
|
|
60
66
|
throw new ProviderDataError("Google preflight returned invalid catalog data", "badjson");
|
|
61
67
|
}
|
|
68
|
+
if (payload.models.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
69
|
+
throw new ProviderDataError("Google preflight catalog exceeds the maximum model count", "badjson");
|
|
70
|
+
}
|
|
62
71
|
const modelIds = new Set(
|
|
63
72
|
payload.models.flatMap((value) => {
|
|
64
73
|
if (!isRecord(value)) return [];
|
package/preflight/groq.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
hasBaseUrlOrigin,
|
|
5
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
6
|
+
ProviderDataError,
|
|
7
|
+
parseRetryAfter,
|
|
8
|
+
} from "@hyav/pi-provider";
|
|
3
9
|
import { GROQ_MODELS_URL } from "../status/groq.ts";
|
|
4
10
|
|
|
5
11
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -43,6 +49,9 @@ export const groqPreflightAdapter: PreflightAdapter = {
|
|
|
43
49
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
44
50
|
throw new ProviderDataError("Groq preflight returned invalid catalog data", "badjson");
|
|
45
51
|
}
|
|
52
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
53
|
+
throw new ProviderDataError("Groq preflight catalog exceeds the maximum model count", "badjson");
|
|
54
|
+
}
|
|
46
55
|
const activeIds = new Set(
|
|
47
56
|
payload.data.flatMap((model) => {
|
|
48
57
|
if (!isRecord(model) || typeof model.id !== "string") return [];
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
hasBaseUrlOrigin,
|
|
5
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
6
|
+
ProviderDataError,
|
|
7
|
+
parseRetryAfter,
|
|
8
|
+
} from "@hyav/pi-provider";
|
|
3
9
|
import { extractCodexAccountId } from "../status/openai-codex.ts";
|
|
4
10
|
|
|
5
11
|
export const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
|
|
@@ -13,6 +19,9 @@ function codexModelIds(payload: unknown): Set<string> {
|
|
|
13
19
|
if (!isRecord(payload) || !Array.isArray(payload.models)) {
|
|
14
20
|
throw new ProviderDataError("OpenAI Codex preflight returned invalid catalog data", "badjson");
|
|
15
21
|
}
|
|
22
|
+
if (payload.models.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
23
|
+
throw new ProviderDataError("OpenAI Codex preflight catalog exceeds the maximum model count", "badjson");
|
|
24
|
+
}
|
|
16
25
|
return new Set(
|
|
17
26
|
payload.models
|
|
18
27
|
.filter(isRecord)
|
package/preflight/openrouter.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
hasBaseUrlOrigin,
|
|
5
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
6
|
+
ProviderDataError,
|
|
7
|
+
parseRetryAfter,
|
|
8
|
+
} from "@hyav/pi-provider";
|
|
3
9
|
import { OPENROUTER_KEY_URL } from "../status/openrouter.ts";
|
|
4
10
|
|
|
5
11
|
export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
@@ -34,6 +40,9 @@ async function collectModelIds(context: Parameters<PreflightAdapter["fetch"]>[0]
|
|
|
34
40
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
35
41
|
throw new ProviderDataError("OpenRouter preflight returned invalid catalog data", "badjson");
|
|
36
42
|
}
|
|
43
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
44
|
+
throw new ProviderDataError("OpenRouter preflight catalog exceeds the maximum model count", "badjson");
|
|
45
|
+
}
|
|
37
46
|
return new Set(
|
|
38
47
|
payload.data
|
|
39
48
|
.filter(isRecord)
|
|
@@ -85,7 +94,7 @@ export const openRouterPreflightAdapter: PreflightAdapter = {
|
|
|
85
94
|
const apiKey = await context.getApiKey();
|
|
86
95
|
const checks: string[] = ["endpoint", "catalog"];
|
|
87
96
|
if (!apiKey || apiKey === "proxy-managed") {
|
|
88
|
-
return { passed:
|
|
97
|
+
return { passed: false, checks: [...checks, "auth"], updatedAt: context.now() };
|
|
89
98
|
}
|
|
90
99
|
// Management keys use /api/v1/credits as the only key endpoint and get 404 here.
|
|
91
100
|
const authStatus = await checkCredential(context, apiKey);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { PreflightAdapter, PreflightSnapshot } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
5
|
+
ProviderDataError,
|
|
6
|
+
parseRetryAfter,
|
|
7
|
+
} from "@hyav/pi-provider";
|
|
3
8
|
import { VERCEL_PROVIDER_ID } from "../status/vercel-ai-gateway/constants.ts";
|
|
4
9
|
|
|
5
10
|
export const VERCEL_MODELS_URL = "https://ai-gateway.vercel.sh/v1/models";
|
|
@@ -12,6 +17,9 @@ export function parseVercelModelIds(payload: unknown): Set<string> {
|
|
|
12
17
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
13
18
|
throw new ProviderDataError("Vercel AI Gateway preflight returned invalid catalog data", "badjson");
|
|
14
19
|
}
|
|
20
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
21
|
+
throw new ProviderDataError("Vercel AI Gateway preflight catalog exceeds the maximum model count", "badjson");
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
const modelIds = new Set<string>();
|
|
17
25
|
for (const value of payload.data) {
|
|
@@ -37,6 +45,8 @@ export function createVercelAIGatewayPreflightAdapter(requestTimeoutMs: number):
|
|
|
37
45
|
cacheTtlMs: 30_000,
|
|
38
46
|
requestTimeoutMs,
|
|
39
47
|
async fetch(context): Promise<PreflightSnapshot> {
|
|
48
|
+
// Vercel's gateway catalog is public and needs no credential; the
|
|
49
|
+
// check intentionally reports no "auth" check.
|
|
40
50
|
const response = await context.fetch(VERCEL_MODELS_URL, {
|
|
41
51
|
headers: {
|
|
42
52
|
Accept: "application/json",
|
package/preflight/xai.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { PreflightAdapter } from "@hyav/pi-provider";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
definePreflightExtension,
|
|
4
|
+
hasBaseUrlOrigin,
|
|
5
|
+
MAX_PROVIDER_MODEL_COUNT,
|
|
6
|
+
ProviderDataError,
|
|
7
|
+
parseRetryAfter,
|
|
8
|
+
} from "@hyav/pi-provider";
|
|
3
9
|
import { XAI_MODELS_URL } from "../status/xai.ts";
|
|
4
10
|
|
|
5
11
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -15,12 +21,18 @@ export const xaiPreflightAdapter: PreflightAdapter = {
|
|
|
15
21
|
supportsModel: (model) => hasBaseUrlOrigin(model.baseUrl, XAI_MODELS_URL),
|
|
16
22
|
async fetch(context) {
|
|
17
23
|
const apiKey = await context.getApiKey();
|
|
24
|
+
if (!apiKey || apiKey === "proxy-managed") {
|
|
25
|
+
// xAI authenticates every route, including /v1/models, so without a
|
|
26
|
+
// credential the catalog request cannot succeed. Fail closed instead
|
|
27
|
+
// of reporting a catalog match as a usable model.
|
|
28
|
+
return { passed: false, checks: ["auth"], updatedAt: context.now() };
|
|
29
|
+
}
|
|
18
30
|
const authHeaders: Record<string, string> = {
|
|
19
31
|
Accept: "application/json",
|
|
20
32
|
"Accept-Encoding": "identity",
|
|
21
33
|
"User-Agent": "@hyav/pi-provider",
|
|
34
|
+
Authorization: `Bearer ${apiKey}`,
|
|
22
35
|
};
|
|
23
|
-
if (apiKey && apiKey !== "proxy-managed") authHeaders.Authorization = `Bearer ${apiKey}`;
|
|
24
36
|
const response = await context.fetch(XAI_MODELS_URL, {
|
|
25
37
|
headers: authHeaders,
|
|
26
38
|
signal: context.signal,
|
|
@@ -42,16 +54,18 @@ export const xaiPreflightAdapter: PreflightAdapter = {
|
|
|
42
54
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
43
55
|
throw new ProviderDataError("xAI preflight returned invalid catalog data", "badjson");
|
|
44
56
|
}
|
|
57
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
58
|
+
throw new ProviderDataError("xAI preflight catalog exceeds the maximum model count", "badjson");
|
|
59
|
+
}
|
|
45
60
|
const modelIds = new Set(
|
|
46
61
|
payload.data
|
|
47
62
|
.filter(isRecord)
|
|
48
63
|
.map((model) => (typeof model.id === "string" ? model.id.trim() : undefined))
|
|
49
64
|
.filter((id): id is string => id !== undefined && id !== ""),
|
|
50
65
|
);
|
|
51
|
-
const checks = apiKey && apiKey !== "proxy-managed" ? ["endpoint", "catalog", "auth"] : ["endpoint", "catalog"];
|
|
52
66
|
return {
|
|
53
67
|
passed: modelIds.has(context.model.id),
|
|
54
|
-
checks,
|
|
68
|
+
checks: ["endpoint", "catalog", "auth"],
|
|
55
69
|
updatedAt: context.now(),
|
|
56
70
|
httpStatus: response.status,
|
|
57
71
|
};
|
|
@@ -156,21 +156,20 @@ function parseDevicePollResponse(payload: unknown): DevicePollResult {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
function parseTokenExchangeResponse(payload: unknown): TokenExchangeResponse {
|
|
159
|
+
const allowedKeys = ["access_token", "token_type", "refresh_token", "expiry", "expires_in", "expires_at"] as const;
|
|
159
160
|
if (
|
|
160
161
|
!isRecord(payload) ||
|
|
162
|
+
!hasOnlyKeys(payload, allowedKeys) ||
|
|
161
163
|
!nonEmptyString(payload.access_token) ||
|
|
162
164
|
!nonEmptyString(payload.token_type) ||
|
|
163
165
|
!nonEmptyString(payload.refresh_token) ||
|
|
164
|
-
|
|
166
|
+
typeof payload.expiry !== "string"
|
|
165
167
|
) {
|
|
166
168
|
throw new Error("Charm Hyper token exchange response is invalid");
|
|
167
169
|
}
|
|
168
170
|
if (Object.hasOwn(payload, "expires_in")) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
!positiveInteger(payload.expires_in) ||
|
|
172
|
-
Object.hasOwn(payload, "expires_at")
|
|
173
|
-
) {
|
|
171
|
+
// expires_in is authoritative when other absolute forms are also present.
|
|
172
|
+
if (!positiveInteger(payload.expires_in)) {
|
|
174
173
|
throw new Error("Charm Hyper token exchange response has an invalid expiry");
|
|
175
174
|
}
|
|
176
175
|
return {
|
|
@@ -180,10 +179,7 @@ function parseTokenExchangeResponse(payload: unknown): TokenExchangeResponse {
|
|
|
180
179
|
};
|
|
181
180
|
}
|
|
182
181
|
if (Object.hasOwn(payload, "expires_at")) {
|
|
183
|
-
if (
|
|
184
|
-
!hasOnlyKeys(payload, ["access_token", "token_type", "refresh_token", "expiry", "expires_at"]) ||
|
|
185
|
-
!positiveInteger(payload.expires_at)
|
|
186
|
-
) {
|
|
182
|
+
if (!positiveInteger(payload.expires_at)) {
|
|
187
183
|
throw new Error("Charm Hyper token exchange response has an invalid expiry");
|
|
188
184
|
}
|
|
189
185
|
return {
|
|
@@ -192,7 +188,17 @@ function parseTokenExchangeResponse(payload: unknown): TokenExchangeResponse {
|
|
|
192
188
|
expiresAtSeconds: payload.expires_at,
|
|
193
189
|
};
|
|
194
190
|
}
|
|
195
|
-
|
|
191
|
+
// Some responses only carry the ISO `expiry` timestamp; derive the
|
|
192
|
+
// absolute expiry from it when no numeric form is present.
|
|
193
|
+
const expiryMs = typeof payload.expiry === "string" ? Date.parse(payload.expiry) : NaN;
|
|
194
|
+
if (!Number.isFinite(expiryMs)) {
|
|
195
|
+
throw new Error("Charm Hyper token exchange response has an invalid expiry");
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
accessToken: payload.access_token,
|
|
199
|
+
refreshToken: payload.refresh_token,
|
|
200
|
+
expiresAtSeconds: Math.floor(expiryMs / 1_000),
|
|
201
|
+
};
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
async function initiateDeviceAuth(
|
package/providers/charm-hyper.ts
CHANGED
|
@@ -9,10 +9,11 @@ import type {
|
|
|
9
9
|
import {
|
|
10
10
|
createModelCatalogLifecycle,
|
|
11
11
|
defineProviderExtension,
|
|
12
|
+
isLegacyNormalizedSnapshot,
|
|
12
13
|
isProviderDataError,
|
|
13
14
|
MAX_PROVIDER_MODEL_COUNT,
|
|
14
|
-
normalizeProviderModels,
|
|
15
15
|
ProviderDataError,
|
|
16
|
+
validateProviderModelDrafts,
|
|
16
17
|
withDeadline,
|
|
17
18
|
} from "@hyav/pi-provider";
|
|
18
19
|
import { HYPER_BASE_URL, HYPER_USER_AGENT, hyperJsonHeaders } from "./charm-hyper/constants.ts";
|
|
@@ -98,7 +99,8 @@ function parseCurrentHyperModel(value: unknown): ProviderModelDraft | undefined
|
|
|
98
99
|
if (typeof value.name !== "string" || value.name.trim() === "") return undefined;
|
|
99
100
|
if (!isFiniteNonNegative(value.cost_per_1m_in)) return undefined;
|
|
100
101
|
if (!isFiniteNonNegative(value.cost_per_1m_out)) return undefined;
|
|
101
|
-
|
|
102
|
+
// Cached prices are optional; the manifest documents 0/missing as no discount.
|
|
103
|
+
if (value.cost_per_1m_in_cached !== undefined && !isFiniteNonNegative(value.cost_per_1m_in_cached)) return undefined;
|
|
102
104
|
if (value.cost_per_1m_out_cached !== undefined && !isFiniteNonNegative(value.cost_per_1m_out_cached))
|
|
103
105
|
return undefined;
|
|
104
106
|
if (!isPositiveInteger(value.context_window) || !isPositiveInteger(value.default_max_tokens)) return undefined;
|
|
@@ -137,8 +139,10 @@ function parseCurrentHyperModel(value: unknown): ProviderModelDraft | undefined
|
|
|
137
139
|
cost: {
|
|
138
140
|
input: value.cost_per_1m_in,
|
|
139
141
|
output: value.cost_per_1m_out,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
// Official Charm mapping: the cached-output price is the cache-read
|
|
143
|
+
// price and the cached-input price is the cache-write price.
|
|
144
|
+
cacheRead: value.cost_per_1m_out_cached ?? 0,
|
|
145
|
+
cacheWrite: value.cost_per_1m_in_cached ?? 0,
|
|
142
146
|
},
|
|
143
147
|
contextWindow: value.context_window,
|
|
144
148
|
maxTokens: value.default_max_tokens,
|
|
@@ -308,7 +312,7 @@ async function discoverHyperModels(
|
|
|
308
312
|
if (parsed.models.length === 0) {
|
|
309
313
|
throw new ProviderDataError("Charm Hyper model discovery returned no valid models", "badjson");
|
|
310
314
|
}
|
|
311
|
-
|
|
315
|
+
validateProviderModelDrafts(parsed.models);
|
|
312
316
|
return parsed;
|
|
313
317
|
},
|
|
314
318
|
timeoutMs,
|
|
@@ -330,15 +334,18 @@ function catalogErrorCode(error: unknown): string {
|
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
type HyperModelsStoreEntry = ProviderRefreshContext["stored"];
|
|
333
|
-
type HyperStoredModel =
|
|
334
|
-
|
|
337
|
+
type HyperStoredModel = ProviderModelDraft & {
|
|
338
|
+
provider: string;
|
|
339
|
+
baseUrl: string;
|
|
340
|
+
api: ProviderModelDraft["api"];
|
|
335
341
|
};
|
|
336
342
|
|
|
337
343
|
function draftsFromStoredModels(entry: HyperModelsStoreEntry): ProviderModelDraft[] | undefined {
|
|
338
344
|
if (!entry || !Array.isArray(entry.models) || entry.models.length === 0) return undefined;
|
|
345
|
+
if (isLegacyNormalizedSnapshot(entry.models)) return undefined;
|
|
339
346
|
try {
|
|
340
347
|
const drafts: ProviderModelDraft[] = entry.models.map(({ provider: _provider, ...model }) => model);
|
|
341
|
-
|
|
348
|
+
validateProviderModelDrafts(drafts);
|
|
342
349
|
return drafts;
|
|
343
350
|
} catch {
|
|
344
351
|
return undefined;
|
|
@@ -346,10 +353,12 @@ function draftsFromStoredModels(entry: HyperModelsStoreEntry): ProviderModelDraf
|
|
|
346
353
|
}
|
|
347
354
|
|
|
348
355
|
function storedModelsFromDrafts(models: ProviderModelDraft[]): HyperStoredModel[] {
|
|
349
|
-
|
|
350
|
-
|
|
356
|
+
validateProviderModelDrafts(models);
|
|
357
|
+
return models.map((model) => {
|
|
358
|
+
const source = model.pricingSource;
|
|
351
359
|
return {
|
|
352
360
|
...model,
|
|
361
|
+
name: typeof model.name === "string" && model.name.trim() !== "" ? model.name.trim() : model.id,
|
|
353
362
|
...(source ? { pricingSource: source } : {}),
|
|
354
363
|
api: model.api ?? "openai-completions",
|
|
355
364
|
provider: "charm-hyper",
|
|
@@ -362,14 +371,19 @@ export function createCharmHyperAdapter(
|
|
|
362
371
|
fetchFn: typeof globalThis.fetch,
|
|
363
372
|
discoveryTimeoutMs: number,
|
|
364
373
|
now: () => number = Date.now,
|
|
374
|
+
initialModels?: ProviderModelDraft[],
|
|
365
375
|
): ProviderAdapter {
|
|
366
376
|
let provider: ProviderAdapter["provider"];
|
|
367
377
|
const lifecycle = createModelCatalogLifecycle({
|
|
378
|
+
initialModels,
|
|
368
379
|
ttlMs: HYPER_MODEL_CATALOG_TTL_MS,
|
|
369
380
|
now,
|
|
370
381
|
discover: (context) => discoverHyperModels(fetchFn, discoveryTimeoutMs, context.signal),
|
|
371
382
|
restore: draftsFromStoredModels,
|
|
372
|
-
persist: (models, checkedAt) => ({
|
|
383
|
+
persist: (models, checkedAt) => ({
|
|
384
|
+
models: storedModelsFromDrafts(models) as unknown as NonNullable<HyperModelsStoreEntry>["models"],
|
|
385
|
+
checkedAt,
|
|
386
|
+
}),
|
|
373
387
|
onUpdate: (models) => {
|
|
374
388
|
if (provider) provider.models = models;
|
|
375
389
|
},
|
|
@@ -387,7 +401,7 @@ export function createCharmHyperAdapter(
|
|
|
387
401
|
oauth: createCharmHyperOAuth(fetchFn, now),
|
|
388
402
|
};
|
|
389
403
|
|
|
390
|
-
return { id: "charm-hyper", catalog: lifecycle.catalog, provider };
|
|
404
|
+
return { id: "charm-hyper", catalog: lifecycle.catalog, lifecycle, provider };
|
|
391
405
|
}
|
|
392
406
|
|
|
393
407
|
const charmHyperProviderExtension = defineProviderExtension({
|
package/status/huggingface.ts
CHANGED
|
@@ -31,10 +31,11 @@ export function parseHuggingFaceAccount(payload: unknown): HuggingFaceAccount {
|
|
|
31
31
|
}
|
|
32
32
|
// Response envelope: { type, id, name, emailVerified, canPay, isPro, plan, periodEnd, credits, ... }
|
|
33
33
|
const plan = safeText(payload.plan);
|
|
34
|
+
const credits = finiteNumber(payload.credits);
|
|
34
35
|
// Older token generations omit the envelope fields entirely.
|
|
35
36
|
return {
|
|
36
37
|
...(plan !== undefined ? { plan } : {}),
|
|
37
|
-
...(
|
|
38
|
+
...(credits !== undefined ? { credits } : {}),
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
package/status/openrouter.ts
CHANGED
|
@@ -149,8 +149,14 @@ export const openRouterStatusAdapter: StatusAdapter = {
|
|
|
149
149
|
unit: "USD",
|
|
150
150
|
};
|
|
151
151
|
} catch (error) {
|
|
152
|
-
//
|
|
153
|
-
|
|
152
|
+
// /credits requires a management key, so permission-shaped failures
|
|
153
|
+
// degrade to the key payload; real failures must still surface.
|
|
154
|
+
if (
|
|
155
|
+
!(error instanceof ProviderDataError) ||
|
|
156
|
+
(error.httpStatus !== 401 && error.httpStatus !== 403 && error.httpStatus !== 404)
|
|
157
|
+
) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
return {
|