@hyav/pi-provider 0.1.2 → 0.1.3

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,15 @@
2
2
 
3
3
  This file is the authoritative user-facing release history for `@hyav/pi-provider`.
4
4
 
5
+ ## 0.1.3 - 2026-08-17
6
+
7
+ - Discover user Adapter files under `<agent-dir>/extensions/pi-provider/` in addition to built-ins; user files load last and override same-ID built-ins.
8
+ - Run the provider runtime inside a single Jiti module graph behind a thin Pi entrypoint; inject the agent directory, stored credentials, and ANSI text wrapping.
9
+ - Export the Adapter API through a Jiti-safe public entrypoint aliased as `@hyav/pi-provider`, aligned with the built-in Adapters as reference templates.
10
+ - Resolve duplicate Adapter IDs and bindings to the latest registration with a warning instead of excluding all colliding entries.
11
+ - Re-read modified Adapter files on `/reload` by clearing the Adapter module cache.
12
+ - Restore the programmatic default OpenRouter metadata cache path under the resolved agent directory.
13
+
5
14
  ## 0.1.2 - 2026-08-17
6
15
 
7
16
  - Expose one Pi package entrypoint while preserving file-level Adapter discovery when `/reload` runs.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @hyav/pi-provider
1
+ # pi-provider
2
2
 
3
3
  [简体中文](README.zh-CN.md)
4
4
 
@@ -46,11 +46,32 @@ Use `/status refresh` for free endpoint, authentication, catalog, and account ch
46
46
  | Name | Required | Default | Effect |
47
47
  |---|---:|---|---|
48
48
  | `HYPER_API_KEY` | For Charm Hyper API-key auth | None | Supplies the built-in `charm-hyper` provider credential; OAuth users may use `/login` |
49
- | `PI_CODING_AGENT_DIR` | No | `~/.pi/agent` | Changes Pi's agent directory; public OpenRouter metadata is cached under `<agent-dir>/pi-provider/` |
49
+ | `PI_CODING_AGENT_DIR` | No | `~/.pi/agent` | Changes Pi's agent directory; public OpenRouter metadata is cached under `<agent-dir>/extensions/pi-provider/` |
50
50
 
51
- Programmatic integrations can configure pricing fallback, pricing policies, request timeouts, metadata URLs, and cache paths through `createPiProviderRuntime()` or `createPiProviderHost()`. Host packages with a custom capability root can call `createPiProviderExtension({ adapterRoot, dependencies })`. The source definition [`PiProviderDependencies`](core/runtime-config.ts) is authoritative.
51
+ 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.
52
52
 
53
- Add or remove Adapter files under `@hyav/pi-provider`'s `providers/`, `status/`, `preflight/`, or `tuners/` directories, then run `/reload` to rediscover them without changing `index.ts`. Standalone trusted Adapter packages still declare their capability entries in their own Pi manifest. See the [adapter extension contract](https://github.com/hyav/pi-provider/blob/main/docs/adapter-extensions.md) for helpers, validation, conflicts, reload behavior, and lifecycle boundaries. The root [`index.ts`](index.ts) defines the public TypeScript exports.
53
+ ## Adapter discovery (file-level plug and play)
54
+
55
+ Built-in Adapters ship inside the package and are always discovered. User Adapters live under Pi's resolved agent directory and are discovered too:
56
+
57
+ ```text
58
+ <agent-dir>/extensions/pi-provider/
59
+ providers/ # provider Adapter files
60
+ status/ # status Adapter files
61
+ preflight/ # preflight Adapter files
62
+ tuners/ # tuner Adapter files
63
+ ```
64
+
65
+
66
+ 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).
67
+
68
+ Adapter files import helpers and types from `@hyav/pi-provider` (aliased inside the loader):
69
+
70
+ ```ts
71
+ import { defineProviderExtension } from "@hyav/pi-provider";
72
+ ```
73
+
74
+ Adapter files must not runtime-import Pi's bundled packages (`@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`, `@earendil-works/pi-ai`); type-only imports are fine. Runtime values such as the agent directory, stored credentials, and the ANSI text wrapper are injected by the Pi entrypoint. See the [adapter extension contract](https://github.com/hyav/pi-provider/blob/main/docs/adapter-extensions.md) for helpers, validation, conflicts, reload behavior, and lifecycle boundaries. The root [`index.ts`](index.ts) defines the public TypeScript exports.
54
75
 
55
76
  ## Before you use it
56
77
 
package/README.zh-CN.md CHANGED
@@ -1,4 +1,4 @@
1
- # @hyav/pi-provider
1
+ # pi-provider
2
2
 
3
3
  [English](README.md)
4
4
 
@@ -46,11 +46,32 @@ pi install npm:@hyav/pi-provider
46
46
  | 名称 | 必需 | 默认值 | 作用 |
47
47
  |---|---:|---|---|
48
48
  | `HYPER_API_KEY` | Charm Hyper API Key 鉴权需要 | 无 | 为内置 `charm-hyper` Provider 提供凭据;OAuth 用户可以使用 `/login` |
