pi-openmodel-provider 0.2.15 → 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.
@@ -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,21 @@ 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
+
8
23
  ## [0.2.14] - 2026-06-22
9
24
 
10
25
  ### Changed
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
  [![npm version](https://img.shields.io/npm/v/pi-openmodel-provider)](https://www.npmjs.com/package/pi-openmodel-provider)
6
+ [![CI](https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/actions/workflows/ci.yml/badge.svg)](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
- try {
23
- models = await fetchOpenModelModels()
24
- } catch (error) {
25
- if (error instanceof TypeError && error.message.includes("fetch")) {
26
- modelError = "🌐 Network error: check your internet connection"
27
- } else {
28
- modelError = `⚠️ ${error instanceof Error ? error.message : "Could not load models"}`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openmodel-provider",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": "pi custom provider for OpenModel.ai - Multi-model AI gateway",
5
5
  "type": "module",
6
6
  "keywords": [
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) {