@hyav/pi-provider 0.1.7 → 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 +23 -0
- package/README.md +7 -7
- package/README.zh-CN.md +7 -7
- package/core/adapter-extensions.ts +9 -2
- package/core/adapter-protocol.ts +4 -2
- package/core/adapter-validation.ts +14 -1
- package/core/catalog-preflight.ts +14 -1
- package/core/host.ts +43 -75
- package/core/model-catalog.ts +289 -0
- package/core/opencode-preflight.ts +6 -0
- package/core/pi-model-metadata.ts +739 -0
- package/core/provider-registration.ts +111 -56
- package/core/public-adapters.ts +16 -1
- package/core/runtime-config.ts +18 -38
- package/core/runtime-entry.ts +11 -6
- package/core/runtime.ts +26 -104
- package/core/status-report.ts +327 -229
- package/core/types.ts +37 -20
- package/index.ts +37 -18
- 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 +106 -236
- 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 -899
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,11 +52,21 @@ 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 {
|
|
63
|
+
cost?: ModelFieldSource;
|
|
64
|
+
costBySku?: ModelCostBySkuSources;
|
|
45
65
|
contextWindow?: ModelFieldSource;
|
|
46
66
|
maxTokens?: ModelFieldSource;
|
|
47
67
|
input?: ModelFieldSource;
|
|
48
68
|
reasoning?: ModelFieldSource;
|
|
69
|
+
thinkingLevelMap?: ModelFieldSource;
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
export interface ProviderPricingAdjustment {
|
|
@@ -70,26 +91,12 @@ export interface ModelPricingDetails {
|
|
|
70
91
|
effectiveCost?: ProviderCost;
|
|
71
92
|
adjustment?: ProviderPricingAdjustment;
|
|
72
93
|
note?: string;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
export interface ModelQualityScore {
|
|
76
|
-
source: string;
|
|
77
|
-
benchmark: string;
|
|
78
|
-
category: string;
|
|
79
|
-
metric: "elo" | "rating" | "score" | "ips";
|
|
80
|
-
value: number;
|
|
81
|
-
rank?: number;
|
|
82
|
-
winRate?: number;
|
|
83
|
-
confidenceInterval?: {
|
|
84
|
-
lower: number;
|
|
85
|
-
upper: number;
|
|
86
|
-
};
|
|
94
|
+
costBySku?: ModelCostBySkuSources;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
export interface ProviderModelMetadata {
|
|
90
98
|
pricing: ModelPricingDetails;
|
|
91
99
|
fieldSources?: ModelFieldSources;
|
|
92
|
-
quality?: ModelQualityScore[];
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
export type ProviderModelDraft = Partial<ProviderModel> &
|
|
@@ -112,12 +119,18 @@ export type ProviderDefinition = Omit<ProviderConfig, "models" | "refreshModels"
|
|
|
112
119
|
refreshModels?: (context: ProviderRefreshContext) => Promise<ProviderModelDraft[]>;
|
|
113
120
|
};
|
|
114
121
|
|
|
115
|
-
export type ModelCatalogSource = "static" | "live" | "fallback";
|
|
122
|
+
export type ModelCatalogSource = "static" | "live" | "cached" | "fallback" | "empty";
|
|
116
123
|
|
|
117
124
|
export interface ModelCatalogStatus {
|
|
118
125
|
source: ModelCatalogSource;
|
|
119
126
|
modelCount: number;
|
|
120
127
|
updatedAt?: number;
|
|
128
|
+
lastSuccessfulRefreshAt?: number;
|
|
129
|
+
lastAttemptAt?: number;
|
|
130
|
+
consecutiveFailures?: number;
|
|
131
|
+
nextRetryAt?: number;
|
|
132
|
+
rejectedCount?: number;
|
|
133
|
+
duplicateCount?: number;
|
|
121
134
|
lastError?: string;
|
|
122
135
|
}
|
|
123
136
|
|
|
@@ -191,12 +204,16 @@ export interface ProviderAdapter {
|
|
|
191
204
|
/** Optional explicit price adjustments owned by this Provider. */
|
|
192
205
|
pricing?: ProviderPricingPolicy;
|
|
193
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;
|
|
194
210
|
/** @internal Draft state shared across isolated Adapter and Host contexts. */
|
|
195
211
|
registration?: {
|
|
196
212
|
modelDrafts: ProviderModelDraft[];
|
|
197
213
|
normalizedModels: ProviderModel[];
|
|
198
214
|
modelMetadata: Record<string, ProviderModelMetadata>;
|
|
199
|
-
|
|
215
|
+
piCatalog?: PiCatalogSnapshot;
|
|
216
|
+
officialPricing?: Record<string, any>;
|
|
200
217
|
activeRefreshes: number;
|
|
201
218
|
deferredRegistration?: () => void;
|
|
202
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 {
|
|
@@ -54,23 +55,25 @@ export type {
|
|
|
54
55
|
LiveCheckSnapshot,
|
|
55
56
|
} from "./core/live-check-manager.ts";
|
|
56
57
|
export { getLiveCheckKey, LIVE_CHECK_SCOPE, LiveCheckManager } from "./core/live-check-manager.ts";
|
|
57
|
-
export {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
getPricingCache,
|
|
65
|
-
getPricingCacheAge,
|
|
66
|
-
type OfficialModelMeta,
|
|
67
|
-
type OfficialPricingFetchOptions,
|
|
68
|
-
OPENROUTER_MODELS_URL,
|
|
69
|
-
parseOpenRouterModels,
|
|
70
|
-
parseOpenRouterPricing,
|
|
71
|
-
setPricingCache,
|
|
72
|
-
} from "./core/official-pricing.ts";
|
|
58
|
+
export type {
|
|
59
|
+
ModelCatalogDiagnostics,
|
|
60
|
+
ModelCatalogDiscoveryResult,
|
|
61
|
+
ModelCatalogLifecycle,
|
|
62
|
+
ModelCatalogLifecycleOptions,
|
|
63
|
+
} from "./core/model-catalog.ts";
|
|
64
|
+
export { createModelCatalogLifecycle } from "./core/model-catalog.ts";
|
|
73
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";
|
|
74
77
|
export type {
|
|
75
78
|
PreflightAdapter,
|
|
76
79
|
PreflightContext,
|
|
@@ -87,18 +90,23 @@ export type { RateLimitWindow } from "./core/ratelimit-headers.ts";
|
|
|
87
90
|
export { parseRetryAfter } from "./core/retry-after.ts";
|
|
88
91
|
export type { StatusDiagnostics, StatusErrorState } from "./core/status-manager.ts";
|
|
89
92
|
export { normalizeStatusSnapshot, StatusManager } from "./core/status-manager.ts";
|
|
93
|
+
export {
|
|
94
|
+
formatProviderStatus,
|
|
95
|
+
getStatusModeCompletions,
|
|
96
|
+
parseStatusMode,
|
|
97
|
+
} from "./core/status-report.ts";
|
|
90
98
|
export { applyTunerAdapters, sortTunerAdapters } from "./core/tuner-manager.ts";
|
|
91
99
|
export type {
|
|
92
100
|
ActiveModel,
|
|
93
101
|
ModelCatalogSource,
|
|
94
102
|
ModelCatalogStatus,
|
|
103
|
+
ModelCostBySkuSources,
|
|
95
104
|
ModelFieldSource,
|
|
96
105
|
ModelFieldSources,
|
|
97
106
|
ModelMetadataState,
|
|
98
107
|
ModelMetadataStatus,
|
|
99
108
|
ModelPricingDetails,
|
|
100
109
|
ModelPricingSource,
|
|
101
|
-
ModelQualityScore,
|
|
102
110
|
PiApi,
|
|
103
111
|
PricingSku,
|
|
104
112
|
ProviderAdapter,
|
|
@@ -146,6 +154,11 @@ export function createPiProviderExtension(
|
|
|
146
154
|
wrapTextWithAnsi: typeof wrapTextWithAnsi;
|
|
147
155
|
adapterRoot?: string;
|
|
148
156
|
dependencies?: Partial<PiProviderDependencies>;
|
|
157
|
+
piCatalogSource: {
|
|
158
|
+
getBuiltinProviders: () => string[];
|
|
159
|
+
getBuiltinModels: (provider: string) => unknown[];
|
|
160
|
+
getBuiltinModelDataGeneratedAt: () => number | undefined;
|
|
161
|
+
};
|
|
149
162
|
},
|
|
150
163
|
) => Promise<void>;
|
|
151
164
|
};
|
|
@@ -155,6 +168,12 @@ export function createPiProviderExtension(
|
|
|
155
168
|
wrapTextWithAnsi,
|
|
156
169
|
adapterRoot: options.adapterRoot,
|
|
157
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
|
+
},
|
|
158
177
|
});
|
|
159
178
|
};
|
|
160
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(
|