49
- | `PI_CODING_AGENT_DIR` | 否 | `~/.pi/agent` | 修改 Pi agent 目录;公开 OpenRouter 元数据缓存在 `<agent-dir>/pi-provider/` 下 |
49
+ | `PI_CODING_AGENT_DIR` | 否 | `~/.pi/agent` | 修改 Pi agent 目录;公开 OpenRouter 元数据缓存在 `<agent-dir>/extensions/pi-provider/` 下 |
50
50
 
51
- 程序化集成可以通过 `createPiProviderRuntime()` 或 `createPiProviderHost()` 配置价格回退、价格策略、请求超时、元数据 URL 和缓存路径;使用自定义 capability 根目录的 Host 包可以调用 `createPiProviderExtension({ adapterRoot, dependencies })`。源码定义 [`PiProviderDependencies`](core/runtime-config.ts) 是权威依据。
51
+ 程序化集成可以通过 `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) 是权威依据。
52
52
 
53
- `@hyav/pi-provider` 的 `providers/`、`status/`、`preflight/` 或 `tuners/` 目录增删 Adapter 文件后,执行 `/reload` 即可重新发现,无需修改 `index.ts`。独立的可信 Adapter 包仍通过自身 Pi manifest 声明 capability 入口。Helper、校验、冲突、reload 行为和生命周期边界见 [Adapter Extension 契约](https://github.com/hyav/pi-provider/blob/main/docs/adapter-extensions.zh-CN.md)。根目录 [`index.ts`](index.ts) 定义公开 TypeScript 导出。
53
+ ## Adapter 发现(文件级即插即用)
54
+
55
+ 内置 Adapter 随包发布,始终被扫描。用户 Adapter 放在 Pi 解析出的 agent 目录下,同样会被发现:
56
+
57
+ ```text
58
+ <agent-dir>/extensions/pi-provider/
59
+ providers/ # Provider Adapter 文件
60
+ status/ # Status Adapter 文件
61
+ preflight/ # Preflight Adapter 文件
62
+ tuners/ # Tuner Adapter 文件
63
+ ```
64
+
65
+
66
+ 在目录中增删或修改文件后执行 `/reload` 即可重新发现,无需改动包;对现有文件的修改会重新从磁盘读取。用户 Adapter 在内置之后加载,因此同 ID 的用户文件会覆盖内置 Adapter(Host 保留最新注册并发出警告)。`createPiProviderExtension({ adapterRoot })` 用自定义根替换默认用户目录;内置目录始终被扫描。包内 `providers/`、`status/`、`preflight/` 下的内置 Adapter 就是采用这种写法的参考模板——复制一份改改即可(Charm Hyper 与 `preflight/openai-codex.ts` 还依赖包内私有辅助文件)。
67
+
68
+ Adapter 文件从 `@hyav/pi-provider` 导入 helper 和类型(加载器内部做了别名映射):
69
+
70
+ ```ts
71
+ import { defineProviderExtension } from "@hyav/pi-provider";
72
+ ```
73
+
74
+ Adapter 文件不得运行时导入 Pi 的内置包(`@earendil-works/pi-coding-agent`、`@earendil-works/pi-tui`、`@earendil-works/pi-ai`),仅 type-only 导入可以;agent 目录、存储凭据和 ANSI 文本包装器等运行时值由 Pi 入口注入。Helper、校验、冲突、reload 行为和生命周期边界见 [Adapter Extension 契约](https://github.com/hyav/pi-provider/blob/main/docs/adapter-extensions.zh-CN.md)。根目录 [`index.ts`](index.ts) 定义公开 TypeScript 导出。
54
75
 
55
76
  ## 使用须知
56
77
 
@@ -1,15 +1,53 @@
1
1
  import type { Dirent } from "node:fs";
2
2
  import { readdir } from "node:fs/promises";
3
- import { dirname, extname, join, relative } from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, extname, join, relative, sep } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
7
  import { createJiti } from "jiti";
7
8
 
8
9
  const adapterDirectories = ["providers", "status", "preflight", "tuners"] as const;
9
- const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
10
+ const defaultPackageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ /** Jiti-safe module that adapter files reach through the `@hyav/pi-provider` alias. */
12
+ const publicAdaptersPath = join(defaultPackageRoot, "core", "public-adapters.ts");
10
13
 
11
14
  type AdapterExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
12
15
 
16
+ export interface AdapterRootOptions {
17
+ /** Package root containing built-in adapters; defaults to this package. */
18
+ packageRoot?: string;
19
+ /** Pi's resolved agent directory; user adapters are discovered under `<agentDir>/extensions/pi-provider`. */
20
+ agentDir?: string;
21
+ /** Explicit user adapter root; replaces the agentDir-based default when provided. */
22
+ userRoot?: string;
23
+ }
24
+
25
+ /** Built-in adapters always load first; user adapters load last so they can override. */
26
+ export function resolveAdapterRoots(options: AdapterRootOptions): string[] {
27
+ const { packageRoot, agentDir, userRoot } = options;
28
+ const defaultUserRoot =
29
+ agentDir !== undefined && agentDir !== "" ? join(agentDir, "extensions", "pi-provider") : undefined;
30
+ const resolvedUserRoot = userRoot ?? defaultUserRoot;
31
+ const roots = [packageRoot ?? defaultPackageRoot];
32
+ if (resolvedUserRoot !== undefined) roots.push(resolvedUserRoot);
33
+ return roots;
34
+ }
35
+
36
+ /**
37
+ * Jiti's module cache is Node's global `require.cache`, so a reload would
38
+ * otherwise reuse previously loaded adapter modules without reading the disk.
39
+ * Drop every cached module under the adapter roots before each load so edits
40
+ * to existing adapter files take effect on `/reload`.
41
+ */
42
+ function clearAdapterModuleCache(roots: string[]): void {
43
+ const require = createRequire(import.meta.url);
44
+ for (const key of Object.keys(require.cache)) {
45
+ if (roots.some((root) => key.startsWith(root.endsWith(sep) ? root : `${root}${sep}`))) {
46
+ delete require.cache[key];
47
+ }
48
+ }
49
+ }
50
+
13
51
  function isAdapterFile(name: string): boolean {
14
52
  if (name.endsWith(".d.ts")) return false;
15
53
  return extname(name) === ".ts" || extname(name) === ".js";
@@ -37,17 +75,25 @@ async function discoverAdapterPaths(root: string): Promise<string[]> {
37
75
  return paths.sort();
38
76
  }
39
77
 
40
- export async function loadPackageAdapterExtensions(pi: ExtensionAPI, root: string = packageRoot): Promise<void> {
41
- const jiti = createJiti(import.meta.url, { moduleCache: false, tryNative: false });
42
- for (const path of await discoverAdapterPaths(root)) {
43
- try {
44
- const factory = (await jiti.import(path, { default: true })) as unknown;
45
- if (typeof factory !== "function") {
46
- throw new TypeError("default export must be a Pi extension factory");
78
+ export async function loadPackageAdapterExtensions(pi: ExtensionAPI, options: AdapterRootOptions = {}): Promise<void> {
79
+ const roots = resolveAdapterRoots(options);
80
+ clearAdapterModuleCache(roots);
81
+ const jiti = createJiti(import.meta.url, {
82
+ moduleCache: true,
83
+ tryNative: false,
84
+ alias: { "@hyav/pi-provider": publicAdaptersPath },
85
+ });
86
+ for (const root of roots) {
87
+ for (const path of await discoverAdapterPaths(root)) {
88
+ try {
89
+ const factory = (await jiti.import(path, { default: true })) as unknown;
90
+ if (typeof factory !== "function") {
91
+ throw new TypeError("default export must be a Pi extension factory");
92
+ }
93
+ await (factory as AdapterExtensionFactory)(pi);
94
+ } catch (error) {
95
+ warnAdapterLoadIssue(relative(root, path).replaceAll("\\", "/"), error);
47
96
  }
48
- await (factory as AdapterExtensionFactory)(pi);
49
- } catch (error) {
50
- warnAdapterLoadIssue(relative(root, path).replaceAll("\\", "/"), error);
51
97
  }
52
98
  }
53
99
  }
package/core/host.ts CHANGED
@@ -148,10 +148,11 @@ export function createPiProviderHost(dependencies: Partial<PiProviderDependencie
148
148
  const result: T[] = [];
149
149
  for (const [id, group] of groups) {
150
150
  if (group.length > 1) {
151
- warnAdapterIssue(`excluded ${group.length} colliding ${label} entries for ${JSON.stringify(id)}`);
152
- continue;
151
+ warnAdapterIssue(
152
+ `resolved ${group.length} colliding ${label} entries for ${JSON.stringify(id)} to the latest registration`,
153
+ );
153
154
  }
154
- result.push(group[0]!);
155
+ result.push(group[group.length - 1]!);
155
156
  }
156
157
  return result;
157
158
  };
@@ -1,7 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
4
  import { withDeadline } from "./deadline.ts";
6
5
  import type { ModelQualityScore, ProviderCost, ProviderModel, ProviderModelDraft } from "./types.ts";
7
6
 
@@ -68,8 +67,8 @@ const pricingCache = new Map<string, PricingCacheEntry>();
68
67
  const pricingRequests = new Map<string, Promise<Record<string, OfficialModelMeta>>>();
69
68
 
70
69
  /** Default cache for OpenRouter metadata, not Pi's native model catalog. */
71
- export function getDefaultOpenRouterMetadataCachePath(): string {
72
- return join(getAgentDir(), "pi-provider", "openrouter-model-metadata.json");
70
+ export function getDefaultOpenRouterMetadataCachePath(agentDir: string): string {
71
+ return join(agentDir, "extensions", "pi-provider", "openrouter-model-metadata.json");
73
72
  }
74
73
 
75
74
  function cloneCost(cost: ProviderCost): ProviderCost {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Jiti-safe public entrypoint for user adapter files discovered from the
3
+ * adapter roots. `loadPackageAdapterExtensions` aliases `@hyav/pi-provider`
4
+ * to this module so adapter files can import the same helpers and types the
5
+ * built-in adapters use, without resolving Pi's bundled runtime packages.
6
+ */
7
+
8
+ export type {
9
+ AdapterExtensionContext,
10
+ PreflightExtensionDefinition,
11
+ ProviderExtensionDefinition,
12
+ StatusExtensionDefinition,
13
+ TunerExtensionDefinition,
14
+ } from "./adapter-extensions.ts";
15
+ export {
16
+ definePreflightExtension,
17
+ defineProviderExtension,
18
+ defineStatusExtension,
19
+ defineTunerExtension,
20
+ } from "./adapter-extensions.ts";
21
+ export { withDeadline } from "./deadline.ts";
22
+ export { isProviderDataError, ProviderDataError } from "./errors.ts";
23
+ export { createOpenCodeCatalogPreflightAdapter } from "./opencode-preflight.ts";
24
+ export type {
25
+ PreflightAdapter,
26
+ PreflightContextLike,
27
+ PreflightModel,
28
+ PreflightSnapshot,
29
+ } from "./preflight-manager.ts";
30
+ export { normalizeProviderModels } from "./provider-registration.ts";
31
+ export { parseRetryAfter } from "./retry-after.ts";
32
+ export type { StatusContextLike } from "./status-manager.ts";
33
+ export type {
34
+ ActiveModel,
35
+ ModelCatalogStatus,
36
+ ProviderAdapter,
37
+ ProviderModel,
38
+ ProviderModelDraft,
39
+ ProviderRefreshContext,
40
+ StatusAdapter,
41
+ StatusContext,
42
+ StatusEntry,
43
+ StatusSnapshot,
44
+ StoredCredentialLike,
45
+ TunerContext,
46
+ } from "./types.ts";
@@ -1,8 +1,10 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
1
3
  import { isValidTimeoutMs } from "./deadline.ts";
2
4
  import type { PiProviderDefinition } from "./definition.ts";
3
5
  import { getDefaultOpenRouterMetadataCachePath, OPENROUTER_MODELS_URL } from "./official-pricing.ts";
4
6
  import { validatePricingPolicy } from "./pricing-adjustments.ts";
5
- import type { ProviderPricingPolicy } from "./types.ts";
7
+ import type { ProviderPricingPolicy, StoredCredentialLike } from "./types.ts";
6
8
 
7
9
  export interface PiProviderDependencies {
8
10
  fetch: typeof globalThis.fetch;
@@ -14,8 +16,14 @@ export interface PiProviderDependencies {
14
16
  officialPricingTimeoutMs: number;
15
17
  officialPricingCacheTtlMs: number;
16
18
  officialPricingMaxStaleMs: number;
19
+ /** Resolved Pi agent directory; empty disables disk persistence of pricing metadata. */
20
+ agentDir: string;
17
21
  /** Persistent cache for OpenRouter metadata used by the pricing fallback. */
18
22
  openRouterMetadataCachePath: string;
23
+ /** Read Pi's stored credential metadata; injected by the Pi entrypoint. */
24
+ readStoredCredential: (providerId: string) => StoredCredentialLike | undefined;
25
+ /** Wrap ANSI-aware text to a render width; injected by the Pi entrypoint. */
26
+ wrapTextWithAnsi: (text: string, width: number) => string[];
19
27
  enableOfficialPricingFallback: boolean;
20
28
  /** Optional Pi Provider-level price policies keyed by Provider ID. */
21
29
  pricingPolicies?: Record<string, ProviderPricingPolicy>;
@@ -23,7 +31,68 @@ export interface PiProviderDependencies {
23
31
 
24
32
  export type PiProviderLoader = (runtime: PiProviderDependencies) => Promise<PiProviderDefinition>;
25
33
 
26
- const defaultDependencies: PiProviderDependencies = {
34
+ /**
35
+ * Resolve Pi's agent directory without importing Pi's bundled packages, so the
36
+ * Jiti module graph and programmatic consumers share the same default. Mirrors
37
+ * Pi's `getAgentDir()`: `PI_CODING_AGENT_DIR` wins, `~/` expands to the home
38
+ * directory, and the fallback is `~/.pi/agent`.
39
+ */
40
+ export function resolveDefaultAgentDir(): string {
41
+ const configured = process.env.PI_CODING_AGENT_DIR;
42
+ const raw =
43
+ configured !== undefined && configured.trim() !== "" ? configured.trim() : join(homedir(), ".pi", "agent");
44
+ if (raw === "~") return homedir();
45
+ if (raw.startsWith("~/")) return join(homedir(), raw.slice(2));
46
+ return raw;
47
+ }
48
+
49
+ /** Degraded fallback used only when the Pi entrypoint does not inject the real wrapper. */
50
+ const WIDE_CHAR_RANGES: Array<[number, number]> = [
51
+ [0x1100, 0x115f],
52
+ [0x2329, 0x232a],
53
+ [0x2e80, 0xa4cf],
54
+ [0xac00, 0xd7a3],
55
+ [0xf900, 0xfaff],
56
+ [0xfe30, 0xfe4f],
57
+ [0xff00, 0xff60],
58
+ [0xffe0, 0xffe6],
59
+ [0x1f300, 0x1f64f],
60
+ [0x1f900, 0x1f9ff],
61
+ [0x20000, 0x2fffd],
62
+ [0x30000, 0x3fffd],
63
+ ];
64
+
65
+ function displayWidth(text: string): number {
66
+ let width = 0;
67
+ for (const ch of text) {
68
+ const code = ch.codePointAt(0) ?? 0;
69
+ const wide = WIDE_CHAR_RANGES.some(([start, end]) => code >= start && code <= end);
70
+ width += wide ? 2 : 1;
71
+ }
72
+ return width;
73
+ }
74
+
75
+ function defaultWrapTextWithAnsi(text: string, width: number): string[] {
76
+ if (width <= 0) return [text];
77
+ const plain = text.replace(/\u001b\[[0-9;]*m/g, "");
78
+ if (displayWidth(plain) <= width) return [text];
79
+ const chunks: string[] = [];
80
+ let chunk = "";
81
+ for (const ch of plain) {
82
+ if (chunk !== "" && displayWidth(chunk) + displayWidth(ch) > width) {
83
+ chunks.push(chunk);
84
+ chunk = ch;
85
+ } else {
86
+ chunk += ch;
87
+ }
88
+ }
89
+ if (chunk !== "") chunks.push(chunk);
90
+ return chunks;
91
+ }
92
+
93
+ type DefaultDependencies = Omit<PiProviderDependencies, "agentDir" | "openRouterMetadataCachePath">;
94
+
95
+ const defaultDependencies: DefaultDependencies = {
27
96
  fetch: globalThis.fetch,
28
97
  now: Date.now,
29
98
  modelDiscoveryTimeoutMs: 3_000,
@@ -33,13 +102,22 @@ const defaultDependencies: PiProviderDependencies = {
33
102
  officialPricingTimeoutMs: 3_000,
34
103
  officialPricingCacheTtlMs: 60 * 60 * 1_000,
35
104
  officialPricingMaxStaleMs: 24 * 60 * 60 * 1_000,
36
- openRouterMetadataCachePath: getDefaultOpenRouterMetadataCachePath(),
105
+ readStoredCredential: () => undefined,
106
+ wrapTextWithAnsi: defaultWrapTextWithAnsi,
37
107
  enableOfficialPricingFallback: true,
38
108
  pricingPolicies: {},
39
109
  };
40
110
 
41
- export function getDefaultPiProviderDependencies(): PiProviderDependencies {
42
- return { ...defaultDependencies };
111
+ /**
112
+ * Programmatic defaults keep the resolved agent directory and its pricing cache
113
+ * path. The Pi entrypoint overrides `agentDir` with Pi's own resolution.
114
+ */
115
+ export function getDefaultPiProviderDependencies(agentDir = resolveDefaultAgentDir()): PiProviderDependencies {
116
+ return {
117
+ ...defaultDependencies,
118
+ agentDir,
119
+ openRouterMetadataCachePath: getDefaultOpenRouterMetadataCachePath(agentDir),
120
+ };
43
121
  }
44
122
 
45
123
  export function validatePiProviderDependencies(runtime: PiProviderDependencies): void {
@@ -64,8 +142,14 @@ export function validatePiProviderDependencies(runtime: PiProviderDependencies):
64
142
  if (typeof runtime.officialPricingUrl !== "string" || runtime.officialPricingUrl.trim() === "") {
65
143
  throw new Error("Pi Provider officialPricingUrl must be a non-empty string");
66
144
  }
67
- if (typeof runtime.openRouterMetadataCachePath !== "string" || runtime.openRouterMetadataCachePath.trim() === "") {
68
- throw new Error("Pi Provider openRouterMetadataCachePath must be a non-empty path");
145
+ if (typeof runtime.openRouterMetadataCachePath !== "string") {
146
+ throw new Error("Pi Provider openRouterMetadataCachePath must be a string");
147
+ }
148
+ if (typeof runtime.readStoredCredential !== "function") {
149
+ throw new Error("Pi Provider readStoredCredential must be a function");
150
+ }
151
+ if (typeof runtime.wrapTextWithAnsi !== "function") {
152
+ throw new Error("Pi Provider wrapTextWithAnsi must be a function");
69
153
  }
70
154
  if (typeof runtime.enableOfficialPricingFallback !== "boolean") {
71
155
  throw new Error("Pi Provider enableOfficialPricingFallback must be a boolean");
@@ -88,7 +172,7 @@ export function validatePiProviderDependencies(runtime: PiProviderDependencies):
88
172
  export function resolvePiProviderDependencies(
89
173
  dependencies: Partial<PiProviderDependencies> = {},
90
174
  ): PiProviderDependencies {
91
- const runtime = { ...defaultDependencies, ...dependencies };
175
+ const runtime = { ...getDefaultPiProviderDependencies(), ...dependencies };
92
176
  if (runtime.pricingPolicies === undefined) runtime.pricingPolicies = {};
93
177
  validatePiProviderDependencies(runtime);
94
178
  return runtime;
@@ -0,0 +1,26 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadPackageAdapterExtensions } from "./adapter-loader.ts";
3
+ import { createPiProviderHost } from "./host.ts";
4
+ import type { PiProviderDependencies } from "./runtime-config.ts";
5
+ import type { StoredCredentialLike } from "./types.ts";
6
+
7
+ /** Runtime values resolved by the Pi-loaded entrypoint and injected into the Jiti graph. */
8
+ export interface PiProviderEntry {
9
+ agentDir: string;
10
+ readStoredCredential: (providerId: string) => StoredCredentialLike | undefined;
11
+ wrapTextWithAnsi: (text: string, width: number) => string[];
12
+ adapterRoot?: string;
13
+ dependencies?: Partial<PiProviderDependencies>;
14
+ }
15
+
16
+ /** Runs the Pi Provider host and adapter discovery inside a single Jiti module graph. */
17
+ export async function runPiProviderEntry(pi: ExtensionAPI, entry: PiProviderEntry): Promise<void> {
18
+ const piProviderHost = createPiProviderHost({
19
+ agentDir: entry.agentDir,
20
+ readStoredCredential: entry.readStoredCredential,
21
+ wrapTextWithAnsi: entry.wrapTextWithAnsi,
22
+ ...entry.dependencies,
23
+ });
24
+ piProviderHost(pi);
25
+ await loadPackageAdapterExtensions(pi, { agentDir: entry.agentDir, userRoot: entry.adapterRoot });
26
+ }
package/core/runtime.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { readStoredCredential } from "@earendil-works/pi-coding-agent";
3
- import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
2
  import type { PiProviderDefinition } from "./definition.ts";
5
3
  import { validatePiProviderDefinition } from "./definition.ts";
6
4
  import { LiveCheckManager, type LiveCheckResult } from "./live-check-manager.ts";
@@ -33,6 +31,7 @@ import type {
33
31
  ProviderCost,
34
32
  ProviderModelDraft,
35
33
  ProviderModelMetadata,
34
+ StoredCredentialLike,
36
35
  } from "./types.ts";
37
36
 
38
37
  type ActiveModel = NonNullable<ExtensionContext["model"]>;
@@ -41,7 +40,10 @@ type StatusNotificationContext = Pick<ExtensionContext, "modelRegistry" | "ui">
41
40
  };
42
41
  const STATUS_WIDGET_KEY = "pi-provider-status";
43
42
 
44
- function readProviderCredentialMetadata(provider: string): unknown {
43
+ function readProviderCredentialMetadata(
44
+ provider: string,
45
+ readStoredCredential: (providerId: string) => StoredCredentialLike | undefined,
46
+ ): unknown {
45
47
  try {
46
48
  const credential = readStoredCredential(provider);
47
49
  if (credential?.type !== "oauth") return undefined;
@@ -59,7 +61,11 @@ function clearTransientStatus(ctx: Pick<ExtensionContext, "ui">): void {
59
61
  ctx.ui.setWidget(STATUS_WIDGET_KEY, undefined);
60
62
  }
61
63
 
62
- function showTransientStatus(message: string, ctx: StatusNotificationContext): boolean {
64
+ function showTransientStatus(
65
+ message: string,
66
+ ctx: StatusNotificationContext,
67
+ wrapTextWithAnsi: (text: string, width: number) => string[],
68
+ ): boolean {
63
69
  if ((ctx.mode !== "tui" && ctx.mode !== "rpc") || typeof ctx.ui.setWidget !== "function") return false;
64
70
  // RPC cannot render component factories, so keep its plain text protocol unchanged.
65
71
  if (ctx.mode === "rpc") {
@@ -301,7 +307,7 @@ export function installPiProviderRuntime(
301
307
  );
302
308
  const message = report.report;
303
309
  if (report.warningLevel !== "hard") {
304
- statusPresentationVisible = showTransientStatus(message, ctx);
310
+ statusPresentationVisible = showTransientStatus(message, ctx, runtime.wrapTextWithAnsi);
305
311
  if (!statusPresentationVisible) ctx.ui.notify(message, "info");
306
312
  return;
307
313
  }
@@ -335,7 +341,7 @@ export function installPiProviderRuntime(
335
341
  model,
336
342
  modelRegistry: ctx.modelRegistry,
337
343
  getCredentialKey: () => ctx.modelRegistry.getApiKeyForProvider(model.provider),
338
- getCredentialMetadata: () => readProviderCredentialMetadata(model.provider),
344
+ getCredentialMetadata: () => readProviderCredentialMetadata(model.provider, runtime.readStoredCredential),
339
345
  };
340
346
  const preflightContext: PreflightContextLike = { model, modelRegistry: ctx.modelRegistry };
341
347
  const refreshChecks: Array<Promise<unknown>> = [];
package/core/types.ts CHANGED
@@ -22,6 +22,16 @@ export interface ModelMetadataStatus {
22
22
  source?: string;
23
23
  }
24
24
 
25
+ /**
26
+ * Narrow credential shape shared across Pi's isolated extension module contexts.
27
+ * The full Credential type comes from Pi's bundled AI package; consumers of
28
+ * injected credential readers must not rely on `instanceof` or extra fields.
29
+ */
30
+ export interface StoredCredentialLike {
31
+ readonly type?: string;
32
+ readonly teamName?: string;
33
+ }
34
+
25
35
  export interface ModelFieldSources {
26
36
  contextWindow?: ModelFieldSource;
27
37
  maxTokens?: ModelFieldSource;
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { loadPackageAdapterExtensions } from "./core/adapter-loader.ts";
3
- import { createPiProviderHost } from "./core/host.ts";
2
+ import { getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
3
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
+ import { createJiti } from "jiti";
4
5
  import type { PiProviderDependencies } from "./core/runtime-config.ts";
5
6
 
6
7
  export type {
@@ -16,6 +17,7 @@ export {
16
17
  defineStatusExtension,
17
18
  defineTunerExtension,
18
19
  } from "./core/adapter-extensions.ts";
20
+ export { withDeadline } from "./core/deadline.ts";
19
21
  export type { ProviderDataErrorLike } from "./core/errors.ts";
20
22
  export { isProviderDataError, ProviderDataError } from "./core/errors.ts";
21
23
  export type {
@@ -59,6 +61,7 @@ export {
59
61
  parseOpenRouterPricing,
60
62
  setPricingCache,
61
63
  } from "./core/official-pricing.ts";
64
+ export { createOpenCodeCatalogPreflightAdapter } from "./core/opencode-preflight.ts";
62
65
  export type {
63
66
  PreflightAdapter,
64
67
  PreflightContext,
@@ -70,6 +73,8 @@ export type {
70
73
  } from "./core/preflight-manager.ts";
71
74
  export { getPreflightKey, normalizePreflightSnapshot, PreflightManager } from "./core/preflight-manager.ts";
72
75
  export { applyPricingAdjustment, resolvePricingDetails } from "./core/pricing-adjustments.ts";
76
+ export { normalizeProviderModels } from "./core/provider-registration.ts";
77
+ export { parseRetryAfter } from "./core/retry-after.ts";
73
78
  export type { StatusDiagnostics, StatusErrorState } from "./core/status-manager.ts";
74
79
  export { normalizeStatusSnapshot, StatusManager } from "./core/status-manager.ts";
75
80
  export { applyTunerAdapters, sortTunerAdapters } from "./core/tuner-manager.ts";
@@ -109,7 +114,7 @@ export type {
109
114
  } from "./core/types.ts";
110
115
 
111
116
  export interface PiProviderExtensionOptions {
112
- /** Package root containing providers, status, preflight, and tuners directories. */
117
+ /** User adapter root; replaces the default `<agentDir>/pi-provider` directory. */
113
118
  adapterRoot?: string;
114
119
  /** Host runtime dependency overrides. */
115
120
  dependencies?: Partial<PiProviderDependencies>;
@@ -119,10 +124,27 @@ export interface PiProviderExtensionOptions {
119
124
  export function createPiProviderExtension(
120
125
  options: PiProviderExtensionOptions = {},
121
126
  ): (pi: ExtensionAPI) => Promise<void> {
122
- const piProviderHost = createPiProviderHost(options.dependencies);
123
127
  return async (pi) => {
124
- piProviderHost(pi);
125
- await loadPackageAdapterExtensions(pi, options.adapterRoot);
128
+ const jiti = createJiti(import.meta.url, { moduleCache: true, tryNative: false });
129
+ const { runPiProviderEntry } = (await jiti.import("./core/runtime-entry.ts")) as {
130
+ runPiProviderEntry: (
131
+ pi: ExtensionAPI,
132
+ entry: {
133
+ agentDir: string;
134
+ readStoredCredential: typeof readStoredCredential;
135
+ wrapTextWithAnsi: typeof wrapTextWithAnsi;
136
+ adapterRoot?: string;
137
+ dependencies?: Partial<PiProviderDependencies>;
138
+ },
139
+ ) => Promise<void>;
140
+ };
141
+ await runPiProviderEntry(pi, {
142
+ agentDir: getAgentDir(),
143
+ readStoredCredential,
144
+ wrapTextWithAnsi,
145
+ adapterRoot: options.adapterRoot,
146
+ dependencies: options.dependencies,
147
+ });
126
148
  };
127
149
  }
128
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyav/pi-provider",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",
@@ -1,7 +1,5 @@
1
- import { definePreflightExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import type { PreflightAdapter } from "../core/preflight-manager.ts";
4
- import { parseRetryAfter } from "../core/retry-after.ts";
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
  import { hyperJsonHeaders } from "../providers/charm-hyper/constants.ts";
6
4
  import { HYPER_MODELS_URL, HYPER_PROVIDER_URL, parseHyperModels } from "../providers/charm-hyper.ts";
7
5
 
@@ -1,7 +1,5 @@
1
- import { definePreflightExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import type { PreflightAdapter } from "../core/preflight-manager.ts";
4
- import { parseRetryAfter } from "../core/retry-after.ts";
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
 
6
4
  export const DEEPSEEK_MODELS_URL = "https://api.deepseek.com/models";
7
5
 
@@ -1,7 +1,5 @@
1
- import { definePreflightExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import type { PreflightAdapter } from "../core/preflight-manager.ts";
4
- import { parseRetryAfter } from "../core/retry-after.ts";
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
 
6
4
  export const GOOGLE_MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
7
5
 
@@ -1,7 +1,5 @@
1
- import { definePreflightExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import type { PreflightAdapter } from "../core/preflight-manager.ts";
4
- import { parseRetryAfter } from "../core/retry-after.ts";
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { definePreflightExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
  import { extractCodexAccountId } from "../status/openai-codex.ts";
6
4
 
7
5
  export const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
@@ -1,6 +1,5 @@
1
- import { definePreflightExtension } from "../core/adapter-extensions.ts";
2
- import { createOpenCodeCatalogPreflightAdapter } from "../core/opencode-preflight.ts";
3
- import type { PreflightAdapter } from "../core/preflight-manager.ts";
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { createOpenCodeCatalogPreflightAdapter, definePreflightExtension } from "@hyav/pi-provider";
4
3
 
5
4
  export const OPENCODE_GO_MODELS_URL = "https://opencode.ai/zen/go/v1/models";
6
5
 
@@ -1,6 +1,5 @@
1
- import { definePreflightExtension } from "../core/adapter-extensions.ts";
2
- import { createOpenCodeCatalogPreflightAdapter } from "../core/opencode-preflight.ts";
3
- import type { PreflightAdapter } from "../core/preflight-manager.ts";
1
+ import type { PreflightAdapter } from "@hyav/pi-provider";
2
+ import { createOpenCodeCatalogPreflightAdapter, definePreflightExtension } from "@hyav/pi-provider";
4
3
 
5
4
  export const OPENCODE_MODELS_URL = "https://opencode.ai/zen/v1/models";
6
5
 
@@ -1,7 +1,3 @@
1
- import { defineProviderExtension } from "../core/adapter-extensions.ts";
2
- import { withDeadline } from "../core/deadline.ts";
3
- import { isProviderDataError, ProviderDataError } from "../core/errors.ts";
4
- import { normalizeProviderModels } from "../core/provider-registration.ts";
5
1
  import type {
6
2
  ModelCatalogStatus,
7
3
  ProviderAdapter,
@@ -9,7 +5,14 @@ import type {
9
5
  ProviderModelDraft,
10
6
  ProviderRefreshContext,
11
7
  ThinkingLevel,
12
- } from "../core/types.ts";
8
+ } from "@hyav/pi-provider";
9
+ import {
10
+ defineProviderExtension,
11
+ isProviderDataError,
12
+ normalizeProviderModels,
13
+ ProviderDataError,
14
+ withDeadline,
15
+ } from "@hyav/pi-provider";
13
16
  import { HYPER_BASE_URL, HYPER_USER_AGENT, hyperJsonHeaders } from "./charm-hyper/constants.ts";
14
17
  import { createCharmHyperOAuth } from "./charm-hyper/oauth.ts";
15
18
 
@@ -1,7 +1,5 @@
1
- import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import { parseRetryAfter } from "../core/retry-after.ts";
4
- import type { StatusAdapter, StatusSnapshot } from "../core/types.ts";
1
+ import type { StatusAdapter, StatusSnapshot } from "@hyav/pi-provider";
2
+ import { defineStatusExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
  import { hyperJsonHeaders } from "../providers/charm-hyper/constants.ts";
6
4
 
7
5
  const CREDITS_URL = "https://hyper.charm.land/v1/credits";
@@ -1,7 +1,5 @@
1
- import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import { parseRetryAfter } from "../core/retry-after.ts";
4
- import type { StatusAdapter, StatusSnapshot } from "../core/types.ts";
1
+ import type { StatusAdapter, StatusSnapshot } from "@hyav/pi-provider";
2
+ import { defineStatusExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
 
6
4
  export const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
7
5
 
@@ -1,7 +1,5 @@
1
- import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import { parseRetryAfter } from "../core/retry-after.ts";
4
- import type { StatusAdapter, StatusEntry, StatusSnapshot } from "../core/types.ts";
1
+ import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
2
+ import { defineStatusExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
 
6
4
  export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
7
5
  const ACCOUNT_ID_CLAIM = "https://api.openai.com/auth";
@@ -1,7 +1,5 @@
1
- import { defineStatusExtension } from "../core/adapter-extensions.ts";
2
- import { ProviderDataError } from "../core/errors.ts";
3
- import { parseRetryAfter } from "../core/retry-after.ts";
4
- import type { StatusAdapter, StatusEntry, StatusSnapshot } from "../core/types.ts";
1
+ import type { StatusAdapter, StatusEntry, StatusSnapshot } from "@hyav/pi-provider";
2
+ import { defineStatusExtension, ProviderDataError, parseRetryAfter } from "@hyav/pi-provider";
5
3
 
6
4
  export const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
7
5