pi-openmodel-provider 0.2.8 → 0.2.9
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 +4 -0
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +24 -0
- package/README.md +41 -12
- package/index.ts +69 -45
- package/package.json +7 -2
- package/src/errors.ts +67 -0
- package/src/models.ts +190 -276
- package/src/stability.ts +22 -3
package/AGENTS.md
CHANGED
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
- See `.agents/skills/pi-openmodel-info/SKILL.md` for full documentation.
|
|
6
6
|
- Follow [CONTRIBUTING.md](CONTRIBUTING.md) before changing code.
|
|
7
7
|
- Use [RELEASE.md](RELEASE.md) for release process.
|
|
8
|
+
|
|
9
|
+
**Maintained by** [Ivan Gabriel Yarupaitan Rivera](https://www.vanchi.pro/)
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.9] - 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `src/errors.ts` with `parseWebError`, `parseProxyError`, `friendlyMessage` helpers
|
|
12
|
+
- Friendly error messages for 401, 402, 404, 429, 5xx and more
|
|
13
|
+
- `thinkingLevelMap` for reasoning models (Messages + Responses protocols)
|
|
14
|
+
- Error handling in model discovery and stability endpoints
|
|
15
|
+
- Comprehensive error handling section in README
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Models now fetched from two public endpoints (no auth required)
|
|
19
|
+
- Pricing, context window, reasoning, and vision capabilities from real API
|
|
20
|
+
- Removed all hardcoded pricing tables (PROVIDER_DEFAULTS, PRICING_OVERRIDES)
|
|
21
|
+
- Simplified index.ts: removed readFileSync and auth.json reading
|
|
22
|
+
- Stability errors now show specific API error codes
|
|
23
|
+
- Updated all tests to use mock fetch with real API response format
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
- Hardcoded PROVIDER_DEFAULTS in src/models.ts
|
|
27
|
+
- Hardcoded PRICING_OVERRIDES, CONTEXT_OVERRIDES, MAX_TOKENS_OVERRIDES
|
|
28
|
+
- Hardcoded REASONING_OVERRIDES in src/models.ts
|
|
29
|
+
- readFileSync import and getApiKeyFromAuth function from index.ts
|
|
30
|
+
|
|
8
31
|
## [0.2.6] - 2026-06-20
|
|
9
32
|
|
|
10
33
|
### Fixed
|
|
@@ -86,6 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
86
109
|
- Import path extensions (.ts → .js)
|
|
87
110
|
- Process import in models.ts
|
|
88
111
|
|
|
112
|
+
[0.2.9]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.9
|
|
89
113
|
[0.2.6]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.6
|
|
90
114
|
[0.2.5]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.5
|
|
91
115
|
[0.2.4]: https://github.com/IvanGabrielYarupaitanRivera/pi-openmodel-provider/releases/tag/v0.2.4
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
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
|
+
[](https://www.npmjs.com/package/pi-openmodel-provider)
|
|
6
|
+
|
|
5
7
|
> **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.
|
|
6
8
|
|
|
7
9
|
> **Note:** This package only provides a model _provider_. It does **not** include an API key. You must bring your own OpenModel API key.
|
|
@@ -16,8 +18,6 @@ pi install npm:pi-openmodel-provider
|
|
|
16
18
|
|
|
17
19
|

|
|
18
20
|
|
|
19
|
-
[▶️ Watch video tutorial](https://youtu.be/aUaXznGVuzg)
|
|
20
|
-
|
|
21
21
|
| Step | What to do |
|
|
22
22
|
|------|------------|
|
|
23
23
|
| 1️⃣ | `/reload` (so OpenModel appears in /login) |
|
|
@@ -57,18 +57,21 @@ Models are fetched live from OpenModel's API at startup, so new models show up w
|
|
|
57
57
|
|
|
58
58
|
## Model discovery
|
|
59
59
|
|
|
60
|
-
On startup, the provider fetches:
|
|
60
|
+
On startup, the provider fetches models from two public endpoints (no authentication required):
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
https://api.openmodel.ai/v1/models
|
|
64
|
-
|
|
62
|
+
- **Model list & protocols:** `https://api.openmodel.ai/v1/models`
|
|
63
|
+
- **Pricing & capabilities:** `https://api.openmodel.ai/web/v1/models`
|
|
64
|
+
|
|
65
|
+
Pricing, context window, reasoning support, and vision capabilities are all provided by the API — no hardcoded data.
|
|
65
66
|
|
|
66
67
|
## Pricing
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
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.
|
|
69
70
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
71
|
+
- Input and output tokens are billed at separate rates
|
|
72
|
+
- Cache reads and writes are billed at reduced rates
|
|
73
|
+
- A `price_multiplier` may apply (e.g., 0.95 = 5% discount)
|
|
74
|
+
- Free models have zero cost
|
|
72
75
|
|
|
73
76
|
## Features
|
|
74
77
|
|
|
@@ -76,7 +79,23 @@ OpenModel does not yet expose model pricing through its Provider API. The provid
|
|
|
76
79
|
- **3 protocols**: Messages (Anthropic), Responses (OpenAI), Gemini (Google)
|
|
77
80
|
- **Model stability metrics** via `/openmodel-stability`
|
|
78
81
|
- **1M context window** for DeepSeek V4 models
|
|
79
|
-
- **
|
|
82
|
+
- **Thinking levels** for reasoning models (DeepSeek, Claude, GPT, Gemini, etc.)
|
|
83
|
+
- **Friendly error messages** with emojis and actionable guidance
|
|
84
|
+
- **No hardcoding** — new models, pricing, and capabilities appear automatically
|
|
85
|
+
|
|
86
|
+
## Error handling
|
|
87
|
+
|
|
88
|
+
Errors from OpenModel's API are shown with friendly messages:
|
|
89
|
+
|
|
90
|
+
| HTTP Status | What you'll see |
|
|
91
|
+
|-------------|-----------------|
|
|
92
|
+
| 401 | 🔑 Invalid API key. Check your credentials or run /login again. |
|
|
93
|
+
| 402 | 💳 Insufficient balance. Top up at console.openmodel.ai |
|
|
94
|
+
| 429 | ⏳ Rate limited. Try again later. |
|
|
95
|
+
| 404 | 🔍 Resource not found. Check the model name. |
|
|
96
|
+
| 5xx | 🔧 OpenModel API error. Try again later. |
|
|
97
|
+
|
|
98
|
+
For stability endpoints, errors include context about what went wrong.
|
|
80
99
|
|
|
81
100
|
## Commands
|
|
82
101
|
|
|
@@ -121,8 +140,14 @@ npm install
|
|
|
121
140
|
# Type check
|
|
122
141
|
npm run typecheck
|
|
123
142
|
|
|
124
|
-
#
|
|
143
|
+
# Run all tests
|
|
144
|
+
npm test
|
|
145
|
+
|
|
146
|
+
# Run specific tests
|
|
125
147
|
npm run test:models
|
|
148
|
+
npm run test:auth
|
|
149
|
+
npm run test:pricing
|
|
150
|
+
npm run test:stability
|
|
126
151
|
```
|
|
127
152
|
|
|
128
153
|
## Contributing
|
|
@@ -135,4 +160,8 @@ See [RELEASE.md](RELEASE.md) for prerelease, npm smoke-test, stable publish, git
|
|
|
135
160
|
|
|
136
161
|
## License
|
|
137
162
|
|
|
138
|
-
MIT
|
|
163
|
+
MIT
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
**Maintained by** [Ivan Gabriel Yarupaitan Rivera](https://www.vanchi.pro/)
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenModel provider for pi.
|
|
3
3
|
*
|
|
4
|
-
* Models are fetched from OpenModel's API at startup.
|
|
4
|
+
* Models are fetched from OpenModel's public API at startup.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
@@ -12,28 +12,19 @@ import {
|
|
|
12
12
|
fetchModelStabilityDetail,
|
|
13
13
|
formatHealthStatus,
|
|
14
14
|
} from "./src/stability.ts"
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
function getApiKeyFromAuth(): string | null {
|
|
18
|
-
try {
|
|
19
|
-
const authPath = "C:/Users/Admin/.pi/agent/auth.json"
|
|
20
|
-
const content = readFileSync(authPath, "utf-8")
|
|
21
|
-
const data = JSON.parse(content)
|
|
22
|
-
return data.openmodel?.access || data.openmodel?.refresh || null
|
|
23
|
-
} catch {
|
|
24
|
-
return null
|
|
25
|
-
}
|
|
26
|
-
}
|
|
15
|
+
import { friendlyMessage } from "./src/errors.ts"
|
|
27
16
|
|
|
28
17
|
export default async function (pi: ExtensionAPI) {
|
|
29
18
|
let models: Awaited<ReturnType<typeof fetchOpenModelModels>> = []
|
|
30
|
-
|
|
19
|
+
let modelError: string | null = null
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
try {
|
|
22
|
+
models = await fetchOpenModelModels()
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
25
|
+
modelError = "🌐 Network error: check your internet connection"
|
|
26
|
+
} else {
|
|
27
|
+
modelError = `⚠️ ${error instanceof Error ? error.message : "Could not load models"}`
|
|
37
28
|
}
|
|
38
29
|
}
|
|
39
30
|
|
|
@@ -48,39 +39,71 @@ export default async function (pi: ExtensionAPI) {
|
|
|
48
39
|
refreshToken,
|
|
49
40
|
getApiKey,
|
|
50
41
|
},
|
|
51
|
-
models: models.map((model) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
models: models.map((model) => {
|
|
43
|
+
const config: Record<string, unknown> = {
|
|
44
|
+
id: model.id,
|
|
45
|
+
name: model.name,
|
|
46
|
+
api: model.api,
|
|
47
|
+
reasoning: model.reasoning,
|
|
48
|
+
input: model.input,
|
|
49
|
+
cost: model.cost,
|
|
50
|
+
contextWindow: model.contextWindow,
|
|
51
|
+
maxTokens: model.maxTokens,
|
|
52
|
+
}
|
|
53
|
+
if (model.thinkingLevelMap) {
|
|
54
|
+
config.thinkingLevelMap = model.thinkingLevelMap
|
|
55
|
+
}
|
|
56
|
+
return config
|
|
57
|
+
}),
|
|
61
58
|
})
|
|
62
59
|
|
|
63
60
|
// /openmodel - Show provider status
|
|
64
61
|
pi.registerCommand("openmodel", {
|
|
65
62
|
description: "Show OpenModel provider status",
|
|
66
63
|
handler: async (_args: string, ctx: any) => {
|
|
67
|
-
const key = getApiKeyFromAuth()
|
|
68
|
-
const status = key ? "✅ Configured" : "❌ Not configured"
|
|
69
64
|
const count = models.length
|
|
65
|
+
const status = count > 0
|
|
66
|
+
? `✅ ${count} models loaded`
|
|
67
|
+
: modelError ?? "❌ No models loaded"
|
|
68
|
+
|
|
69
|
+
// Detect if user has configured an API key in auth.json
|
|
70
|
+
let hasApiKey = false
|
|
71
|
+
try {
|
|
72
|
+
const { readFileSync } = await import("node:fs")
|
|
73
|
+
const authPath = `${require("node:os").homedir()}/.pi/agent/auth.json`
|
|
74
|
+
const content = readFileSync(authPath, "utf-8")
|
|
75
|
+
const data = JSON.parse(content)
|
|
76
|
+
hasApiKey = !!(data.openmodel?.access || data.openmodel?.refresh)
|
|
77
|
+
} catch {
|
|
78
|
+
// Auth file not found
|
|
79
|
+
}
|
|
70
80
|
|
|
71
81
|
const lines = [
|
|
72
|
-
"
|
|
73
|
-
"║ OpenModel.ai
|
|
74
|
-
"
|
|
75
|
-
`║
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"║ Commands:
|
|
79
|
-
"║ /model openmodel/...
|
|
80
|
-
"║ /openmodel-stability
|
|
81
|
-
"
|
|
82
|
+
"╔══════════════════════════════════╗",
|
|
83
|
+
"║ OpenModel.ai ║",
|
|
84
|
+
"╠══════════════════════════════════╣",
|
|
85
|
+
`║ Models: ${String(count).padStart(3)} loaded ║`,
|
|
86
|
+
hasApiKey ? "║ API Key: ✅ Configured ║" : "║ API Key: ❌ Not configured ║",
|
|
87
|
+
"╠══════════════════════════════════╣",
|
|
88
|
+
"║ Commands: ║",
|
|
89
|
+
"║ /model openmodel/... ║",
|
|
90
|
+
"║ /openmodel-stability ║",
|
|
91
|
+
"╚══════════════════════════════════╝",
|
|
82
92
|
]
|
|
83
|
-
|
|
93
|
+
|
|
94
|
+
const hints: string[] = []
|
|
95
|
+
if (!hasApiKey) {
|
|
96
|
+
hints.push("ℹ️ Run /login → OpenModel → paste your API key")
|
|
97
|
+
}
|
|
98
|
+
if (count === 0 && hasApiKey) {
|
|
99
|
+
hints.push("ℹ️ Run /reload after setting your API key")
|
|
100
|
+
}
|
|
101
|
+
if (count === 0 && modelError) {
|
|
102
|
+
hints.push(`ℹ️ ${modelError}`)
|
|
103
|
+
}
|
|
104
|
+
hints.push("ℹ️ Press Ctrl+L to select a model")
|
|
105
|
+
|
|
106
|
+
ctx.ui.notify([...lines, ...hints].join("\n"), "info")
|
|
84
107
|
},
|
|
85
108
|
})
|
|
86
109
|
|
|
@@ -106,7 +129,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
106
129
|
} else {
|
|
107
130
|
const summary = await fetchModelStabilitySummary()
|
|
108
131
|
if (summary.length === 0) {
|
|
109
|
-
ctx.ui.notify("No stability data available.", "warning")
|
|
132
|
+
ctx.ui.notify("📊 No stability data available for any model yet.", "warning")
|
|
110
133
|
return
|
|
111
134
|
}
|
|
112
135
|
const lines = ["📊 OpenModel Stability (24h)", "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"]
|
|
@@ -120,8 +143,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
120
143
|
}
|
|
121
144
|
ctx.ui.notify(lines.join("\n"), "info")
|
|
122
145
|
}
|
|
123
|
-
} catch {
|
|
124
|
-
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const msg = error instanceof Error ? error.message : "Unknown error"
|
|
148
|
+
ctx.ui.notify(`❌ ${msg}`, "error")
|
|
125
149
|
}
|
|
126
150
|
},
|
|
127
151
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openmodel-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "pi custom provider for OpenModel.ai - Multi-model AI gateway",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,12 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"test:models": "tsx
|
|
38
|
+
"test:models": "tsx tests/test-models.ts",
|
|
39
|
+
"test:auth": "tsx tests/test-auth.ts",
|
|
40
|
+
"test:pricing": "tsx tests/test-pricing.ts",
|
|
41
|
+
"test:stability": "tsx tests/test-stability.ts",
|
|
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"
|
|
39
44
|
},
|
|
40
45
|
"pi": {
|
|
41
46
|
"extensions": [
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error handling for OpenModel API responses.
|
|
3
|
+
*
|
|
4
|
+
* OpenModel Web API returns errors in this format:
|
|
5
|
+
* { success: false, error: { code: string, msg: string, detail?: string } }
|
|
6
|
+
*
|
|
7
|
+
* Proxy endpoints return errors in provider-specific formats (Anthropic, OpenAI, Gemini).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Parse an OpenModel Web API error response body */
|
|
11
|
+
export function parseWebError(body: unknown): { code: string; message: string; detail?: string } {
|
|
12
|
+
const err = (body as any)?.error
|
|
13
|
+
if (err?.code && err?.msg) {
|
|
14
|
+
const result: { code: string; message: string; detail?: string } = {
|
|
15
|
+
code: String(err.code),
|
|
16
|
+
message: String(err.msg),
|
|
17
|
+
}
|
|
18
|
+
if (err.detail) {
|
|
19
|
+
result.detail = String(err.detail)
|
|
20
|
+
}
|
|
21
|
+
return result
|
|
22
|
+
}
|
|
23
|
+
return { code: "UNKNOWN", message: "An unknown error occurred" }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse an OpenModel proxy API error body (any format) */
|
|
27
|
+
export function parseProxyError(body: unknown): { code: string; message: string } {
|
|
28
|
+
const b = body as any
|
|
29
|
+
|
|
30
|
+
// Anthropic format: { type: "error", error: { type, message } }
|
|
31
|
+
if (b?.type === "error" && b?.error?.message) {
|
|
32
|
+
return { code: b.error.type ?? "UNKNOWN", message: b.error.message }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// OpenAI format: { error: { message, type, code } }
|
|
36
|
+
if (b?.error?.message) {
|
|
37
|
+
return { code: b.error.code ?? b.error.type ?? "UNKNOWN", message: b.error.message }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Gemini format: { error: { code, message, status } }
|
|
41
|
+
if (b?.error?.status) {
|
|
42
|
+
return { code: b.error.status, message: b.error.message }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { code: "UNKNOWN", message: "An unknown error occurred" }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Return a user-friendly message for known error codes */
|
|
49
|
+
export function friendlyMessage(code: string, fallback: string): string {
|
|
50
|
+
const map: Record<string, string> = {
|
|
51
|
+
UNAUTHORIZED: "🔑 Invalid API key. Check your credentials or run /login again.",
|
|
52
|
+
INVALID_TOKEN: "🔑 Invalid or expired API key. Run /login again.",
|
|
53
|
+
TOKEN_EXPIRED: "🔑 API key expired. Run /login again.",
|
|
54
|
+
FORBIDDEN: "🚫 Permission denied. Check your account permissions.",
|
|
55
|
+
NOT_FOUND: "🔍 Resource not found. Check the model name or endpoint.",
|
|
56
|
+
RESOURCE_NOT_FOUND: "🔍 Model not found. Check the name and try again.",
|
|
57
|
+
TOO_MANY_REQUESTS: "⏳ Rate limited. Try again later.",
|
|
58
|
+
INSUFFICIENT_BALANCE: "💳 Insufficient balance. Top up at console.openmodel.ai",
|
|
59
|
+
PAYLOAD_TOO_LARGE: "📦 Request too large. Reduce the input size.",
|
|
60
|
+
VALIDATION_FAILED: "⚠️ Invalid request. Check your parameters.",
|
|
61
|
+
INTERNAL_ERROR: "🔧 OpenModel API error. Try again later.",
|
|
62
|
+
SERVICE_UNAVAIL: "🔧 Service temporarily unavailable. Try again later.",
|
|
63
|
+
BAD_REQUEST: "⚠️ Invalid request. Check the parameters.",
|
|
64
|
+
CONFIG_NOT_READY: "⏳ System not ready. Try again later.",
|
|
65
|
+
}
|
|
66
|
+
return map[code] ?? fallback
|
|
67
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -1,309 +1,223 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenModel.ai model fetching and parsing.
|
|
3
3
|
*
|
|
4
|
-
* Fetches available models from OpenModel's API
|
|
5
|
-
* and
|
|
6
|
-
*
|
|
7
|
-
* Rather than hardcoding per-model metadata, we infer capabilities
|
|
8
|
-
* from the provider (owned_by) and model name patterns. This way
|
|
9
|
-
* new models added by OpenModel are automatically supported.
|
|
4
|
+
* Fetches available models from OpenModel's public API (no auth required).
|
|
5
|
+
* Pricing, context window, and capabilities are all provided by the API.
|
|
10
6
|
*/
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_API_BASE = "https://api.openmodel.ai";
|
|
8
|
+
import { parseWebError, parseProxyError, friendlyMessage } from "./errors.ts"
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const DEFAULT_WEB_MODELS_URL = "https://api.openmodel.ai/web/v1/models"
|
|
11
|
+
export const DEFAULT_LEGACY_MODELS_URL = "https://api.openmodel.ai/v1/models"
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
export interface OpenModelProviderModel {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
reasoning: boolean
|
|
17
|
+
thinkingLevelMap?: Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh", string | null>>
|
|
18
|
+
input: readonly ("text" | "image")[]
|
|
19
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number }
|
|
20
|
+
contextWindow: number
|
|
21
|
+
maxTokens: number
|
|
22
|
+
api: "anthropic-messages" | "openai-responses" | "google-generative-ai"
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
interface WebApiModel {
|
|
26
|
+
key: string
|
|
27
|
+
provider_key: string
|
|
28
|
+
provider_name: string
|
|
29
|
+
prices: Record<string, number | Record<string, number>>
|
|
30
|
+
max: {
|
|
31
|
+
max_input_tokens?: number
|
|
32
|
+
max_output_tokens?: number
|
|
33
|
+
max_tokens?: number
|
|
34
|
+
}
|
|
35
|
+
supports: {
|
|
36
|
+
supports_reasoning?: boolean
|
|
37
|
+
supports_vision?: boolean
|
|
38
|
+
supports_image_generation?: boolean
|
|
39
|
+
}
|
|
40
|
+
price_multiplier: number
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
reasoning: boolean;
|
|
39
|
-
input: readonly ("text" | "image")[];
|
|
40
|
-
cost: {
|
|
41
|
-
input: number;
|
|
42
|
-
output: number;
|
|
43
|
-
cacheRead: number;
|
|
44
|
-
cacheWrite: number;
|
|
45
|
-
};
|
|
46
|
-
contextWindow: number;
|
|
47
|
-
maxTokens: number;
|
|
48
|
-
api: "anthropic-messages" | "openai-responses" | "google-generative-ai";
|
|
43
|
+
interface WebApiResponse {
|
|
44
|
+
success: boolean
|
|
45
|
+
data: WebApiModel[]
|
|
46
|
+
meta: { pagination: { page: number; pageSize: number; total: number; totalPages: number } }
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
contextWindow: number;
|
|
57
|
-
maxTokens: number;
|
|
58
|
-
reasoning: boolean;
|
|
59
|
-
supportsImages: boolean;
|
|
60
|
-
costPerMInput: number; // $ per million input tokens
|
|
61
|
-
costPerMOutput: number; // $ per million output tokens
|
|
49
|
+
interface LegacyApiModel {
|
|
50
|
+
id: string
|
|
51
|
+
object: string
|
|
52
|
+
owned_by: string
|
|
53
|
+
supported_protocols: string[]
|
|
62
54
|
}
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
maxTokens: 8_192,
|
|
68
|
-
reasoning: true,
|
|
69
|
-
supportsImages: true,
|
|
70
|
-
costPerMInput: 3,
|
|
71
|
-
costPerMOutput: 15,
|
|
72
|
-
},
|
|
73
|
-
deepseek: {
|
|
74
|
-
contextWindow: 1_000_000,
|
|
75
|
-
maxTokens: 65_536,
|
|
76
|
-
reasoning: true,
|
|
77
|
-
supportsImages: false,
|
|
78
|
-
costPerMInput: 0.14,
|
|
79
|
-
costPerMOutput: 0.28,
|
|
80
|
-
},
|
|
81
|
-
openai: {
|
|
82
|
-
contextWindow: 128_000,
|
|
83
|
-
maxTokens: 16_384,
|
|
84
|
-
reasoning: true,
|
|
85
|
-
supportsImages: true,
|
|
86
|
-
costPerMInput: 2.5,
|
|
87
|
-
costPerMOutput: 10,
|
|
88
|
-
},
|
|
89
|
-
gemini: {
|
|
90
|
-
contextWindow: 1_000_000,
|
|
91
|
-
maxTokens: 8_192,
|
|
92
|
-
reasoning: true,
|
|
93
|
-
supportsImages: true,
|
|
94
|
-
costPerMInput: 0.3,
|
|
95
|
-
costPerMOutput: 1.2,
|
|
96
|
-
},
|
|
97
|
-
moonshot: {
|
|
98
|
-
contextWindow: 128_000,
|
|
99
|
-
maxTokens: 65_536,
|
|
100
|
-
reasoning: true,
|
|
101
|
-
supportsImages: true,
|
|
102
|
-
costPerMInput: 0.6,
|
|
103
|
-
costPerMOutput: 3,
|
|
104
|
-
},
|
|
105
|
-
zai: {
|
|
106
|
-
contextWindow: 128_000,
|
|
107
|
-
maxTokens: 16_384,
|
|
108
|
-
reasoning: true,
|
|
109
|
-
supportsImages: false,
|
|
110
|
-
costPerMInput: 1,
|
|
111
|
-
costPerMOutput: 3.2,
|
|
112
|
-
},
|
|
113
|
-
dashscope: {
|
|
114
|
-
contextWindow: 131_072,
|
|
115
|
-
maxTokens: 16_384,
|
|
116
|
-
reasoning: true,
|
|
117
|
-
supportsImages: true,
|
|
118
|
-
costPerMInput: 0.5,
|
|
119
|
-
costPerMOutput: 3,
|
|
120
|
-
},
|
|
121
|
-
minimax: {
|
|
122
|
-
contextWindow: 128_000,
|
|
123
|
-
maxTokens: 16_384,
|
|
124
|
-
reasoning: true,
|
|
125
|
-
supportsImages: false,
|
|
126
|
-
costPerMInput: 0.27,
|
|
127
|
-
costPerMOutput: 0.95,
|
|
128
|
-
},
|
|
129
|
-
mimo: {
|
|
130
|
-
contextWindow: 128_000,
|
|
131
|
-
maxTokens: 16_384,
|
|
132
|
-
reasoning: true,
|
|
133
|
-
supportsImages: false,
|
|
134
|
-
costPerMInput: 0,
|
|
135
|
-
costPerMOutput: 0,
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const DEFAULT_FALLBACK: ProviderDefaults = {
|
|
140
|
-
contextWindow: 128_000,
|
|
141
|
-
maxTokens: 16_384,
|
|
142
|
-
reasoning: true,
|
|
143
|
-
supportsImages: false,
|
|
144
|
-
costPerMInput: 0,
|
|
145
|
-
costPerMOutput: 0,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
function getDefaults(ownedBy: string): ProviderDefaults {
|
|
149
|
-
return PROVIDER_DEFAULTS[ownedBy.toLowerCase()] ?? DEFAULT_FALLBACK;
|
|
56
|
+
interface LegacyApiResponse {
|
|
57
|
+
data: LegacyApiModel[]
|
|
58
|
+
object: string
|
|
150
59
|
}
|
|
151
60
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
/** Fine-tune contextWindow for specific model IDs that differ from their provider default */
|
|
157
|
-
const CONTEXT_OVERRIDES: Record<string, number> = {
|
|
158
|
-
// Some older/smaller models have less context
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
/** Fine-tune maxTokens for specific model IDs */
|
|
162
|
-
const MAX_TOKENS_OVERRIDES: Record<string, number> = {
|
|
163
|
-
// e.g., "some-small-model": 4096,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/** Fine-tune reasoning for specific model IDs */
|
|
167
|
-
const REASONING_OVERRIDES: Record<string, boolean> = {
|
|
168
|
-
"gpt-5.4-mini": false,
|
|
169
|
-
"gemini-3.1-flash-lite-preview": false,
|
|
170
|
-
"gemini-3-flash-preview": false,
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/** Known pricing exceptions (model-specific overrides to provider defaults) */
|
|
174
|
-
const PRICING_OVERRIDES: Record<string, { input: number; output: number }> = {
|
|
175
|
-
"claude-opus-4-7": { input: 15, output: 75 },
|
|
176
|
-
"claude-opus-4-6": { input: 15, output: 75 },
|
|
177
|
-
"claude-opus-4-8": { input: 15, output: 75 },
|
|
178
|
-
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
179
|
-
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
180
|
-
"claude-haiku-4-5-20251001": { input: 0.25, output: 1.25 },
|
|
181
|
-
"deepseek-v4-pro": { input: 0.435, output: 0.87 },
|
|
182
|
-
"deepseek-v4-flash": { input: 0.14, output: 0.28 },
|
|
183
|
-
"gpt-5.5-pro": { input: 10, output: 40 },
|
|
184
|
-
"gpt-5.5": { input: 5, output: 20 },
|
|
185
|
-
"gpt-5.4-pro": { input: 5, output: 20 },
|
|
186
|
-
"gpt-5.4": { input: 2.5, output: 10 },
|
|
187
|
-
"gpt-5.4-mini": { input: 0.4, output: 1.6 },
|
|
188
|
-
"gpt-5.3-codex": { input: 2, output: 8 },
|
|
189
|
-
"gpt-5.2-pro": { input: 5, output: 20 },
|
|
190
|
-
"gpt-5.2": { input: 2, output: 8 },
|
|
191
|
-
"gemini-3.5-flash": { input: 0.3, output: 1.2 },
|
|
192
|
-
"gemini-3.1-pro-preview": { input: 1.5, output: 6.0 },
|
|
193
|
-
"gemini-3-flash-preview": { input: 0.15, output: 0.6 },
|
|
194
|
-
"kimi-k2.6": { input: 0.95, output: 4 },
|
|
195
|
-
"kimi-k2.5": { input: 0.6, output: 3 },
|
|
196
|
-
"kimi-k2.7-code": { input: 0.95, output: 4 },
|
|
197
|
-
"glm-5.2": { input: 1.4, output: 5.6 },
|
|
198
|
-
"glm-5.1": { input: 1.4, output: 4.4 },
|
|
199
|
-
"glm-5": { input: 1, output: 3.2 },
|
|
200
|
-
"glm-4.7": { input: 0.5, output: 2 },
|
|
201
|
-
"qwen3.7-max": { input: 2, output: 6 },
|
|
202
|
-
"qwen3.6-max-preview": { input: 1.3, output: 7.8 },
|
|
203
|
-
"qwen3.6-plus": { input: 0.5, output: 3 },
|
|
204
|
-
"qwen3.6-flash": { input: 0.2, output: 1 },
|
|
205
|
-
"qwen3.5-plus": { input: 0.5, output: 3 },
|
|
206
|
-
"qwen3-max": { input: 2.5, output: 6 },
|
|
207
|
-
"MiniMax-M3": { input: 0.5, output: 2 },
|
|
208
|
-
"MiniMax-M2.7": { input: 0.3, output: 1.2 },
|
|
209
|
-
"MiniMax-M2.5": { input: 0.27, output: 0.95 },
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
// Mapping
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
|
|
216
|
-
/** Map OpenModel protocol to pi API type */
|
|
217
|
-
function protocolToApi(
|
|
218
|
-
protocols: SupportedProtocol[],
|
|
219
|
-
): "anthropic-messages" | "openai-responses" | "google-generative-ai" | null {
|
|
220
|
-
if (protocols.includes("messages")) return "anthropic-messages";
|
|
221
|
-
if (protocols.includes("responses")) return "openai-responses";
|
|
222
|
-
if (protocols.includes("gemini")) return "google-generative-ai";
|
|
223
|
-
return null; // images-only, skip
|
|
61
|
+
function pricePerMillion(costPerToken: number | undefined): number {
|
|
62
|
+
if (costPerToken === undefined || costPerToken === null) return 0
|
|
63
|
+
return Math.round(costPerToken * 1_000_000 * 1000) / 1000
|
|
224
64
|
}
|
|
225
65
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (!api) return null; // skip image-only models
|
|
232
|
-
|
|
233
|
-
const defaults = getDefaults(raw.owned_by);
|
|
234
|
-
const pricing = PRICING_OVERRIDES[raw.id] ?? {
|
|
235
|
-
input: defaults.costPerMInput,
|
|
236
|
-
output: defaults.costPerMOutput,
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
id: raw.id,
|
|
241
|
-
name: raw.id,
|
|
242
|
-
reasoning: REASONING_OVERRIDES[raw.id] ?? defaults.reasoning,
|
|
243
|
-
input: defaults.supportsImages
|
|
244
|
-
? (["text", "image"] as const)
|
|
245
|
-
: (["text"] as const),
|
|
246
|
-
cost: {
|
|
247
|
-
input: pricing.input,
|
|
248
|
-
output: pricing.output,
|
|
249
|
-
cacheRead: pricing.input * 0.1,
|
|
250
|
-
cacheWrite: pricing.input * 0.25,
|
|
251
|
-
},
|
|
252
|
-
contextWindow: CONTEXT_OVERRIDES[raw.id] ?? defaults.contextWindow,
|
|
253
|
-
maxTokens: MAX_TOKENS_OVERRIDES[raw.id] ?? defaults.maxTokens,
|
|
254
|
-
api,
|
|
255
|
-
};
|
|
66
|
+
function determineApi(protocols: string[], provider: string): "anthropic-messages" | "openai-responses" | "google-generative-ai" | null {
|
|
67
|
+
if (protocols.includes("messages")) return "anthropic-messages"
|
|
68
|
+
if (protocols.includes("responses")) return "openai-responses"
|
|
69
|
+
if (protocols.includes("gemini")) return "google-generative-ai"
|
|
70
|
+
return null
|
|
256
71
|
}
|
|
257
72
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
73
|
+
function thinkingLevelMapForApi(api: "anthropic-messages" | "openai-responses" | "google-generative-ai"): Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh", string | null>> {
|
|
74
|
+
if (api === "anthropic-messages") {
|
|
75
|
+
return {
|
|
76
|
+
minimal: "low",
|
|
77
|
+
low: "medium",
|
|
78
|
+
medium: "high",
|
|
79
|
+
high: "high",
|
|
80
|
+
xhigh: "max",
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (api === "openai-responses") {
|
|
84
|
+
return {
|
|
85
|
+
minimal: "low",
|
|
86
|
+
low: "low",
|
|
87
|
+
medium: "medium",
|
|
88
|
+
high: "high",
|
|
89
|
+
xhigh: "high",
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {}
|
|
93
|
+
}
|
|
266
94
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
95
|
+
/** Fetch all models from the web API (public, no auth required) */
|
|
96
|
+
async function fetchWebModels(options?: {
|
|
97
|
+
url?: string
|
|
98
|
+
fetchImpl?: typeof fetch
|
|
99
|
+
}): Promise<Map<string, WebApiModel>> {
|
|
100
|
+
const baseUrl = options?.url ?? DEFAULT_WEB_MODELS_URL
|
|
101
|
+
const fetchImpl = options?.fetchImpl ?? fetch
|
|
102
|
+
|
|
103
|
+
const modelMap = new Map<string, WebApiModel>()
|
|
104
|
+
let page = 1
|
|
105
|
+
let totalPages = 1
|
|
106
|
+
|
|
107
|
+
while (page <= totalPages) {
|
|
108
|
+
const response = await fetchImpl(`${baseUrl}?page=${page}`, {
|
|
109
|
+
headers: { accept: "application/json" },
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
let body: any
|
|
114
|
+
try { body = await response.json() } catch {}
|
|
115
|
+
const err = parseWebError(body)
|
|
116
|
+
throw new Error(`Failed to fetch models: ${response.status} ${err.code} — ${friendlyMessage(err.code, err.message)}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const body = (await response.json()) as WebApiResponse
|
|
120
|
+
if (!body.success) {
|
|
121
|
+
throw new Error(`Failed to fetch models — ${friendlyMessage("INTERNAL_ERROR", "Unknown error")}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
totalPages = body.meta.pagination.totalPages
|
|
125
|
+
for (const model of body.data) {
|
|
126
|
+
modelMap.set(model.key, model)
|
|
127
|
+
}
|
|
128
|
+
page++
|
|
270
129
|
}
|
|
271
130
|
|
|
272
|
-
|
|
131
|
+
return modelMap
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Fetch protocol info from legacy models endpoint */
|
|
135
|
+
async function fetchLegacyModels(options?: {
|
|
136
|
+
url?: string
|
|
137
|
+
fetchImpl?: typeof fetch
|
|
138
|
+
}): Promise<Map<string, LegacyApiModel>> {
|
|
139
|
+
const url = options?.url ?? DEFAULT_LEGACY_MODELS_URL
|
|
140
|
+
const fetchImpl = options?.fetchImpl ?? fetch
|
|
141
|
+
|
|
142
|
+
const response = await fetchImpl(url, {
|
|
143
|
+
headers: { accept: "application/json" },
|
|
144
|
+
})
|
|
273
145
|
|
|
274
146
|
if (!response.ok) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
147
|
+
let body: any
|
|
148
|
+
try { body = await response.json() } catch {}
|
|
149
|
+
const err = parseProxyError(body)
|
|
150
|
+
throw new Error(`Failed to fetch models: ${response.status} — ${friendlyMessage(err.code, err.message)}`)
|
|
278
151
|
}
|
|
279
152
|
|
|
280
|
-
const body = (await response.json()) as
|
|
281
|
-
const
|
|
153
|
+
const body = (await response.json()) as LegacyApiResponse
|
|
154
|
+
const modelMap = new Map<string, LegacyApiModel>()
|
|
282
155
|
|
|
283
|
-
for (const
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
156
|
+
for (const model of body.data) {
|
|
157
|
+
if (model.object === "model") {
|
|
158
|
+
modelMap.set(model.id, model)
|
|
159
|
+
}
|
|
287
160
|
}
|
|
288
161
|
|
|
289
|
-
return
|
|
162
|
+
return modelMap
|
|
290
163
|
}
|
|
291
164
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
165
|
+
/** Fetch models from OpenModel API (public, no auth required) */
|
|
166
|
+
export async function fetchOpenModelModels(options?: {
|
|
167
|
+
webUrl?: string
|
|
168
|
+
legacyUrl?: string
|
|
169
|
+
fetchImpl?: typeof fetch
|
|
170
|
+
}): Promise<readonly OpenModelProviderModel[]> {
|
|
171
|
+
const fetchImpl = options?.fetchImpl ?? fetch
|
|
172
|
+
|
|
173
|
+
const [webModels, legacyModels] = await Promise.all([
|
|
174
|
+
fetchWebModels({ fetchImpl }),
|
|
175
|
+
fetchLegacyModels({ fetchImpl }),
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
const models: OpenModelProviderModel[] = []
|
|
179
|
+
|
|
180
|
+
for (const [id, web] of webModels) {
|
|
181
|
+
// Skip image-only models
|
|
182
|
+
if (web.supports.supports_image_generation && !web.supports.supports_vision && !web.supports.supports_reasoning) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const legacy = legacyModels.get(id)
|
|
187
|
+
const protocols = legacy?.supported_protocols ?? []
|
|
188
|
+
const api = determineApi(protocols, web.provider_key)
|
|
189
|
+
if (!api) continue
|
|
190
|
+
|
|
191
|
+
const inputPrice = pricePerMillion(web.prices.input_cost_per_token as number)
|
|
192
|
+
const outputPrice = pricePerMillion(web.prices.output_cost_per_token as number)
|
|
193
|
+
const cacheRead = pricePerMillion(web.prices.cache_read_input_token_cost as number)
|
|
194
|
+
const cacheWrite = pricePerMillion(web.prices.cache_creation_input_token_cost as number)
|
|
195
|
+
|
|
196
|
+
const reasoning = web.supports.supports_reasoning ?? false
|
|
197
|
+
|
|
198
|
+
const base = {
|
|
199
|
+
id,
|
|
200
|
+
name: id,
|
|
201
|
+
reasoning,
|
|
202
|
+
input: web.supports.supports_vision ? ["text", "image"] as const : ["text"] as const,
|
|
203
|
+
cost: {
|
|
204
|
+
input: inputPrice * (web.price_multiplier ?? 1),
|
|
205
|
+
output: outputPrice * (web.price_multiplier ?? 1),
|
|
206
|
+
cacheRead,
|
|
207
|
+
cacheWrite,
|
|
208
|
+
},
|
|
209
|
+
contextWindow: web.max.max_input_tokens ?? 128_000,
|
|
210
|
+
maxTokens: web.max.max_output_tokens ?? web.max.max_tokens ?? 16_384,
|
|
211
|
+
api,
|
|
212
|
+
} as const
|
|
213
|
+
|
|
214
|
+
const model = {
|
|
215
|
+
...base,
|
|
216
|
+
...(reasoning ? { thinkingLevelMap: thinkingLevelMapForApi(api) } : {}),
|
|
217
|
+
} as unknown as OpenModelProviderModel
|
|
218
|
+
|
|
219
|
+
models.push(model)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return models
|
|
223
|
+
}
|
package/src/stability.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* GET https://api.openmodel.ai/web/v1/model-stability/:modelKey
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { parseWebError, friendlyMessage } from "./errors.ts"
|
|
13
|
+
|
|
12
14
|
export const STABILITY_SUMMARY_URL =
|
|
13
15
|
"https://api.openmodel.ai/web/v1/model-stability/summary";
|
|
14
16
|
|
|
@@ -82,6 +84,13 @@ export async function fetchModelStabilitySummary(options?: {
|
|
|
82
84
|
headers: { accept: "application/json" },
|
|
83
85
|
});
|
|
84
86
|
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
let errBody: any
|
|
89
|
+
try { errBody = await response.json() } catch {}
|
|
90
|
+
const err = parseWebError(errBody)
|
|
91
|
+
throw new Error(`stability — ${friendlyMessage(err.code, err.message)}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
const body = (await response.json()) as {
|
|
86
95
|
success: boolean;
|
|
87
96
|
data: Array<{
|
|
@@ -93,7 +102,9 @@ export async function fetchModelStabilitySummary(options?: {
|
|
|
93
102
|
}>;
|
|
94
103
|
};
|
|
95
104
|
|
|
96
|
-
if (!body.success)
|
|
105
|
+
if (!body.success) {
|
|
106
|
+
throw new Error(`stability — ${friendlyMessage("INTERNAL_ERROR", "Summary request failed")}`)
|
|
107
|
+
}
|
|
97
108
|
|
|
98
109
|
return body.data.map((item) => ({
|
|
99
110
|
...item,
|
|
@@ -118,6 +129,13 @@ export async function fetchModelStabilityDetail(
|
|
|
118
129
|
{ headers: { accept: "application/json" } },
|
|
119
130
|
);
|
|
120
131
|
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
let errBody: any
|
|
134
|
+
try { errBody = await response.json() } catch {}
|
|
135
|
+
const err = parseWebError(errBody)
|
|
136
|
+
throw new Error(`stability — ${friendlyMessage(err.code, err.message)}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
const body = (await response.json()) as {
|
|
122
140
|
success: boolean;
|
|
123
141
|
data: {
|
|
@@ -141,8 +159,9 @@ export async function fetchModelStabilityDetail(
|
|
|
141
159
|
};
|
|
142
160
|
};
|
|
143
161
|
|
|
144
|
-
if (!body.success)
|
|
145
|
-
throw new Error(`
|
|
162
|
+
if (!body.success) {
|
|
163
|
+
throw new Error(`stability — ${friendlyMessage("NOT_FOUND", `Model "${modelKey}" not found`)}`)
|
|
164
|
+
}
|
|
146
165
|
|
|
147
166
|
return {
|
|
148
167
|
...body.data,
|