pi-freeflow 1.40.1 → 1.41.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 +55 -0
- package/README.md +36 -19
- package/package.json +1 -1
- package/src/catalog.ts +33 -21
- package/src/cline-accounts.ts +4 -3
- package/src/config.ts +13 -0
- package/src/models.ts +240 -64
- package/src/opencode-fingerprint.ts +78 -17
- package/src/proxy.ts +339 -12
- package/src/relay.ts +38 -0
- package/src/responses.ts +6 -0
- package/src/stream-pipe.ts +391 -67
- package/src/tool-translation.ts +15 -12
- package/src/toolcall-audit.ts +580 -0
package/src/models.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static model definitions and upstream routing catalogs for pi-freeflow
|
|
3
3
|
*
|
|
4
|
-
* Defines the
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* - 2 Cline direct-only models (per-user pool; one is Responses API)
|
|
4
|
+
* Defines the 37 verified free models (live-verified 2026-10-22):
|
|
5
|
+
* - 12 OpenCode Zen models (all Chat Completions)
|
|
6
|
+
* - 16 KiloCode Keyless Gateway models (OpenRouter format)
|
|
7
|
+
* - 1 Cline direct-only model (per-user pool; z-ai/glm-5.3-flash)
|
|
9
8
|
* - 8 OVHcloud AI Endpoints models (keyless, direct-only)
|
|
9
|
+
*
|
|
10
|
+
* No static entry declares `api: "openai-responses"` any more: the Zen
|
|
11
|
+
* muse-spark contributor ids went first (1.3 on 2026-10-17, 1.2 on 2026-10-18)
|
|
12
|
+
* and Cline's Muse Spark 1.3 entry stopped resolving upstream on 2026-10-22.
|
|
13
|
+
* The responses plumbing stays for stale caches and future entries.
|
|
14
|
+
*
|
|
15
|
+
* Spec policy (2026-10-22): a fixed model declares its highest well-known
|
|
16
|
+
* spec. The canonical vendor sheet, the twin rows and the built-in catalog are
|
|
17
|
+
* compared field by field (context, output, modalities, effort ladder), and
|
|
18
|
+
* derived conventions are ignored (0.9x output caps, output repeated as the
|
|
19
|
+
* context, router pool ceilings, paid-tier serving configs) — a lane's own
|
|
20
|
+
* top_provider 0.9x figure is used only when the id has no published output
|
|
21
|
+
* and no cross-lane twin. A masked or
|
|
22
|
+
* mixed-backend id is the exception: it follows the serving provider's own
|
|
23
|
+
* declared spec, never a guessed backing model and never a live probe. Where a
|
|
24
|
+
* lane enforces less than the declared spec at runtime, the entry keeps the
|
|
25
|
+
* declared figure and notes the lane cap beside it, so a caller above that cap
|
|
26
|
+
* sees the authentic upstream error rather than a silently reduced catalog.
|
|
10
27
|
*/
|
|
11
28
|
|
|
12
29
|
import type { ModelDef, ThinkingLevelMap, Upstream } from "./types.ts";
|
|
@@ -243,41 +260,6 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
243
260
|
max: "max",
|
|
244
261
|
},
|
|
245
262
|
},
|
|
246
|
-
{
|
|
247
|
-
// Added 2026-10-16 (recheck): live free id on the Zen lane, listed on the
|
|
248
|
-
// official free lineup while models.dev already tags it deprecated — the
|
|
249
|
-
// tag also sits on live ids and on already-dead ones (mimo-v2.5-free), so
|
|
250
|
-
// the live answer decides: chat 200 with tools + stream, and the tool call
|
|
251
|
-
// round-trips. Chat is the only
|
|
252
|
-
// native shape (/v1/responses answers 400 "Model does not support this
|
|
253
|
-
// protocol"), so `api` stays unset. Reasoning: the published ladder lists
|
|
254
|
-
// `high` only; high streams thinking and the unavailable rungs stay null
|
|
255
|
-
// so the picker never offers them. Vision verified live (a 1x1 PNG reads
|
|
256
|
-
// back as "Pink"). Context: published 1,048,576; a dual-canary prompt at
|
|
257
|
-
// ~400K words (~520K tokens, canaries at ~30%/~97% depth) recalls both
|
|
258
|
-
// markers, and a 1.2M-word prompt (~1.56M tokens) draws an empty-bodied
|
|
259
|
-
// 400 — the wall sits between, matching the published figure. maxTokens
|
|
260
|
-
// 131,072 published; 131,073 accepted (acceptance is not a cap, so the
|
|
261
|
-
// published figure stays the budget).
|
|
262
|
-
id: "exo-free",
|
|
263
|
-
name: "Exo [OpenCode]",
|
|
264
|
-
reasoning: true,
|
|
265
|
-
contextWindow: 1_048_576,
|
|
266
|
-
maxTokens: 131_072,
|
|
267
|
-
input: ["text", "image"],
|
|
268
|
-
// The free datasheet lists only `high`, but the paid twin publishes
|
|
269
|
-
// minimal..xhigh, so the shared-model union exposes those rungs; `max` is
|
|
270
|
-
// not published by either and stays null.
|
|
271
|
-
thinkingLevelMap: {
|
|
272
|
-
off: null,
|
|
273
|
-
minimal: "minimal",
|
|
274
|
-
low: "low",
|
|
275
|
-
medium: "medium",
|
|
276
|
-
high: "high",
|
|
277
|
-
xhigh: "xhigh",
|
|
278
|
-
max: null,
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
263
|
{
|
|
282
264
|
// Added 2026-10-15: new free model on the OpenCode Zen lane. Live-verified by
|
|
283
265
|
// probe, NOT copied from the published catalog — the entry for this exact
|
|
@@ -316,6 +298,82 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
316
298
|
max: "max",
|
|
317
299
|
},
|
|
318
300
|
},
|
|
301
|
+
{
|
|
302
|
+
// Added 2026-10-22: new free model on the OpenCode Zen lane, live-probed
|
|
303
|
+
// against the gateway rather than copied from a published row.
|
|
304
|
+
// Availability: 5/5 probes answered 200 through the fingerprint path
|
|
305
|
+
// (chat + stream + tool call), so this is a stable listing.
|
|
306
|
+
// Context: 1,000,000 measured — a needle planted at prompt_tokens 991,641
|
|
307
|
+
// is recalled, and a request above the window is refused. That makes the
|
|
308
|
+
// declared figure the lane's own 1M cap, not a published twin value.
|
|
309
|
+
// maxTokens: the rejection boundary is 524,288 (600,000 is refused with
|
|
310
|
+
// "does not support max tokens > 524288"), so the exact served ceiling is
|
|
311
|
+
// the budget. models.dev publishes the rounded 512,000; acceptance is not a
|
|
312
|
+
// cap, so the boundary wins.
|
|
313
|
+
// Vision verified live. Effort ladder probed level-by-level: low, medium,
|
|
314
|
+
// high, xhigh and max each answer 200 with reasoning deltas. `off` answers
|
|
315
|
+
// 400 and `minimal` is accepted but absent from the published ladder, so
|
|
316
|
+
// both stay null and the picker never offers them. Chat is the native
|
|
317
|
+
// shape, so `api` stays unset.
|
|
318
|
+
// Known caveat, measured on trivial prompts at high effort: the model
|
|
319
|
+
// sometimes calls the injected fingerprint placeholder tool, which the
|
|
320
|
+
// cloak strips — the host then sees a tool_calls finish with no visible
|
|
321
|
+
// calls. The turn still completes; nothing is dropped from a real call.
|
|
322
|
+
id: "minimax-m3.1-flash-free",
|
|
323
|
+
name: "MiniMax M3.1 Flash [OpenCode]",
|
|
324
|
+
reasoning: true,
|
|
325
|
+
contextWindow: 1_000_000,
|
|
326
|
+
maxTokens: 524_288,
|
|
327
|
+
input: ["text", "image"],
|
|
328
|
+
thinkingLevelMap: {
|
|
329
|
+
off: null,
|
|
330
|
+
minimal: null,
|
|
331
|
+
low: "low",
|
|
332
|
+
medium: "medium",
|
|
333
|
+
high: "high",
|
|
334
|
+
xhigh: "xhigh",
|
|
335
|
+
max: "max",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
// Added 2026-10-22. Availability on this lane is bursty: the same day it
|
|
340
|
+
// answered 1 of 19 probes across ~25 minutes and later 8 of 8 over 43
|
|
341
|
+
// seconds through an owned proxy. A 503 "Endpoint is unavailable" burst is
|
|
342
|
+
// retryable, not a death signal — never prune on it.
|
|
343
|
+
// Context: 262,144 measured. The gateway refuses a 323,580-token input with
|
|
344
|
+
// "longer than the model's context length (262144)", and a needle planted
|
|
345
|
+
// at prompt_tokens 129,736 is recalled. One near-window sample at 258,970
|
|
346
|
+
// missed once, so the top of the window is a recall boundary, not a
|
|
347
|
+
// truncation boundary.
|
|
348
|
+
// maxTokens: 65,536 published (models.dev /opencode). Acceptance is not a
|
|
349
|
+
// cap — 65,536 and 70,000 both answer 200 — so the published figure stays
|
|
350
|
+
// the budget.
|
|
351
|
+
// Effort ladder: low/medium/high/xhigh/max all answer 200 with reasoning.
|
|
352
|
+
// `off` and a bogus value answer 422 with the generic "Endpoint is
|
|
353
|
+
// unavailable" body, and `minimal` is accepted but absent from the
|
|
354
|
+
// published ladder, so all three stay null.
|
|
355
|
+
// Verified natively on BOTH shapes: chat stream + tool call + agent loop,
|
|
356
|
+
// and /v1/responses with bare-string and array input. /v1/messages is
|
|
357
|
+
// refused 400 ModelProtocolUnsupported. `api` stays unset (chat is the
|
|
358
|
+
// default) so the responses path keeps its native mapping. The
|
|
359
|
+
// /v1/responses + tools path was not reachable during the outage window,
|
|
360
|
+
// so that specific combination is unverified.
|
|
361
|
+
id: "flint-40b-free",
|
|
362
|
+
name: "Flint 40B [OpenCode]",
|
|
363
|
+
reasoning: true,
|
|
364
|
+
contextWindow: 262_144,
|
|
365
|
+
maxTokens: 65_536,
|
|
366
|
+
input: ["text"],
|
|
367
|
+
thinkingLevelMap: {
|
|
368
|
+
off: null,
|
|
369
|
+
minimal: null,
|
|
370
|
+
low: "low",
|
|
371
|
+
medium: "medium",
|
|
372
|
+
high: "high",
|
|
373
|
+
xhigh: "xhigh",
|
|
374
|
+
max: "max",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
319
377
|
];
|
|
320
378
|
|
|
321
379
|
/**
|
|
@@ -469,13 +527,138 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
469
527
|
thinkingFormat: "openrouter",
|
|
470
528
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
471
529
|
},
|
|
530
|
+
{
|
|
531
|
+
// Added 2026-10-22: exact gateway id, tool call + agent loop verified on
|
|
532
|
+
// both the chat and /v1/responses paths. 1,000,000 / 524,288 is the
|
|
533
|
+
// gateway `top_provider` pair, and the Zen twin (minimax-m3.1-flash-free)
|
|
534
|
+
// measured the same 524,288 output boundary — two lanes, one figure, so the
|
|
535
|
+
// twin reading is corroboration rather than a copy. Conservative twin note:
|
|
536
|
+
// both entries declare the served boundary; the rounded 512,000 some
|
|
537
|
+
// datasheets publish is not what either lane enforces.
|
|
538
|
+
// Reasoning is mandatory upstream: `none` and `off` answer 400 "Reasoning
|
|
539
|
+
// is mandatory", which is why both rungs stay null. When tools are present
|
|
540
|
+
// no reasoning deltas are reported at all (silent-thinking tool turns), so
|
|
541
|
+
// the map's level labels describe the effort sent, not visible thought.
|
|
542
|
+
id: "minimax/minimax-m3.1-flash:free",
|
|
543
|
+
name: "MiniMax M3.1 Flash [Kilo]",
|
|
544
|
+
reasoning: true,
|
|
545
|
+
contextWindow: 1_000_000,
|
|
546
|
+
maxTokens: 524_288,
|
|
547
|
+
input: ["text", "image"],
|
|
548
|
+
thinkingFormat: "openrouter",
|
|
549
|
+
thinkingLevelMap: {
|
|
550
|
+
off: null,
|
|
551
|
+
minimal: null,
|
|
552
|
+
low: "low",
|
|
553
|
+
medium: "medium",
|
|
554
|
+
high: "high",
|
|
555
|
+
xhigh: "xhigh",
|
|
556
|
+
max: "max",
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
// Added 2026-10-22: exact gateway id, tool call + agent loop verified on
|
|
561
|
+
// both the chat and /v1/responses paths.
|
|
562
|
+
// 1,000,000 / 131,072 is the FREE row, and it agrees everywhere it is
|
|
563
|
+
// published: the Kilo free listing, the gateway `top_provider`, the Kilo
|
|
564
|
+
// model page, OpenRouter, and models.dev's free row. The 262,144 / 235,929
|
|
565
|
+
// pair belongs to the PAID row behind a different id — never copy it here.
|
|
566
|
+
// Thinking: the gateway's reasoning_options expose a type toggle; every
|
|
567
|
+
// effort value answers 200 but the deltas are cosmetic, so the shared Kilo
|
|
568
|
+
// map is the honest declaration.
|
|
569
|
+
// Data caveat: NVIDIA's trial terms set mayTrainOnYourPrompts true for this
|
|
570
|
+
// free id, so treat prompts as non-private.
|
|
571
|
+
// Reliability: `max` can draw a transient 502/503 upstream; it is
|
|
572
|
+
// retryable and does not indicate model death.
|
|
573
|
+
id: "nvidia/nemotron-3.5-super-vl:free",
|
|
574
|
+
name: "Nemotron 3.5 Super VL [Kilo]",
|
|
575
|
+
reasoning: true,
|
|
576
|
+
contextWindow: 1_000_000,
|
|
577
|
+
maxTokens: 131_072,
|
|
578
|
+
input: ["text", "image"],
|
|
579
|
+
thinkingFormat: "openrouter",
|
|
580
|
+
thinkingLevelMap: KILO_REASONING_MAP,
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
// Added 2026-10-22: this is the Kilo twin of the Zen `beam-free` entry
|
|
584
|
+
// (same Reflection AI weights). 1,048,576 / 131,072 agrees across the
|
|
585
|
+
// gateway, models.dev (kilo, vercel and opencode rows) and the vendor docs,
|
|
586
|
+
// so no reconciliation was needed. Tool call + agent loop verified on chat
|
|
587
|
+
// and /v1/responses.
|
|
588
|
+
// Always-on reasoning: the model thinks even with `none`/`off`, so the
|
|
589
|
+
// toggle cannot disable it and only the effort ladder is exposed. `minimal`
|
|
590
|
+
// is accepted but ignored upstream, hence null. `max` is a real level.
|
|
591
|
+
// Upstream echoes the bare model id `reflection/beam` without a provider
|
|
592
|
+
// field; the catalog keeps the `reflection/beam-free` listing id here so
|
|
593
|
+
// alongside `beam-kilo` routing stays unambiguous against the Zen `beam`.
|
|
594
|
+
id: "reflection/beam-free",
|
|
595
|
+
name: "Beam 1.0 [Kilo]",
|
|
596
|
+
reasoning: true,
|
|
597
|
+
contextWindow: 1_048_576,
|
|
598
|
+
maxTokens: 131_072,
|
|
599
|
+
input: ["text"],
|
|
600
|
+
thinkingFormat: "openrouter",
|
|
601
|
+
thinkingLevelMap: {
|
|
602
|
+
off: null,
|
|
603
|
+
minimal: null,
|
|
604
|
+
low: "low",
|
|
605
|
+
medium: "medium",
|
|
606
|
+
high: "high",
|
|
607
|
+
xhigh: "xhigh",
|
|
608
|
+
max: "max",
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
// Added 2026-10-22: exact gateway id, tool call + agent loop verified on
|
|
613
|
+
// both the chat and /v1/responses paths. Same weights as the Zen
|
|
614
|
+
// `flint-40b-free` entry, which publishes 65,536 output for them, so the
|
|
615
|
+
// declared output is the conservative published figure.
|
|
616
|
+
// Context: the vendor card says prompt and output SHARE the 262,144 window.
|
|
617
|
+
// The gateway reports 235,929 for this id, which is its 0.9x-window
|
|
618
|
+
// convention rather than a measured cap; the shared published window is the
|
|
619
|
+
// declared value.
|
|
620
|
+
// Thinking is mandatory: `none` answers 400, so `off` stays null. Reasoning
|
|
621
|
+
// is verbose — on a tight max_tokens budget the thinking can consume the
|
|
622
|
+
// whole allowance and end the turn with empty content, which is a budget
|
|
623
|
+
// symptom, not a dead model.
|
|
624
|
+
id: "recursive/flint-v1.0:free",
|
|
625
|
+
name: "Flint 40B [Kilo]",
|
|
626
|
+
reasoning: true,
|
|
627
|
+
contextWindow: 262_144,
|
|
628
|
+
maxTokens: 65_536,
|
|
629
|
+
input: ["text"],
|
|
630
|
+
thinkingFormat: "openrouter",
|
|
631
|
+
thinkingLevelMap: KILO_REASONING_MAP,
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
// Added 2026-10-22: exact gateway id, tool call + agent loop verified on
|
|
635
|
+
// both the chat and /v1/responses paths. 262,144 / 235,929 is the pair this
|
|
636
|
+
// lane publishes through the gateway `top_provider`, and no cross-lane cap
|
|
637
|
+
// exists for the id — there is no twin elsewhere to reconcile against.
|
|
638
|
+
// Both figures follow the same 0.9x-window convention: prompt and output
|
|
639
|
+
// share the 262,144 window, so 235,929 is the output budget that still
|
|
640
|
+
// leaves prompt headroom.
|
|
641
|
+
// `none` disables thinking and answers 200, but like `off` everywhere else
|
|
642
|
+
// it maps nowhere here, so the shared Kilo ladder is declared.
|
|
643
|
+
// Reliability: roughly 60 percent of attempts hit the shared Phala 429
|
|
644
|
+
// quota. A 429 means the model is alive and rate-limited — never prune on
|
|
645
|
+
// it; the same id answers 200 once the shared pool frees up.
|
|
646
|
+
id: "xingchen-agi/xing4.0-29b-a4b:free",
|
|
647
|
+
name: "Xing4.0 29B A4B [Kilo]",
|
|
648
|
+
reasoning: true,
|
|
649
|
+
contextWindow: 262_144,
|
|
650
|
+
maxTokens: 235_929,
|
|
651
|
+
input: ["text"],
|
|
652
|
+
thinkingFormat: "openrouter",
|
|
653
|
+
thinkingLevelMap: KILO_REASONING_MAP,
|
|
654
|
+
},
|
|
472
655
|
];
|
|
473
656
|
|
|
474
657
|
/**
|
|
475
|
-
* Narrow effort map for the Cline
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
658
|
+
* Narrow effort map for the Cline model in the catalog (GLM 5.3 Flash) — the
|
|
659
|
+
* reference reasoningOptions list effort values [low, high, max] only (no
|
|
660
|
+
* toggle on GLM); minimal/medium/xhigh are not offered so the host never
|
|
661
|
+
* sends an unsupported level (the proxy forwards effort verbatim).
|
|
479
662
|
*/
|
|
480
663
|
const CLINE_FLASH_REASONING_MAP: ThinkingLevelMap = {
|
|
481
664
|
off: null,
|
|
@@ -493,26 +676,13 @@ const CLINE_FLASH_REASONING_MAP: ThinkingLevelMap = {
|
|
|
493
676
|
* and collision-free against the OpenCode/Kilo catalogs (the Kilo-routed
|
|
494
677
|
* poolside/laguna-s-2.1:free is intentionally NOT duplicated here).
|
|
495
678
|
* Endpoint: https://api.cline.bot/api/v1/chat/completions
|
|
679
|
+
*
|
|
680
|
+
* `cline-free/muse-spark-1.3-contributor` was the last entry declaring
|
|
681
|
+
* `api: "openai-responses"`; it stopped resolving upstream on 2026-10-22
|
|
682
|
+
* (Cline answers 404 "Cline has no such model"), so the catalog now carries
|
|
683
|
+
* chat-completions models only on this lane.
|
|
496
684
|
*/
|
|
497
685
|
export const CLINE_MODELS: ModelDef[] = [
|
|
498
|
-
{
|
|
499
|
-
id: "cline-free/muse-spark-1.3-contributor",
|
|
500
|
-
name: "Muse Spark 1.3 [Cline]",
|
|
501
|
-
reasoning: true,
|
|
502
|
-
contextWindow: 1_048_576,
|
|
503
|
-
maxTokens: 131_072,
|
|
504
|
-
api: "openai-responses",
|
|
505
|
-
input: ["text", "image"],
|
|
506
|
-
thinkingLevelMap: {
|
|
507
|
-
off: null,
|
|
508
|
-
minimal: "minimal",
|
|
509
|
-
low: "low",
|
|
510
|
-
medium: "medium",
|
|
511
|
-
high: "high",
|
|
512
|
-
xhigh: "xhigh",
|
|
513
|
-
max: null,
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
686
|
{
|
|
517
687
|
id: "z-ai/glm-5.3-flash",
|
|
518
688
|
name: "GLM 5.3 Flash [Cline]",
|
|
@@ -697,7 +867,6 @@ export const MODEL_ALIASES: Record<string, string> = {
|
|
|
697
867
|
"north-mini-code": "cohere/north-mini-code:free",
|
|
698
868
|
"lfm-2.5": "liquid/lfm-2.5-2.6b:free",
|
|
699
869
|
"glm-5.3-flash": "glm-5.3-flash-free",
|
|
700
|
-
"exo": "exo-free",
|
|
701
870
|
"glyph-cluster": "stealth/glyph-cluster",
|
|
702
871
|
"mimo-v2.6-flash": "mimo-v2.6-flash-free",
|
|
703
872
|
"nemotron-3.5-super-vl": "nemotron-3.5-super-vl-free",
|
|
@@ -705,6 +874,13 @@ export const MODEL_ALIASES: Record<string, string> = {
|
|
|
705
874
|
"longcat": "longcat-2.5-preview-free",
|
|
706
875
|
"beam": "beam-free",
|
|
707
876
|
"deepseek-v4.1-flash": "deepseek-v4.1-flash-free",
|
|
877
|
+
// Added 2026-10-22. "flint" stays on the Zen lane; the Kilo twin gets its
|
|
878
|
+
// own name so neither entry can shadow the other.
|
|
879
|
+
"minimax-m3.1-flash": "minimax-m3.1-flash-free",
|
|
880
|
+
"flint": "flint-40b-free",
|
|
881
|
+
"flint-kilo": "recursive/flint-v1.0:free",
|
|
882
|
+
"beam-kilo": "reflection/beam-free",
|
|
883
|
+
"xing4.0": "xingchen-agi/xing4.0-29b-a4b:free",
|
|
708
884
|
// provider-prefixed short aliases (slash-normalized)
|
|
709
885
|
"laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
|
|
710
886
|
"laguna-xs-2.1:free": "poolside/laguna-xs-2.1:free",
|
|
@@ -758,7 +934,7 @@ export const OVH_MODEL_IDS = new Set<string>([
|
|
|
758
934
|
.map(([alias]) => alias),
|
|
759
935
|
]);
|
|
760
936
|
/**
|
|
761
|
-
* Combined list of all
|
|
937
|
+
* Combined list of all 37 static free models (canonical)
|
|
762
938
|
*/
|
|
763
939
|
export const ALL_MODELS: ModelDef[] = [...OPENCODE_MODELS, ...KILO_MODELS, ...CLINE_MODELS, ...OVH_MODELS];
|
|
764
940
|
|
|
@@ -109,9 +109,9 @@ export function isPlaceholderToolName(name: unknown): boolean {
|
|
|
109
109
|
* Fill for one missing fingerprint slot: the shared OMP real definition when
|
|
110
110
|
* the caller is OMP-like, otherwise the empty COMPAT placeholder Pi keeps.
|
|
111
111
|
* `edit` is always COMPAT, even for OMP-like callers: no single static edit
|
|
112
|
-
*
|
|
113
|
-
* edits[]), so it must stay in the cloaked `injected` list
|
|
114
|
-
*
|
|
112
|
+
* executes on either host (OMP is edit-mode dependent, Pi wants
|
|
113
|
+
* edits[]), so it must stay in the cloaked `injected` list even when the
|
|
114
|
+
* caller would otherwise receive real definitions. Single lockstep source for the three wire-shape ensure
|
|
115
115
|
* functions below.
|
|
116
116
|
*/
|
|
117
117
|
function fingerprintFill(name: FingerprintToolName, omp: boolean): { description: string; schema: Record<string, unknown> } {
|
|
@@ -364,6 +364,30 @@ function restoreNativeEffortAfterChoiceDrop(body: Record<string, unknown>): void
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Highest non-null level a model's declared map supports, or null when the
|
|
369
|
+
* model declares no map or only supports `off` (nothing to fall back to).
|
|
370
|
+
*/
|
|
371
|
+
/**
|
|
372
|
+
* Chat-lane mirror of the responses effort clamp (normalizeResponsesBody):
|
|
373
|
+
* a host targeting the chat path spells the effort flat — `reasoning_effort`
|
|
374
|
+
* or `reasoningEffort` — and the same clampedEffortFor algorithm applies.
|
|
375
|
+
* A level the model's map declares unsupported is clamped to the highest
|
|
376
|
+
* supported one; a supported level or a key the map does not know is left
|
|
377
|
+
* for upstream to rule on; genuine `off` is never rewritten. The Zen
|
|
378
|
+
* messages lane carries no flat effort spelling, so it is not touched.
|
|
379
|
+
*/
|
|
380
|
+
function clampChatReasoningEffort(body: Record<string, unknown>): void {
|
|
381
|
+
const model = body.model;
|
|
382
|
+
if (typeof model !== "string") return;
|
|
383
|
+
for (const key of ["reasoning_effort", "reasoningEffort"] as const) {
|
|
384
|
+
const flat = body[key];
|
|
385
|
+
if (typeof flat !== "string") continue;
|
|
386
|
+
const clamped = clampedEffortFor(model, flat);
|
|
387
|
+
if (clamped !== null) body[key] = clamped;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
367
391
|
/**
|
|
368
392
|
* Highest non-null level a model's declared map supports, or null when the
|
|
369
393
|
* model declares no map or only supports `off` (nothing to fall back to).
|
|
@@ -477,10 +501,11 @@ export function normalizeResponsesBody(body: Record<string, unknown>): void {
|
|
|
477
501
|
* no-ops for Pi. `edit` is always the cloaked placeholder (no static edit
|
|
478
502
|
* schema executes on either host). Returns the original stream flag,
|
|
479
503
|
* caller-tools flag, whether injection added tools, and the bounded
|
|
480
|
-
* per-request restore records (caseRestore, findGlob, injected
|
|
481
|
-
* for downstream cloaking. Cloaking strips by `injected` only:
|
|
482
|
-
*
|
|
483
|
-
*
|
|
504
|
+
* per-request restore records (caseRestore, findGlob, injected)
|
|
505
|
+
* for downstream cloaking. Cloaking strips by `injected` only: any missing
|
|
506
|
+
* slot the caller received with an executable definition is not a
|
|
507
|
+
* placeholder, so model calls to it execute downstream instead of being
|
|
508
|
+
* cloaked.
|
|
484
509
|
*
|
|
485
510
|
* `argSchemas` (lowercased caller tool name -> declared parameters) lets
|
|
486
511
|
* downstream repair malformed arguments against the caller's own schema; it
|
|
@@ -497,7 +522,6 @@ export function enforceOpencodeFingerprint(
|
|
|
497
522
|
caseRestore: CaseRestoreMap;
|
|
498
523
|
findGlob: FindGlobRestore;
|
|
499
524
|
injected: string[];
|
|
500
|
-
injectedReal: string[];
|
|
501
525
|
argSchemas: Map<string, Record<string, unknown>>;
|
|
502
526
|
} {
|
|
503
527
|
const clientRequestedStream = body.stream === true;
|
|
@@ -505,10 +529,12 @@ export function enforceOpencodeFingerprint(
|
|
|
505
529
|
// Zen responses shape conformance first (Chat leftovers, tool_choice, spark
|
|
506
530
|
// effort clamp, store default): Kilo/Cline bodies never reach here.
|
|
507
531
|
if (pathname.endsWith("/responses")) normalizeResponsesBody(body);
|
|
532
|
+
else if (pathname.endsWith("/chat/completions")) clampChatReasoningEffort(body);
|
|
508
533
|
// Chat path mirrors the responses drop: Zen free tier accepts only auto, so a
|
|
509
|
-
// caller "none" (string or { type: "none" }) would 400 upstream.
|
|
510
|
-
//
|
|
511
|
-
|
|
534
|
+
// caller "none" (string or { type: "none" }) would 400 upstream. The Zen
|
|
535
|
+
// messages lane keeps its own choice semantics: Anthropic names choices with
|
|
536
|
+
// `any` / `{ type: "tool", name: ... }`, and the drop would delete them.
|
|
537
|
+
if (!pathname.endsWith("/messages")) enforceAutoToolChoice(body);
|
|
512
538
|
// Upstream Zen free tier mandates stream: true for all free requests
|
|
513
539
|
body.stream = true;
|
|
514
540
|
|
|
@@ -541,7 +567,6 @@ export function enforceOpencodeFingerprint(
|
|
|
541
567
|
// executable when injected); real definitions survive downstream so
|
|
542
568
|
// injected OMP calls execute.
|
|
543
569
|
const injected = missing.filter((n) => !ompLike || n === "edit");
|
|
544
|
-
const injectedReal = ompLike ? missing.filter((n) => n !== "edit") : [];
|
|
545
570
|
|
|
546
571
|
// Zen alone asks for a rename: its free-tier gate looks for the sextet name
|
|
547
572
|
// `glob`, so a Pi caller that sent `find` needs it renamed into that slot.
|
|
@@ -561,13 +586,15 @@ export function enforceOpencodeFingerprint(
|
|
|
561
586
|
const after = Array.isArray(body.tools) ? body.tools.length : 0;
|
|
562
587
|
|
|
563
588
|
// Note: Upstream OpenCode Zen explicitly rejects any tool_choice other than "auto"
|
|
564
|
-
// with HTTP 400 (only "auto" is supported). tool_choice is auto-or-absent
|
|
565
|
-
//
|
|
566
|
-
//
|
|
589
|
+
// with HTTP 400 (only "auto" is supported). tool_choice is auto-or-absent on the
|
|
590
|
+
// responses and chat paths (normalizeResponsesBody / enforceAutoToolChoice above),
|
|
591
|
+
// the Zen messages lane keeps the caller's own choice semantics (Anthropic
|
|
592
|
+
// spells choices with `any` / `{ type: "tool", name: ... }`), and we never
|
|
593
|
+
// impose a choice.
|
|
567
594
|
// Empty-placeholder callers stay silent via the COMPAT description and the
|
|
568
595
|
// output-aggregator; real OMP definitions execute normally.
|
|
569
596
|
|
|
570
|
-
return { clientRequestedStream, callerHadTools, addedTools: after > before, caseRestore, findGlob, injected,
|
|
597
|
+
return { clientRequestedStream, callerHadTools, addedTools: after > before, caseRestore, findGlob, injected, argSchemas };
|
|
571
598
|
}
|
|
572
599
|
|
|
573
600
|
/**
|
|
@@ -685,6 +712,32 @@ function cloakChatMessage(
|
|
|
685
712
|
else delete m.tool_calls;
|
|
686
713
|
}
|
|
687
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Fold one streamed `function.name` delta into the accumulated tool name.
|
|
717
|
+
*
|
|
718
|
+
* Providers stream names in two legitimate shapes, and the difference is the
|
|
719
|
+
* presence of arguments on the same delta:
|
|
720
|
+
*
|
|
721
|
+
* - **Fragment** (`name`, no `arguments`): the ordinary piecewise stream — the
|
|
722
|
+
* delta carries the next slice of the name, so append it.
|
|
723
|
+
* - **Repeat header** (`name` AND `arguments`): OpenCode Zen restates the
|
|
724
|
+
* WHOLE name on the frame that opens the arguments (observed live
|
|
725
|
+
* 2026-10-22: `name:"get_weather"` alone, then `name:"get_weather",
|
|
726
|
+
* arguments:"{…}"`). The name there is authoritative, so replace instead of
|
|
727
|
+
* appending — appending produced `get_weatherget_weather`, a tool name no
|
|
728
|
+
* tool declaration matches. A full mid-stream restatement is the same shape.
|
|
729
|
+
*
|
|
730
|
+
* Shared by both aggregators (the non-streamed completion cloak and the
|
|
731
|
+
* streaming cloak) so streamed and aggregated output always agree.
|
|
732
|
+
*/
|
|
733
|
+
export function accumulateToolCallName(
|
|
734
|
+
previous: string | undefined,
|
|
735
|
+
delta: string,
|
|
736
|
+
carriesArguments: boolean,
|
|
737
|
+
): string {
|
|
738
|
+
return carriesArguments ? delta : `${previous ?? ""}${delta}`;
|
|
739
|
+
}
|
|
740
|
+
|
|
688
741
|
export function sseToChatCompletionJson(
|
|
689
742
|
sseText: string,
|
|
690
743
|
callerHadTools = true,
|
|
@@ -750,7 +803,15 @@ export function sseToChatCompletionJson(
|
|
|
750
803
|
};
|
|
751
804
|
if (tc.id) existing.id = tc.id;
|
|
752
805
|
if (tc.type) existing.type = tc.type;
|
|
753
|
-
if (tc.function?.name)
|
|
806
|
+
if (tc.function?.name) {
|
|
807
|
+
// See accumulateToolCallName: a name on an arguments-carrying delta
|
|
808
|
+
// is the whole name restated, not another fragment.
|
|
809
|
+
existing.function.name = accumulateToolCallName(
|
|
810
|
+
existing.function.name,
|
|
811
|
+
tc.function.name,
|
|
812
|
+
typeof tc.function.arguments === "string" && tc.function.arguments !== "",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
754
815
|
if (tc.function?.arguments) existing.function.arguments += tc.function.arguments;
|
|
755
816
|
toolCallsMap.set(key, existing);
|
|
756
817
|
}
|