pi-freeflow 1.38.1 → 1.38.3
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/CHANGELOG.md +13 -0
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/catalog.ts +23 -3
- package/src/models.ts +96 -59
- package/src/stream-pipe.ts +49 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.38.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Absorb tool-call argument fragments arriving on terminal finish_reason chunks into buffered prefixes to prevent argument rotation and corrupted JSON loops. Add Reflection AI's Beam Free (1M context) to the OpenCode Zen lineup, and retire deprecated LL Alpha and Muse Spark 1.3 contributor free models.
|
|
8
|
+
|
|
9
|
+
## 1.38.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Add Exo to the OpenCode free tier — a live preview model with chat, tools, reasoning, and image input that the official free lineup lists.
|
|
14
|
+
- Add GLM-5.3 Flash and LL Alpha to the OpenCode free tier, and retire Step 5 Preview — it is gone from the Zen lineup and demoted to paid on the Kilo lane, so those entries can no longer be selected.
|
|
15
|
+
|
|
3
16
|
## 1.38.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Thin by design: a model list, a relay proxy, and a log. The host (`pi-ai`) handl
|
|
|
16
16
|
|
|
17
17
|
| Feature | What it does | Cost |
|
|
18
18
|
| :--- | :--- | :--- |
|
|
19
|
-
| **36 free models** |
|
|
19
|
+
| **36 free models** | 13 from OpenCode Zen, 13 from KiloCode Gateway, 2 from Cline, 8 from OVHcloud AI Endpoints, context windows up to 1M. Full list below. | **$0** |
|
|
20
20
|
| **Relay pool** | Route requests through your own Cloudflare Workers, Deno Deploy, and Vercel Edge relays. Traffic spreads across the pool, or stays on one sticky relay. A relay that rate-limits, times out, or drops the connection cools down while healthy ones take its traffic. | **$0** beyond your platforms' free tiers |
|
|
21
21
|
| **Automatic fallback** | When every relay is cooling down, requests go direct to upstream instead of failing. | **$0** |
|
|
22
22
|
| **Short model names** | Most models have a slash-free, colon-free alias, plus an optional `:effort` suffix for thinking depth. You type `freeflow/<name>`; the two Cline models have no alias and must be typed in full. | **$0** |
|
|
@@ -204,7 +204,7 @@ The same command set works identically in OMP and Pi:
|
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
Every model specification is reconciled against [models.dev](https://models.dev/), [OpenRouter](https://openrouter.ai/), and OMP's built-in catalog, configured with the highest verified context, output ceiling, and reasoning capabilities across twin models.
|
|
207
|
-
#### OpenCode Zen (
|
|
207
|
+
#### OpenCode Zen (13 models), Responses and Chat API
|
|
208
208
|
|
|
209
209
|
Good defaults for long coding sessions and agentic work.
|
|
210
210
|
|
|
@@ -212,7 +212,6 @@ Good defaults for long coding sessions and agentic work.
|
|
|
212
212
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
213
213
|
| `cat-jam-free` | Cat Jam | **256K** (262.144) | **131K** (131.072) | `low … max` | ❌ |
|
|
214
214
|
| `muse-spark-1.2-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
|
|
215
|
-
| `muse-spark-1.3-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
|
|
216
215
|
| `mimo-v2.6-flash-free` | Xiaomi MiMo | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh`\* | ✅ |
|
|
217
216
|
| `nemotron-3.5-lightning-free` | NVIDIA | **1M** (1.000.000) | **262K** (262.144) | `minimal … xhigh` | ❌ |
|
|
218
217
|
| `nemotron-3.5-super-vl-free` | NVIDIA | **1M** (1.000.000) | **131K** (131.072) | `minimal … xhigh` | ✅ |
|
|
@@ -220,10 +219,12 @@ Good defaults for long coding sessions and agentic work.
|
|
|
220
219
|
| `big-pickle` | Big Pickle | **200K** (200.000) | **32K** (32.000) | `high / max` | ❌ |
|
|
221
220
|
| `space-bunny-free` | Stealth preview (lab undisclosed) | **1M** (1.048.576) | **512K** (524.288) | `low … max` | ✅ |
|
|
222
221
|
| `longcat-2.5-preview-free` | Meituan LongCat | **1M** (1.000.000) | **131K** (131.072) | `minimal … max` | ✅ |
|
|
223
|
-
| `
|
|
222
|
+
| `glm-5.3-flash-free` | Zhipu AI | **1M** (1.000.000) | **32K** (32.000) | `minimal … max` | ✅ |
|
|
223
|
+
| `beam-free` | Reflection AI | **1M** (1.048.576) | **131K** (131.072) | `low … max` | ❌ |
|
|
224
|
+
| `exo-free` | Undisclosed | **1M** (1.048.576) | **131K** (131.072) | `high` | ✅ |
|
|
224
225
|
| `deepseek-v4.1-flash-free` | DeepSeek | **1M** (1.000.000) | **384K** (384.000) | `minimal … max` | ✅ |
|
|
225
226
|
|
|
226
|
-
#### KiloCode Gateway (
|
|
227
|
+
#### KiloCode Gateway (13 models), OpenRouter compatible
|
|
227
228
|
|
|
228
229
|
Keyless access. Short aliases work for every row (the full ID is in parentheses).
|
|
229
230
|
|
|
@@ -241,7 +242,6 @@ Keyless access. Short aliases work for every row (the full ID is in parentheses)
|
|
|
241
242
|
| `kilo-auto` (`kilo-auto/free`) | Kilo Gateway Auto | **256K** (256.000) | **10K** (10.000) | `minimal…xhigh`\* | ❌ |
|
|
242
243
|
| `openrouter` (`openrouter/free`) | OpenRouter Free | **200K** (200.000) | **65K** (65.536) | `minimal…xhigh`\* | ✅ |
|
|
243
244
|
| `glyph-cluster` (`stealth/glyph-cluster`) | Stealth preview (lab undisclosed) | **256K** (256.000) | **256K** (256.000) | `minimal…xhigh`\* | ❌ |
|
|
244
|
-
| `step-5-preview:kilo` (`stepfun/...:free`) | StepFun | **1M** (1.000.000) | **64K** (65.536) | `minimal…xhigh`\* | ✅ |
|
|
245
245
|
| `inkling-small` (`thinkingmachines/inkling-small:free`) | Thinking Machines | **1M** (1.048.576) | **262K** (262.144) | `minimal…xhigh`\* | ✅ |
|
|
246
246
|
|
|
247
247
|
\* Levels are forwarded as-is through the OpenRouter-style nested `reasoning` parameter; effort mapping is decided by each model. MiMo collapses `minimal→low` and `xhigh→high` upstream, so its selector shows 5 labels but only 3 distinct effort values.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.38.
|
|
4
|
+
"version": "1.38.3",
|
|
5
5
|
"description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
|
|
6
6
|
"main": "extensions/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
package/src/catalog.ts
CHANGED
|
@@ -43,8 +43,14 @@ import type {
|
|
|
43
43
|
* - inclusionai/ling-3.0-flash-fin:free: gone from Kilo (2026-10-01) — keyless chat 404
|
|
44
44
|
* "The requested model ... does not exist".
|
|
45
45
|
* - mimo-v2.5-free: decommissioned upstream (2026-10-07) — HTTP 401 "Model mimo-v2.5-free is not supported".
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Re-probed 2026-10-16: 410 "Model mimo-v2.5-free has been deprecated. Use
|
|
47
|
+
* mimo-v2.6-flash-free instead." The docs still list it as free; they are stale.
|
|
48
|
+
* - exo-free: dead-listed 2026-10-09 (410 "Model exo-free has been deprecated")
|
|
49
|
+
* but revived upstream on 2026-10-16 — back on the official free lineup and
|
|
50
|
+
* answering live through the proxy (chat 200 with tools, stream, reasoning,
|
|
51
|
+
* vision; dual-canary recall at ~520K tokens; a 1.2M-word prompt draws the
|
|
52
|
+
* empty-bodied 400). Re-registered statically the same day: do NOT add this
|
|
53
|
+
* id back to this set unless it dies again.
|
|
48
54
|
* - fledge-alpha-free: withdrawn upstream (2026-10-09) — absent from the live
|
|
49
55
|
* Zen list, chat 401 "Model fledge-alpha-free is not supported".
|
|
50
56
|
* - inclusionai/ling-3.0-flash-sante:free: demoted to paid on Kilo (2026-10-09) —
|
|
@@ -94,7 +100,6 @@ export const DEAD_MODEL_IDS = new Set<string>([
|
|
|
94
100
|
"ling-3.0-flash-fin-free",
|
|
95
101
|
"inclusionai/ling-3.0-flash-fin:free",
|
|
96
102
|
"mimo-v2.5-free",
|
|
97
|
-
"exo-free",
|
|
98
103
|
"stealth/space-bunny-alpha",
|
|
99
104
|
"qwen/qwen3.8-27b:free",
|
|
100
105
|
"nvidia/nemotron-3.5-content-safety:free",
|
|
@@ -110,6 +115,21 @@ export const DEAD_MODEL_IDS = new Set<string>([
|
|
|
110
115
|
"inclusionai/ling-3.1-flash",
|
|
111
116
|
// ovh-free/Mistral-7B-Instruct-v0.3: removed upstream by OVH (2026-10-15) — answers 404 no route matched.
|
|
112
117
|
"ovh-free/Mistral-7B-Instruct-v0.3",
|
|
118
|
+
// step-5-preview-free: removed from the Zen free docs (2026-10-16 drift,
|
|
119
|
+
// v1.19.2) and the endpoint answers 400 "Model is unavailable" on both the
|
|
120
|
+
// chat and the responses shape — the same retirement signature as
|
|
121
|
+
// deepseek-v4-flash-free.
|
|
122
|
+
"step-5-preview-free",
|
|
123
|
+
// stepfun/step-5-preview-free: demoted to paid on Kilo (2026-10-16) —
|
|
124
|
+
// answers 401 PAID_MODEL_AUTH_REQUIRED while the sibling z-ai/glm-5.3-flash
|
|
125
|
+
// answers 200 keyless with the same request shape.
|
|
126
|
+
"stepfun/step-5-preview-free",
|
|
127
|
+
// ll-alpha-free: decommissioned upstream (2026-10-17) — gone from live
|
|
128
|
+
// Zen /v1/models list, answers HTTP 410 "Model ll-alpha-free has been deprecated".
|
|
129
|
+
"ll-alpha-free",
|
|
130
|
+
// muse-spark-1.3-contributor-free: decommissioned upstream (2026-10-17) — gone from
|
|
131
|
+
// live Zen /v1/models list, answers HTTP 410 "Model muse-spark-1.3-contributor-free has been deprecated".
|
|
132
|
+
"muse-spark-1.3-contributor-free",
|
|
113
133
|
]);
|
|
114
134
|
/**
|
|
115
135
|
* Free-tier allowlist for anything entering the picker via network or stale disk.
|
package/src/models.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static model definitions and upstream routing catalogs for pi-freeflow
|
|
3
3
|
*
|
|
4
|
-
* Defines the 36 verified free models (live-verified 2026-10-
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Defines the 36 verified free models (live-verified 2026-10-17):
|
|
5
|
+
* - 13 OpenCode Zen models (1 Responses API + 12 Chat Completions)
|
|
6
|
+
* - 13 KiloCode Keyless Gateway models (OpenRouter format)
|
|
7
7
|
* - 2 Cline direct-only models (per-user pool)
|
|
8
8
|
* - 8 OVHcloud AI Endpoints models (keyless, direct-only)
|
|
9
9
|
*/
|
|
@@ -59,24 +59,6 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
59
59
|
max: null,
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
-
{
|
|
63
|
-
id: "muse-spark-1.3-contributor-free",
|
|
64
|
-
name: "Muse Spark 1.3 [OpenCode]",
|
|
65
|
-
reasoning: true,
|
|
66
|
-
contextWindow: 1_048_576,
|
|
67
|
-
maxTokens: 131_072,
|
|
68
|
-
api: "openai-responses",
|
|
69
|
-
input: ["text", "image", "video"],
|
|
70
|
-
thinkingLevelMap: {
|
|
71
|
-
off: null,
|
|
72
|
-
minimal: "minimal",
|
|
73
|
-
low: "low",
|
|
74
|
-
medium: "medium",
|
|
75
|
-
high: "high",
|
|
76
|
-
xhigh: "xhigh",
|
|
77
|
-
max: null,
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
62
|
{
|
|
81
63
|
id: "mimo-v2.6-flash-free",
|
|
82
64
|
name: "MiMo V2.6 Flash [OpenCode]",
|
|
@@ -216,30 +198,97 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
216
198
|
},
|
|
217
199
|
},
|
|
218
200
|
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
201
|
+
// Added 2026-10-16 (issue #71 drift): Zen v1.19.2 added this free id;
|
|
202
|
+
// served through the fingerprint path (chat 200 with tools + stream).
|
|
203
|
+
// Not published on pi.dev yet, and models.dev's 200,000 context is
|
|
204
|
+
// contradicted by probe: a marker planted at 90% depth (~250K words) is
|
|
205
|
+
// recalled, and only above ~1M words does the gateway refuse with
|
|
206
|
+
// context_length_exceeded. Declared 1,000,000 like its Zen siblings and
|
|
207
|
+
// the Kilo twin (z-ai/glm-5.3-flash, 1,048,576 in this catalog); the
|
|
208
|
+
// 450K/700K-word recall probes timed out upstream and stayed inconclusive.
|
|
209
|
+
// maxTokens 32,000 per models.dev — the gateway accepted up to 131,072,
|
|
210
|
+
// and acceptance is not a cap, so the published figure stays the budget.
|
|
211
|
+
// Effort ladder probed level-by-level 2026-10-16: minimal/low/medium/high/
|
|
212
|
+
// xhigh/max all answer 200 (an unsupported value answers 400
|
|
213
|
+
// [invalid_value], so the gateway validates per model), and minimal vs max
|
|
214
|
+
// visibly changes reasoning length. Chat is the only native shape
|
|
215
|
+
// (/v1/responses answers 400 "Model does not support this protocol"),
|
|
216
|
+
// so `api` stays unset. Vision verified live (a 1x1 PNG reads back as a
|
|
217
|
+
// colour); video/pdf from the published entry stay unverified and are not
|
|
218
|
+
// offered.
|
|
219
|
+
id: "glm-5.3-flash-free",
|
|
220
|
+
name: "GLM-5.3 Flash [OpenCode]",
|
|
221
|
+
reasoning: true,
|
|
222
|
+
contextWindow: 1_000_000,
|
|
223
|
+
maxTokens: 32_000,
|
|
224
|
+
input: ["text", "image"],
|
|
225
|
+
thinkingLevelMap: {
|
|
226
|
+
off: null,
|
|
227
|
+
minimal: "minimal",
|
|
228
|
+
low: "low",
|
|
229
|
+
medium: "medium",
|
|
230
|
+
high: "high",
|
|
231
|
+
xhigh: "xhigh",
|
|
232
|
+
max: "max",
|
|
233
|
+
},
|
|
242
234
|
},
|
|
235
|
+
{
|
|
236
|
+
// Added 2026-10-17: Reflection AI's Beam 501B open-weight MoE model (23B active),
|
|
237
|
+
// added as a limited-time free model on OpenCode Zen (/zen/v1/models: beam-free).
|
|
238
|
+
// Live-probed 2026-10-17: 200 with stream:true and tool sextet.
|
|
239
|
+
// Context window: 1_048_576 tokens (1M), maxTokens: 131_072.
|
|
240
|
+
// Probed boundary: filler up to 900,000 words (~1.1M tokens) answers 200.
|
|
241
|
+
// Input: text-only (image probe answers 400 "Beam supports text input only").
|
|
242
|
+
// Wire format: Chat Completions (/v1/responses answers 400 ModelProtocolUnsupported).
|
|
243
|
+
// Reasoning effort ladder: minimal is rejected with 400 (unsupported_value),
|
|
244
|
+
// low/medium/high/xhigh/max all answer 200 with reasoning.
|
|
245
|
+
id: "beam-free",
|
|
246
|
+
name: "Beam Free [OpenCode]",
|
|
247
|
+
reasoning: true,
|
|
248
|
+
contextWindow: 1_048_576,
|
|
249
|
+
maxTokens: 131_072,
|
|
250
|
+
input: ["text"],
|
|
251
|
+
thinkingLevelMap: {
|
|
252
|
+
off: null,
|
|
253
|
+
minimal: null,
|
|
254
|
+
low: "low",
|
|
255
|
+
medium: "medium",
|
|
256
|
+
high: "high",
|
|
257
|
+
xhigh: "xhigh",
|
|
258
|
+
max: "max",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
// Added 2026-10-16 (recheck): live free id on the Zen lane, listed on the
|
|
263
|
+
// official free lineup while models.dev already tags it deprecated — the
|
|
264
|
+
// tag also sits on live ids (muse-spark-1.2-contributor-free) and on
|
|
265
|
+
// already-dead ones (mimo-v2.5-free), so the live answer decides: chat 200
|
|
266
|
+
// with tools + stream, and the tool call round-trips. Chat is the only
|
|
267
|
+
// native shape (/v1/responses answers 400 "Model does not support this
|
|
268
|
+
// protocol"), so `api` stays unset. Reasoning: the published ladder lists
|
|
269
|
+
// `high` only; high streams thinking and the unavailable rungs stay null
|
|
270
|
+
// so the picker never offers them. Vision verified live (a 1x1 PNG reads
|
|
271
|
+
// back as "Pink"). Context: published 1,048,576; a dual-canary prompt at
|
|
272
|
+
// ~400K words (~520K tokens, canaries at ~30%/~97% depth) recalls both
|
|
273
|
+
// markers, and a 1.2M-word prompt (~1.56M tokens) draws an empty-bodied
|
|
274
|
+
// 400 — the wall sits between, matching the published figure. maxTokens
|
|
275
|
+
// 131,072 published; 131,073 accepted (acceptance is not a cap, so the
|
|
276
|
+
// published figure stays the budget).
|
|
277
|
+
id: "exo-free",
|
|
278
|
+
name: "Exo [OpenCode]",
|
|
279
|
+
reasoning: true,
|
|
280
|
+
contextWindow: 1_048_576,
|
|
281
|
+
maxTokens: 131_072,
|
|
282
|
+
input: ["text", "image"],
|
|
283
|
+
thinkingLevelMap: {
|
|
284
|
+
off: null,
|
|
285
|
+
minimal: null,
|
|
286
|
+
low: null,
|
|
287
|
+
medium: null,
|
|
288
|
+
high: "high",
|
|
289
|
+
xhigh: null,
|
|
290
|
+
max: null,
|
|
291
|
+
},
|
|
243
292
|
},
|
|
244
293
|
{
|
|
245
294
|
// Added 2026-10-15: new free model on the OpenCode Zen lane. Live-verified by
|
|
@@ -409,19 +458,6 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
409
458
|
thinkingFormat: "openrouter",
|
|
410
459
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
411
460
|
},
|
|
412
|
-
{
|
|
413
|
-
// Added 2026-10-09: replaces step-3.7-flash (demoted to paid upstream —
|
|
414
|
-
// :free ID 404s keyless). Live on Kilo free list (isFree:true, 0/0
|
|
415
|
-
// pricing, 1M ctx) + keyless chat served (429 provider concurrency).
|
|
416
|
-
id: "stepfun/step-5-preview-free",
|
|
417
|
-
name: "Step 5 Preview [Kilo]",
|
|
418
|
-
reasoning: true,
|
|
419
|
-
contextWindow: 1_000_000,
|
|
420
|
-
maxTokens: 65_536,
|
|
421
|
-
input: ["text", "image", "video"],
|
|
422
|
-
thinkingFormat: "openrouter",
|
|
423
|
-
thinkingLevelMap: KILO_REASONING_MAP,
|
|
424
|
-
},
|
|
425
461
|
{
|
|
426
462
|
id: "thinkingmachines/inkling-small:free",
|
|
427
463
|
name: "Inkling Small [Kilo]",
|
|
@@ -656,7 +692,8 @@ export const MODEL_ALIASES: Record<string, string> = {
|
|
|
656
692
|
"nemotron-3-super": "nvidia/nemotron-3-super-120b-a12b:free",
|
|
657
693
|
"north-mini-code": "cohere/north-mini-code:free",
|
|
658
694
|
"lfm-2.5": "liquid/lfm-2.5-2.6b:free",
|
|
659
|
-
"
|
|
695
|
+
"glm-5.3-flash": "glm-5.3-flash-free",
|
|
696
|
+
"exo": "exo-free",
|
|
660
697
|
"glyph-cluster": "stealth/glyph-cluster",
|
|
661
698
|
"inkling-small": "thinkingmachines/inkling-small:free",
|
|
662
699
|
"mimo-v2.6-flash": "mimo-v2.6-flash-free",
|
|
@@ -664,7 +701,7 @@ export const MODEL_ALIASES: Record<string, string> = {
|
|
|
664
701
|
"space-bunny": "space-bunny-free",
|
|
665
702
|
"longcat-2.5-preview": "longcat-2.5-preview-free",
|
|
666
703
|
"longcat": "longcat-2.5-preview-free",
|
|
667
|
-
"
|
|
704
|
+
"beam": "beam-free",
|
|
668
705
|
"deepseek-v4.1-flash": "deepseek-v4.1-flash-free",
|
|
669
706
|
// provider-prefixed short aliases (slash-normalized)
|
|
670
707
|
"laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
|
package/src/stream-pipe.ts
CHANGED
|
@@ -442,6 +442,53 @@ function cloakStreamResponsesObject(resp: unknown, cloak: StreamCloakOptions): v
|
|
|
442
442
|
(resp as Record<string, unknown>).output = kept;
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Terminal chunk for chat choice: absorb any tool-call argument fragment
|
|
447
|
+
* riding the finish_reason chunk into the buffered prefix, then re-emit every
|
|
448
|
+
* buffered tool call's whole argument packet, repaired.
|
|
449
|
+
* Returns true if delta was mutated.
|
|
450
|
+
*/
|
|
451
|
+
function flushChatTerminalToolCalls(
|
|
452
|
+
d: Record<string, unknown>,
|
|
453
|
+
state: SseStreamCloakState,
|
|
454
|
+
cloak: StreamCloakOptions,
|
|
455
|
+
flushedIndices: Set<number>,
|
|
456
|
+
): boolean {
|
|
457
|
+
if (state.chatArgsByIndex.size === 0) return false;
|
|
458
|
+
|
|
459
|
+
for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) {
|
|
460
|
+
if (!tc || typeof tc !== "object" || Array.isArray(tc)) continue;
|
|
461
|
+
const rec = tc as Record<string, unknown>;
|
|
462
|
+
const idx = typeof rec.index === "number" ? rec.index : 0;
|
|
463
|
+
const fn = rec.function;
|
|
464
|
+
const fnRec = fn && typeof fn === "object" && !Array.isArray(fn) ? (fn as Record<string, unknown>) : null;
|
|
465
|
+
if (fnRec && state.chatArgsByIndex.has(idx) && typeof fnRec.arguments === "string") {
|
|
466
|
+
state.chatArgsByIndex.set(idx, (state.chatArgsByIndex.get(idx) ?? "") + fnRec.arguments);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const flushed: unknown[] = [];
|
|
471
|
+
for (const [idx, args] of state.chatArgsByIndex) {
|
|
472
|
+
const name = state.chatNameByIndex.get(idx);
|
|
473
|
+
if (name === undefined) continue;
|
|
474
|
+
flushedIndices.add(idx);
|
|
475
|
+
flushed.push({ index: idx, type: "function", function: { name, arguments: repairToolArguments(name, args, cloak.argSchemas) } });
|
|
476
|
+
}
|
|
477
|
+
state.chatArgsByIndex.clear();
|
|
478
|
+
state.chatNameByIndex.clear();
|
|
479
|
+
|
|
480
|
+
if (flushed.length === 0) return false;
|
|
481
|
+
|
|
482
|
+
const remaining = (Array.isArray(d.tool_calls) ? d.tool_calls : []).filter((tc) => {
|
|
483
|
+
if (!tc || typeof tc !== "object" || Array.isArray(tc)) return true;
|
|
484
|
+
const rec = tc as Record<string, unknown>;
|
|
485
|
+
const idx = typeof rec.index === "number" ? rec.index : 0;
|
|
486
|
+
return !flushedIndices.has(idx);
|
|
487
|
+
});
|
|
488
|
+
d.tool_calls = [...remaining, ...flushed];
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
445
492
|
/**
|
|
446
493
|
* Rewrite one Chat Completions SSE block: drop injected delta.tool_calls
|
|
447
494
|
* (including nameless argument continuations via per-index tracking), restore
|
|
@@ -489,20 +536,8 @@ function rewriteChatBlock(
|
|
|
489
536
|
// Indices flushed on this block: their arguments are final, so the
|
|
490
537
|
// per-delta loop below must not buffer them a second time.
|
|
491
538
|
const flushedIndices = new Set<number>();
|
|
492
|
-
if (c.finish_reason && state
|
|
493
|
-
|
|
494
|
-
for (const [idx, args] of state.chatArgsByIndex) {
|
|
495
|
-
const name = state.chatNameByIndex.get(idx);
|
|
496
|
-
if (name === undefined) continue;
|
|
497
|
-
flushedIndices.add(idx);
|
|
498
|
-
flushed.push({ index: idx, type: "function", function: { name, arguments: repairToolArguments(name, args, cloak.argSchemas) } });
|
|
499
|
-
}
|
|
500
|
-
state.chatArgsByIndex.clear();
|
|
501
|
-
state.chatNameByIndex.clear();
|
|
502
|
-
if (flushed.length > 0) {
|
|
503
|
-
d.tool_calls = [...(Array.isArray(d.tool_calls) ? d.tool_calls : []), ...flushed];
|
|
504
|
-
changed = true;
|
|
505
|
-
}
|
|
539
|
+
if (c.finish_reason && flushChatTerminalToolCalls(d, state, cloak, flushedIndices)) {
|
|
540
|
+
changed = true;
|
|
506
541
|
}
|
|
507
542
|
if (!Array.isArray(d.tool_calls)) {
|
|
508
543
|
if (Object.keys(d).length > 0) allDeltasEmpty = false;
|