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/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 32 verified free models (live-verified 2026-10-21):
5
- * - 11 OpenCode Zen models (all Chat Completions — the last Responses model,
6
- * muse-spark-1.2-contributor-free, was deprecated upstream 2026-10-18)
7
- * - 11 KiloCode Keyless Gateway models (OpenRouter format)
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 flash models still in the catalog (GLM 5.3
476
- * Flash) — the reference reasoningOptions list effort values [low, high, max]
477
- * only (no toggle on GLM); minimal/medium/xhigh are not offered so the host
478
- * never sends an unsupported level (the proxy forwards effort verbatim).
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 34 static free models (canonical)
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
- * schema executes on either host (OMP is edit-mode dependent, Pi wants
113
- * edits[]), so it must stay in the cloaked `injected` list, never
114
- * `injectedReal`. Single lockstep source for the three wire-shape ensure
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, injectedReal)
481
- * for downstream cloaking. Cloaking strips by `injected` only: names in
482
- * `injectedReal` were served with executable definitions, so model calls to
483
- * them execute downstream instead of being cloaked.
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. Messages
510
- // path keeps its own choice semantics.
511
- enforceAutoToolChoice(body);
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 here:
565
- // normalizeResponsesBody drops "none" on the responses path, the chat branch above
566
- // drops it on the chat path, and we never impose a choice.
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, injectedReal, argSchemas };
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) existing.function.name += 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
  }