pi-openmodel-provider 0.2.14 → 0.2.16
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 +13 -0
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +44 -0
- package/README.md +12 -1
- package/index.ts +24 -10
- package/package.json +1 -1
- package/src/cache.ts +61 -0
- package/src/models.ts +44 -0
- package/src/stability.ts +4 -1
|
@@ -26,12 +26,25 @@ Models are fetched live from OpenModel's API at startup:
|
|
|
26
26
|
|
|
27
27
|
If the API key is not configured yet, models still load — protocols are inferred automatically from the provider name.
|
|
28
28
|
|
|
29
|
+
### Caching
|
|
30
|
+
|
|
31
|
+
Models are cached locally at `~/.pi/agent/cache/openmodel-models.json` with a **5-minute TTL**. On subsequent startups or `/reload`, the cached list is used instead of hitting the API again. The `/openmodel` command shows `(cached)` when the cache is active.
|
|
32
|
+
|
|
29
33
|
## Thinking levels
|
|
30
34
|
|
|
31
35
|
Reasoning models support thinking levels:
|
|
32
36
|
- **Messages protocol:** minimal → low, low → medium, medium → high
|
|
33
37
|
- **Responses protocol:** `reasoning_effort` levels (low, medium, high)
|
|
34
38
|
|
|
39
|
+
## Compat flags
|
|
40
|
+
|
|
41
|
+
Compat flags are automatically set per provider for optimal protocol compatibility:
|
|
42
|
+
- **OpenAI:** `supportsReasoningEffort: true`
|
|
43
|
+
- **Anthropic:** `sendSessionAffinityHeaders`, `supportsCacheControlOnTools`, `supportsEagerToolInputStreaming`
|
|
44
|
+
- **DeepSeek (reasoning):** `thinkingFormat: "deepseek"`
|
|
45
|
+
- **Qwen (reasoning):** `thinkingFormat: "qwen-chat-template"`
|
|
46
|
+
- **ZAI / GLM (reasoning):** `thinkingFormat: "zai"`
|
|
47
|
+
|
|
35
48
|
## Available commands
|
|
36
49
|
|
|
37
50
|
- `/openmodel` — Show provider status
|
package/AGENTS.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
- This package is **pi-openmodel-provider** for OpenModel.ai, **NOT** OpenRouter.
|
|
4
4
|
- OpenModel is a multi-model AI gateway, similar to OpenRouter but a different service.
|
|
5
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.
|
|
6
8
|
- If the `/v1/models` endpoint fails (no API key), protocols are inferred from the provider.
|
|
7
9
|
- See `.agents/skills/pi-openmodel-info/SKILL.md` for full documentation.
|
|
8
10
|
- Follow [CONTRIBUTING.md](CONTRIBUTING.md) before changing code.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.16] - 2026-06-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Local model cache at `~/.pi/agent/cache/openmodel-models.json` with 5-minute TTL
|
|
12
|
+
- `src/cache.ts` module for cache read/write operations
|
|
13
|
+
- Compat flags per provider (openai, anthropic, deepseek, qwen, zai) for optimal protocol compatibility
|
|
14
|
+
- AbortSignal support in stability fetch functions
|
|
15
|
+
- CI workflow (`.github/workflows/ci.yml`) for typecheck + tests on push and PR
|
|
16
|
+
- Typecheck and test steps before publish in `.github/workflows/publish.yml`
|
|
17
|
+
- `(cached)` indicator in `/openmodel` status output
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Models now load from cache first, falling back to API fetch
|
|
21
|
+
- Updated `actions/checkout` and `actions/setup-node` to v5 (Node 24 native)
|
|
22
|
+
|
|
23
|
+
## [0.2.14] - 2026-06-22
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Updated `actions/checkout` and `actions/setup-node` to v5 (Node 24 native)
|
|
27
|
+
|
|
28
|
+
## [0.2.13] - 2026-06-22
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- OIDC Trusted Publisher: added `npm install -g npm@latest` before publish
|
|
32
|
+
- Forced Node.js 24 in workflow for OIDC compatibility
|
|
33
|
+
|
|
34
|
+
## [0.2.12] - 2026-06-22
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- GitHub Actions workflow for auto-publishing to npm via OIDC Trusted Publisher
|
|
38
|
+
- Provenance statements via `npm publish --provenance`
|
|
39
|
+
|
|
40
|
+
## [0.2.11] - 2026-06-22
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- Updated npm badge from `badge.fury.io` to `shields.io` (faster updates)
|
|
44
|
+
- Updated README endpoint descriptions (protocol endpoint requires API key)
|
|
45
|
+
- Updated SKILL.md with model discovery, fallback, and thinking levels info
|
|
46
|
+
- Updated AGENTS.md with dynamic fetch and fallback notes
|
|
47
|
+
|
|
8
48
|
## [0.2.10] - 2026-06-22
|
|
9
49
|
|
|
10
50
|
### Fixed
|
|
@@ -119,6 +159,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
119
159
|
- Import path extensions (.ts → .js)
|
|
120
160
|
- Process import in models.ts
|
|
121
161
|
|
|
162
|
+
[0.2.14]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.14
|
|
163
|
+
[0.2.13]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.13
|
|
164
|
+
[0.2.12]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.12
|
|
165
|
+
[0.2.11]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.11
|
|
122
166
|
[0.2.10]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.10
|
|
123
167
|
[0.2.9]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.9
|
|
124
168
|
[0.2.6]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.6
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
A [pi](https://github.com/earendil-works/pi-mono) custom provider that connects pi to [OpenModel.ai](https://www.openmodel.ai) — a unified AI API gateway.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/pi-openmodel-provider)
|
|
6
|
+
[](https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/actions/workflows/ci.yml)
|
|
6
7
|
|
|
7
8
|
> **Disclaimer:** This is an unofficial, community-maintained package. I am not affiliated with, endorsed by, or connected to OpenModel in any way. This provider simply forwards requests to the public OpenModel API using your own API key.
|
|
8
9
|
|
|
@@ -64,6 +65,12 @@ On startup, the provider fetches models from two endpoints:
|
|
|
64
65
|
|
|
65
66
|
Pricing, context window, reasoning support, and vision capabilities are all provided by the API — no hardcoded data.
|
|
66
67
|
|
|
68
|
+
### Caching
|
|
69
|
+
|
|
70
|
+
Models are cached locally at `~/.pi/agent/cache/openmodel-models.json` with a **5-minute TTL**. On subsequent startups or `/reload`, the cached list is used instead of hitting the API again. The `/openmodel` command shows `(cached)` when the cache is active.
|
|
71
|
+
|
|
72
|
+
To force a fresh fetch, wait 5 minutes or delete the cache file manually.
|
|
73
|
+
|
|
67
74
|
## Pricing
|
|
68
75
|
|
|
69
76
|
Model pricing is fetched live from OpenModel's public API (`/web/v1/models`). Each model returns its real per-token rates in microdollars, converted to dollars per million tokens for display.
|
|
@@ -75,13 +82,17 @@ Model pricing is fetched live from OpenModel's public API (`/web/v1/models`). Ea
|
|
|
75
82
|
|
|
76
83
|
## Features
|
|
77
84
|
|
|
78
|
-
- **41 models** from 9+ providers (dynamically fetched)
|
|
85
|
+
- **41+ models** from 9+ providers (dynamically fetched)
|
|
79
86
|
- **3 protocols**: Messages (Anthropic), Responses (OpenAI), Gemini (Google)
|
|
80
87
|
- **Model stability metrics** via `/openmodel-stability`
|
|
81
88
|
- **1M context window** for DeepSeek V4 models
|
|
82
89
|
- **Thinking levels** for reasoning models (DeepSeek, Claude, GPT, Gemini, etc.)
|
|
90
|
+
- **Compat flags** per provider for optimal protocol compatibility
|
|
91
|
+
- **Local caching** with 5-minute TTL to reduce API calls
|
|
92
|
+
- **AbortSignal support** in stability commands for cancellation
|
|
83
93
|
- **Friendly error messages** with emojis and actionable guidance
|
|
84
94
|
- **No hardcoding** — new models, pricing, and capabilities appear automatically
|
|
95
|
+
- **CI workflow** — typecheck and tests run on every push and PR
|
|
85
96
|
|
|
86
97
|
## Error handling
|
|
87
98
|
|
package/index.ts
CHANGED
|
@@ -13,19 +13,30 @@ import {
|
|
|
13
13
|
formatHealthStatus,
|
|
14
14
|
} from "./src/stability.ts"
|
|
15
15
|
import { friendlyMessage } from "./src/errors.ts"
|
|
16
|
+
import { readModelCache, writeModelCache } from "./src/cache.ts"
|
|
16
17
|
import { homedir } from "node:os"
|
|
17
18
|
|
|
18
19
|
export default async function (pi: ExtensionAPI) {
|
|
19
20
|
let models: Awaited<ReturnType<typeof fetchOpenModelModels>> = []
|
|
20
21
|
let modelError: string | null = null
|
|
22
|
+
let fromCache = false
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// Try local cache first to avoid hitting the API on every startup
|
|
25
|
+
const cached = await readModelCache()
|
|
26
|
+
if (cached) {
|
|
27
|
+
models = cached
|
|
28
|
+
fromCache = true
|
|
29
|
+
} else {
|
|
30
|
+
try {
|
|
31
|
+
models = await fetchOpenModelModels()
|
|
32
|
+
// Fire-and-forget cache write (failures are silently ignored)
|
|
33
|
+
writeModelCache(models)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
36
|
+
modelError = "🌐 Network error: check your internet connection"
|
|
37
|
+
} else {
|
|
38
|
+
modelError = `⚠️ ${error instanceof Error ? error.message : "Could not load models"}`
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
|
@@ -54,6 +65,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
54
65
|
if (model.thinkingLevelMap) {
|
|
55
66
|
config.thinkingLevelMap = model.thinkingLevelMap
|
|
56
67
|
}
|
|
68
|
+
if (model.compat) {
|
|
69
|
+
config.compat = model.compat
|
|
70
|
+
}
|
|
57
71
|
return config
|
|
58
72
|
}),
|
|
59
73
|
})
|
|
@@ -83,7 +97,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
83
97
|
"╔══════════════════════════════════╗",
|
|
84
98
|
"║ OpenModel.ai ║",
|
|
85
99
|
"╠══════════════════════════════════╣",
|
|
86
|
-
`║ Models: ${String(count).padStart(3)} loaded
|
|
100
|
+
`║ Models: ${String(count).padStart(3)} loaded${fromCache ? " (cached)" : ""} ║`,
|
|
87
101
|
hasApiKey ? "║ API Key: ✅ Configured ║" : "║ API Key: ❌ Not configured ║",
|
|
88
102
|
"╠══════════════════════════════════╣",
|
|
89
103
|
"║ Commands: ║",
|
|
@@ -115,7 +129,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
115
129
|
try {
|
|
116
130
|
if (args?.trim()) {
|
|
117
131
|
const name = args.trim()
|
|
118
|
-
const detail = await fetchModelStabilityDetail(name)
|
|
132
|
+
const detail = await fetchModelStabilityDetail(name, { signal: ctx.signal })
|
|
119
133
|
const lines = [
|
|
120
134
|
`📊 ${detail.model_name}`,
|
|
121
135
|
`━━━━━━━━━━━━━━━━━━━━━━`,
|
|
@@ -128,7 +142,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
128
142
|
]
|
|
129
143
|
ctx.ui.notify(lines.join("\n"), "info")
|
|
130
144
|
} else {
|
|
131
|
-
const summary = await fetchModelStabilitySummary()
|
|
145
|
+
const summary = await fetchModelStabilitySummary({ signal: ctx.signal })
|
|
132
146
|
if (summary.length === 0) {
|
|
133
147
|
ctx.ui.notify("📊 No stability data available for any model yet.", "warning")
|
|
134
148
|
return
|
package/package.json
CHANGED
package/src/cache.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local cache for fetched OpenModel models.
|
|
3
|
+
*
|
|
4
|
+
* Avoids hitting the OpenModel API on every startup or /reload.
|
|
5
|
+
* Cache is stored at ~/.pi/agent/cache/openmodel-models.json with a 5-minute TTL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises"
|
|
9
|
+
import { join } from "node:path"
|
|
10
|
+
import { homedir } from "node:os"
|
|
11
|
+
import type { OpenModelProviderModel } from "./models.ts"
|
|
12
|
+
|
|
13
|
+
export const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
|
|
14
|
+
|
|
15
|
+
const CACHE_DIR = join(homedir(), ".pi", "agent", "cache")
|
|
16
|
+
const CACHE_FILE = join(CACHE_DIR, "openmodel-models.json")
|
|
17
|
+
|
|
18
|
+
interface ModelCache {
|
|
19
|
+
/** Unix timestamp (ms) when the cache was written */
|
|
20
|
+
timestamp: number
|
|
21
|
+
/** Cached model list */
|
|
22
|
+
models: readonly OpenModelProviderModel[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read models from cache.
|
|
27
|
+
* Returns null if cache is missing, expired, or corrupted.
|
|
28
|
+
*/
|
|
29
|
+
export async function readModelCache(): Promise<readonly OpenModelProviderModel[] | null> {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await readFile(CACHE_FILE, "utf-8")
|
|
32
|
+
const cache: ModelCache = JSON.parse(raw)
|
|
33
|
+
|
|
34
|
+
if (typeof cache.timestamp !== "number" || !Array.isArray(cache.models)) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const age = Date.now() - cache.timestamp
|
|
39
|
+
if (age >= CACHE_TTL_MS) {
|
|
40
|
+
return null // expired
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return cache.models
|
|
44
|
+
} catch {
|
|
45
|
+
return null // no cache or invalid JSON
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Write models to the local cache.
|
|
51
|
+
* Failures are silently ignored — cache is optional.
|
|
52
|
+
*/
|
|
53
|
+
export async function writeModelCache(models: readonly OpenModelProviderModel[]): Promise<void> {
|
|
54
|
+
try {
|
|
55
|
+
await mkdir(CACHE_DIR, { recursive: true })
|
|
56
|
+
const cache: ModelCache = { timestamp: Date.now(), models }
|
|
57
|
+
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8")
|
|
58
|
+
} catch {
|
|
59
|
+
// Cache writes are best-effort
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface OpenModelProviderModel {
|
|
|
20
20
|
contextWindow: number
|
|
21
21
|
maxTokens: number
|
|
22
22
|
api: "anthropic-messages" | "openai-responses" | "google-generative-ai"
|
|
23
|
+
compat?: Record<string, unknown>
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
interface WebApiModel {
|
|
@@ -70,6 +71,47 @@ function determineApi(protocols: string[], provider: string): "anthropic-message
|
|
|
70
71
|
return null
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Determine compat flags based on provider and API.
|
|
76
|
+
* These tell pi about provider-specific quirks and capabilities.
|
|
77
|
+
*/
|
|
78
|
+
function compatForProvider(
|
|
79
|
+
providerKey: string,
|
|
80
|
+
api: "anthropic-messages" | "openai-responses" | "google-generative-ai",
|
|
81
|
+
reasoning: boolean,
|
|
82
|
+
): Record<string, unknown> | undefined {
|
|
83
|
+
switch (providerKey) {
|
|
84
|
+
case "openai":
|
|
85
|
+
return { supportsReasoningEffort: true }
|
|
86
|
+
case "deepseek":
|
|
87
|
+
if (reasoning) {
|
|
88
|
+
return { thinkingFormat: "deepseek" }
|
|
89
|
+
}
|
|
90
|
+
return undefined
|
|
91
|
+
case "anthropic":
|
|
92
|
+
return {
|
|
93
|
+
sendSessionAffinityHeaders: true,
|
|
94
|
+
supportsCacheControlOnTools: true,
|
|
95
|
+
supportsEagerToolInputStreaming: true,
|
|
96
|
+
}
|
|
97
|
+
case "google":
|
|
98
|
+
case "gemini":
|
|
99
|
+
return undefined
|
|
100
|
+
case "qwen":
|
|
101
|
+
if (reasoning) {
|
|
102
|
+
return { thinkingFormat: "qwen-chat-template" }
|
|
103
|
+
}
|
|
104
|
+
return undefined
|
|
105
|
+
case "zai":
|
|
106
|
+
if (reasoning) {
|
|
107
|
+
return { thinkingFormat: "zai" }
|
|
108
|
+
}
|
|
109
|
+
return undefined
|
|
110
|
+
default:
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
function thinkingLevelMapForApi(api: "anthropic-messages" | "openai-responses" | "google-generative-ai"): Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh", string | null>> {
|
|
74
116
|
if (api === "anthropic-messages") {
|
|
75
117
|
return {
|
|
@@ -199,6 +241,7 @@ export async function fetchOpenModelModels(options?: {
|
|
|
199
241
|
const cacheWrite = pricePerMillion(web.prices.cache_creation_input_token_cost as number)
|
|
200
242
|
|
|
201
243
|
const reasoning = web.supports.supports_reasoning ?? false
|
|
244
|
+
const compat = compatForProvider(web.provider_key, api, reasoning)
|
|
202
245
|
|
|
203
246
|
const base = {
|
|
204
247
|
id,
|
|
@@ -219,6 +262,7 @@ export async function fetchOpenModelModels(options?: {
|
|
|
219
262
|
const model = {
|
|
220
263
|
...base,
|
|
221
264
|
...(reasoning ? { thinkingLevelMap: thinkingLevelMapForApi(api) } : {}),
|
|
265
|
+
...(compat ? { compat } : {}),
|
|
222
266
|
} as unknown as OpenModelProviderModel
|
|
223
267
|
|
|
224
268
|
models.push(model)
|
package/src/stability.ts
CHANGED
|
@@ -74,6 +74,7 @@ export async function fetchModelStabilitySummary(options?: {
|
|
|
74
74
|
url?: string;
|
|
75
75
|
fetchImpl?: typeof fetch;
|
|
76
76
|
hours?: number;
|
|
77
|
+
signal?: AbortSignal;
|
|
77
78
|
}): Promise<ModelStability[]> {
|
|
78
79
|
const url = options?.url ?? STABILITY_SUMMARY_URL;
|
|
79
80
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
@@ -82,6 +83,7 @@ export async function fetchModelStabilitySummary(options?: {
|
|
|
82
83
|
const params = new URLSearchParams({ hours: String(hours) });
|
|
83
84
|
const response = await fetchImpl(`${url}?${params}`, {
|
|
84
85
|
headers: { accept: "application/json" },
|
|
86
|
+
signal: options?.signal ?? null,
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
if (!response.ok) {
|
|
@@ -118,6 +120,7 @@ export async function fetchModelStabilityDetail(
|
|
|
118
120
|
options?: {
|
|
119
121
|
fetchImpl?: typeof fetch;
|
|
120
122
|
hours?: number;
|
|
123
|
+
signal?: AbortSignal;
|
|
121
124
|
},
|
|
122
125
|
): Promise<ModelStabilityDetail> {
|
|
123
126
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
@@ -126,7 +129,7 @@ export async function fetchModelStabilityDetail(
|
|
|
126
129
|
const params = new URLSearchParams({ hours: String(hours) });
|
|
127
130
|
const response = await fetchImpl(
|
|
128
131
|
`https://api.openmodel.ai/web/v1/model-stability/${encodeURIComponent(modelKey)}?${params}`,
|
|
129
|
-
{ headers: { accept: "application/json" } },
|
|
132
|
+
{ headers: { accept: "application/json" }, signal: options?.signal ?? null },
|
|
130
133
|
);
|
|
131
134
|
|
|
132
135
|
if (!response.ok) {
|