pi-coralbricks-provider 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -27
- package/deprecated-models.json +1 -38
- package/index.ts +8 -4
- package/models.json +5 -36
- package/package.json +12 -12
- package/patch.json +16 -0
- package/scripts/update-models.js +73 -14
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# 🪸 pi-coralbricks-provider
|
|
4
4
|
|
|
5
|
-
**GLM 5.3, GLM 5.3 Flash
|
|
5
|
+
**GLM 5.3, GLM 5.3 Flash & DeepSeek V4.1 Flash through [Coral Bricks](https://www.coralbricks.ai)**
|
|
6
6
|
|
|
7
7
|
_A [pi](https://github.com/earendil-works/pi-coding-agent) provider extension for Coral's OpenAI-compatible inference gateway — up to **1M context** on open models._
|
|
8
8
|
|
|
@@ -17,13 +17,13 @@ _A [pi](https://github.com/earendil-works/pi-coding-agent) provider extension fo
|
|
|
17
17
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
|
-
- **
|
|
21
|
-
- **1M token context** on GLM and
|
|
20
|
+
- **3 reasoning models** from Coral's live catalog — GLM 5.3 FP4, GLM 5.3 Flash, and DeepSeek V4.1 Flash
|
|
21
|
+
- **1M token context** on GLM and DeepSeek, with vision (image input) on GLM 5.3 Flash and DeepSeek V4.1 Flash
|
|
22
22
|
- **OpenAI-compatible API** — standard `/v1/chat/completions`, streaming, and tool calling
|
|
23
|
-
- **Per-family thinking levels** — zai-style `thinking` control for GLM (including a *real* off switch), `reasoning_effort` for
|
|
24
|
-
- **Accurate cost tracking** —
|
|
23
|
+
- **Per-family thinking levels** — zai-style `thinking` control for GLM (including a *real* off switch), `reasoning_effort` for DeepSeek V4.1 Flash
|
|
24
|
+
- **Accurate cost tracking** — input, cache-write and output rates mirror Coral's [published pricing](https://www.coralbricks.ai/pricing), and cached reads are **$0 on every model**
|
|
25
25
|
- **Self-healing model sync** — stale-while-revalidate from the authenticated `/v1/models` (or the unauthenticated [public catalog](https://www.coralbricks.ai/api/public/models) before auth), hot-swapped at session start
|
|
26
|
-
- **synbad-validated** — [synbad](https://github.com/synthetic-lab/synbad) tool-calling and reasoning-parsing evals pass 13/13 in every run on GLM 5.3
|
|
26
|
+
- **synbad-validated** — [synbad](https://github.com/synthetic-lab/synbad) tool-calling and reasoning-parsing evals pass 13/13 in every run on GLM 5.3 in both unary and streaming modes
|
|
27
27
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
@@ -78,14 +78,13 @@ pi
|
|
|
78
78
|
|
|
79
79
|
## Available Models
|
|
80
80
|
|
|
81
|
-
| Model | Context | Vision | Reasoning | Input $/M | Cache Read $/M | Output $/M |
|
|
82
|
-
|
|
83
|
-
| DeepSeek V4.1 Flash | 1.0M |
|
|
84
|
-
| GLM 5.3 Flash | 1.0M | ✅ | ✅ | $0.15 | — | $0.50 |
|
|
85
|
-
| GLM 5.3 FP4 | 1.0M | ❌ | ✅ | $1.12 | — | $4.40 |
|
|
86
|
-
| GPT-OSS 120B | 131K | ❌ | ✅ | $0.12 | — | $0.60 |
|
|
81
|
+
| Model | Context | Vision | Reasoning | Input $/M | Cache Read $/M | Cache Write $/M | Output $/M |
|
|
82
|
+
|-------|---------|--------|-----------|-----------|-----------------|------------------|------------|
|
|
83
|
+
| DeepSeek V4.1 Flash | 1.0M | ✅ | ✅ | $0.30 | — | $0.09 | $1.20 |
|
|
84
|
+
| GLM 5.3 Flash | 1.0M | ✅ | ✅ | $0.15 | — | $0.23 | $0.50 |
|
|
85
|
+
| GLM 5.3 FP4 | 1.0M | ❌ | ✅ | $1.12 | — | $1.68 | $4.40 |
|
|
87
86
|
|
|
88
|
-
*Costs are per million tokens. Cache Read shows — because Coral bills cached input at **$0** on every model. Prices subject to change — check [Coral's live catalog](https://www.coralbricks.ai/api/public/models).*
|
|
87
|
+
*Costs are per million tokens. Cache Read shows — because Coral bills cached input at **$0** on every model. Prompt tokens Coral has not cached yet are billed once at the Cache Write rate, in place of the Input rate. Prices subject to change — check [Coral's live catalog](https://www.coralbricks.ai/api/public/models).*
|
|
89
88
|
|
|
90
89
|
## Usage
|
|
91
90
|
|
|
@@ -98,10 +97,10 @@ After loading the extension, use the `/model` command in pi to select your prefe
|
|
|
98
97
|
Or start pi directly with a Coral model:
|
|
99
98
|
|
|
100
99
|
```bash
|
|
101
|
-
pi -e /path/to/pi-coralbricks-provider --model coralbricks/
|
|
100
|
+
pi -e /path/to/pi-coralbricks-provider --model coralbricks/deepseek-v4.1-flash-fast-fp4:high
|
|
102
101
|
```
|
|
103
102
|
|
|
104
|
-
Thinking levels attach to the model id with `:<level>` — e.g. `:low`, `:high`, `:max`, or `:off` (GLM
|
|
103
|
+
Thinking levels attach to the model id with `:<level>` — e.g. `:low`, `:high`, `:max`, or `:off` (GLM and DeepSeek).
|
|
105
104
|
|
|
106
105
|
## Authentication
|
|
107
106
|
|
|
@@ -131,12 +130,10 @@ Verified against the live gateway:
|
|
|
131
130
|
|-------|--------|-----|-----|--------|------|-----|
|
|
132
131
|
| GLM 5.3 FP4 | `thinking: {type}` + `reasoning_effort` | ✅ | ✅ | — | ✅ | ✅ |
|
|
133
132
|
| GLM 5.3 Flash | `thinking: {type}` + `reasoning_effort` | ✅ | ✅ | — | ✅ | ✅ |
|
|
134
|
-
|
|
|
135
|
-
| GPT-OSS 120B | `reasoning_effort` | — | ✅ | ✅ | ✅ | — |
|
|
133
|
+
| DeepSeek V4.1 Flash | `reasoning_effort` | ✅ | ✅ | — | ✅ | ✅ |
|
|
136
134
|
|
|
137
135
|
- **GLM** accepts zai-style `thinking: {type: "disabled"}` on Coral — pi's *off* level turns thinking off, though a short preamble of a few dozen tokens can still appear (the upstream Z.ai API has no off at all, so this differs from the canonical Z.ai map).
|
|
138
|
-
- **
|
|
139
|
-
- **GPT-OSS** exposes the standard low/medium/high reasoning efforts; reasoning arrives in `reasoning_content`.
|
|
136
|
+
- **DeepSeek V4.1 Flash** reasons only when asked: a request without `reasoning_effort`, or with `"none"`, gets no reasoning, which is what pi's *off* sends. `thinking: {type}` has no effect on this model.
|
|
140
137
|
- Coral streams a duplicate `reasoning` field alongside `reasoning_content`; pi dedupes these automatically.
|
|
141
138
|
|
|
142
139
|
## Compat Settings
|
|
@@ -146,30 +143,29 @@ Coral's gateway follows the OpenAI Chat Completions API:
|
|
|
146
143
|
- **`supportsStore: false`** / **`supportsDeveloperRole: false`** — all models; Coral serves open models on the classic roles.
|
|
147
144
|
- **`maxTokensField: "max_tokens"`** — all models.
|
|
148
145
|
- **`thinkingFormat: "zai"`** — GLM 5.3: `thinking: {type: "enabled"|"disabled"}` toggles reasoning, `reasoning_effort` picks the depth.
|
|
149
|
-
- **`thinkingFormat: "openai"`** —
|
|
150
|
-
- **`supportsStrictMode: false`** — Kimi K3 (no strict JSON-schema tool definitions).
|
|
151
|
-
- **`requiresReasoningContentOnAssistantMessages: true`** — Kimi K3.
|
|
146
|
+
- **`thinkingFormat: "openai"`** — DeepSeek V4.1 Flash: `reasoning_effort` drives thinking depth.
|
|
152
147
|
|
|
153
148
|
### Patch Overrides & Custom Models
|
|
154
149
|
|
|
155
|
-
- **`patch.json`** — per-model overrides applied on top of `models.json` (reasoning flags, pricing corrections, compat settings, thinking level maps). Currently carries
|
|
150
|
+
- **`patch.json`** — per-model overrides applied on top of `models.json` (reasoning flags, pricing corrections, compat settings, thinking level maps). Currently carries two entries: GLM 5.3 Flash (reasoning, image input, its thinking level map and compat settings) and DeepSeek V4.1 Flash (reasoning, its thinking level map and compat settings).
|
|
156
151
|
- **`custom-models.json`** — full model definitions for models Coral doesn't list. Merged after patch.
|
|
157
152
|
|
|
158
153
|
Merge order: `[live|cache|embedded] → patch.json → custom-models.json`
|
|
159
154
|
|
|
160
155
|
## Inference-Quality Testing
|
|
161
156
|
|
|
162
|
-
Validated with [synbad](https://github.com/synthetic-lab/synbad) — Synthetic's tool-calling and reasoning-parsing eval suite for LLM inference providers (current 13-eval suite, `--reasoning-effort high`, five `--count 1` runs per mode on 2026-09-15; a cell shows the evals that passed in every run):
|
|
157
|
+
Validated with [synbad](https://github.com/synthetic-lab/synbad) — Synthetic's tool-calling and reasoning-parsing eval suite for LLM inference providers (current 13-eval suite, `--reasoning-effort high`, five `--count 1` runs per mode on 2026-09-15, DeepSeek on 2026-09-22; a cell shows the evals that passed in every run):
|
|
163
158
|
|
|
164
159
|
| Model | Unary | Stream | Notes |
|
|
165
160
|
|-------|-------|--------|-------|
|
|
166
161
|
| GLM 5.3 FP4 | 13/13 ✅ | 13/13 ✅ | 5/5 runs |
|
|
167
162
|
| GLM 5.3 Flash | 12/13 ⚠️ | 12/13 ⚠️ | `reasoning/reasoning-parsing` passes 4 of 5 runs; see below |
|
|
168
|
-
|
|
|
169
|
-
|
|
|
163
|
+
| GPT-OSS 120B (retired) | 12/13 ⚠️ | 12/13 ⚠️ | `tools/parallel-tool` 0 of 5 runs, before Coral retired the model |
|
|
164
|
+
| DeepSeek V4.1 Flash | 12/13 ⚠️ | 12/13 ⚠️ | `tools/octo-list-no-optional-args` passes 1 of 5 runs; see below |
|
|
165
|
+
| Kimi K3 (retired) | 13/13 ✅ | 13/13 ✅ | 5/5 runs, before Coral retired the model |
|
|
170
166
|
| GLM 5.2 FP4 (retired) | 15/15 ✅ | 15/15 ✅ | 15-eval suite, single run, before Coral retired the model |
|
|
171
167
|
|
|
172
|
-
The
|
|
168
|
+
The misses are model-side, not transport: gpt-oss answered "Paris and London" with one call after another instead of two parallel calls even when `parallel_tool_calls: true`, in every run; on the trivial prompt `reasoning-parsing` uses, GLM 5.3 Flash answers without a think block about one run in five, in both modes, with the answer itself unaffected; and asked to "call the list tool with no args", DeepSeek V4.1 Flash usually fills in the optional `dirPath` as `"."`.
|
|
173
169
|
|
|
174
170
|
Reproduce:
|
|
175
171
|
|
package/deprecated-models.json
CHANGED
|
@@ -1,38 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"kimi-k3": {
|
|
3
|
-
"id": "kimi-k3",
|
|
4
|
-
"name": "Kimi K3",
|
|
5
|
-
"reasoning": true,
|
|
6
|
-
"input": [
|
|
7
|
-
"text",
|
|
8
|
-
"image"
|
|
9
|
-
],
|
|
10
|
-
"cost": {
|
|
11
|
-
"input": 3,
|
|
12
|
-
"output": 15,
|
|
13
|
-
"cacheRead": 0,
|
|
14
|
-
"cacheWrite": 0
|
|
15
|
-
},
|
|
16
|
-
"contextWindow": 1048576,
|
|
17
|
-
"maxTokens": 131072,
|
|
18
|
-
"thinkingLevelMap": {
|
|
19
|
-
"off": null,
|
|
20
|
-
"minimal": null,
|
|
21
|
-
"low": "low",
|
|
22
|
-
"medium": null,
|
|
23
|
-
"high": "high",
|
|
24
|
-
"xhigh": null,
|
|
25
|
-
"max": "max"
|
|
26
|
-
},
|
|
27
|
-
"compat": {
|
|
28
|
-
"supportsStore": false,
|
|
29
|
-
"supportsDeveloperRole": false,
|
|
30
|
-
"supportsReasoningEffort": true,
|
|
31
|
-
"maxTokensField": "max_tokens",
|
|
32
|
-
"supportsStrictMode": false,
|
|
33
|
-
"thinkingFormat": "openai",
|
|
34
|
-
"requiresReasoningContentOnAssistantMessages": true
|
|
35
|
-
},
|
|
36
|
-
"deprecatedAt": "2026-09-21T00:00:07.264Z"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
{}
|
package/index.ts
CHANGED
|
@@ -224,7 +224,8 @@ function baseCompat() {
|
|
|
224
224
|
/**
|
|
225
225
|
* Transform a Coral /v1/models row. Coral reports per-million USD pricing
|
|
226
226
|
* (input_per_m / output_per_m / cached_input_per_m — cached reads are $0 on
|
|
227
|
-
* every model
|
|
227
|
+
* every model — and cache_write_per_m, the rate for prompt tokens written to
|
|
228
|
+
* the cache), context_length, and supports_image_input.
|
|
228
229
|
*/
|
|
229
230
|
function transformApiModel(apiModel: any): JsonModel | null {
|
|
230
231
|
if (!apiModel?.id) return null;
|
|
@@ -238,7 +239,7 @@ function transformApiModel(apiModel: any): JsonModel | null {
|
|
|
238
239
|
input: toNumber(pricing.input_per_m),
|
|
239
240
|
output: toNumber(pricing.output_per_m),
|
|
240
241
|
cacheRead: toNumber(pricing.cached_input_per_m),
|
|
241
|
-
cacheWrite:
|
|
242
|
+
cacheWrite: toNumber(pricing.cache_write_per_m),
|
|
242
243
|
},
|
|
243
244
|
contextWindow: parseContextWindow(apiModel.context_length),
|
|
244
245
|
maxTokens: DEFAULT_MAX_TOKENS[apiModel.id] ?? FALLBACK_MAX_TOKENS,
|
|
@@ -252,7 +253,9 @@ function transformApiModel(apiModel: any): JsonModel | null {
|
|
|
252
253
|
* (https://www.coralbricks.ai/api/public/models) — the no-auth mirror of the
|
|
253
254
|
* gateway control plane. Fields: slug, name, contextWindow ("1M"),
|
|
254
255
|
* inputPerM / outputPerM. Coral's own prices are authoritative here; the
|
|
255
|
-
* parity/field vendor comparison fields are ignored.
|
|
256
|
+
* parity/field vendor comparison fields are ignored. The catalog carries no
|
|
257
|
+
* cache-write rate, so cacheWrite stays 0 and mergeWithEmbedded keeps the
|
|
258
|
+
* embedded rate.
|
|
256
259
|
*/
|
|
257
260
|
function transformCatalogModel(entry: any): JsonModel | null {
|
|
258
261
|
if (!entry?.slug) return null;
|
|
@@ -347,7 +350,8 @@ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[])
|
|
|
347
350
|
// Self-heal: live API pricing is authoritative field-by-field. Prefer the
|
|
348
351
|
// live cost when the API reports it (non-zero); fall back to embedded when
|
|
349
352
|
// the API is silent (0). Coral reports cached_input_per_m as 0 because
|
|
350
|
-
// cached reads are free, which matches the curated cacheRead: 0.
|
|
353
|
+
// cached reads are free, which matches the curated cacheRead: 0. The
|
|
354
|
+
// public catalog has no cache-write rate, so its 0 keeps the embedded one.
|
|
351
355
|
// Curation (reasoning/input/compat/name/thinkingLevelMap/maxTokens) still wins via ...embedded.
|
|
352
356
|
result.push({
|
|
353
357
|
...liveModel,
|
package/models.json
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
"name": "DeepSeek V4.1 Flash",
|
|
5
5
|
"reasoning": false,
|
|
6
6
|
"input": [
|
|
7
|
-
"text"
|
|
7
|
+
"text",
|
|
8
|
+
"image"
|
|
8
9
|
],
|
|
9
10
|
"cost": {
|
|
10
11
|
"input": 0.3,
|
|
11
12
|
"output": 1.2,
|
|
12
13
|
"cacheRead": 0,
|
|
13
|
-
"cacheWrite": 0
|
|
14
|
+
"cacheWrite": 0.09
|
|
14
15
|
},
|
|
15
16
|
"contextWindow": 1048576,
|
|
16
17
|
"maxTokens": 32768,
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"input": 0.15,
|
|
33
34
|
"output": 0.5,
|
|
34
35
|
"cacheRead": 0,
|
|
35
|
-
"cacheWrite": 0
|
|
36
|
+
"cacheWrite": 0.23
|
|
36
37
|
},
|
|
37
38
|
"contextWindow": 1048576,
|
|
38
39
|
"maxTokens": 32768,
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
"input": 1.12,
|
|
54
55
|
"output": 4.4,
|
|
55
56
|
"cacheRead": 0,
|
|
56
|
-
"cacheWrite":
|
|
57
|
+
"cacheWrite": 1.68
|
|
57
58
|
},
|
|
58
59
|
"contextWindow": 1048576,
|
|
59
60
|
"maxTokens": 131072,
|
|
@@ -73,37 +74,5 @@
|
|
|
73
74
|
"maxTokensField": "max_tokens",
|
|
74
75
|
"thinkingFormat": "zai"
|
|
75
76
|
}
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
"id": "gpt-oss-120b",
|
|
79
|
-
"name": "GPT-OSS 120B",
|
|
80
|
-
"reasoning": true,
|
|
81
|
-
"input": [
|
|
82
|
-
"text"
|
|
83
|
-
],
|
|
84
|
-
"cost": {
|
|
85
|
-
"input": 0.12,
|
|
86
|
-
"output": 0.6,
|
|
87
|
-
"cacheRead": 0,
|
|
88
|
-
"cacheWrite": 0
|
|
89
|
-
},
|
|
90
|
-
"contextWindow": 131072,
|
|
91
|
-
"maxTokens": 40960,
|
|
92
|
-
"thinkingLevelMap": {
|
|
93
|
-
"off": null,
|
|
94
|
-
"minimal": null,
|
|
95
|
-
"low": "low",
|
|
96
|
-
"medium": "medium",
|
|
97
|
-
"high": "high",
|
|
98
|
-
"xhigh": null,
|
|
99
|
-
"max": null
|
|
100
|
-
},
|
|
101
|
-
"compat": {
|
|
102
|
-
"supportsStore": false,
|
|
103
|
-
"supportsDeveloperRole": false,
|
|
104
|
-
"supportsReasoningEffort": true,
|
|
105
|
-
"maxTokensField": "max_tokens",
|
|
106
|
-
"thinkingFormat": "openai"
|
|
107
|
-
}
|
|
108
77
|
}
|
|
109
78
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-coralbricks-provider",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CoralBricks provider extension for pi - Access GLM 5.3, GLM 5.3 Flash,
|
|
3
|
+
"version": "1.0.18",
|
|
4
|
+
"description": "CoralBricks provider extension for pi - Access GLM 5.3, GLM 5.3 Flash, and DeepSeek V4.1 Flash through the Coral Inference API",
|
|
5
5
|
"author": "monotykamary",
|
|
6
6
|
"homepage": "https://github.com/monotykamary/pi-coralbricks-provider#readme",
|
|
7
7
|
"repository": {
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "index.ts",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"clean": "echo 'nothing to clean'",
|
|
18
|
+
"build": "echo 'nothing to build'",
|
|
19
|
+
"check": "tsc --noEmit && vitest run",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"update-models": "node scripts/update-models.js"
|
|
22
|
+
},
|
|
16
23
|
"keywords": [
|
|
17
24
|
"pi-package",
|
|
18
25
|
"pi",
|
|
@@ -23,8 +30,8 @@
|
|
|
23
30
|
"ai",
|
|
24
31
|
"llm",
|
|
25
32
|
"glm",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
33
|
+
"deepseek",
|
|
34
|
+
"deepseek"
|
|
28
35
|
],
|
|
29
36
|
"license": "MIT",
|
|
30
37
|
"files": [
|
|
@@ -50,12 +57,5 @@
|
|
|
50
57
|
"dependencies": {
|
|
51
58
|
"@earendil-works/pi-ai": "0.87.0",
|
|
52
59
|
"@earendil-works/pi-coding-agent": "0.87.0"
|
|
53
|
-
},
|
|
54
|
-
"scripts": {
|
|
55
|
-
"clean": "echo 'nothing to clean'",
|
|
56
|
-
"build": "echo 'nothing to build'",
|
|
57
|
-
"check": "tsc --noEmit && vitest run",
|
|
58
|
-
"test": "vitest run",
|
|
59
|
-
"update-models": "node scripts/update-models.js"
|
|
60
60
|
}
|
|
61
|
-
}
|
|
61
|
+
}
|
package/patch.json
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
+
"deepseek-v4.1-flash-fast-fp4": {
|
|
3
|
+
"reasoning": true,
|
|
4
|
+
"thinkingLevelMap": {
|
|
5
|
+
"off": "none",
|
|
6
|
+
"minimal": null,
|
|
7
|
+
"low": "low",
|
|
8
|
+
"medium": null,
|
|
9
|
+
"high": "high",
|
|
10
|
+
"xhigh": null,
|
|
11
|
+
"max": "max"
|
|
12
|
+
},
|
|
13
|
+
"compat": {
|
|
14
|
+
"supportsReasoningEffort": true,
|
|
15
|
+
"thinkingFormat": "openai"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
2
18
|
"glm-5.3-flash-fp4": {
|
|
3
19
|
"reasoning": true,
|
|
4
20
|
"input": ["text", "image"],
|
package/scripts/update-models.js
CHANGED
|
@@ -136,6 +136,7 @@ function resolveApiKey() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const MODELS_API_URL = 'https://inference.coralbricks.ai/v1/models';
|
|
139
|
+
const CHAT_COMPLETIONS_URL = 'https://inference.coralbricks.ai/v1/chat/completions';
|
|
139
140
|
const PUBLIC_CATALOG_URL = 'https://www.coralbricks.ai/api/public/models';
|
|
140
141
|
const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
|
|
141
142
|
const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
|
|
@@ -249,6 +250,7 @@ function transformApiModel(apiModel, existingModelsMap) {
|
|
|
249
250
|
if (typeof pricing.input_per_m === 'number') existing.cost.input = pricing.input_per_m;
|
|
250
251
|
if (typeof pricing.output_per_m === 'number') existing.cost.output = pricing.output_per_m;
|
|
251
252
|
existing.cost.cacheRead = toNumber(pricing.cached_input_per_m);
|
|
253
|
+
if (typeof pricing.cache_write_per_m === 'number') existing.cost.cacheWrite = pricing.cache_write_per_m;
|
|
252
254
|
existing.input = input;
|
|
253
255
|
return existing;
|
|
254
256
|
}
|
|
@@ -263,7 +265,7 @@ function transformApiModel(apiModel, existingModelsMap) {
|
|
|
263
265
|
input: toNumber(pricing.input_per_m),
|
|
264
266
|
output: toNumber(pricing.output_per_m),
|
|
265
267
|
cacheRead: toNumber(pricing.cached_input_per_m),
|
|
266
|
-
cacheWrite:
|
|
268
|
+
cacheWrite: toNumber(pricing.cache_write_per_m),
|
|
267
269
|
},
|
|
268
270
|
contextWindow,
|
|
269
271
|
maxTokens,
|
|
@@ -392,8 +394,8 @@ function formatCost(cost) {
|
|
|
392
394
|
|
|
393
395
|
function generateReadmeTable(models) {
|
|
394
396
|
const lines = [
|
|
395
|
-
'| Model | Context | Vision | Reasoning | Input $/M | Cache Read $/M | Output $/M |',
|
|
396
|
-
'
|
|
397
|
+
'| Model | Context | Vision | Reasoning | Input $/M | Cache Read $/M | Cache Write $/M | Output $/M |',
|
|
398
|
+
'|-------|---------|--------|-----------|-----------|-----------------|------------------|------------|',
|
|
397
399
|
];
|
|
398
400
|
|
|
399
401
|
for (const model of models) {
|
|
@@ -402,9 +404,10 @@ function generateReadmeTable(models) {
|
|
|
402
404
|
const reasoning = model.reasoning ? '✅' : '❌';
|
|
403
405
|
const inputCost = formatCost(model.cost.input);
|
|
404
406
|
const cacheReadCost = formatCost(model.cost.cacheRead);
|
|
407
|
+
const cacheWriteCost = formatCost(model.cost.cacheWrite);
|
|
405
408
|
const outputCost = formatCost(model.cost.output);
|
|
406
409
|
|
|
407
|
-
lines.push(`| ${model.name} | ${context} | ${vision} | ${reasoning} | ${inputCost} | ${cacheReadCost} | ${outputCost} |`);
|
|
410
|
+
lines.push(`| ${model.name} | ${context} | ${vision} | ${reasoning} | ${inputCost} | ${cacheReadCost} | ${cacheWriteCost} | ${outputCost} |`);
|
|
408
411
|
}
|
|
409
412
|
|
|
410
413
|
return lines.join('\n');
|
|
@@ -429,9 +432,41 @@ function updateReadme(models) {
|
|
|
429
432
|
// longer lists into deprecated-models.json (stamped with deprecatedAt) instead
|
|
430
433
|
// of dropping them; the runtime appends them back so sessions and saved model
|
|
431
434
|
// settings keep working, and after 14 days they are evicted permanently.
|
|
435
|
+
//
|
|
436
|
+
// The grace period exists for a model that is still ANSWERING while it leaves
|
|
437
|
+
// the catalog. A retired one is not: Coral answers 404 with
|
|
438
|
+
// `error.code == "model_retired"`, so keeping it registered only offers the
|
|
439
|
+
// user a model that cannot reply, and /model still lists it for two weeks.
|
|
440
|
+
// Each delisted model is probed once, and a retired one is dropped instead of
|
|
441
|
+
// parked. Anything else — an unknown id, a key without access, a network
|
|
442
|
+
// blip — keeps the grace period, because only `model_retired` is a statement
|
|
443
|
+
// that the model is gone for everybody rather than missing for this key.
|
|
432
444
|
const DEPRECATED_MODEL_TTL_MS = 14 * 24 * 60 * 60 * 1000;
|
|
445
|
+
const RETIRED_PROBE_TIMEOUT_MS = 10000;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Ask the gateway whether a model is retired. Returns true ONLY on an explicit
|
|
449
|
+
* 404 `model_retired`; every other outcome returns false, so a probe that
|
|
450
|
+
* cannot answer never shortens a grace period.
|
|
451
|
+
*/
|
|
452
|
+
async function isRetiredUpstream(id, apiKey) {
|
|
453
|
+
if (!apiKey) return false;
|
|
454
|
+
try {
|
|
455
|
+
const response = await fetch(CHAT_COMPLETIONS_URL, {
|
|
456
|
+
method: 'POST',
|
|
457
|
+
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
458
|
+
body: JSON.stringify({ model: id, max_tokens: 1, messages: [{ role: 'user', content: 'ping' }] }),
|
|
459
|
+
signal: AbortSignal.timeout(RETIRED_PROBE_TIMEOUT_MS),
|
|
460
|
+
});
|
|
461
|
+
if (response.status !== 404) return false;
|
|
462
|
+
const body = await response.json();
|
|
463
|
+
return body?.error?.code === 'model_retired';
|
|
464
|
+
} catch {
|
|
465
|
+
return false; // offline, timeout, unparseable body: keep the grace period
|
|
466
|
+
}
|
|
467
|
+
}
|
|
433
468
|
|
|
434
|
-
function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
469
|
+
async function updateDeprecatedModels(modelsJsonPath, newModels, apiKey) {
|
|
435
470
|
const deprecatedPath = path.join(path.dirname(modelsJsonPath), 'deprecated-models.json');
|
|
436
471
|
|
|
437
472
|
let oldModels = [];
|
|
@@ -451,12 +486,25 @@ function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
|
451
486
|
const added = [];
|
|
452
487
|
const resurrected = [];
|
|
453
488
|
const evicted = [];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
489
|
+
const retired = [];
|
|
490
|
+
|
|
491
|
+
const candidates = oldModels.filter((old) => old && old.id && !currentIds.has(old.id) && !deprecated[old.id]);
|
|
492
|
+
// Probe the delisted models plus anything already parked: a model can retire
|
|
493
|
+
// during its own grace period, which is exactly when it starts 404ing.
|
|
494
|
+
const parked = Object.keys(deprecated).filter((id) => !currentIds.has(id));
|
|
495
|
+
const toProbe = [...new Set([...candidates.map((m) => m.id), ...parked])];
|
|
496
|
+
const retiredIds = new Set(
|
|
497
|
+
(await Promise.all(toProbe.map(async (id) => ((await isRetiredUpstream(id, apiKey)) ? id : null))))
|
|
498
|
+
.filter((id) => id !== null)
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
for (const old of candidates) {
|
|
502
|
+
if (retiredIds.has(old.id)) {
|
|
503
|
+
retired.push(old.id);
|
|
504
|
+
continue; // retired upstream: 404s, so a grace period buys nothing
|
|
459
505
|
}
|
|
506
|
+
deprecated[old.id] = { ...old, deprecatedAt: now };
|
|
507
|
+
added.push(old.id);
|
|
460
508
|
}
|
|
461
509
|
|
|
462
510
|
for (const [id, entry] of Object.entries(deprecated)) {
|
|
@@ -465,6 +513,11 @@ function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
|
465
513
|
resurrected.push(id);
|
|
466
514
|
continue;
|
|
467
515
|
}
|
|
516
|
+
if (retiredIds.has(id)) {
|
|
517
|
+
delete deprecated[id];
|
|
518
|
+
retired.push(id);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
468
521
|
const removedAt = Date.parse(entry && entry.deprecatedAt ? entry.deprecatedAt : '');
|
|
469
522
|
if (Number.isNaN(removedAt) || Date.now() - removedAt > DEPRECATED_MODEL_TTL_MS) {
|
|
470
523
|
delete deprecated[id];
|
|
@@ -472,9 +525,9 @@ function updateDeprecatedModels(modelsJsonPath, newModels) {
|
|
|
472
525
|
}
|
|
473
526
|
}
|
|
474
527
|
|
|
475
|
-
if (added.length > 0 || resurrected.length > 0 || evicted.length > 0) {
|
|
528
|
+
if (added.length > 0 || resurrected.length > 0 || evicted.length > 0 || retired.length > 0) {
|
|
476
529
|
fs.writeFileSync(deprecatedPath, JSON.stringify(deprecated, null, 2) + '\n');
|
|
477
|
-
console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted }));
|
|
530
|
+
console.log('Updated deprecated-models.json ' + JSON.stringify({ added, resurrected, evicted, retired }));
|
|
478
531
|
}
|
|
479
532
|
}
|
|
480
533
|
|
|
@@ -517,7 +570,7 @@ async function main() {
|
|
|
517
570
|
|
|
518
571
|
// Save models.json (pure API output, no patch/custom baked in)
|
|
519
572
|
// Move delisted models to deprecated-models.json BEFORE models.json is overwritten
|
|
520
|
-
updateDeprecatedModels(MODELS_JSON_PATH, models);
|
|
573
|
+
await updateDeprecatedModels(MODELS_JSON_PATH, models, resolveApiKey());
|
|
521
574
|
saveJson(MODELS_JSON_PATH, models);
|
|
522
575
|
|
|
523
576
|
// Build the README model list: base → patch → custom. Grace-period models stay
|
|
@@ -549,4 +602,10 @@ async function main() {
|
|
|
549
602
|
}
|
|
550
603
|
}
|
|
551
604
|
|
|
552
|
-
|
|
605
|
+
// Run only when invoked as a script, so the deprecation logic below can be
|
|
606
|
+
// imported by tests without kicking off a sync.
|
|
607
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
608
|
+
main();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export { isRetiredUpstream, updateDeprecatedModels };
|