pi-openmodel-provider 0.2.17 → 0.2.18

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/AGENTS.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # Agent Instructions
2
2
 
3
- - This package is **pi-openmodel-provider** for OpenModel.ai, **NOT** OpenRouter.
4
- - OpenModel is a multi-model AI gateway, similar to OpenRouter but a different service.
5
- - Models are fetched dynamically from OpenModel's API at startup — no hardcoded model list.
6
- - Models are cached locally at `~/.pi/agent/cache/openmodel-models.json` with a 5-minute TTL to avoid hitting the API on every startup.
7
- - Compat flags are set per provider (openai, anthropic, deepseek, qwen, zai) for optimal protocol compatibility.
8
- - If the `/v1/models` endpoint fails (no API key), protocols are inferred from the provider.
9
- - See `.agents/skills/pi-openmodel-info/SKILL.md` for full documentation.
10
- - Follow [CONTRIBUTING.md](CONTRIBUTING.md) before changing code.
11
- - Use [RELEASE.md](RELEASE.md) for release process.
3
+ This package is **pi-openmodel-provider** for [OpenModel.ai](https://www.openmodel.ai) **NOT** OpenRouter.
4
+
5
+ ## Key facts for LLMs
6
+
7
+ - Models are fetched dynamically from OpenModel's API at startup. No hardcoded model list.
8
+ - Cached at `~/.pi/agent/cache/openmodel-models.json` with 5-min TTL.
9
+ - Compat flags per provider (openai, anthropic, deepseek, qwen, zai) for optimal protocol compatibility.
10
+ - If `/v1/models` fails (no API key), protocols are inferred from the provider.
11
+ - Full docs at `.agents/skills/pi-openmodel-info/SKILL.md`.
12
+ - General project docs: [README.md](README.md), [CONTRIBUTING.md](CONTRIBUTING.md), [RELEASE.md](RELEASE.md).
12
13
 
13
14
  **Maintained by** [Ivan Gabriel Yarupaitan Rivera](https://www.vanchi.pro/)
package/CHANGELOG.md CHANGED
@@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.2.17] - 2026-06-23
8
+ ## [0.2.18] - 2026-06-26
9
+
10
+ ### Added
11
+ - `src/health.ts` — shared `HealthStatus` type and `determineHealth()` function, extracted from `src/api/stability.ts` and `src/formatters/stability.ts` to eliminate code duplication
12
+ - `tests/test-cache.ts` — 12 tests covering cache read (valid, expired, corrupted, missing) and write (success, directory creation, error suppression)
13
+ - `CacheFs` interface in `src/cache.ts` for dependency injection (matching `fetchImpl` pattern)
14
+ - `test:cache` npm script
15
+
16
+ ### Changed
17
+ - `src/api/stability.ts` — removed local `determineHealthFallback()` copy, imports from `src/health.ts` instead
18
+ - `src/formatters/stability.ts` — removed local `determineHealth()` copy, imports from `src/health.ts` instead
19
+ - `src/api/models.ts` — eliminated unsafe `as unknown as OpenModelProviderModel` cast and 4 `as number` price casts via type annotation and `getNumberPrice()` helper; removed `as const` on model base object
20
+ - `src/providers/compat.ts` — removed unused `api` parameter from `compatForProvider()`
21
+ - `index.ts` — replaced blocking `readFileSync` with `await readFile` from `fs/promises`
22
+ - `LICENSE` — added copyright holder name
23
+ - `tsconfig.json` — enabled `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`
24
+ - `AGENTS.md` — reduced to LLM-focused bullet points with references to README and SKILL.md
25
+ - `README.md` — added `health.ts` to architecture tree, added `test:cache` to development section
9
26
 
10
27
  ### Changed
11
28
  - **Major refactor (SRP):** Reorganized `src/` into single-responsibility modules
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026
3
+ Copyright (c) 2026 Ivan Gabriel Yarupaitan Rivera
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -161,6 +161,7 @@ npm run test:auth
161
161
  npm run test:pricing
162
162
  npm run test:stability
163
163
  npm run test:edge
164
+ npm run test:cache
164
165
  ```
165
166
 
166
167
  ### Codebase Architecture
@@ -181,6 +182,7 @@ src/
181
182
  │ └── validate.ts # sanitizeApiKey() + isValidApiKey()
182
183
  ├── formatters/ # Pure display formatting
183
184
  │ └── stability.ts # formatHealthStatus() + formatConfidence()
185
+ ├── health.ts # Shared health status determination
184
186
  ├── cache.ts # Local model cache (read/write)
185
187
  ├── errors.ts # API error parsing + friendly messages
186
188
  └── stub.d.ts # Type stubs for pi peer dependency
package/index.ts CHANGED
@@ -12,9 +12,8 @@ import {
12
12
  fetchModelStabilityDetail,
13
13
  } from "./src/api/stability.ts"
14
14
  import { formatHealthStatus } from "./src/formatters/stability.ts"
15
- import { friendlyMessage } from "./src/errors.ts"
16
15
  import { readModelCache, writeModelCache } from "./src/cache.ts"
17
- import { readFileSync } from "node:fs"
16
+ import { readFile } from "node:fs/promises"
18
17
  import { homedir } from "node:os"
19
18
 
20
19
  export default async function (pi: ExtensionAPI) {
@@ -78,15 +77,12 @@ export default async function (pi: ExtensionAPI) {
78
77
  description: "Show OpenModel provider status",
79
78
  handler: async (_args: string, ctx: any) => {
80
79
  const count = models.length
81
- const status = count > 0
82
- ? `✅ ${count} models loaded`
83
- : modelError ?? "❌ No models loaded"
84
80
 
85
81
  // Detect if user has configured an API key in auth.json
86
82
  let hasApiKey = false
87
83
  try {
88
84
  const authPath = `${homedir()}/.pi/agent/auth.json`
89
- const content = readFileSync(authPath, "utf-8")
85
+ const content = await readFile(authPath, "utf-8")
90
86
  const data = JSON.parse(content)
91
87
  hasApiKey = !!(data.openmodel?.access || data.openmodel?.refresh)
92
88
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openmodel-provider",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "pi custom provider for OpenModel.ai - Multi-model AI gateway",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -40,7 +40,8 @@
40
40
  "test:pricing": "tsx tests/test-pricing.ts",
41
41
  "test:stability": "tsx tests/test-stability.ts",
42
42
  "test:edge": "tsx tests/test-edge-cases.ts",
43
- "test": "tsx tests/test-models.ts && tsx tests/test-auth.ts && tsx tests/test-pricing.ts && tsx tests/test-stability.ts && tsx tests/test-edge-cases.ts"
43
+ "test:cache": "tsx tests/test-cache.ts",
44
+ "test": "tsx tests/test-models.ts && tsx tests/test-auth.ts && tsx tests/test-pricing.ts && tsx tests/test-stability.ts && tsx tests/test-edge-cases.ts && tsx tests/test-cache.ts"
44
45
  },
45
46
  "pi": {
46
47
  "extensions": [
package/src/api/models.ts CHANGED
@@ -161,6 +161,15 @@ async function fetchLegacyModels(options?: {
161
161
  // Orchestration
162
162
  // ──────────────────────────────────────────────
163
163
 
164
+ /** Safely extract a numeric price from the prices record */
165
+ function getNumberPrice(
166
+ prices: Record<string, number | Record<string, number>>,
167
+ key: string,
168
+ ): number | undefined {
169
+ const val = prices[key]
170
+ return typeof val === "number" ? val : undefined
171
+ }
172
+
164
173
  /**
165
174
  * Fetch all models from OpenModel API (public, no auth required for web endpoint).
166
175
  *
@@ -200,17 +209,17 @@ export async function fetchOpenModelModels(options?: {
200
209
  api = inferApiFromProvider(web.provider_key)
201
210
  }
202
211
 
203
- // Parse pricing
204
- const inputPrice = pricePerMillion(web.prices.input_cost_per_token as number)
205
- const outputPrice = pricePerMillion(web.prices.output_cost_per_token as number)
206
- const cacheRead = pricePerMillion(web.prices.cache_read_input_token_cost as number)
207
- const cacheWrite = pricePerMillion(web.prices.cache_creation_input_token_cost as number)
212
+ // Parse pricing (safely — some price fields may be Record<string, number>)
213
+ const inputPrice = pricePerMillion(getNumberPrice(web.prices, "input_cost_per_token"))
214
+ const outputPrice = pricePerMillion(getNumberPrice(web.prices, "output_cost_per_token"))
215
+ const cacheRead = pricePerMillion(getNumberPrice(web.prices, "cache_read_input_token_cost"))
216
+ const cacheWrite = pricePerMillion(getNumberPrice(web.prices, "cache_creation_input_token_cost"))
208
217
 
209
218
  // Build model config
210
219
  const reasoning = web.supports.supports_reasoning ?? false
211
- const compat = compatForProvider(web.provider_key, api, reasoning)
220
+ const compat = compatForProvider(web.provider_key, reasoning)
212
221
 
213
- const base = {
222
+ const model: OpenModelProviderModel = {
214
223
  id,
215
224
  name: id,
216
225
  reasoning,
@@ -224,13 +233,9 @@ export async function fetchOpenModelModels(options?: {
224
233
  contextWindow: web.max.max_input_tokens ?? 128_000,
225
234
  maxTokens: web.max.max_output_tokens ?? web.max.max_tokens ?? 16_384,
226
235
  api,
227
- } as const
228
-
229
- const model = {
230
- ...base,
231
236
  ...(reasoning ? { thinkingLevelMap: thinkingLevelMapForApi(api) } : {}),
232
237
  ...(compat ? { compat } : {}),
233
- } as unknown as OpenModelProviderModel
238
+ }
234
239
 
235
240
  models.push(model)
236
241
  }
@@ -12,18 +12,12 @@
12
12
  */
13
13
 
14
14
  import { parseWebError, friendlyMessage } from "../errors.ts"
15
+ import { determineHealth } from "../health.ts"
16
+ import type { HealthStatus } from "../health.ts"
15
17
 
16
18
  export const STABILITY_SUMMARY_URL =
17
19
  "https://api.openmodel.ai/web/v1/model-stability/summary"
18
20
 
19
- /** Health status derived from success rate */
20
- export type HealthStatus =
21
- | "operational"
22
- | "healthy"
23
- | "degraded"
24
- | "unstable"
25
- | "no_data"
26
-
27
21
  /** Confidence level based on sample size */
28
22
  export type ConfidenceLevel = "high" | "medium" | "low"
29
23
 
@@ -100,7 +94,7 @@ export async function fetchModelStabilitySummary(options?: {
100
94
 
101
95
  return body.data.map((item) => ({
102
96
  ...item,
103
- health_status: determineHealthFallback(item.success_rate, item.confidence),
97
+ health_status: determineHealth(item.success_rate, item.confidence),
104
98
  }))
105
99
  }
106
100
 
@@ -161,24 +155,9 @@ export async function fetchModelStabilityDetail(
161
155
 
162
156
  return {
163
157
  ...body.data,
164
- health_status: determineHealthFallback(
158
+ health_status: determineHealth(
165
159
  body.data.summary.success_rate,
166
160
  body.data.confidence,
167
161
  ),
168
162
  }
169
163
  }
170
-
171
- /**
172
- * Inline fallback to avoid circular dependency with formatters.
173
- * determineHealth() in formatters/stability.ts is the canonical version.
174
- */
175
- function determineHealthFallback(
176
- successRate: number,
177
- confidence: ConfidenceLevel,
178
- ): HealthStatus {
179
- if (confidence === "low") return "no_data"
180
- if (successRate >= 99.9) return "operational"
181
- if (successRate >= 99) return "healthy"
182
- if (successRate >= 95) return "degraded"
183
- return "unstable"
184
- }
package/src/cache.ts CHANGED
@@ -22,13 +22,27 @@ interface ModelCache {
22
22
  models: readonly OpenModelProviderModel[]
23
23
  }
24
24
 
25
+ /** Minimal fs interface matching what cache.ts actually uses */
26
+ export interface CacheFs {
27
+ readFile(path: string, encoding: string): Promise<string>
28
+ writeFile(path: string, data: string, encoding?: string): Promise<void>
29
+ mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
30
+ }
31
+
32
+ const DEFAULT_FS: CacheFs = {
33
+ readFile: readFile as CacheFs["readFile"],
34
+ writeFile: writeFile as CacheFs["writeFile"],
35
+ mkdir: mkdir as CacheFs["mkdir"],
36
+ }
37
+
25
38
  /**
26
39
  * Read models from cache.
27
40
  * Returns null if cache is missing, expired, or corrupted.
28
41
  */
29
- export async function readModelCache(): Promise<readonly OpenModelProviderModel[] | null> {
42
+ export async function readModelCache(fsImpl?: CacheFs): Promise<readonly OpenModelProviderModel[] | null> {
43
+ const { readFile: rf } = fsImpl ?? DEFAULT_FS
30
44
  try {
31
- const raw = await readFile(CACHE_FILE, "utf-8")
45
+ const raw = await rf(CACHE_FILE, "utf-8")
32
46
  const cache: ModelCache = JSON.parse(raw)
33
47
 
34
48
  if (typeof cache.timestamp !== "number" || !Array.isArray(cache.models)) {
@@ -50,11 +64,12 @@ export async function readModelCache(): Promise<readonly OpenModelProviderModel[
50
64
  * Write models to the local cache.
51
65
  * Failures are silently ignored — cache is optional.
52
66
  */
53
- export async function writeModelCache(models: readonly OpenModelProviderModel[]): Promise<void> {
67
+ export async function writeModelCache(models: readonly OpenModelProviderModel[], fsImpl?: CacheFs): Promise<void> {
68
+ const { mkdir: mkd, writeFile: wf } = fsImpl ?? DEFAULT_FS
54
69
  try {
55
- await mkdir(CACHE_DIR, { recursive: true })
70
+ await mkd(CACHE_DIR, { recursive: true })
56
71
  const cache: ModelCache = { timestamp: Date.now(), models }
57
- await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8")
72
+ await wf(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8")
58
73
  } catch {
59
74
  // Cache writes are best-effort
60
75
  }
@@ -5,20 +5,8 @@
5
5
  * Transforms health/confidence data into display-ready strings.
6
6
  */
7
7
 
8
- import type { HealthStatus, ConfidenceLevel } from "../api/stability.ts"
9
-
10
- /** Determine health status from success rate and confidence */
11
- export function determineHealth(
12
- successRate: number,
13
- confidence: ConfidenceLevel,
14
- ): HealthStatus {
15
- if (confidence === "low") return "no_data"
16
- if (successRate >= 99.9) return "operational"
17
- if (successRate >= 99) return "healthy"
18
- if (successRate >= 95) return "degraded"
19
- return "unstable"
20
- }
21
-
8
+ import type { HealthStatus } from "../health.ts"
9
+ import type { ConfidenceLevel } from "../api/stability.ts"
22
10
  /** Format health status with emoji */
23
11
  export function formatHealthStatus(status: HealthStatus): string {
24
12
  switch (status) {
package/src/health.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared health status determination for model stability.
3
+ *
4
+ * Extracted from stability.ts and formatters/stability.ts to avoid
5
+ * code duplication — both modules need the same logic.
6
+ */
7
+
8
+ import type { ConfidenceLevel } from "./api/stability.ts"
9
+
10
+ export type HealthStatus =
11
+ | "operational"
12
+ | "healthy"
13
+ | "degraded"
14
+ | "unstable"
15
+ | "no_data"
16
+
17
+ /**
18
+ * Determine health status from success rate and confidence.
19
+ * Low confidence → no_data regardless of success rate.
20
+ */
21
+ export function determineHealth(
22
+ successRate: number,
23
+ confidence: ConfidenceLevel,
24
+ ): HealthStatus {
25
+ if (confidence === "low") return "no_data"
26
+ if (successRate >= 99.9) return "operational"
27
+ if (successRate >= 99) return "healthy"
28
+ if (successRate >= 95) return "degraded"
29
+ return "unstable"
30
+ }
@@ -6,15 +6,12 @@
6
6
  * affinity, cache control, etc.).
7
7
  */
8
8
 
9
- import type { ApiProtocol } from "./protocols.ts"
10
-
11
9
  /**
12
- * Determine compat flags based on provider and API.
10
+ * Determine compat flags based on provider.
13
11
  * Returns undefined when no special flags are needed.
14
12
  */
15
13
  export function compatForProvider(
16
14
  providerKey: string,
17
- api: ApiProtocol,
18
15
  reasoning: boolean,
19
16
  ): Record<string, unknown> | undefined {
20
17
  switch (providerKey) {