pi-freeflow 1.33.2 → 1.34.0
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 +42 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/catalog.ts +17 -2
- package/src/models.ts +55 -42
- package/src/opencode-fingerprint.ts +20 -7
- package/src/tool-args.ts +48 -9
- package/src/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.34.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- aa6f259: Three new free models, picked the same way as the rest (`/model` → `freeflow` → pick):
|
|
8
|
+
|
|
9
|
+
- **Step 5 Preview** on OpenCode Zen (`step-5-preview-free`, short name `step-5-preview`) — 1M context, answers with vision.
|
|
10
|
+
- **Step 5 Preview** on KiloCode (`stepfun/step-5-preview-free`, short name `step-5-preview:kilo`) — 1M context, answers with vision.
|
|
11
|
+
- **Glyph Cluster** on KiloCode (`stealth/glyph-cluster`, short name `glyph-cluster`) — 256K context, text answers.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 50981fb: Align nine model entries with the specs their upstream catalogs publish. Models now advertise video input where the vendor lists it, two entries pick up a larger context window, and Step 5 Preview on KiloCode reports the full 64K output allowance.
|
|
16
|
+
- f637c58: Recover tool calls that arrive with the tail of the previous call stuck to the
|
|
17
|
+
front. Some models open a new call by echoing the end of the one they have just
|
|
18
|
+
seen, so a single call is delivered as `<tail of previous>{"…this call…"}` and
|
|
19
|
+
the host refuses it as invalid JSON. When such a payload does not parse, the
|
|
20
|
+
proxy now locates the complete call behind the stray text and hands that back.
|
|
21
|
+
|
|
22
|
+
Only payloads that already fail to parse are rescanned, and only a position that
|
|
23
|
+
yields a complete object is accepted — a well-formed call is never rewritten,
|
|
24
|
+
and a truncated one is still refused rather than completed.
|
|
25
|
+
|
|
26
|
+
Also adds a whole-inventory fidelity test covering every tool either host can
|
|
27
|
+
declare, across all three API shapes.
|
|
28
|
+
|
|
29
|
+
- aa6f259: Remove three free models that stopped serving upstream, so they no longer appear in the model list:
|
|
30
|
+
|
|
31
|
+
- **Fledge Alpha** on OpenCode (`fledge-alpha-free`) — withdrawn upstream and no longer listed.
|
|
32
|
+
- **Ling 3.0 Flash Sante** on KiloCode (`inclusionai/ling-3.0-flash-sante:free`) — the free variant is gone; only the paid one remains.
|
|
33
|
+
- **Step 3.7 Flash** on KiloCode (`stepfun/step-3.7-flash:free`) — the free variant is gone; only the paid one remains.
|
|
34
|
+
|
|
35
|
+
All three are excluded permanently, so a stale on-disk catalog or a later catalog refresh cannot bring them back.
|
|
36
|
+
|
|
37
|
+
- 50981fb: Correct the OpenCode Zen Step 5 Preview entry: it is a reasoning model with 1M context, 64K max output, text/image/video input, and only `low`/`medium`/`high` thinking levels (`minimal` is explicitly disabled). Previously the entry mirrored the Kilo sibling and the fallback heuristic, so the picker showed no thinking support.
|
|
38
|
+
|
|
39
|
+
## 1.33.3
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Fix tool calls being rejected with an invalid-arguments error when a model makes several calls in one turn. Some models label every parallel call `0`, so the pieces of different calls were joined together and arrived as one unreadable blob — a planning tool could then reject every update, even a minimal one, and leave its task list stuck mid-turn. Calls are now kept apart by their own identifier, and a call whose text arrives in several pieces still reassembles whole.
|
|
44
|
+
|
|
3
45
|
## 1.33.2
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ Good defaults for long coding sessions and agentic work.
|
|
|
205
205
|
| `big-pickle` | Big Pickle | **200K** (200.000) | **32K** (32.000) | `high / max` | ❌ |
|
|
206
206
|
| `space-bunny-free` | Stealth preview (lab undisclosed) | **1M** (1.048.576) | **512K** (524.288) | `low … max` | ✅ |
|
|
207
207
|
| `longcat-2.5-preview-free` | Meituan LongCat | **1M** (1.000.000) | **131K** (131.072) | `minimal … max` | ✅ |
|
|
208
|
-
| `
|
|
208
|
+
| `step-5-preview-free` | StepFun | **1M** (1.000.000) | **64K** (65.536) | `low … high` | ✅ |
|
|
209
209
|
| `ling-3.1-flash-free` | Inclusion AI | **262K** (262.144) | **32K** (32.768) | `minimal … max` | ❌ |
|
|
210
210
|
|
|
211
211
|
#### KiloCode Gateway (16 models), OpenRouter compatible
|
|
@@ -225,8 +225,8 @@ Keyless access. Short aliases work for every row (the full ID is in parentheses)
|
|
|
225
225
|
| `lfm-2.5` (`liquid/lfm-2.5-2.6b:free`) | Liquid AI | **65K** (65.536) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
|
|
226
226
|
| `kilo-auto` (`kilo-auto/free`) | Kilo Gateway Auto | **256K** (256.000) | **10K** (10.000) | `minimal…xhigh`\* | ❌ |
|
|
227
227
|
| `openrouter` (`openrouter/free`) | OpenRouter Free | **200K** (200.000) | **65K** (65.536) | `minimal…xhigh`\* | ✅ |
|
|
228
|
-
| `
|
|
229
|
-
| `step-
|
|
228
|
+
| `glyph-cluster` (`stealth/glyph-cluster`) | Stealth preview (lab undisclosed) | **256K** (256.000) | **256K** (256.000) | `minimal…xhigh`\* | ❌ |
|
|
229
|
+
| `step-5-preview:kilo` (`stepfun/...:free`) | StepFun | **1M** (1.000.000) | **64K** (64.000) | `minimal…xhigh`\* | ✅ |
|
|
230
230
|
| `inkling-small` (`thinkingmachines/inkling-small:free`) | Thinking Machines | **1M** (1.048.576) | **262K** (262.144) | `minimal…xhigh`\* | ✅ |
|
|
231
231
|
| `ling-3.1-flash-kilo` (`inclusionai/ling-3.1-flash`) | Inclusion AI | **262K** (262.144) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
|
|
232
232
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.34.0",
|
|
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
|
@@ -30,7 +30,8 @@ import type {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Pruned model IDs that must never re-enter the catalog via disk cache or upstream merge.
|
|
33
|
-
* - jev-1.13-free: non-chat decision model, chat-incompatible
|
|
33
|
+
* - jev-1.13-free: non-chat decision model, chat-incompatible — never registered.
|
|
34
|
+
* Re-confirmed 2026-10-09: 400 through the plugin while every catalog model 200s.
|
|
34
35
|
* - deepseek-v4-flash-free: listed but unserved upstream (live chat 400 "Model is unavailable").
|
|
35
36
|
* - nex-agi/nex-n2.5-pro:free + nex-agi/nex-n2.5-mini:free: gone 2026-09-28 — zero nex-agi
|
|
36
37
|
* IDs on the live Kilo list, keyless chat 404 "does not exist".
|
|
@@ -43,7 +44,18 @@ import type {
|
|
|
43
44
|
* - inclusionai/ling-3.0-flash-fin:free: gone from Kilo (2026-10-01) — keyless chat 404
|
|
44
45
|
* "The requested model ... does not exist".
|
|
45
46
|
* - mimo-v2.5-free: decommissioned upstream (2026-10-07) — HTTP 401 "Model mimo-v2.5-free is not supported".
|
|
46
|
-
* - exo-free:
|
|
47
|
+
* - exo-free: deprecated upstream (2026-10-09) — HTTP 410 "Model exo-free has
|
|
48
|
+
* been deprecated." (was 503 "Endpoint is unavailable" on 2026-10-07).
|
|
49
|
+
* - fledge-alpha-free: withdrawn upstream (2026-10-09) — absent from the live
|
|
50
|
+
* Zen list, chat 401 "Model fledge-alpha-free is not supported".
|
|
51
|
+
* - inclusionai/ling-3.0-flash-sante:free: demoted to paid on Kilo (2026-10-09) —
|
|
52
|
+
* :free ID gone from the live list, keyless chat 404 "does not exist";
|
|
53
|
+
* only the paid counterpart remains.
|
|
54
|
+
* - stepfun/step-3.7-flash:free: demoted to paid on Kilo (2026-10-09) — same
|
|
55
|
+
* shape: :free ID gone, keyless 404; paid ID remains.
|
|
56
|
+
* - meituan/longcat-2.0-free: demoted to paid on Kilo (2026-10-09) —
|
|
57
|
+
* `meituan/longcat-2.0` remains live with isFree:false; keyless chat
|
|
58
|
+
* answers 401 PAID_MODEL_AUTH_REQUIRED.
|
|
47
59
|
* - stealth/space-bunny-alpha: gone from Kilo (2026-10-07) — keyless chat 404 "The requested model ... does not exist".
|
|
48
60
|
* - qwen/qwen3.8-27b:free: gone from Kilo (2026-10-07) — keyless chat 404 "The requested model is currently unavailable.".
|
|
49
61
|
* - nvidia/nemotron-3.5-content-safety:free: tool-less guardrail model (2026-10-08),
|
|
@@ -78,6 +90,9 @@ export const DEAD_MODEL_IDS = new Set<string>([
|
|
|
78
90
|
"stealth/space-bunny-alpha",
|
|
79
91
|
"qwen/qwen3.8-27b:free",
|
|
80
92
|
"nvidia/nemotron-3.5-content-safety:free",
|
|
93
|
+
"fledge-alpha-free",
|
|
94
|
+
"inclusionai/ling-3.0-flash-sante:free",
|
|
95
|
+
"stepfun/step-3.7-flash:free",
|
|
81
96
|
]);
|
|
82
97
|
/**
|
|
83
98
|
* Free-tier allowlist for anything entering the picker via network or stale disk.
|
package/src/models.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static model definitions and upstream routing catalogs for pi-freeflow
|
|
3
3
|
*
|
|
4
|
-
* Defines the 30 verified free models (live-verified 2026-10-
|
|
4
|
+
* Defines the 30 verified free models (live-verified 2026-10-09):
|
|
5
5
|
* - 10 OpenCode Zen models (2 Responses API + 8 Chat Completions)
|
|
6
6
|
* - 15 KiloCode Keyless Gateway models (OpenRouter format)
|
|
7
7
|
* - 5 Cline direct-only models (per-user pool)
|
|
@@ -21,7 +21,7 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
21
21
|
contextWindow: 1_048_576,
|
|
22
22
|
maxTokens: 131_072,
|
|
23
23
|
api: "openai-responses",
|
|
24
|
-
|
|
24
|
+
input: ["text", "image", "video"],
|
|
25
25
|
thinkingLevelMap: {
|
|
26
26
|
off: null,
|
|
27
27
|
minimal: "minimal",
|
|
@@ -39,7 +39,7 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
39
39
|
contextWindow: 1_048_576,
|
|
40
40
|
maxTokens: 131_072,
|
|
41
41
|
api: "openai-responses",
|
|
42
|
-
|
|
42
|
+
input: ["text", "image", "video"],
|
|
43
43
|
thinkingLevelMap: {
|
|
44
44
|
off: null,
|
|
45
45
|
minimal: "minimal",
|
|
@@ -56,7 +56,7 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
56
56
|
reasoning: true,
|
|
57
57
|
contextWindow: 1_048_576,
|
|
58
58
|
maxTokens: 131_072,
|
|
59
|
-
|
|
59
|
+
input: ["text", "image", "video"],
|
|
60
60
|
thinkingLevelMap: {
|
|
61
61
|
off: null,
|
|
62
62
|
minimal: "low",
|
|
@@ -124,7 +124,7 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
124
124
|
reasoning: true,
|
|
125
125
|
contextWindow: 1_048_576,
|
|
126
126
|
maxTokens: 524_288,
|
|
127
|
-
|
|
127
|
+
input: ["text", "image", "video"],
|
|
128
128
|
thinkingLevelMap: {
|
|
129
129
|
off: null,
|
|
130
130
|
minimal: null,
|
|
@@ -153,21 +153,30 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
153
153
|
},
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
// Added 2026-10-09: replaces fledge-alpha-free (withdrawn upstream —
|
|
157
|
+
// absent from the live list, chat 401 "not supported"). Live on the Zen
|
|
158
|
+
// list; gated-lane 403 matches the working models from a refused egress,
|
|
159
|
+
// so it rides the same fingerprint path. Specs per models.dev
|
|
160
|
+
// (opencode/step-5-preview-free) + StepFun docs, NOT the enrichModelDef
|
|
161
|
+
// substring heuristic (which yields reasoning:false / 262K / text-only):
|
|
162
|
+
// 1M ctx, 64K max output, text+image+video, efforts low/medium/high only.
|
|
163
|
+
// minimal is explicitly null (omitting the key leaves it selectable in
|
|
164
|
+
// the host); xhigh/max are unsupported upstream.
|
|
165
|
+
id: "step-5-preview-free",
|
|
166
|
+
name: "Step 5 Preview [OpenCode]",
|
|
167
|
+
reasoning: true,
|
|
168
|
+
contextWindow: 1_000_000,
|
|
169
|
+
maxTokens: 65_536,
|
|
170
|
+
input: ["text", "image", "video"],
|
|
171
|
+
thinkingLevelMap: {
|
|
172
|
+
off: null,
|
|
173
|
+
minimal: null,
|
|
174
|
+
low: "low",
|
|
175
|
+
medium: "medium",
|
|
176
|
+
high: "high",
|
|
177
|
+
xhigh: null,
|
|
178
|
+
max: null,
|
|
179
|
+
},
|
|
171
180
|
},
|
|
172
181
|
{
|
|
173
182
|
id: "ling-3.1-flash-free",
|
|
@@ -191,9 +200,10 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
191
200
|
/**
|
|
192
201
|
* Shared effort map for Kilo reasoning models — verified live 2026-08-29:
|
|
193
202
|
* gateway accepts flat reasoning_effort minimal..xhigh for every reasoning
|
|
194
|
-
* model;
|
|
195
|
-
*
|
|
196
|
-
* host guessing) and matches
|
|
203
|
+
* model; a StepFun reasoning model (step-3.7-flash, since retired from the
|
|
204
|
+
* free list) measured monotonic 77→313 thinking chars across minimal→xhigh.
|
|
205
|
+
* Declaring the map locks the picker (instead of host guessing) and matches
|
|
206
|
+
* the OpenCode-model pattern.
|
|
197
207
|
*/
|
|
198
208
|
const KILO_REASONING_MAP: ThinkingLevelMap = {
|
|
199
209
|
off: null,
|
|
@@ -225,7 +235,7 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
225
235
|
reasoning: true,
|
|
226
236
|
contextWindow: 256_000,
|
|
227
237
|
maxTokens: 131_072,
|
|
228
|
-
|
|
238
|
+
input: ["text", "image", "video"],
|
|
229
239
|
thinkingFormat: "openrouter",
|
|
230
240
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
231
241
|
},
|
|
@@ -320,14 +330,15 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
320
330
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
321
331
|
},
|
|
322
332
|
{
|
|
323
|
-
//
|
|
324
|
-
// (isFree:true, 0/0
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
// Added 2026-10-09: replaces step-3.7-flash (demoted to paid upstream —
|
|
334
|
+
// :free ID 404s keyless). Live on Kilo free list (isFree:true, 0/0
|
|
335
|
+
// pricing, 1M ctx) + keyless chat served (429 provider concurrency).
|
|
336
|
+
id: "stepfun/step-5-preview-free",
|
|
337
|
+
name: "Step 5 Preview [Kilo]",
|
|
327
338
|
reasoning: true,
|
|
328
|
-
contextWindow:
|
|
329
|
-
|
|
330
|
-
|
|
339
|
+
contextWindow: 1_000_000,
|
|
340
|
+
maxTokens: 65_536,
|
|
341
|
+
input: ["text", "image", "video"],
|
|
331
342
|
thinkingFormat: "openrouter",
|
|
332
343
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
333
344
|
},
|
|
@@ -342,11 +353,13 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
342
353
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
343
354
|
},
|
|
344
355
|
{
|
|
345
|
-
|
|
346
|
-
|
|
356
|
+
// Added 2026-10-09: live on Kilo free list (isFree:true, 0/0 pricing,
|
|
357
|
+
// 256K ctx) + keyless chat 200.
|
|
358
|
+
id: "stealth/glyph-cluster",
|
|
359
|
+
name: "Glyph Cluster [Kilo]",
|
|
347
360
|
reasoning: true,
|
|
348
|
-
contextWindow:
|
|
349
|
-
maxTokens:
|
|
361
|
+
contextWindow: 256_000,
|
|
362
|
+
maxTokens: 256_000,
|
|
350
363
|
input: ["text"],
|
|
351
364
|
thinkingFormat: "openrouter",
|
|
352
365
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
@@ -418,7 +431,7 @@ export const CLINE_MODELS: ModelDef[] = [
|
|
|
418
431
|
contextWindow: 1_048_576,
|
|
419
432
|
maxTokens: 131_072,
|
|
420
433
|
api: "openai-responses",
|
|
421
|
-
|
|
434
|
+
input: ["text", "image", "video"],
|
|
422
435
|
thinkingLevelMap: {
|
|
423
436
|
off: null,
|
|
424
437
|
minimal: "minimal",
|
|
@@ -433,9 +446,9 @@ export const CLINE_MODELS: ModelDef[] = [
|
|
|
433
446
|
id: "z-ai/glm-5.3-flash",
|
|
434
447
|
name: "GLM 5.3 Flash [Cline]",
|
|
435
448
|
reasoning: true,
|
|
436
|
-
|
|
449
|
+
contextWindow: 1_048_576,
|
|
437
450
|
maxTokens: 131_072,
|
|
438
|
-
|
|
451
|
+
input: ["text", "image", "video"],
|
|
439
452
|
thinkingLevelMap: CLINE_FLASH_REASONING_MAP,
|
|
440
453
|
},
|
|
441
454
|
{
|
|
@@ -448,7 +461,7 @@ export const CLINE_MODELS: ModelDef[] = [
|
|
|
448
461
|
reasoning: true,
|
|
449
462
|
contextWindow: 1_048_576,
|
|
450
463
|
maxTokens: 131_072,
|
|
451
|
-
|
|
464
|
+
input: ["text", "image", "video"],
|
|
452
465
|
thinkingLevelMap: CLINE_FLASH_REASONING_MAP,
|
|
453
466
|
},
|
|
454
467
|
{
|
|
@@ -477,14 +490,14 @@ export const MODEL_ALIASES: Record<string, string> = {
|
|
|
477
490
|
"nemotron-3-super": "nvidia/nemotron-3-super-120b-a12b:free",
|
|
478
491
|
"north-mini-code": "cohere/north-mini-code:free",
|
|
479
492
|
"lfm-2.5": "liquid/lfm-2.5-2.6b:free",
|
|
480
|
-
"
|
|
481
|
-
"
|
|
493
|
+
"step-5-preview:kilo": "stepfun/step-5-preview-free",
|
|
494
|
+
"glyph-cluster": "stealth/glyph-cluster",
|
|
482
495
|
"inkling-small": "thinkingmachines/inkling-small:free",
|
|
483
496
|
"mimo-v2.6-flash": "mimo-v2.6-flash-free",
|
|
484
497
|
"space-bunny": "space-bunny-free",
|
|
485
498
|
"longcat-2.5-preview": "longcat-2.5-preview-free",
|
|
486
499
|
"longcat": "longcat-2.5-preview-free",
|
|
487
|
-
"
|
|
500
|
+
"step-5-preview": "step-5-preview-free",
|
|
488
501
|
"ling-3.1-flash": "ling-3.1-flash-free",
|
|
489
502
|
"ling-3.1-flash:kilo": "inclusionai/ling-3.1-flash",
|
|
490
503
|
// provider-prefixed short aliases (slash-normalized)
|
|
@@ -564,10 +564,16 @@ export function sseToChatCompletionJson(
|
|
|
564
564
|
let reasoningContent = "";
|
|
565
565
|
let role = "assistant";
|
|
566
566
|
let usage: unknown = undefined;
|
|
567
|
+
// Keyed by tool-call id whenever the provider sends one. Index is NOT
|
|
568
|
+
// trustworthy: free models routinely label every parallel call `index: 0`,
|
|
569
|
+
// and merging their fragments produces one unparseable argument blob
|
|
570
|
+
// (observed live: `{"i":"a","op":"done"}{"i":"b","op":"done"}`). Index is
|
|
571
|
+
// only the fallback for providers that omit an id entirely.
|
|
567
572
|
const toolCallsMap = new Map<
|
|
568
|
-
|
|
573
|
+
string,
|
|
569
574
|
{ id: string; type: string; function: { name: string; arguments: string } }
|
|
570
575
|
>();
|
|
576
|
+
const keyByIndex = new Map<number, string>();
|
|
571
577
|
|
|
572
578
|
for (const ev of events) {
|
|
573
579
|
if (ev.data === "[DONE]") continue;
|
|
@@ -591,8 +597,15 @@ export function sseToChatCompletionJson(
|
|
|
591
597
|
}
|
|
592
598
|
if (Array.isArray(delta.tool_calls)) {
|
|
593
599
|
for (const tc of delta.tool_calls) {
|
|
594
|
-
|
|
595
|
-
|
|
600
|
+
// The first delta of a call carries its id; later deltas carry only
|
|
601
|
+
// `arguments`. A later id-less delta therefore belongs to whatever
|
|
602
|
+
// call this index already opened.
|
|
603
|
+
const idx = typeof tc.index === "number" ? tc.index : 0;
|
|
604
|
+
const hasId = typeof tc.id === "string" && tc.id !== "";
|
|
605
|
+
let key = hasId ? `id:${tc.id}` : keyByIndex.get(idx);
|
|
606
|
+
if (key === undefined) key = `idx:${idx}`;
|
|
607
|
+
if (hasId) keyByIndex.set(idx, key);
|
|
608
|
+
const existing = toolCallsMap.get(key) ?? {
|
|
596
609
|
id: tc.id || "",
|
|
597
610
|
type: tc.type || "function",
|
|
598
611
|
function: { name: "", arguments: "" },
|
|
@@ -601,7 +614,7 @@ export function sseToChatCompletionJson(
|
|
|
601
614
|
if (tc.type) existing.type = tc.type;
|
|
602
615
|
if (tc.function?.name) existing.function.name += tc.function.name;
|
|
603
616
|
if (tc.function?.arguments) existing.function.arguments += tc.function.arguments;
|
|
604
|
-
toolCallsMap.set(
|
|
617
|
+
toolCallsMap.set(key, existing);
|
|
605
618
|
}
|
|
606
619
|
}
|
|
607
620
|
}
|
|
@@ -617,9 +630,9 @@ export function sseToChatCompletionJson(
|
|
|
617
630
|
}
|
|
618
631
|
}
|
|
619
632
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
633
|
+
// Map insertion order is emission order, which is what the host expects. The
|
|
634
|
+
// previous numeric sort assumed the keys were indices; they are now ids.
|
|
635
|
+
const toolCalls = Array.from(toolCallsMap.values());
|
|
623
636
|
|
|
624
637
|
const message: Record<string, unknown> = {
|
|
625
638
|
role,
|
package/src/tool-args.ts
CHANGED
|
@@ -342,6 +342,46 @@ export function buildArgRepairSchemas(tools: unknown[]): Map<string, Record<stri
|
|
|
342
342
|
return schemas;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parse a tool-call argument string, salvaging one that arrives with a leading
|
|
348
|
+
* fragment of the *previous* call spliced onto its front.
|
|
349
|
+
*
|
|
350
|
+
* Observed live on space-bunny-free: a model that emitted a call, saw the
|
|
351
|
+
* result, then opened its next call by echoing the tail of the old one. The
|
|
352
|
+
* stream carried a single tool call, so keying fragments by call id cannot
|
|
353
|
+
* separate them, and the host rejected the payload:
|
|
354
|
+
*
|
|
355
|
+
* `,"timeout":400}{"i":"…","command":"…"}`
|
|
356
|
+
* `]}]}{"i":"Planning research phases","op":"init",…}`
|
|
357
|
+
*
|
|
358
|
+
* Both are one complete JSON object with junk in front of it. Only strings that
|
|
359
|
+
* do not parse as-is are considered, and only a position that yields a complete
|
|
360
|
+
* object is accepted, so a well-formed call never reaches this path and a
|
|
361
|
+
* truncated one is still refused rather than completed.
|
|
362
|
+
*
|
|
363
|
+
* Returns undefined when nothing salvageable is present.
|
|
364
|
+
*/
|
|
365
|
+
function parseArguments(argsJson: string): { value: Record<string, unknown>; salvaged: boolean } | undefined {
|
|
366
|
+
try {
|
|
367
|
+
const value = JSON.parse(argsJson);
|
|
368
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
369
|
+
return { value: value as Record<string, unknown>, salvaged: false };
|
|
370
|
+
} catch {
|
|
371
|
+
// fall through to the salvage scan
|
|
372
|
+
}
|
|
373
|
+
for (let i = argsJson.indexOf("{"); i !== -1; i = argsJson.indexOf("{", i + 1)) {
|
|
374
|
+
try {
|
|
375
|
+
const value = JSON.parse(argsJson.slice(i));
|
|
376
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
377
|
+
return { value: value as Record<string, unknown>, salvaged: true };
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
// not a start position; try the next brace
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
345
385
|
/**
|
|
346
386
|
* Repair one tool call's arguments against its declared schema. `name` is the
|
|
347
387
|
* downstream (restored) caller tool name. Returns the input string untouched
|
|
@@ -356,21 +396,20 @@ export function repairToolArguments(
|
|
|
356
396
|
if (schemas === undefined || schemas.size === 0) return argsJson;
|
|
357
397
|
const schema = schemas.get(name.trim().toLowerCase());
|
|
358
398
|
if (schema === undefined) return argsJson;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// Truncated or non-JSON arguments: paper over nothing.
|
|
364
|
-
return argsJson;
|
|
365
|
-
}
|
|
366
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return argsJson;
|
|
367
|
-
const rec = parsed as Record<string, unknown>;
|
|
399
|
+
const parsed = parseArguments(argsJson);
|
|
400
|
+
// Nothing parses: truncated or non-JSON arguments. Paper over nothing.
|
|
401
|
+
if (parsed === undefined) return argsJson;
|
|
402
|
+
const rec = parsed.value;
|
|
368
403
|
const repaired = repairValue(rec, schema, 0);
|
|
369
404
|
const value = repaired.value === null || typeof repaired.value !== "object" || Array.isArray(repaired.value)
|
|
370
405
|
? rec
|
|
371
406
|
: (repaired.value as Record<string, unknown>);
|
|
372
407
|
const promoted = promoteStringArray(value, schema, 0);
|
|
373
408
|
const result = promoted === value ? value : promoted;
|
|
409
|
+
// A salvaged payload needs no further reshaping: what follows the junk is
|
|
410
|
+
// already the caller's own object, and re-serializing it would be a change
|
|
411
|
+
// the repair was never asked to make.
|
|
412
|
+
if (parsed.salvaged) return matchesSchema(rec, schema, 0) ? JSON.stringify(rec) : argsJson;
|
|
374
413
|
if (!repaired.changed && promoted === value) return argsJson;
|
|
375
414
|
if (!matchesSchema(result, schema, 0)) return argsJson;
|
|
376
415
|
return JSON.stringify(result);
|
package/src/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type ThinkingLevel =
|
|
|
16
16
|
|
|
17
17
|
export type ThinkingLevelMap = Partial<Record<ThinkingLevel, string | null>>;
|
|
18
18
|
|
|
19
|
-
export type ModelInputType = "text" | "image";
|
|
19
|
+
export type ModelInputType = "text" | "image" | "video";
|
|
20
20
|
|
|
21
21
|
export interface ModelDef {
|
|
22
22
|
id: string;
|