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 +10 -9
- package/CHANGELOG.md +18 -1
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/index.ts +2 -6
- package/package.json +3 -2
- package/src/api/models.ts +17 -12
- package/src/api/stability.ts +4 -25
- package/src/cache.ts +20 -5
- package/src/formatters/stability.ts +2 -14
- package/src/health.ts +30 -0
- package/src/providers/compat.ts +1 -4
package/AGENTS.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Agent Instructions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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.
|
|
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
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 {
|
|
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 =
|
|
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.
|
|
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-
|
|
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
|
|
205
|
-
const outputPrice = pricePerMillion(web.prices
|
|
206
|
-
const cacheRead = pricePerMillion(web.prices
|
|
207
|
-
const cacheWrite = pricePerMillion(web.prices
|
|
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,
|
|
220
|
+
const compat = compatForProvider(web.provider_key, reasoning)
|
|
212
221
|
|
|
213
|
-
const
|
|
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
|
-
}
|
|
238
|
+
}
|
|
234
239
|
|
|
235
240
|
models.push(model)
|
|
236
241
|
}
|
package/src/api/stability.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
|
70
|
+
await mkd(CACHE_DIR, { recursive: true })
|
|
56
71
|
const cache: ModelCache = { timestamp: Date.now(), models }
|
|
57
|
-
await
|
|
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
|
|
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
|
+
}
|
package/src/providers/compat.ts
CHANGED
|
@@ -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
|
|
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) {
|