@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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the authoritative user-facing release history for `@hyav/pi-provider`.
|
|
4
4
|
|
|
5
|
+
## 0.2.0 - 2026-09-06
|
|
6
|
+
|
|
7
|
+
- Add Pi catalog fallback based on `@earendil-works/pi-ai` built-in models, scoped to original manufacturer providers (`anthropic`, `openai`, `google`, `deepseek`, `mistral`, `xai`, `minimax`, `minimax-cn`, `moonshotai`, `moonshotai-cn`, `kimi-coding`, `zai`, `zai-coding-cn`, `xiaomi`, and `ant-ling`), strictly excluding OpenRouter, third-party proxies, aggregators, and token plans.
|
|
8
|
+
- Supplement registered provider models with missing fields (`cost`, `contextWindow`, `maxTokens`, `input` modalities, `reasoning`, `thinkingLevelMap`, and `compat`) using deterministic matching and field-level provenance (`provider`, `pi`, `mixed`, `default`, `normalized`); support disabling fallback per adapter via `usePiModelMetaFallback: false`.
|
|
9
|
+
- Establish dynamic provider adapter persistence contract: dynamic adapters must persist raw model drafts instead of normalized default models to prevent defaulted capabilities from suppressing manufacturer metadata in the Pi catalog fallback; detect and invalidate legacy normalized snapshots.
|
|
10
|
+
- Add a copyable Ant Digital MaaS Provider and Preflight example with authenticated dynamic discovery, bounded catalog parsing, raw-draft cache migration, timeout handling, and model matching.
|
|
11
|
+
- Prevent the OpenRouter Preflight adapter from reporting a successful authentication check when no usable credential is resolved.
|
|
12
|
+
- Refactor `/status` reporting: compress `Catalog:`, `Health:`, and `Account:` to compact single lines, merge `Reasoning:` and `Thinking levels:` into a single line, and display field-level provenance.
|
|
13
|
+
- Remove OpenRouter generic model metadata, pricing completion, and cache mechanisms (`openrouter-model-metadata.json`, `fetchOfficialModelMetadata()`, `fetchOfficialPricing()`, `applyOfficialModelMetadata()`, `applyOfficialModelCosts()`, `parseOpenRouterModels()`, `parseOpenRouterPricing()`, `findOfficialMeta()`, `findOfficialCost()`, `getDefaultOpenRouterMetadataCachePath()`, and `OPENROUTER_MODELS_URL`); OpenRouter as a native Pi provider with its own status/preflight adapters remains fully supported.
|
|
14
|
+
- Remove benchmark quality runtime, adapters, caching, and diagnostics (`QualityManager`, Artificial Analysis, LiveBench, Agent Arena, `/status quality`, `ARTIFICIAL_ANALYSIS_API_KEY`, and quality cache directories).
|
|
15
|
+
|
|
16
|
+
## 0.1.8 - 2026-09-01
|
|
17
|
+
|
|
18
|
+
- Make Charm Hyper model catalogs online-only: use the last successful online snapshot when refresh is unavailable and an empty catalog when no snapshot exists; remove package-maintained static model, pricing, and model-specific capability fallbacks.
|
|
19
|
+
- Skip malformed individual Charm Hyper models while retaining valid entries, and use Provider metadata before OpenRouter metadata when filling model fields.
|
|
20
|
+
- Add a public model-catalog lifecycle helper for cached snapshot restoration, TTL checks, generation-guarded publication, persistence fallback, complete live replacement, and failure retention; migrate Charm Hyper to it.
|
|
21
|
+
- Share one model-catalog discovery request across concurrent callers with different cancellation signals, while keeping caller cancellation and generation-guarded publication independent.
|
|
22
|
+
- Separate successful-catalog TTL from exponential failure backoff, expose attempt, success, failure-count, and retry diagnostics, and show retry timing in `/status`.
|
|
23
|
+
- Report bounded invalid and duplicate model counts for accepted online catalogs without retaining remote model IDs or payload content.
|
|
24
|
+
- Add accurately named OpenRouter metadata APIs (`fetchOfficialModelMetadata()` and `applyOfficialModelMetadata()`), migrate internal callers, and retain the pricing-named APIs as deprecated compatibility wrappers.
|
|
25
|
+
- Extend field-level provenance to normalized cost and thinking-level maps while keeping `pricing.source` authoritative for known and effective pricing.
|
|
26
|
+
- Normalize partial model costs before applying pricing adjustments so registered model costs and pricing sidecars remain consistent; report fields rewritten at the registration boundary as `normalized`.
|
|
27
|
+
|
|
5
28
|
## 0.1.7 - 2026-08-24
|
|
6
29
|
|
|
7
30
|
- Refresh the active model catalog dynamically on `/status refresh` and `/status check` by delegating to Pi's model registry with forced network revalidation, keeping the displayed model catalog and model counts up to date without requiring `/reload`.
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ A provider extension toolkit for [Pi](https://pi.dev). It registers LLM provider
|
|
|
10
10
|
|
|
11
11
|
- One Pi Provider Host for registration, status, preflight checks, live checks, and request tuners
|
|
12
12
|
- Provider, status, preflight, and tuner Adapter files discovered by one Pi entrypoint on `/reload`
|
|
13
|
-
- Resilient model catalogs with cached
|
|
14
|
-
- Provider-first
|
|
13
|
+
- Resilient model catalogs with cached online snapshots, bounded background refresh, and failure retention
|
|
14
|
+
- Provider-first metadata completed by Pi native manufacturer catalog, with deterministic field-level provenance
|
|
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
17
|
- Status/preflight adapters for the native Pi providers Anthropic, GitHub Copilot, OpenRouter, Groq, and xAI
|
|
@@ -45,7 +45,7 @@ pi install npm:@hyav/pi-provider
|
|
|
45
45
|
|
|
46
46
|
Use `/status refresh` for free endpoint, authentication, catalog, and account checks. Use `/status check` only when you explicitly accept a real model request and possible usage charges.
|
|
47
47
|
|
|
48
|
-
A dynamic Provider whose API key references environment variables keeps its
|
|
48
|
+
A dynamic Provider whose API key references environment variables keeps its last successful online catalog snapshot and skips network catalog refreshes until those variables or a stored credential are available. Providers without a successful snapshot expose an empty catalog rather than inventing models. This prevents unconfigured Providers from surfacing model-refresh warnings.
|
|
49
49
|
|
|
50
50
|
## Common configuration
|
|
51
51
|
|
|
@@ -53,9 +53,9 @@ A dynamic Provider whose API key references environment variables keeps its cach
|
|
|
53
53
|
|---|---:|---|---|
|
|
54
54
|
| `HYPER_API_KEY` | For Charm Hyper API-key auth | None | Supplies the built-in `charm-hyper` provider credential; OAuth users may use `/login` |
|
|
55
55
|
| `ANTHROPIC_USAGE_URL` | No | `https://claude.ai/api/usage` | Custom Anthropic usage endpoint; default endpoint is subscription OAuth only |
|
|
56
|
-
| `PI_CODING_AGENT_DIR` | No | `~/.pi/agent` | Changes Pi's agent directory
|
|
56
|
+
| `PI_CODING_AGENT_DIR` | No | `~/.pi/agent` | Changes Pi's agent directory |
|
|
57
57
|
|
|
58
|
-
Programmatic integrations can configure pricing
|
|
58
|
+
Programmatic integrations can configure pricing policies, request timeouts, and provider options through `createPiProviderRuntime()` or `createPiProviderHost()`. Programmatic defaults resolve the agent directory from `PI_CODING_AGENT_DIR` (falling back to `~/.pi/agent`); 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.
|
|
59
59
|
|
|
60
60
|
## Adapter discovery (file-level plug and play)
|
|
61
61
|
|
|
@@ -70,7 +70,7 @@ Built-in Adapters ship inside the package and are always discovered. User Adapte
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
Add, remove, or modify files there, then run `/reload` to rediscover them without touching the package; edits to existing files are re-read from disk. User Adapters load after built-ins, so a same-ID file overrides the built-in Adapter (the Host keeps the latest registration and warns). `createPiProviderExtension({ adapterRoot })` replaces the default user directory with a custom root; built-ins are always scanned. The built-in Adapters under the package's `providers/`, `status/`, and `preflight/` are reference templates with this exact shape — copy one and customize it (Charm Hyper and `preflight/openai-codex.ts` also use package-private helpers).
|
|
73
|
+
Add, remove, or modify files there, then run `/reload` to rediscover them without touching the package; edits to existing files are re-read from disk. User Adapters load after built-ins, so a same-ID file overrides the built-in Adapter (the Host keeps the latest registration and warns). `createPiProviderExtension({ adapterRoot })` replaces the default user directory with a custom root; built-ins are always scanned. The built-in Adapters under the package's `providers/`, `status/`, and `preflight/` are reference templates with this exact shape — copy one and customize it (Charm Hyper and `preflight/openai-codex.ts` also use package-private helpers). Complete non-built-in references are available for [Ant Digital MaaS](examples/maas/) and [Command Code](examples/command-code/).
|
|
74
74
|
|
|
75
75
|
Adapter files import helpers and types from `@hyav/pi-provider` (aliased inside the loader):
|
|
76
76
|
|
|
@@ -82,7 +82,7 @@ Adapter files must not runtime-import Pi's bundled packages (`@earendil-works/pi
|
|
|
82
82
|
|
|
83
83
|
## Before you use it
|
|
84
84
|
|
|
85
|
-
`/status` is offline, `/status refresh` performs free remote checks, and `/status check` sends a live model request that may consume quota. Configured credentials are sent only to the corresponding provider endpoints and are omitted from status output.
|
|
85
|
+
`/status` is offline, `/status refresh` performs free remote checks and updates catalog caches, and `/status check` sends a live model request that may consume quota. Configured credentials are sent only to the corresponding provider endpoints and are omitted from status output.
|
|
86
86
|
|
|
87
87
|
## License
|
|
88
88
|
|
package/README.zh-CN.md
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
- 由一个 Pi Provider Host 统一负责注册、Status、Preflight、实时检查和请求 Tuner
|
|
12
12
|
- 由单一 Pi 入口在 `/reload` 时发现 Provider、Status、Preflight 和 Tuner Adapter 文件
|
|
13
|
-
-
|
|
14
|
-
- 优先采用 Provider
|
|
13
|
+
- 通过缓存在线快照、有界后台刷新和失败保留提供可靠的模型目录
|
|
14
|
+
- 优先采用 Provider 元数据,并由 Pi 原厂模型目录补全价格与能力,具备确定性的字段级来源诊断
|
|
15
15
|
- 显式诊断:缓存 `/status`、免费 `/status refresh` 和可能计费的 `/status check`
|
|
16
16
|
- 内置 Charm Hyper、DeepSeek、Google Gemini、OpenAI Codex、OpenCode Zen 和 OpenCode Go 集成
|
|
17
17
|
- Status/Preflight 适配覆盖 Pi 原生 Provider:Anthropic、GitHub Copilot、OpenRouter、Groq、xAI
|
|
@@ -45,7 +45,7 @@ pi install npm:@hyav/pi-provider
|
|
|
45
45
|
|
|
46
46
|
使用 `/status refresh` 执行免费的端点、鉴权、目录和账户检查。只有明确接受一次真实模型请求及其可能产生的用量费用时,才使用 `/status check`。
|
|
47
47
|
|
|
48
|
-
动态 Provider 的 API Key
|
|
48
|
+
动态 Provider 的 API Key 引用环境变量时,如果这些变量和已存储凭据均未配置,将保留最近一次成功获取的在线目录快照;如果从未成功获取过在线目录,则使用空目录,并跳过网络刷新,避免未配置的 Provider 产生模型目录刷新警告。
|
|
49
49
|
|
|
50
50
|
## 常用配置
|
|
51
51
|
|
|
@@ -53,9 +53,9 @@ pi install npm:@hyav/pi-provider
|
|
|
53
53
|
|---|---:|---|---|
|
|
54
54
|
| `HYPER_API_KEY` | Charm Hyper API Key 鉴权需要 | 无 | 为内置 `charm-hyper` Provider 提供凭据;OAuth 用户可以使用 `/login` |
|
|
55
55
|
| `ANTHROPIC_USAGE_URL` | 否 | `https://claude.ai/api/usage` | 自定义 Anthropic 用量端点;默认端点仅支持订阅 OAuth |
|
|
56
|
-
| `PI_CODING_AGENT_DIR` | 否 | `~/.pi/agent` | 修改 Pi agent
|
|
56
|
+
| `PI_CODING_AGENT_DIR` | 否 | `~/.pi/agent` | 修改 Pi agent 目录 |
|
|
57
57
|
|
|
58
|
-
程序化集成可以通过 `createPiProviderRuntime()` 或 `createPiProviderHost()`
|
|
58
|
+
程序化集成可以通过 `createPiProviderRuntime()` 或 `createPiProviderHost()` 配置价格策略、请求超时和 Provider 选项。程序化默认值会从 `PI_CODING_AGENT_DIR`(回退到 `~/.pi/agent`)解析 agent 目录;Pi 入口会用 Pi 自身的解析覆盖它。使用自定义 capability 根目录的 Host 包可以调用 `createPiProviderExtension({ adapterRoot, dependencies })`。源码定义 [`PiProviderDependencies`](core/runtime-config.ts) 是权威依据。
|
|
59
59
|
|
|
60
60
|
## Adapter 发现(文件级即插即用)
|
|
61
61
|
|
|
@@ -70,7 +70,7 @@ pi install npm:@hyav/pi-provider
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
在目录中增删或修改文件后执行 `/reload` 即可重新发现,无需改动包;对现有文件的修改会重新从磁盘读取。用户 Adapter 在内置之后加载,因此同 ID 的用户文件会覆盖内置 Adapter(Host 保留最新注册并发出警告)。`createPiProviderExtension({ adapterRoot })` 用自定义根替换默认用户目录;内置目录始终被扫描。包内 `providers/`、`status/`、`preflight/` 下的内置 Adapter 就是采用这种写法的参考模板——复制一份改改即可(Charm Hyper 与 `preflight/openai-codex.ts`
|
|
73
|
+
在目录中增删或修改文件后执行 `/reload` 即可重新发现,无需改动包;对现有文件的修改会重新从磁盘读取。用户 Adapter 在内置之后加载,因此同 ID 的用户文件会覆盖内置 Adapter(Host 保留最新注册并发出警告)。`createPiProviderExtension({ adapterRoot })` 用自定义根替换默认用户目录;内置目录始终被扫描。包内 `providers/`、`status/`、`preflight/` 下的内置 Adapter 就是采用这种写法的参考模板——复制一份改改即可(Charm Hyper 与 `preflight/openai-codex.ts` 还依赖包内私有辅助文件)。完整且不会被默认加载的参考实现包括 [蚂蚁数科 MaaS](examples/maas/) 和 [Command Code](examples/command-code/)。
|
|
74
74
|
|
|
75
75
|
Adapter 文件从 `@hyav/pi-provider` 导入 helper 和类型(加载器内部做了别名映射):
|
|
76
76
|
|
|
@@ -82,7 +82,7 @@ Adapter 文件不得运行时导入 Pi 的内置包(`@earendil-works/pi-coding
|
|
|
82
82
|
|
|
83
83
|
## 使用须知
|
|
84
84
|
|
|
85
|
-
`/status` 离线运行,`/status refresh`
|
|
85
|
+
`/status` 离线运行,`/status refresh` 执行免费远程检查并更新目录缓存,`/status check` 会发送可能消耗配额的真实模型请求。配置的凭据只会发送给对应 Provider 端点,并且不会出现在 Status 输出中。
|
|
86
86
|
|
|
87
87
|
## 许可证
|
|
88
88
|
|
|
@@ -93,7 +93,14 @@ function createAdapterExtension<TAdapter extends ProviderAdapter | StatusAdapter
|
|
|
93
93
|
// Host runs in a different Pi module context and cannot use this
|
|
94
94
|
// extension's module-local state. The factory is retained so a Host
|
|
95
95
|
// loaded later can recreate the adapter with its configured runtime.
|
|
96
|
-
const
|
|
96
|
+
const piCatalog = await bridge.piCatalog;
|
|
97
|
+
const registeredProvider = registerProviderAdapter(
|
|
98
|
+
pi,
|
|
99
|
+
providerAdapter,
|
|
100
|
+
bridge.dependencies,
|
|
101
|
+
piCatalog,
|
|
102
|
+
modelDrafts,
|
|
103
|
+
);
|
|
97
104
|
emitRegistration(
|
|
98
105
|
pi,
|
|
99
106
|
{
|
|
@@ -110,7 +117,7 @@ function createAdapterExtension<TAdapter extends ProviderAdapter | StatusAdapter
|
|
|
110
117
|
// repeat OAuth-only replacement after binding so /reload clears a
|
|
111
118
|
// raw environment key retained by Pi's merge semantics.
|
|
112
119
|
registeredProvider.apiKey === undefined
|
|
113
|
-
? () => registerProviderAdapter(pi, providerAdapter, bridge.dependencies,
|
|
120
|
+
? () => registerProviderAdapter(pi, providerAdapter, bridge.dependencies, piCatalog, modelDrafts)
|
|
114
121
|
: undefined,
|
|
115
122
|
);
|
|
116
123
|
return;
|
package/core/adapter-protocol.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PiCatalogSnapshot } from "./pi-model-metadata.ts";
|
|
3
3
|
import type { PreflightAdapter } from "./preflight-manager.ts";
|
|
4
4
|
import type { PiProviderDependencies } from "./runtime-config.ts";
|
|
5
5
|
import type { ProviderAdapter, ProviderModelDraft, StatusAdapter, TunerAdapter } from "./types.ts";
|
|
@@ -74,7 +74,9 @@ export type AdapterRegistrationEnvelope =
|
|
|
74
74
|
|
|
75
75
|
export interface StartupBridge {
|
|
76
76
|
dependencies: PiProviderDependencies;
|
|
77
|
-
|
|
77
|
+
piCatalog?: Promise<PiCatalogSnapshot>;
|
|
78
|
+
/** @deprecated Generic official pricing has been removed. */
|
|
79
|
+
officialPricing?: Promise<Record<string, unknown>>;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export interface StartupBridgeRequest {
|
|
@@ -147,7 +147,9 @@ export function validateProviderAdapter(adapter: unknown): asserts adapter is Pr
|
|
|
147
147
|
if (
|
|
148
148
|
adapter.catalog.source !== "static" &&
|
|
149
149
|
adapter.catalog.source !== "live" &&
|
|
150
|
-
adapter.catalog.source !== "
|
|
150
|
+
adapter.catalog.source !== "cached" &&
|
|
151
|
+
adapter.catalog.source !== "fallback" &&
|
|
152
|
+
adapter.catalog.source !== "empty"
|
|
151
153
|
) {
|
|
152
154
|
throw new Error(`Provider ${adapter.id} has invalid catalog source`);
|
|
153
155
|
}
|
|
@@ -160,6 +162,17 @@ export function validateProviderAdapter(adapter: unknown): asserts adapter is Pr
|
|
|
160
162
|
}
|
|
161
163
|
if (adapter.catalog.updatedAt !== undefined)
|
|
162
164
|
assertFiniteNonNegative(adapter.catalog.updatedAt, "Catalog updatedAt");
|
|
165
|
+
for (const field of ["lastSuccessfulRefreshAt", "lastAttemptAt", "nextRetryAt"] as const) {
|
|
166
|
+
if (adapter.catalog[field] !== undefined) {
|
|
167
|
+
assertFiniteNonNegative(adapter.catalog[field], `Catalog ${field}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const field of ["consecutiveFailures", "rejectedCount", "duplicateCount"] as const) {
|
|
171
|
+
const count = adapter.catalog[field];
|
|
172
|
+
if (count !== undefined && (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)) {
|
|
173
|
+
throw new Error(`Provider ${adapter.id} has invalid catalog ${field}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
163
176
|
if (adapter.catalog.lastError !== undefined && !isSafeText(adapter.catalog.lastError)) {
|
|
164
177
|
throw new Error(`Provider ${adapter.id} has invalid catalog error`);
|
|
165
178
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared helpers for Provider-agnostic, OpenAI-style model-catalog checks. */
|
|
2
2
|
|
|
3
|
+
import { MAX_PROVIDER_MODEL_COUNT } from "./adapter-validation.ts";
|
|
3
4
|
import { authDefinesHeader, getContextAuth, hasBaseUrlOrigin, mergeDiagnosticHeaders } from "./diagnostic-auth.ts";
|
|
4
5
|
import { ProviderDataError } from "./errors.ts";
|
|
5
6
|
import type { PreflightAdapter } from "./preflight-manager.ts";
|
|
@@ -45,6 +46,12 @@ export function createCatalogPreflightAdapter(
|
|
|
45
46
|
async fetch(context) {
|
|
46
47
|
const auth = await getContextAuth(context);
|
|
47
48
|
const apiKey = auth.apiKey;
|
|
49
|
+
const hasResolvedAuthHeaders = Object.values(auth.headers ?? {}).some(
|
|
50
|
+
(value) => typeof value === "string" && value !== "",
|
|
51
|
+
);
|
|
52
|
+
if (config.requireAuth !== false && !isUsableApiKey(apiKey) && !hasResolvedAuthHeaders) {
|
|
53
|
+
return { passed: false, checks: ["auth"], updatedAt: context.now() };
|
|
54
|
+
}
|
|
48
55
|
const credential = context.getCredentialType
|
|
49
56
|
? await context.getCredentialType().catch(() => undefined)
|
|
50
57
|
: undefined;
|
|
@@ -53,7 +60,7 @@ export function createCatalogPreflightAdapter(
|
|
|
53
60
|
"Accept-Encoding": "identity",
|
|
54
61
|
...(config.headers ?? {}),
|
|
55
62
|
});
|
|
56
|
-
if (apiKey
|
|
63
|
+
if (isUsableApiKey(apiKey)) {
|
|
57
64
|
if (config.authHeaders) {
|
|
58
65
|
for (const [name, value] of Object.entries(config.authHeaders(apiKey, credential))) {
|
|
59
66
|
if (!authDefinesHeader(auth, name)) headers.set(name, value);
|
|
@@ -90,6 +97,9 @@ export function createCatalogPreflightAdapter(
|
|
|
90
97
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
91
98
|
throw new ProviderDataError(`${config.name} preflight returned invalid catalog data`, "badjson");
|
|
92
99
|
}
|
|
100
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
101
|
+
throw new ProviderDataError(`${config.name} preflight catalog exceeds the maximum model count`, "badjson");
|
|
102
|
+
}
|
|
93
103
|
const modelIds = new Set(
|
|
94
104
|
payload.data
|
|
95
105
|
.filter(isRecord)
|
|
@@ -120,6 +130,9 @@ export function collectCatalogIds(
|
|
|
120
130
|
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
121
131
|
throw new ProviderDataError("Catalog response returned invalid catalog data", "badjson");
|
|
122
132
|
}
|
|
133
|
+
if (payload.data.length > MAX_PROVIDER_MODEL_COUNT) {
|
|
134
|
+
throw new ProviderDataError("Catalog response exceeds the maximum model count", "badjson");
|
|
135
|
+
}
|
|
123
136
|
return new Set(
|
|
124
137
|
payload.data
|
|
125
138
|
.filter(isRecord)
|
package/core/host.ts
CHANGED
|
@@ -17,9 +17,13 @@ import {
|
|
|
17
17
|
type PiProviderRuntimeController,
|
|
18
18
|
prepareProviderRegistration,
|
|
19
19
|
} from "./extension.ts";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createEmptyCatalogSnapshot,
|
|
22
|
+
loadPiCatalog,
|
|
23
|
+
type PiCatalogSnapshot,
|
|
24
|
+
type PiCatalogSource,
|
|
25
|
+
} from "./pi-model-metadata.ts";
|
|
21
26
|
import type { PreflightAdapter } from "./preflight-manager.ts";
|
|
22
|
-
import { refreshProviderRegistrations } from "./provider-registration.ts";
|
|
23
27
|
import { scheduleModelCatalogRefresh } from "./runtime.ts";
|
|
24
28
|
import type { PiProviderDependencies } from "./runtime-config.ts";
|
|
25
29
|
import { resolvePiProviderDependencies } from "./runtime-config.ts";
|
|
@@ -45,7 +49,15 @@ function warnAdapterIssue(message: string): void {
|
|
|
45
49
|
* event-bus envelopes are collected and assembled at the first session-level
|
|
46
50
|
* operation after Pi's session_start registration barrier.
|
|
47
51
|
*/
|
|
48
|
-
export
|
|
52
|
+
export interface PiProviderHostOptions {
|
|
53
|
+
/** Catalog functions captured from Pi's outer extension module graph. */
|
|
54
|
+
piCatalogSource?: PiCatalogSource;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createPiProviderHost(
|
|
58
|
+
dependencies: Partial<PiProviderDependencies> = {},
|
|
59
|
+
options: PiProviderHostOptions = {},
|
|
60
|
+
): (pi: ExtensionAPI) => void {
|
|
49
61
|
const runtime = resolvePiProviderDependencies(dependencies);
|
|
50
62
|
return (pi) => {
|
|
51
63
|
const hostToken = {};
|
|
@@ -66,52 +78,19 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
66
78
|
let readyPromise: Promise<PiProviderRuntimeController | undefined> | undefined;
|
|
67
79
|
let disposed = false;
|
|
68
80
|
let lifecycleGeneration = 0;
|
|
69
|
-
let pricingRefreshController: AbortController | undefined;
|
|
70
|
-
let latestBackgroundPricing: Record<string, OfficialModelMeta> | undefined;
|
|
71
|
-
let installedDefinition:
|
|
72
|
-
| {
|
|
73
|
-
generation: number;
|
|
74
|
-
definition: PiProviderDefinition;
|
|
75
|
-
providerDrafts: Map<ProviderAdapter, ProviderModelDraft[]>;
|
|
76
|
-
}
|
|
77
|
-
| undefined;
|
|
78
|
-
|
|
79
|
-
const onBackgroundRefresh = (snapshot: Record<string, OfficialModelMeta>): void => {
|
|
80
|
-
latestBackgroundPricing = snapshot;
|
|
81
|
-
if (disposed || installedDefinition === undefined || installedDefinition.generation !== lifecycleGeneration)
|
|
82
|
-
return;
|
|
83
|
-
active?.updateOfficialPricing?.(snapshot);
|
|
84
|
-
// Re-register from the adapter's current registration state. A dynamic
|
|
85
|
-
// refreshModels() may have replaced the startup drafts since assembly.
|
|
86
|
-
refreshProviderRegistrations(pi, installedDefinition.definition.providers, runtime, snapshot);
|
|
87
|
-
};
|
|
88
|
-
const officialPricing = runtime.enableOfficialPricingFallback
|
|
89
|
-
? fetchOfficialPricingForHost(runtime, { allowNetwork: false })
|
|
90
|
-
: Promise.resolve({});
|
|
91
|
-
const bridge: StartupBridge = { dependencies: runtime, officialPricing };
|
|
92
81
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
void fetchOfficialPricingForHost(runtime, { signal: controller.signal })
|
|
102
|
-
.then((snapshot) => {
|
|
103
|
-
if (controller.signal.aborted || disposed) return;
|
|
104
|
-
onBackgroundRefresh(snapshot);
|
|
105
|
-
})
|
|
106
|
-
.catch(() => undefined)
|
|
107
|
-
.finally(() => {
|
|
108
|
-
if (pricingRefreshController === controller) pricingRefreshController = undefined;
|
|
109
|
-
});
|
|
82
|
+
const piCatalogPromise = loadPiCatalog({
|
|
83
|
+
builtinCatalog: options.piCatalogSource,
|
|
84
|
+
fetch: runtime.fetch,
|
|
85
|
+
}).catch(() => createEmptyCatalogSnapshot());
|
|
86
|
+
const bridge: StartupBridge = {
|
|
87
|
+
dependencies: runtime,
|
|
88
|
+
piCatalog: piCatalogPromise,
|
|
89
|
+
officialPricing: Promise.resolve({}),
|
|
110
90
|
};
|
|
111
91
|
|
|
112
92
|
const invalidateRuntime = (): void => {
|
|
113
93
|
lifecycleGeneration++;
|
|
114
|
-
installedDefinition = undefined;
|
|
115
94
|
active?.shutdown();
|
|
116
95
|
active = undefined;
|
|
117
96
|
readyPromise = undefined;
|
|
@@ -196,7 +175,7 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
196
175
|
): Promise<{
|
|
197
176
|
definition: PiProviderDefinition;
|
|
198
177
|
providerDrafts: Map<ProviderAdapter, ProviderModelDraft[]>;
|
|
199
|
-
|
|
178
|
+
piCatalog: PiCatalogSnapshot;
|
|
200
179
|
}> => {
|
|
201
180
|
const envelopes = [...registrations.values()];
|
|
202
181
|
const providerEnvelopes = groupedWithoutConflicts(
|
|
@@ -207,7 +186,11 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
207
186
|
(entry) => entry.id,
|
|
208
187
|
"provider",
|
|
209
188
|
);
|
|
210
|
-
const
|
|
189
|
+
const piCatalogPromise = loadPiCatalog({
|
|
190
|
+
modelRegistry: ctx?.modelRegistry,
|
|
191
|
+
builtinCatalog: options.piCatalogSource,
|
|
192
|
+
fetch: runtime.fetch,
|
|
193
|
+
}).catch(() => createEmptyCatalogSnapshot());
|
|
211
194
|
const providerResultsPromise = Promise.all(
|
|
212
195
|
providerEnvelopes.map(async (entry) => {
|
|
213
196
|
try {
|
|
@@ -220,7 +203,7 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
220
203
|
}
|
|
221
204
|
}),
|
|
222
205
|
);
|
|
223
|
-
const [
|
|
206
|
+
const [piCatalog, providerResults] = await Promise.all([piCatalogPromise, providerResultsPromise]);
|
|
224
207
|
const providers: ProviderAdapter[] = [];
|
|
225
208
|
const providerDrafts = new Map<ProviderAdapter, ProviderModelDraft[]>();
|
|
226
209
|
for (const result of providerResults) {
|
|
@@ -236,7 +219,16 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
236
219
|
try {
|
|
237
220
|
const modelDrafts =
|
|
238
221
|
result.entry.adapter.registration?.modelDrafts ?? result.entry.modelDrafts ?? adapter.provider.models;
|
|
239
|
-
|
|
222
|
+
const lifecycle = adapter.lifecycle ?? (adapter.provider.refreshModels as any)?.lifecycle;
|
|
223
|
+
if (lifecycle && modelDrafts && modelDrafts.length > 0) {
|
|
224
|
+
lifecycle.setModels(
|
|
225
|
+
modelDrafts,
|
|
226
|
+
result.entry.adapter.catalog?.source,
|
|
227
|
+
result.entry.adapter.catalog?.updatedAt,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
prepareProviderRegistration(adapter, runtime, piCatalog, modelDrafts);
|
|
231
|
+
result.entry.adapter = adapter;
|
|
240
232
|
providerDrafts.set(adapter, modelDrafts);
|
|
241
233
|
providers.push(adapter);
|
|
242
234
|
} catch (error) {
|
|
@@ -380,7 +372,7 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
380
372
|
tuners: tuners.sort(compareAdapterIds),
|
|
381
373
|
};
|
|
382
374
|
validatePiProviderDefinition(definition);
|
|
383
|
-
return { definition, providerDrafts,
|
|
375
|
+
return { definition, providerDrafts, piCatalog };
|
|
384
376
|
};
|
|
385
377
|
|
|
386
378
|
const ensureReady = (ctx?: ExtensionContext): Promise<PiProviderRuntimeController | undefined> => {
|
|
@@ -389,7 +381,7 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
389
381
|
const generation = lifecycleGeneration;
|
|
390
382
|
const pending = (async (): Promise<PiProviderRuntimeController | undefined> => {
|
|
391
383
|
if (disposed || generation !== lifecycleGeneration) return undefined;
|
|
392
|
-
const { definition, providerDrafts,
|
|
384
|
+
const { definition, providerDrafts, piCatalog } = await buildDefinition(ctx);
|
|
393
385
|
if (disposed || generation !== lifecycleGeneration) return undefined;
|
|
394
386
|
const providerIds = new Set(
|
|
395
387
|
[...registrations.values()]
|
|
@@ -403,7 +395,7 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
403
395
|
for (const providerId of providerIds) pi.unregisterProvider(providerId);
|
|
404
396
|
}
|
|
405
397
|
if (disposed || generation !== lifecycleGeneration) return undefined;
|
|
406
|
-
const controller = installPiProviderRuntime(pi, runtime, definition,
|
|
398
|
+
const controller = installPiProviderRuntime(pi, runtime, definition, piCatalog, {
|
|
407
399
|
registerHandlers: false,
|
|
408
400
|
providerDrafts,
|
|
409
401
|
});
|
|
@@ -412,8 +404,6 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
412
404
|
return undefined;
|
|
413
405
|
}
|
|
414
406
|
active = controller;
|
|
415
|
-
installedDefinition = { generation, definition, providerDrafts };
|
|
416
|
-
if (latestBackgroundPricing !== undefined) onBackgroundRefresh(latestBackgroundPricing);
|
|
417
407
|
return controller;
|
|
418
408
|
})();
|
|
419
409
|
readyPromise = pending.catch((error) => {
|
|
@@ -432,7 +422,6 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
432
422
|
});
|
|
433
423
|
pi.on("session_start", (event, ctx) => {
|
|
434
424
|
invalidateRuntime();
|
|
435
|
-
startOfficialPricingRefresh();
|
|
436
425
|
scheduleModelCatalogRefresh(ctx, event.reason);
|
|
437
426
|
});
|
|
438
427
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
@@ -447,8 +436,6 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
447
436
|
});
|
|
448
437
|
pi.on("session_shutdown", () => {
|
|
449
438
|
disposed = true;
|
|
450
|
-
pricingRefreshController?.abort();
|
|
451
|
-
pricingRefreshController = undefined;
|
|
452
439
|
invalidateRuntime();
|
|
453
440
|
unsubscribeHostClaim();
|
|
454
441
|
unsubscribeBridge();
|
|
@@ -464,22 +451,3 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
|
|
|
464
451
|
});
|
|
465
452
|
};
|
|
466
453
|
}
|
|
467
|
-
|
|
468
|
-
function fetchOfficialPricingForHost(
|
|
469
|
-
runtime: PiProviderDependencies,
|
|
470
|
-
options: { allowNetwork?: boolean; signal?: AbortSignal } = {},
|
|
471
|
-
) {
|
|
472
|
-
return fetchOfficialPricing(
|
|
473
|
-
runtime.fetch,
|
|
474
|
-
runtime.officialPricingUrl,
|
|
475
|
-
runtime.officialPricingTimeoutMs,
|
|
476
|
-
runtime.officialPricingCacheTtlMs,
|
|
477
|
-
runtime.officialPricingMaxStaleMs,
|
|
478
|
-
runtime.now,
|
|
479
|
-
{
|
|
480
|
-
cachePath:
|
|
481
|
-
runtime.officialPricingUrl === OPENROUTER_MODELS_URL ? runtime.openRouterMetadataCachePath : undefined,
|
|
482
|
-
...options,
|
|
483
|
-
},
|
|
484
|
-
);
|
|
485
|
-
}
|