pi-openmodel-provider 0.2.17 → 0.2.19
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/skills/pi-openmodel-info/SKILL.md +25 -3
- package/AGENTS.md +10 -9
- package/CHANGELOG.md +33 -1
- package/LICENSE +1 -1
- package/README.md +2 -0
- package/index.ts +30 -64
- package/package.json +3 -2
- package/src/api/models.ts +17 -12
- package/src/api/stability.ts +4 -25
- package/src/auth/login.ts +17 -0
- package/src/cache.ts +20 -5
- package/src/formatters/stability.ts +26 -13
- package/src/formatters/status.ts +46 -0
- package/src/health.ts +30 -0
- package/src/providers/compat.ts +1 -4
|
@@ -32,9 +32,31 @@ Models are cached locally at `~/.pi/agent/cache/openmodel-models.json` with a **
|
|
|
32
32
|
|
|
33
33
|
## Thinking levels
|
|
34
34
|
|
|
35
|
-
Reasoning models support thinking levels:
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
Reasoning models support thinking levels mapped per protocol:
|
|
36
|
+
|
|
37
|
+
**Messages protocol (`anthropic-messages`):**
|
|
38
|
+
|
|
39
|
+
| Level | Mapped value |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `off` | `null` |
|
|
42
|
+
| `minimal` | `low` |
|
|
43
|
+
| `low` | `medium` |
|
|
44
|
+
| `medium` | `high` |
|
|
45
|
+
| `high` | `high` |
|
|
46
|
+
| `xhigh` | `max` |
|
|
47
|
+
|
|
48
|
+
**Responses protocol (`openai-responses`):** uses `reasoning_effort`
|
|
49
|
+
|
|
50
|
+
| Level | Mapped value |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `off` | `null` |
|
|
53
|
+
| `minimal` | `low` |
|
|
54
|
+
| `low` | `low` |
|
|
55
|
+
| `medium` | `medium` |
|
|
56
|
+
| `high` | `high` |
|
|
57
|
+
| `xhigh` | `high` |
|
|
58
|
+
|
|
59
|
+
**Gemini protocol (`google-generative-ai`):** no thinking level mapping (returns empty).
|
|
38
60
|
|
|
39
61
|
## Compat flags
|
|
40
62
|
|
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,39 @@ 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.19] - 2026-06-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `src/auth/login.ts` — `hasApiKey()` function to check for configured credentials (was inline in `index.ts`)
|
|
12
|
+
- `src/formatters/status.ts` — new module with pure `formatProviderStatus()` for the `/openmodel` command display
|
|
13
|
+
- `formatStabilityDetail()` and `formatStabilitySummaryLine()` in `src/formatters/stability.ts` — extract stability formatting from `index.ts`
|
|
14
|
+
- `CommandContext` interface in `index.ts` — types the command handler context instead of using `any`
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `index.ts` — refactored to pure orchestration: `/openmodel` and `/openmodel-stability` handlers delegate I/O, formatting, and display logic to extracted functions
|
|
18
|
+
- `.agents/skills/pi-openmodel-info/SKILL.md` — completed thinking levels section with full mappings (`minimal` through `xhigh`) for both protocols
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
- `import { readFile } from "node:fs/promises"` and `import { homedir } from "node:os"` from `index.ts` (moved into `hasApiKey()`)
|
|
22
|
+
|
|
23
|
+
## [0.2.18] - 2026-06-26
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `src/health.ts` — shared `HealthStatus` type and `determineHealth()` function, extracted from `src/api/stability.ts` and `src/formatters/stability.ts` to eliminate code duplication
|
|
27
|
+
- `tests/test-cache.ts` — 12 tests covering cache read (valid, expired, corrupted, missing) and write (success, directory creation, error suppression)
|
|
28
|
+
- `CacheFs` interface in `src/cache.ts` for dependency injection (matching `fetchImpl` pattern)
|
|
29
|
+
- `test:cache` npm script
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `src/api/stability.ts` — removed local `determineHealthFallback()` copy, imports from `src/health.ts` instead
|
|
33
|
+
- `src/formatters/stability.ts` — removed local `determineHealth()` copy, imports from `src/health.ts` instead
|
|
34
|
+
- `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
|
|
35
|
+
- `src/providers/compat.ts` — removed unused `api` parameter from `compatForProvider()`
|
|
36
|
+
- `index.ts` — replaced blocking `readFileSync` with `await readFile` from `fs/promises`
|
|
37
|
+
- `LICENSE` — added copyright holder name
|
|
38
|
+
- `tsconfig.json` — enabled `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`
|
|
39
|
+
- `AGENTS.md` — reduced to LLM-focused bullet points with references to README and SKILL.md
|
|
40
|
+
- `README.md` — added `health.ts` to architecture tree, added `test:cache` to development section
|
|
9
41
|
|
|
10
42
|
### Changed
|
|
11
43
|
- **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
|
@@ -6,16 +6,25 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
8
8
|
import { fetchOpenModelModels } from "./src/api/models.ts"
|
|
9
|
-
import { login, refreshToken, getApiKey } from "./src/auth/login.ts"
|
|
9
|
+
import { login, refreshToken, getApiKey, hasApiKey } from "./src/auth/login.ts"
|
|
10
10
|
import {
|
|
11
11
|
fetchModelStabilitySummary,
|
|
12
12
|
fetchModelStabilityDetail,
|
|
13
13
|
} from "./src/api/stability.ts"
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
formatStabilityDetail,
|
|
16
|
+
formatStabilitySummaryLine,
|
|
17
|
+
} from "./src/formatters/stability.ts"
|
|
18
|
+
import { formatProviderStatus } from "./src/formatters/status.ts"
|
|
16
19
|
import { readModelCache, writeModelCache } from "./src/cache.ts"
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
/** Minimal command context type for pi extension command handlers. */
|
|
22
|
+
interface CommandContext {
|
|
23
|
+
signal?: AbortSignal
|
|
24
|
+
ui: {
|
|
25
|
+
notify(message: string, type: string): void
|
|
26
|
+
}
|
|
27
|
+
}
|
|
19
28
|
|
|
20
29
|
export default async function (pi: ExtensionAPI) {
|
|
21
30
|
let models: Awaited<ReturnType<typeof fetchOpenModelModels>> = []
|
|
@@ -76,73 +85,31 @@ export default async function (pi: ExtensionAPI) {
|
|
|
76
85
|
// /openmodel - Show provider status
|
|
77
86
|
pi.registerCommand("openmodel", {
|
|
78
87
|
description: "Show OpenModel provider status",
|
|
79
|
-
handler: async (_args: string, ctx:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const content = readFileSync(authPath, "utf-8")
|
|
90
|
-
const data = JSON.parse(content)
|
|
91
|
-
hasApiKey = !!(data.openmodel?.access || data.openmodel?.refresh)
|
|
92
|
-
} catch {
|
|
93
|
-
// Auth file not found
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const lines = [
|
|
97
|
-
"╔══════════════════════════════════╗",
|
|
98
|
-
"║ OpenModel.ai ║",
|
|
99
|
-
"╠══════════════════════════════════╣",
|
|
100
|
-
`║ Models: ${String(count).padStart(3)} loaded${fromCache ? " (cached)" : ""} ║`,
|
|
101
|
-
hasApiKey ? "║ API Key: ✅ Configured ║" : "║ API Key: ❌ Not configured ║",
|
|
102
|
-
"╠══════════════════════════════════╣",
|
|
103
|
-
"║ Commands: ║",
|
|
104
|
-
"║ /model openmodel/... ║",
|
|
105
|
-
"║ /openmodel-stability ║",
|
|
106
|
-
"╚══════════════════════════════════╝",
|
|
107
|
-
]
|
|
108
|
-
|
|
109
|
-
const hints: string[] = []
|
|
110
|
-
if (!hasApiKey) {
|
|
111
|
-
hints.push("ℹ️ Run /login → OpenModel → paste your API key")
|
|
112
|
-
}
|
|
113
|
-
if (count === 0 && hasApiKey) {
|
|
114
|
-
hints.push("ℹ️ Run /reload after setting your API key")
|
|
115
|
-
}
|
|
116
|
-
if (count === 0 && modelError) {
|
|
117
|
-
hints.push(`ℹ️ ${modelError}`)
|
|
118
|
-
}
|
|
119
|
-
hints.push("ℹ️ Press Ctrl+L to select a model")
|
|
120
|
-
|
|
121
|
-
ctx.ui.notify([...lines, ...hints].join("\n"), "info")
|
|
88
|
+
handler: async (_args: string, ctx: CommandContext) => {
|
|
89
|
+
ctx.ui.notify(
|
|
90
|
+
formatProviderStatus({
|
|
91
|
+
count: models.length,
|
|
92
|
+
fromCache,
|
|
93
|
+
hasApiKey: await hasApiKey(),
|
|
94
|
+
modelError,
|
|
95
|
+
}),
|
|
96
|
+
"info",
|
|
97
|
+
)
|
|
122
98
|
},
|
|
123
99
|
})
|
|
124
100
|
|
|
125
101
|
// /openmodel-stability - Show model health metrics
|
|
126
102
|
pi.registerCommand("openmodel-stability", {
|
|
127
103
|
description: "Show model stability metrics (24h)",
|
|
128
|
-
handler: async (args: string | undefined, ctx:
|
|
104
|
+
handler: async (args: string | undefined, ctx: CommandContext) => {
|
|
129
105
|
try {
|
|
106
|
+
const fetchOptions = ctx.signal ? { signal: ctx.signal } : {}
|
|
130
107
|
if (args?.trim()) {
|
|
131
108
|
const name = args.trim()
|
|
132
|
-
const detail = await fetchModelStabilityDetail(name,
|
|
133
|
-
|
|
134
|
-
`📊 ${detail.model_name}`,
|
|
135
|
-
`━━━━━━━━━━━━━━━━━━━━━━`,
|
|
136
|
-
`Health: ${formatHealthStatus(detail.health_status)}`,
|
|
137
|
-
`Success: ${detail.summary.success_rate.toFixed(2)}%`,
|
|
138
|
-
`Latency: ${detail.summary.avg_latency_ms.toFixed(0)}ms`,
|
|
139
|
-
`TTFT: ${detail.summary.avg_ttft_ms.toFixed(0)}ms`,
|
|
140
|
-
`Throughput: ${detail.summary.avg_tps.toFixed(1)} t/s`,
|
|
141
|
-
`Confidence: ${detail.confidence}`,
|
|
142
|
-
]
|
|
143
|
-
ctx.ui.notify(lines.join("\n"), "info")
|
|
109
|
+
const detail = await fetchModelStabilityDetail(name, fetchOptions)
|
|
110
|
+
ctx.ui.notify(formatStabilityDetail(detail), "info")
|
|
144
111
|
} else {
|
|
145
|
-
const summary = await fetchModelStabilitySummary(
|
|
112
|
+
const summary = await fetchModelStabilitySummary(fetchOptions)
|
|
146
113
|
if (summary.length === 0) {
|
|
147
114
|
ctx.ui.notify("📊 No stability data available for any model yet.", "warning")
|
|
148
115
|
return
|
|
@@ -153,8 +120,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
153
120
|
return (order[a.health_status] ?? 5) - (order[b.health_status] ?? 5)
|
|
154
121
|
})
|
|
155
122
|
for (const s of sorted) {
|
|
156
|
-
|
|
157
|
-
lines.push(`${emoji} ${s.model_name.padEnd(28)} ${s.success_rate.toFixed(1).padStart(5)}% ${s.avg_latency_ms.toFixed(0).padStart(5)}ms ${s.avg_tps.toFixed(1).padStart(6)} t/s`)
|
|
123
|
+
lines.push(formatStabilitySummaryLine(s))
|
|
158
124
|
}
|
|
159
125
|
ctx.ui.notify(lines.join("\n"), "info")
|
|
160
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openmodel-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
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/auth/login.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* Since OpenModel API keys don't expire, "refresh" is a no-op.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { readFile } from "node:fs/promises"
|
|
14
|
+
import { join } from "node:path"
|
|
15
|
+
import { homedir } from "node:os"
|
|
13
16
|
import { sanitizeApiKey, isValidApiKey } from "./validate.ts"
|
|
14
17
|
|
|
15
18
|
export interface OAuthLoginCallbacks {
|
|
@@ -130,3 +133,17 @@ export async function refreshToken(
|
|
|
130
133
|
export function getApiKey(credentials: OAuthCredentials): string {
|
|
131
134
|
return credentials.access
|
|
132
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if the user has configured an OpenModel API key in pi's auth file.
|
|
139
|
+
*/
|
|
140
|
+
export async function hasApiKey(): Promise<boolean> {
|
|
141
|
+
const authPath = join(homedir(), ".pi", "agent", "auth.json")
|
|
142
|
+
try {
|
|
143
|
+
const content = await readFile(authPath, "utf-8")
|
|
144
|
+
const data = JSON.parse(content)
|
|
145
|
+
return !!(data.openmodel?.access || data.openmodel?.refresh)
|
|
146
|
+
} catch {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
}
|
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,19 +5,12 @@
|
|
|
5
5
|
* Transforms health/confidence data into display-ready strings.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { HealthStatus
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
}
|
|
8
|
+
import type { HealthStatus } from "../health.ts"
|
|
9
|
+
import type {
|
|
10
|
+
ConfidenceLevel,
|
|
11
|
+
ModelStability,
|
|
12
|
+
ModelStabilityDetail,
|
|
13
|
+
} from "../api/stability.ts"
|
|
21
14
|
|
|
22
15
|
/** Format health status with emoji */
|
|
23
16
|
export function formatHealthStatus(status: HealthStatus): string {
|
|
@@ -46,3 +39,23 @@ export function formatConfidence(level: ConfidenceLevel): string {
|
|
|
46
39
|
return "⚪ Low"
|
|
47
40
|
}
|
|
48
41
|
}
|
|
42
|
+
|
|
43
|
+
/** Format detailed stability view for a single model */
|
|
44
|
+
export function formatStabilityDetail(detail: ModelStabilityDetail): string {
|
|
45
|
+
return [
|
|
46
|
+
`📊 ${detail.model_name}`,
|
|
47
|
+
`━━━━━━━━━━━━━━━━━━━━━━`,
|
|
48
|
+
`Health: ${formatHealthStatus(detail.health_status)}`,
|
|
49
|
+
`Success: ${detail.summary.success_rate.toFixed(2)}%`,
|
|
50
|
+
`Latency: ${detail.summary.avg_latency_ms.toFixed(0)}ms`,
|
|
51
|
+
`TTFT: ${detail.summary.avg_ttft_ms.toFixed(0)}ms`,
|
|
52
|
+
`Throughput: ${detail.summary.avg_tps.toFixed(1)} t/s`,
|
|
53
|
+
`Confidence: ${formatConfidence(detail.confidence)}`,
|
|
54
|
+
].join("\n")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Format a single summary line for the stability list */
|
|
58
|
+
export function formatStabilitySummaryLine(s: ModelStability): string {
|
|
59
|
+
const emoji = formatHealthStatus(s.health_status).split(" ")[0]
|
|
60
|
+
return `${emoji} ${s.model_name.padEnd(28)} ${s.success_rate.toFixed(1).padStart(5)}% ${s.avg_latency_ms.toFixed(0).padStart(5)}ms ${s.avg_tps.toFixed(1).padStart(6)} t/s`
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatters for the /openmodel provider status display.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions — no side effects, no network calls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ProviderStatusOptions {
|
|
8
|
+
count: number
|
|
9
|
+
fromCache: boolean
|
|
10
|
+
hasApiKey: boolean
|
|
11
|
+
modelError: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format the full /openmodel status display including hints.
|
|
16
|
+
*/
|
|
17
|
+
export function formatProviderStatus(options: ProviderStatusOptions): string {
|
|
18
|
+
const { count, fromCache, hasApiKey, modelError } = options
|
|
19
|
+
|
|
20
|
+
const lines = [
|
|
21
|
+
"╔══════════════════════════════════╗",
|
|
22
|
+
"║ OpenModel.ai ║",
|
|
23
|
+
"╠══════════════════════════════════╣",
|
|
24
|
+
`║ Models: ${String(count).padStart(3)} loaded${fromCache ? " (cached)" : ""} ║`,
|
|
25
|
+
hasApiKey ? "║ API Key: ✅ Configured ║" : "║ API Key: ❌ Not configured ║",
|
|
26
|
+
"╠══════════════════════════════════╣",
|
|
27
|
+
"║ Commands: ║",
|
|
28
|
+
"║ /model openmodel/... ║",
|
|
29
|
+
"║ /openmodel-stability ║",
|
|
30
|
+
"╚══════════════════════════════════╝",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const hints: string[] = []
|
|
34
|
+
if (!hasApiKey) {
|
|
35
|
+
hints.push("ℹ️ Run /login → OpenModel → paste your API key")
|
|
36
|
+
}
|
|
37
|
+
if (count === 0 && hasApiKey) {
|
|
38
|
+
hints.push("ℹ️ Run /reload after setting your API key")
|
|
39
|
+
}
|
|
40
|
+
if (count === 0 && modelError) {
|
|
41
|
+
hints.push(`ℹ️ ${modelError}`)
|
|
42
|
+
}
|
|
43
|
+
hints.push("ℹ️ Press Ctrl+L to select a model")
|
|
44
|
+
|
|
45
|
+
return [...lines, ...hints].join("\n")
|
|
46
|
+
}
|
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) {
|