@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 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 fallback, bounded background refresh, and failure retention
14
- - Provider-first pricing metadata with optional OpenRouter completion and quality indicators
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 cached or fallback model catalog and skips network catalog refreshes until those variables or a stored credential are available. This prevents unconfigured Providers from surfacing model-refresh warnings.
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; public OpenRouter metadata is cached under `<agent-dir>/extensions/pi-provider/` |
56
+ | `PI_CODING_AGENT_DIR` | No | `~/.pi/agent` | Changes Pi's agent directory |
57
57
 
58
- Programmatic integrations can configure pricing fallback, pricing policies, request timeouts, metadata URLs, and cache paths through `createPiProviderRuntime()` or `createPiProviderHost()`. Programmatic defaults resolve the agent directory from `PI_CODING_AGENT_DIR` (falling back to `~/.pi/agent`) and keep the OpenRouter metadata cache on disk under `<agent-dir>/extensions/pi-provider/`, matching the table above; the Pi entrypoint overrides it with Pi's own resolution. Host packages with a custom capability root can call `createPiProviderExtension({ adapterRoot, dependencies })`. The source definition [`PiProviderDependencies`](core/runtime-config.ts) is authoritative.
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). A complete non-built-in Command Code reference is available in [`examples/command-code`](examples/command-code/).
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 价格元数据,并可由 OpenRouter 补全价格和质量指标
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 引用环境变量时,如果这些变量和已存储凭据均未配置,将保留缓存或回退模型目录并跳过网络刷新,避免未配置的 Provider 产生模型目录刷新警告。
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 目录;公开 OpenRouter 元数据缓存在 `<agent-dir>/extensions/pi-provider/` 下 |
56
+ | `PI_CODING_AGENT_DIR` | 否 | `~/.pi/agent` | 修改 Pi agent 目录 |
57
57
 
58
- 程序化集成可以通过 `createPiProviderRuntime()` 或 `createPiProviderHost()` 配置价格回退、价格策略、请求超时、元数据 URL 和缓存路径。程序化默认值会从 `PI_CODING_AGENT_DIR`(回退到 `~/.pi/agent`)解析 agent 目录,并保持 OpenRouter 元数据缓存落盘到 `<agent-dir>/extensions/pi-provider/`,与上表一致;Pi 入口会用 Pi 自身的解析覆盖它。使用自定义 capability 根目录的 Host 包可以调用 `createPiProviderExtension({ adapterRoot, dependencies })`。源码定义 [`PiProviderDependencies`](core/runtime-config.ts) 是权威依据。
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` 还依赖包内私有辅助文件)。完整且不会被默认加载的 Command Code 参考实现见 [`examples/command-code`](examples/command-code/)。
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` 执行免费远程检查,`/status check` 会发送可能消耗配额的真实模型请求。配置的凭据只会发送给对应 Provider 端点,并且不会出现在 Status 输出中。
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 registeredProvider = registerProviderAdapter(pi, providerAdapter, bridge.dependencies, {}, modelDrafts);
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, {}, modelDrafts)
120
+ ? () => registerProviderAdapter(pi, providerAdapter, bridge.dependencies, piCatalog, modelDrafts)
114
121
  : undefined,
115
122
  );
116
123
  return;
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { OfficialModelMeta } from "./official-pricing.ts";
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
- officialPricing: Promise<Record<string, OfficialModelMeta>>;
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 !== "fallback"
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 && apiKey !== "proxy-managed") {
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 { fetchOfficialPricing, type OfficialModelMeta, OPENROUTER_MODELS_URL } from "./official-pricing.ts";
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 function createPiProviderHost(dependencies: Partial<PiProviderDependencies> = {}): (pi: ExtensionAPI) => void {
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 startOfficialPricingRefresh = (): void => {
94
- pricingRefreshController?.abort();
95
- pricingRefreshController = undefined;
96
- if (!runtime.enableOfficialPricingFallback || disposed) {
97
- return;
98
- }
99
- const controller = new AbortController();
100
- pricingRefreshController = controller;
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
- pricing: Record<string, OfficialModelMeta>;
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 pricingPromise = officialPricing.catch(() => ({}));
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 [pricing, providerResults] = await Promise.all([pricingPromise, providerResultsPromise]);
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
- prepareProviderRegistration(adapter, runtime, pricing, modelDrafts);
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, pricing };
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, pricing } = await buildDefinition(ctx);
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, pricing, {
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
- }