pi-fireworks-provider 1.2.0 → 1.4.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.
Files changed (4) hide show
  1. package/README.md +24 -1
  2. package/index.ts +500 -3
  3. package/package.json +1 -1
  4. package/patch.json +43 -90
package/README.md CHANGED
@@ -19,7 +19,8 @@ _Kimi, MiniMax, GLM, DeepSeek, GPT-OSS — via Fireworks AI's Anthropic Messages
19
19
  - **Dual API support** via Fireworks AI's Anthropic Messages and OpenAI-compatible completions endpoints (per-model routing, matching pi core's Fireworks provider)
20
20
  - **Service tiers** — toggle Fireworks `priority` vs `standard` per request on supported models (with priority pricing reflected in cost tracking), via a keybinding, `/fireworks-tier`, and a footer status area
21
21
  - **Preserved thinking** — toggle Fireworks' `reasoning_history: "preserved"` so prior assistant reasoning is retained across turns (better multi-turn recall; uses more tokens), via the `/fireworks-settings` panel, with a model-select notification. Matches neuralwatt/makora's settings-only UX, adapted to Fireworks' single global `reasoning_history` knob
22
- - **Settings panel** — `/fireworks-settings` (TUI) to configure preserved thinking, service tier, and display preferences; persisted to `~/.pi/agent/extensions/fireworks.json`
22
+ - **Logit bias** — set an OpenAI-style `logit_bias` map (token ID → -100..100) via a nested `/fireworks-settings` panel (add / edit / delete arbitrary token IDs), sent on every Fireworks OpenAI-completions request; persisted to `~/.pi/agent/extensions/fireworks.json`
23
+ - **Settings panel** — `/fireworks-settings` (TUI) to configure preserved thinking, logit bias, service tier, and display preferences; persisted to `~/.pi/agent/extensions/fireworks.json`
23
24
  - **Cost Tracking** with per-model pricing for budget management
24
25
  - **Reasoning Models** support for advanced reasoning capabilities
25
26
  - **Vision Support** for image-capable models
@@ -149,6 +150,10 @@ The selection is persisted per session (survives `/reload` and resume). When `pr
149
150
  },
150
151
  "preserveThinking": {
151
152
  "default": false
153
+ },
154
+ "logitBias": {
155
+ "enabled": false,
156
+ "biases": {}
152
157
  }
153
158
  }
154
159
  ```
@@ -157,6 +162,8 @@ The selection is persisted per session (survives `/reload` and resume). When `pr
157
162
  - `serviceTier.keybinding` — any [pi key format](https://github.com/earendil-works/pi-coding-agent/blob/main/docs/keybindings.md) (e.g. `ctrl+shift+l`, `ctrl+shift+k`). Requires `/reload` after changing. On macOS browser terminals (localterm), avoid `alt`/`ctrl+alt` (Option produces special chars) and `ctrl+shift+t/w/n/c/v` (browser/localterm tab + copy/paste shortcuts).
158
163
  - `serviceTier.display` — `statusbar` (footer status area) or `off` (hide the tier indicator).
159
164
  - `preserveThinking.default` — whether `reasoning_history: "preserved"` is injected (`true` | `false`, default `false`). Also settable via `/fireworks-settings`.
165
+ - `logitBias.enabled` — whether the `logit_bias` map is sent on OpenAI-completions requests (`true` | `false`, default `false`). Also settable via `/fireworks-settings`.
166
+ - `logitBias.biases` — a map of token ID (string) → bias (integer -100..100), e.g. `{"123": 5, "456": -100}`. Editable via the nested `/fireworks-settings` → *Logit bias* panel. -100 effectively bans the token; other values shift its logit additively before sampling.
160
167
 
161
168
  > **Note:** The OpenAI completions endpoint accepts `service_tier` directly (per Fireworks' API). The Anthropic Messages endpoint passes the top-level field through as an extra. If a supported Anthropic-routed model rejects it, file an issue so we can gate injection by API.
162
169
 
@@ -175,6 +182,22 @@ Unlike neuralwatt/makora (which use per-model vLLM `chat_template_kwargs` flags
175
182
 
176
183
  Preserved thinking is **off by default** to match pi core and Fireworks' default (stripped). There is intentionally no `/fireworks-preserve` command or keybinding — it's settings-panel-only, mirroring neuralwatt/makora.
177
184
 
185
+ ## Logit Bias
186
+
187
+ Fireworks' OpenAI-compatible completions endpoint accepts the standard OpenAI `logit_bias` parameter: a map of token ID → bias (integer -100..100), added to the token's logit before sampling. `-100` effectively bans the token; other values shift its likelihood up or down. See [the Fireworks chat-completions API reference](https://docs.fireworks.ai/api-reference/post-chatcompletions).
188
+
189
+ This extension forwards the map verbatim as the top-level `logit_bias` field on every Fireworks OpenAI-completions request when `logitBias.enabled` is `true` and the map is non-empty.
190
+
191
+ **Important:** token IDs are **tokenizer-specific**. A token ID that biases one model correctly may be wrong for another (different vocabularies / tokenizers). You must compute token IDs against the exact tokenizer of the model you're calling — Fireworks uses each model's native tokenizer (Llama, Mistral, etc.). There's no per-tokenizer validation here; biasing a wrong ID silently does nothing (or worse, biases an unrelated token).
192
+
193
+ **OpenAI-completions only.** `logit_bias` is injected only on the OpenAI-completions transport. Models routed via the Anthropic Messages transport (`api: "anthropic-messages"`, only set via a patch/custom model — the provider default is `openai-completions`) are skipped, since the Anthropic Messages API has no `logit_bias` equivalent.
194
+
195
+ **Edit it:**
196
+
197
+ - **`/fireworks-settings`** (TUI) → *Logit bias* → open the nested panel. A search `>` area at the top filters entries by token ID (type digits); `↑↓` move · `Enter` activate (toggle *Enabled* / edit a bias / start *Add token…*) · `d` delete the selected entry · `Esc` back. Add / edit use free-text entry (token ID = non-negative integer, bias = -100..100). Changes take effect immediately and persist.
198
+
199
+ The top-level *Logit bias* row shows a one-line summary (e.g. `3 entries · on`, `off`). Off by default; an empty map (or `enabled: false`) means nothing is sent.
200
+
178
201
  ## Usage
179
202
 
180
203
  After loading the extension, use the `/model` command in pi to select your preferred model:
package/index.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { getAgentDir, type ExtensionAPI, type ModelRegistry } from "@earendil-works/pi-coding-agent";
29
+ import type { Input, matchesKey, Key, truncateToWidth, visibleWidth, wrapTextWithAnsi, fuzzyFilter, SettingsListTheme } from "@earendil-works/pi-tui";
29
30
  import modelsData from "./models.json" with { type: "json" };
30
31
  import customModelsData from "./custom-models.json" with { type: "json" };
31
32
  import patchData from "./patch.json" with { type: "json" };
@@ -378,9 +379,21 @@ interface PreserveThinkingConfig {
378
379
  default: PreserveMode;
379
380
  }
380
381
 
382
+ // Logit bias: an OpenAI-style map of token ID → bias (-100..100), forwarded
383
+ // verbatim as the top-level `logit_bias` request field on Fireworks'
384
+ // OpenAI-compatible completions endpoint. Token IDs are tokenizer-specific
385
+ // (the caller must match the model's tokenizer). Disabled by default; an empty
386
+ // map or enabled=false means no injection. String-keyed so it serializes 1:1
387
+ // to the wire format.
388
+ interface LogitBiasConfig {
389
+ enabled: boolean;
390
+ biases: Record<string, number>;
391
+ }
392
+
381
393
  interface FireworksConfig {
382
394
  serviceTier: ServiceTierConfig;
383
395
  preserveThinking: PreserveThinkingConfig;
396
+ logitBias: LogitBiasConfig;
384
397
  }
385
398
 
386
399
  const FIREWORKS_CONFIG_PATH = path.join(getAgentDir(), "extensions", "fireworks.json");
@@ -398,9 +411,14 @@ const DEFAULT_SERVICE_TIER_CONFIG: ServiceTierConfig = {
398
411
  const DEFAULT_PRESERVE_CONFIG: PreserveThinkingConfig = {
399
412
  default: false,
400
413
  };
414
+ const DEFAULT_LOGIT_BIAS_CONFIG: LogitBiasConfig = {
415
+ enabled: false,
416
+ biases: {},
417
+ };
401
418
  const DEFAULT_FIREWORKS_CONFIG: FireworksConfig = {
402
419
  serviceTier: DEFAULT_SERVICE_TIER_CONFIG,
403
420
  preserveThinking: DEFAULT_PRESERVE_CONFIG,
421
+ logitBias: DEFAULT_LOGIT_BIAS_CONFIG,
404
422
  };
405
423
 
406
424
  function isValidTier(v: unknown): v is ServiceTier {
@@ -411,11 +429,63 @@ function isValidKeybinding(v: unknown): v is string {
411
429
  return typeof v === "string" && v.length > 0;
412
430
  }
413
431
 
432
+ // A valid logit-bias value: integer in [-100, 100]. -100 effectively bans the
433
+ // token (sets its logit to -inf on Fireworks / vLLM); other values shift the
434
+ // logit additively before sampling (per OpenAI / Fireworks semantics).
435
+ function isValidBiasValue(v: unknown): v is number {
436
+ return typeof v === "number" && Number.isInteger(v) && v >= -100 && v <= 100;
437
+ }
438
+
439
+ // Validate the logit_bias map (tokenId-string → bias). Keys must be pure
440
+ // non-negative-integer strings ("1", "007"); they're normalized to canonical
441
+ // "String(parseInt)" keys. Invalid keys / values are dropped silently so a
442
+ // malformed config can't crash model registration. Rejects "1.5", "-1", "abc".
443
+ function parseLogitBiasMap(raw: unknown): Record<string, number> {
444
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
445
+ const result: Record<string, number> = {};
446
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
447
+ if (!/^\d+$/.test(key)) continue;
448
+ const tokenId = Number.parseInt(key, 10);
449
+ if (!Number.isInteger(tokenId) || tokenId < 0) continue;
450
+ if (!isValidBiasValue(value)) continue;
451
+ result[String(tokenId)] = value;
452
+ }
453
+ return result;
454
+ }
455
+
456
+ // Validate the `logitBias` config object. Non-object / array → defaults.
457
+ function parseLogitBiasConfig(raw: unknown): LogitBiasConfig {
458
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
459
+ return { enabled: false, biases: {} };
460
+ }
461
+ const lb = raw as Record<string, unknown>;
462
+ return {
463
+ enabled: lb.enabled === true,
464
+ biases: parseLogitBiasMap(lb.biases),
465
+ };
466
+ }
467
+
468
+ // Strict integer parse for user-typed logit-bias inputs. Rejects decimals
469
+ // ("1.5"), signs ("+5"), empty, and non-numeric strings — Number.parseInt
470
+ // would silently truncate those, masking typos. Used by the TUI editor.
471
+ function parseTokenIdInput(raw: string): number | null {
472
+ if (!/^\d+$/.test(raw)) return null;
473
+ const n = Number.parseInt(raw, 10);
474
+ return Number.isInteger(n) && n >= 0 ? n : null;
475
+ }
476
+
477
+ function parseBiasInput(raw: string): number | null {
478
+ if (!/^-?\d+$/.test(raw)) return null;
479
+ const n = Number.parseInt(raw, 10);
480
+ return Number.isInteger(n) && n >= -100 && n <= 100 ? n : null;
481
+ }
482
+
414
483
  function loadFireworksConfig(): FireworksConfig {
415
484
  try {
416
485
  const raw = JSON.parse(fs.readFileSync(FIREWORKS_CONFIG_PATH, "utf8"));
417
486
  const st = raw?.serviceTier ?? {};
418
487
  const pt = raw?.preserveThinking ?? {};
488
+ const lb = raw?.logitBias ?? {};
419
489
  return {
420
490
  serviceTier: {
421
491
  default: isValidTier(st.default) ? st.default : DEFAULT_SERVICE_TIER_CONFIG.default,
@@ -425,6 +495,7 @@ function loadFireworksConfig(): FireworksConfig {
425
495
  preserveThinking: {
426
496
  default: typeof pt.default === "boolean" ? pt.default : DEFAULT_PRESERVE_CONFIG.default,
427
497
  },
498
+ logitBias: parseLogitBiasConfig(lb),
428
499
  };
429
500
  } catch {
430
501
  // Config missing or invalid — write defaults so the user can discover it.
@@ -434,7 +505,7 @@ function loadFireworksConfig(): FireworksConfig {
434
505
  } catch {
435
506
  // Write failure is non-fatal — defaults still work in memory.
436
507
  }
437
- return { serviceTier: { ...DEFAULT_SERVICE_TIER_CONFIG }, preserveThinking: { ...DEFAULT_PRESERVE_CONFIG } };
508
+ return { serviceTier: { ...DEFAULT_SERVICE_TIER_CONFIG }, preserveThinking: { ...DEFAULT_PRESERVE_CONFIG }, logitBias: { enabled: false, biases: {} } };
438
509
  }
439
510
  }
440
511
 
@@ -460,6 +531,33 @@ function writeRawFireworksConfig(raw: Record<string, any>): void {
460
531
 
461
532
  let fireworksConfig = loadFireworksConfig();
462
533
 
534
+ // Logit Bias state — read/refresh/mutate the logit_bias config. The in-memory
535
+ // cache (`fireworksConfig`) is refreshed on session_start and after every
536
+ // settings write (mutateLogitBias); before_provider_request reads it directly.
537
+ function getLogitBias(): LogitBiasConfig {
538
+ return fireworksConfig.logitBias;
539
+ }
540
+
541
+ // Re-read + refresh the in-memory config from disk so the settings editor sees
542
+ // the latest file state (handles hand-edits since session_start). Called when
543
+ // the logit-bias submenu opens (mirrors makora's "fresh state on each open").
544
+ function reloadLogitBias(): LogitBiasConfig {
545
+ fireworksConfig = loadFireworksConfig();
546
+ return fireworksConfig.logitBias;
547
+ }
548
+
549
+ // Read-modify-write the validated logit-bias config, then refresh the in-memory
550
+ // cache so before_provider_request picks up the change on the next request.
551
+ // The mutator receives a normalized LogitBiasConfig it can mutate in place.
552
+ function mutateLogitBias(mutator: (lb: LogitBiasConfig) => void): void {
553
+ const raw = readRawFireworksConfig();
554
+ const current = parseLogitBiasConfig(raw.logitBias);
555
+ mutator(current);
556
+ raw.logitBias = current;
557
+ writeRawFireworksConfig(raw);
558
+ fireworksConfig = loadFireworksConfig();
559
+ }
560
+
463
561
  // Held so module-scope helpers (setTier) can call pi.appendEntry.
464
562
  let piRef: ExtensionAPI | null = null;
465
563
 
@@ -587,6 +685,359 @@ function setPreserve(on: boolean): void {
587
685
  preserveOn = on;
588
686
  }
589
687
 
688
+ // Logit Bias Editor (TUI) — custom Component for the "Logit bias" submenu.
689
+ //
690
+ // The makora nested-UI idiom is SettingsList → submenu → SettingsList, but
691
+ // SettingsList only cycles fixed `values` or opens a sub-Component — it can't
692
+ // take free text, and "add an arbitrary token ID + bias" needs free text. So
693
+ // this editor IS the submenu Component: it owns an ordered entry list (add /
694
+ // edit / delete mutate it in place and re-render without closing the submenu)
695
+ // and uses pi-tui's Input for the token-ID and bias prompts. Every mutation is
696
+ // persisted immediately via mutateLogitBias; Esc returns to the parent settings
697
+ // list, passing back a one-line summary so the top-level row's value refreshes.
698
+
699
+ type LogitBiasEntry = { tokenId: number; bias: number };
700
+
701
+ type Row = { label: string; value: string; description: string; kind: "enabled" | "entry" | "add"; tokenId?: number };
702
+
703
+ interface LogitBiasEditorDeps {
704
+ InputCtor: typeof Input;
705
+ matchesKey: typeof matchesKey;
706
+ Key: typeof Key;
707
+ truncateToWidth: typeof truncateToWidth;
708
+ visibleWidth: typeof visibleWidth;
709
+ wrapTextWithAnsi: typeof wrapTextWithAnsi;
710
+ fuzzyFilter: typeof fuzzyFilter;
711
+ settingsListTheme: SettingsListTheme;
712
+ theme: { fg(name: string, text: string): string };
713
+ notify: (msg: string, level: "info" | "error") => void;
714
+ subDone: (value?: string) => void;
715
+ }
716
+
717
+ class LogitBiasEditor {
718
+ private entries: LogitBiasEntry[];
719
+ private enabled: boolean;
720
+ private selectedIndex = 0;
721
+ private mode: "list" | "addToken" | "addBias" | "editBias" = "list";
722
+ private input: Input;
723
+ // Search `>` area at the top of the panel (matches SettingsList's search).
724
+ // Filters rows by label via fuzzyFilter; queries are numeric (token IDs), so
725
+ // `d` is reserved as a delete shortcut when the query is empty.
726
+ private searchInput: Input;
727
+ private searchQuery = "";
728
+ private pendingTokenId: number | null = null;
729
+ private editingTokenId: number | null = null;
730
+ private readonly maxVisible = 10;
731
+
732
+ constructor(private deps: LogitBiasEditorDeps) {
733
+ const lb = reloadLogitBias();
734
+ this.enabled = lb.enabled;
735
+ this.entries = LogitBiasEditor.sorted(lb.biases);
736
+ this.input = new deps.InputCtor();
737
+ this.input.onSubmit = () => this.submitInput();
738
+ this.input.onEscape = () => this.cancelInput();
739
+ this.searchInput = new deps.InputCtor();
740
+ }
741
+
742
+ private static sorted(biases: Record<string, number>): LogitBiasEntry[] {
743
+ return Object.entries(biases)
744
+ .map(([k, bias]) => ({ tokenId: Number.parseInt(k, 10), bias }))
745
+ .sort((a, b) => a.tokenId - b.tokenId);
746
+ }
747
+
748
+ private persist(): void {
749
+ const biases: Record<string, number> = {};
750
+ for (const e of this.entries) biases[String(e.tokenId)] = e.bias;
751
+ mutateLogitBias((lb) => {
752
+ lb.enabled = this.enabled;
753
+ lb.biases = biases;
754
+ });
755
+ }
756
+
757
+ private summary(): string {
758
+ const n = this.entries.length;
759
+ if (n === 0) return this.enabled ? "on · empty" : "off";
760
+ return `${n} ${n === 1 ? "entry" : "entries"} · ${this.enabled ? "on" : "off"}`;
761
+ }
762
+
763
+ // Virtual rows: [Enabled toggle] + entries + [Add token…].
764
+ private buildRows(): Row[] {
765
+ const rows: Row[] = [];
766
+ rows.push({
767
+ label: "Enabled",
768
+ value: this.enabled ? "on" : "off",
769
+ kind: "enabled",
770
+ description: "When on, the logit_bias map is sent on every Fireworks OpenAI-completions request, biasing the listed token IDs by their bias (-100..100). Off = nothing sent (entries are kept). Token IDs are tokenizer-specific — match the model's tokenizer.",
771
+ });
772
+ for (const e of this.entries) {
773
+ rows.push({
774
+ label: `token ${e.tokenId}`,
775
+ value: String(e.bias),
776
+ kind: "entry",
777
+ tokenId: e.tokenId,
778
+ description: `token ${e.tokenId} → ${e.bias}. Enter to edit the bias (-100..100), d to delete. Token IDs are tokenizer-specific.`,
779
+ });
780
+ }
781
+ rows.push({
782
+ label: "Add token…",
783
+ value: "",
784
+ kind: "add",
785
+ description: "Add a new entry: prompts for the token ID (non-negative integer), then the bias (-100..100).",
786
+ });
787
+ return rows;
788
+ }
789
+
790
+ // Rows visible after the search filter. With no query, returns all rows.
791
+ private filteredRows(): Row[] {
792
+ const rows = this.buildRows();
793
+ if (!this.searchQuery) return rows;
794
+ return this.deps.fuzzyFilter(rows, this.searchQuery, (r) => r.label);
795
+ }
796
+
797
+ private applyFilter(): void {
798
+ this.searchQuery = this.searchInput.getValue();
799
+ this.selectedIndex = 0;
800
+ }
801
+
802
+ private clearSearch(): void {
803
+ this.searchInput.setValue("");
804
+ this.searchQuery = "";
805
+ }
806
+
807
+ handleInput(data: string): void {
808
+ if (this.mode !== "list") {
809
+ this.input.handleInput(data);
810
+ return;
811
+ }
812
+ const { matchesKey, Key } = this.deps;
813
+ if (matchesKey(data, Key.up)) {
814
+ const n = this.filteredRows().length;
815
+ if (n > 0) this.selectedIndex = (this.selectedIndex - 1 + n) % n;
816
+ } else if (matchesKey(data, Key.down)) {
817
+ const n = this.filteredRows().length;
818
+ if (n > 0) this.selectedIndex = (this.selectedIndex + 1) % n;
819
+ } else if (matchesKey(data, Key.enter) || data === " ") {
820
+ this.activateSelected();
821
+ } else if (matchesKey(data, Key.escape)) {
822
+ this.close();
823
+ } else if (this.searchQuery === "" && (data === "d" || data === "D")) {
824
+ // `d` deletes the selected entry. Only active when not searching — with a
825
+ // query active, `d` is routed to the search input (token IDs are numeric,
826
+ // so `d` is never a useful search term, but routing avoids accidental
827
+ // deletes mid-search).
828
+ this.deleteSelected();
829
+ } else {
830
+ // Route everything else (printables, backspace, cursor arrows) to the
831
+ // search input, mirroring SettingsList. Spaces are dropped (sanitized)
832
+ // so they never enter the query.
833
+ const sanitized = data.replace(/ /g, "");
834
+ if (!sanitized) return;
835
+ this.searchInput.handleInput(sanitized);
836
+ this.applyFilter();
837
+ }
838
+ }
839
+
840
+ private activateSelected(): void {
841
+ const row = this.filteredRows()[this.selectedIndex];
842
+ if (!row) return;
843
+ if (row.kind === "enabled") {
844
+ this.toggleEnabled();
845
+ } else if (row.kind === "add") {
846
+ this.startAddToken();
847
+ } else if (row.kind === "entry" && row.tokenId !== undefined) {
848
+ const entry = this.entries.find((e) => e.tokenId === row.tokenId);
849
+ if (entry) this.startEditBias(entry);
850
+ }
851
+ }
852
+
853
+ private toggleEnabled(): void {
854
+ this.enabled = !this.enabled;
855
+ this.persist();
856
+ this.deps.notify(`Logit bias ${this.enabled ? "on" : "off"}.`, "info");
857
+ }
858
+
859
+ private deleteSelected(): void {
860
+ const row = this.filteredRows()[this.selectedIndex];
861
+ if (!row || row.kind !== "entry" || row.tokenId === undefined) return;
862
+ const idx = this.entries.findIndex((e) => e.tokenId === row.tokenId);
863
+ if (idx < 0) return;
864
+ const [removed] = this.entries.splice(idx, 1);
865
+ this.persist();
866
+ const newLen = this.filteredRows().length;
867
+ if (this.selectedIndex >= newLen) this.selectedIndex = Math.max(0, newLen - 1);
868
+ if (removed) this.deps.notify(`Removed token ${removed.tokenId}.`, "info");
869
+ }
870
+
871
+ private startAddToken(): void {
872
+ this.clearSearch();
873
+ this.mode = "addToken";
874
+ this.pendingTokenId = null;
875
+ this.input.setValue("");
876
+ this.input.focused = true;
877
+ }
878
+
879
+ private startEditBias(entry: LogitBiasEntry): void {
880
+ this.clearSearch();
881
+ // Re-select the entry in the (now unfiltered) full list so the cursor
882
+ // stays on it after returning from the bias prompt.
883
+ const fullIdx = this.buildRows().findIndex((r) => r.kind === "entry" && r.tokenId === entry.tokenId);
884
+ this.selectedIndex = fullIdx >= 0 ? fullIdx : 0;
885
+ this.mode = "editBias";
886
+ this.editingTokenId = entry.tokenId;
887
+ this.input.setValue(String(entry.bias));
888
+ this.input.focused = true;
889
+ }
890
+
891
+ private submitInput(): void {
892
+ const raw = this.input.getValue().trim();
893
+ if (this.mode === "addToken") {
894
+ const tokenId = parseTokenIdInput(raw);
895
+ if (tokenId === null) {
896
+ this.deps.notify("Token ID must be a non-negative integer.", "error");
897
+ return;
898
+ }
899
+ if (this.entries.some((e) => e.tokenId === tokenId)) {
900
+ this.deps.notify(`Token ${tokenId} already has a bias — edit it instead.`, "error");
901
+ return;
902
+ }
903
+ this.pendingTokenId = tokenId;
904
+ this.mode = "addBias";
905
+ this.input.setValue("0");
906
+ this.input.focused = true;
907
+ return;
908
+ }
909
+ if (this.mode === "addBias") {
910
+ const bias = parseBiasInput(raw);
911
+ if (bias === null) {
912
+ this.deps.notify("Bias must be an integer from -100 to 100.", "error");
913
+ return;
914
+ }
915
+ const tokenId = this.pendingTokenId!;
916
+ this.entries.push({ tokenId, bias });
917
+ this.entries.sort((a, b) => a.tokenId - b.tokenId);
918
+ this.persist();
919
+ const newIdx = this.entries.findIndex((e) => e.tokenId === tokenId);
920
+ this.selectedIndex = 1 + newIdx;
921
+ this.pendingTokenId = null;
922
+ this.mode = "list";
923
+ this.input.focused = false;
924
+ this.deps.notify(`Added token ${tokenId} → ${bias}.`, "info");
925
+ return;
926
+ }
927
+ if (this.mode === "editBias") {
928
+ const bias = parseBiasInput(raw);
929
+ if (bias === null) {
930
+ this.deps.notify("Bias must be an integer from -100 to 100.", "error");
931
+ return;
932
+ }
933
+ const tokenId = this.editingTokenId!;
934
+ const entry = this.entries.find((e) => e.tokenId === tokenId);
935
+ if (entry) {
936
+ entry.bias = bias;
937
+ this.persist();
938
+ }
939
+ this.editingTokenId = null;
940
+ this.mode = "list";
941
+ this.input.focused = false;
942
+ this.deps.notify(`Set token ${tokenId} → ${bias}.`, "info");
943
+ return;
944
+ }
945
+ }
946
+
947
+ private cancelInput(): void {
948
+ this.mode = "list";
949
+ this.pendingTokenId = null;
950
+ this.editingTokenId = null;
951
+ this.input.focused = false;
952
+ }
953
+
954
+ private close(): void {
955
+ this.input.focused = false;
956
+ this.searchInput.focused = false;
957
+ this.deps.subDone(this.summary());
958
+ }
959
+
960
+ private renderList(width: number): string[] {
961
+ const t = this.deps.settingsListTheme;
962
+ const { truncateToWidth, visibleWidth, wrapTextWithAnsi } = this.deps;
963
+ const lines: string[] = [];
964
+ // Search `>` area — mirrors SettingsList (renderMainList) so the panel stays
965
+ // visually consistent with the rest of /fireworks-settings.
966
+ lines.push(...this.searchInput.render(width));
967
+ lines.push("");
968
+
969
+ const rows = this.filteredRows();
970
+ const total = rows.length;
971
+ if (total === 0) {
972
+ lines.push(t.hint(truncateToWidth(" No matching tokens", width)));
973
+ } else {
974
+ const maxLabel = Math.min(30, Math.max(...rows.map((r) => visibleWidth(r.label)), 1));
975
+ const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), total - this.maxVisible));
976
+ const end = Math.min(start + this.maxVisible, total);
977
+ for (let i = start; i < end; i++) {
978
+ const row = rows[i];
979
+ const selected = i === this.selectedIndex;
980
+ const prefix = selected ? t.cursor : " ";
981
+ const labelPadded = row.label + " ".repeat(Math.max(0, maxLabel - visibleWidth(row.label)));
982
+ const label = t.label(labelPadded, selected);
983
+ const sep = " ";
984
+ const valueWidth = width - visibleWidth(prefix) - maxLabel - visibleWidth(sep) - 2;
985
+ const value = row.value ? t.value(truncateToWidth(row.value, Math.max(0, valueWidth), ""), selected) : "";
986
+ lines.push(truncateToWidth(prefix + label + sep + value, width));
987
+ }
988
+ if (start > 0 || end < total) {
989
+ lines.push(t.hint(truncateToWidth(` (${this.selectedIndex + 1}/${total})`, width - 2, "")));
990
+ }
991
+ const sel = rows[this.selectedIndex];
992
+ if (sel?.description) {
993
+ lines.push("");
994
+ for (const ln of wrapTextWithAnsi(sel.description, width - 4)) {
995
+ lines.push(t.description(` ${ln}`));
996
+ }
997
+ }
998
+ }
999
+
1000
+ lines.push("");
1001
+ lines.push(truncateToWidth(t.hint(" Type to search · ↑↓ move · Enter activate · d delete · Esc back"), width));
1002
+ return lines;
1003
+ }
1004
+
1005
+ private renderInput(width: number): string[] {
1006
+ const { settingsListTheme: t, theme, truncateToWidth } = this.deps;
1007
+ const lines: string[] = [];
1008
+ let prompt = "";
1009
+ if (this.mode === "addToken") {
1010
+ prompt = "Token ID (non-negative integer):";
1011
+ } else if (this.mode === "addBias") {
1012
+ prompt = `Bias for token ${this.pendingTokenId} (-100..100):`;
1013
+ } else if (this.mode === "editBias") {
1014
+ prompt = `New bias for token ${this.editingTokenId} (-100..100):`;
1015
+ }
1016
+ lines.push(truncateToWidth(theme.fg("accent", prompt), width));
1017
+ lines.push(...this.input.render(width));
1018
+ lines.push("");
1019
+ lines.push(truncateToWidth(t.hint(" Enter submit · Esc cancel"), width));
1020
+ return lines;
1021
+ }
1022
+
1023
+ render(width: number): string[] {
1024
+ return this.mode === "list" ? this.renderList(width) : this.renderInput(width);
1025
+ }
1026
+
1027
+ invalidate(): void {
1028
+ this.input.invalidate?.();
1029
+ this.searchInput.invalidate?.();
1030
+ }
1031
+ }
1032
+
1033
+ // One-line summary of the logit-bias config for the top-level settings row.
1034
+ function logitBiasSummary(): string {
1035
+ const lb = getLogitBias();
1036
+ const n = Object.keys(lb.biases).length;
1037
+ if (n === 0) return lb.enabled ? "on · empty" : "off";
1038
+ return `${n} ${n === 1 ? "entry" : "entries"} · ${lb.enabled ? "on" : "off"}`;
1039
+ }
1040
+
590
1041
  // ─── Extension Entry Point ────────────────────────────────────────────────────
591
1042
 
592
1043
  export {
@@ -613,6 +1064,16 @@ export {
613
1064
  updateTierStatus,
614
1065
  isPreserveEligible,
615
1066
  setPreserve,
1067
+ isValidBiasValue,
1068
+ parseLogitBiasMap,
1069
+ parseLogitBiasConfig,
1070
+ parseBiasInput,
1071
+ parseTokenIdInput,
1072
+ getLogitBias,
1073
+ reloadLogitBias,
1074
+ mutateLogitBias,
1075
+ logitBiasSummary,
1076
+ LogitBiasEditor,
616
1077
  };
617
1078
 
618
1079
  export type {
@@ -624,6 +1085,8 @@ export type {
624
1085
  ServiceTierConfig,
625
1086
  PreserveThinkingConfig,
626
1087
  FireworksConfig,
1088
+ LogitBiasConfig,
1089
+ LogitBiasEntry,
627
1090
  };
628
1091
 
629
1092
  export default function (pi: ExtensionAPI) {
@@ -731,6 +1194,20 @@ export default function (pi: ExtensionAPI) {
731
1194
  modified = true;
732
1195
  }
733
1196
 
1197
+ // Logit bias: forward the user's logit_bias map (token ID → -100..100) on
1198
+ // Fireworks OpenAI-completions requests. The map is OpenAI-compatible and
1199
+ // Fireworks adds each bias to the token's logit before sampling. Gated to
1200
+ // the OpenAI-completions transport — the Anthropic Messages API has no
1201
+ // logit_bias equivalent, so we skip models routed via `api:
1202
+ // "anthropic-messages"` (the provider default is "openai-completions"; only
1203
+ // a patched/custom model sets the Anthropic transport). Token IDs are
1204
+ // tokenizer-specific — the caller must match the model's tokenizer.
1205
+ const lb = fireworksConfig.logitBias;
1206
+ if (lb.enabled && Object.keys(lb.biases).length > 0 && model.api !== "anthropic-messages") {
1207
+ payload.logit_bias = lb.biases;
1208
+ modified = true;
1209
+ }
1210
+
734
1211
  // Kimi anchor-bleed sanitization (Kimi K2.x pattern bug). Only applies to
735
1212
  // Kimi models, but a single request can be both Kimi and priority-tiered.
736
1213
  if (isFireworksKimiModel(model)) {
@@ -838,13 +1315,13 @@ export default function (pi: ExtensionAPI) {
838
1315
  // settings-only (no command/keybinding), exactly like the siblings; the
839
1316
  // service-tier keybinding is load-time only, so keybinding changes need /reload.
840
1317
  pi.registerCommand("fireworks-settings", {
841
- description: "Configure Fireworks: preserved thinking + service tier + display",
1318
+ description: "Configure Fireworks: preserved thinking + service tier + logit bias + display",
842
1319
  async handler(_args, ctx) {
843
1320
  if (ctx.mode !== "tui") {
844
1321
  ctx.ui.notify("/fireworks-settings requires TUI mode.", "error");
845
1322
  return;
846
1323
  }
847
- const { SettingsList, Container } = await import("@earendil-works/pi-tui");
1324
+ const { SettingsList, Container, Input, matchesKey, Key, truncateToWidth, visibleWidth, wrapTextWithAnsi, fuzzyFilter } = await import("@earendil-works/pi-tui");
848
1325
  const { getSettingsListTheme, DynamicBorder } = await import("@earendil-works/pi-coding-agent");
849
1326
 
850
1327
  await ctx.ui.custom((_tui, theme, _kb, done) => {
@@ -872,6 +1349,26 @@ export default function (pi: ExtensionAPI) {
872
1349
  currentValue: fireworksConfig.serviceTier.display,
873
1350
  values: ["statusbar", "off"],
874
1351
  },
1352
+ {
1353
+ id: "logitBias",
1354
+ label: "Logit bias",
1355
+ description: "Send an OpenAI-style logit_bias map (token ID → -100..100) on every Fireworks OpenAI-completions request. Open the nested panel to add / edit / delete token IDs and biases. Token IDs are tokenizer-specific — use the exact tokenizer of the model you're calling. Not sent on Anthropic-routed models.",
1356
+ currentValue: logitBiasSummary(),
1357
+ submenu: (_cv: string, subDone: (v?: string) => void) =>
1358
+ new LogitBiasEditor({
1359
+ InputCtor: Input,
1360
+ matchesKey,
1361
+ Key,
1362
+ truncateToWidth,
1363
+ visibleWidth,
1364
+ wrapTextWithAnsi,
1365
+ fuzzyFilter,
1366
+ settingsListTheme: getSettingsListTheme(),
1367
+ theme,
1368
+ notify: (msg, level) => { try { ctx.ui.notify(msg, level); } catch { /* notify is a no-op without a UI runner */ } },
1369
+ subDone,
1370
+ }),
1371
+ },
875
1372
  ];
876
1373
 
877
1374
  const container = new Container();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-fireworks-provider",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Fireworks AI provider extension for pi - Access Kimi, MiniMax, GLM, DeepSeek, and GPT-OSS models through the Fireworks AI API",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/patch.json CHANGED
@@ -33,9 +33,14 @@
33
33
  },
34
34
  "accounts/fireworks/models/deepseek-v4-flash": {
35
35
  "name": "DeepSeek V4 Flash",
36
- "api": "anthropic-messages",
37
- "baseUrl": "https://api.fireworks.ai/inference",
38
36
  "reasoning": true,
37
+ "thinkingLevelMap": {
38
+ "off": "none",
39
+ "minimal": null,
40
+ "low": "high",
41
+ "medium": "high",
42
+ "xhigh": "max"
43
+ },
39
44
  "input": ["text"],
40
45
  "cost": {
41
46
  "input": 0.14,
@@ -46,17 +51,19 @@
46
51
  "contextWindow": 1000000,
47
52
  "maxTokens": 384000,
48
53
  "compat": {
49
- "sendSessionAffinityHeaders": true,
50
- "supportsEagerToolInputStreaming": false,
51
- "supportsCacheControlOnTools": false,
52
- "supportsLongCacheRetention": false
54
+ "supportsReasoningEffort": true
53
55
  }
54
56
  },
55
57
  "accounts/fireworks/models/deepseek-v4-pro": {
56
58
  "name": "DeepSeek V4 Pro",
57
- "api": "anthropic-messages",
58
- "baseUrl": "https://api.fireworks.ai/inference",
59
59
  "reasoning": true,
60
+ "thinkingLevelMap": {
61
+ "off": "none",
62
+ "minimal": null,
63
+ "low": "high",
64
+ "medium": "high",
65
+ "xhigh": "max"
66
+ },
60
67
  "input": ["text"],
61
68
  "cost": {
62
69
  "input": 1.74,
@@ -67,10 +74,7 @@
67
74
  "contextWindow": 1000000,
68
75
  "maxTokens": 384000,
69
76
  "compat": {
70
- "sendSessionAffinityHeaders": true,
71
- "supportsEagerToolInputStreaming": false,
72
- "supportsCacheControlOnTools": false,
73
- "supportsLongCacheRetention": false
77
+ "supportsReasoningEffort": true
74
78
  }
75
79
  },
76
80
  "accounts/fireworks/models/glm-4p5": {
@@ -139,8 +143,6 @@
139
143
  },
140
144
  "accounts/fireworks/models/glm-5p1": {
141
145
  "name": "GLM 5.1",
142
- "api": "anthropic-messages",
143
- "baseUrl": "https://api.fireworks.ai/inference",
144
146
  "reasoning": true,
145
147
  "input": ["text"],
146
148
  "cost": {
@@ -152,10 +154,7 @@
152
154
  "contextWindow": 202800,
153
155
  "maxTokens": 131072,
154
156
  "compat": {
155
- "sendSessionAffinityHeaders": true,
156
- "supportsEagerToolInputStreaming": false,
157
- "supportsCacheControlOnTools": false,
158
- "supportsLongCacheRetention": false
157
+ "supportsReasoningEffort": true
159
158
  }
160
159
  },
161
160
  "accounts/fireworks/models/glm-5p2": {
@@ -186,8 +185,6 @@
186
185
  },
187
186
  "accounts/fireworks/models/gpt-oss-120b": {
188
187
  "name": "GPT OSS 120B",
189
- "api": "anthropic-messages",
190
- "baseUrl": "https://api.fireworks.ai/inference",
191
188
  "reasoning": true,
192
189
  "input": ["text"],
193
190
  "cost": {
@@ -199,16 +196,11 @@
199
196
  "contextWindow": 131072,
200
197
  "maxTokens": 32768,
201
198
  "compat": {
202
- "sendSessionAffinityHeaders": true,
203
- "supportsEagerToolInputStreaming": false,
204
- "supportsCacheControlOnTools": false,
205
- "supportsLongCacheRetention": false
199
+ "supportsReasoningEffort": true
206
200
  }
207
201
  },
208
202
  "accounts/fireworks/models/gpt-oss-20b": {
209
203
  "name": "GPT OSS 20B",
210
- "api": "anthropic-messages",
211
- "baseUrl": "https://api.fireworks.ai/inference",
212
204
  "reasoning": true,
213
205
  "input": ["text"],
214
206
  "cost": {
@@ -220,10 +212,7 @@
220
212
  "contextWindow": 131072,
221
213
  "maxTokens": 32768,
222
214
  "compat": {
223
- "sendSessionAffinityHeaders": true,
224
- "supportsEagerToolInputStreaming": false,
225
- "supportsCacheControlOnTools": false,
226
- "supportsLongCacheRetention": false
215
+ "supportsReasoningEffort": true
227
216
  }
228
217
  },
229
218
  "accounts/fireworks/models/gemma-4-26b-a4b-it": {
@@ -303,8 +292,6 @@
303
292
  },
304
293
  "accounts/fireworks/models/kimi-k2p6": {
305
294
  "name": "Kimi K2.6",
306
- "api": "anthropic-messages",
307
- "baseUrl": "https://api.fireworks.ai/inference",
308
295
  "reasoning": true,
309
296
  "input": ["text", "image"],
310
297
  "cost": {
@@ -316,16 +303,11 @@
316
303
  "contextWindow": 262000,
317
304
  "maxTokens": 262000,
318
305
  "compat": {
319
- "sendSessionAffinityHeaders": true,
320
- "supportsEagerToolInputStreaming": false,
321
- "supportsCacheControlOnTools": false,
322
- "supportsLongCacheRetention": false
306
+ "supportsReasoningEffort": true
323
307
  }
324
308
  },
325
309
  "accounts/fireworks/models/kimi-k2p7-code": {
326
310
  "name": "Kimi K2.7 Code",
327
- "api": "anthropic-messages",
328
- "baseUrl": "https://api.fireworks.ai/inference",
329
311
  "reasoning": true,
330
312
  "input": ["text", "image"],
331
313
  "cost": {
@@ -337,10 +319,7 @@
337
319
  "contextWindow": 262000,
338
320
  "maxTokens": 262000,
339
321
  "compat": {
340
- "sendSessionAffinityHeaders": true,
341
- "supportsEagerToolInputStreaming": false,
342
- "supportsCacheControlOnTools": false,
343
- "supportsLongCacheRetention": false
322
+ "supportsReasoningEffort": true
344
323
  }
345
324
  },
346
325
  "accounts/fireworks/models/minimax-m2p1": {
@@ -377,8 +356,6 @@
377
356
  },
378
357
  "accounts/fireworks/models/minimax-m2p7": {
379
358
  "name": "MiniMax-M2.7",
380
- "api": "anthropic-messages",
381
- "baseUrl": "https://api.fireworks.ai/inference",
382
359
  "reasoning": true,
383
360
  "input": ["text"],
384
361
  "cost": {
@@ -390,16 +367,11 @@
390
367
  "contextWindow": 196608,
391
368
  "maxTokens": 196608,
392
369
  "compat": {
393
- "sendSessionAffinityHeaders": true,
394
- "supportsEagerToolInputStreaming": false,
395
- "supportsCacheControlOnTools": false,
396
- "supportsLongCacheRetention": false
370
+ "supportsReasoningEffort": true
397
371
  }
398
372
  },
399
373
  "accounts/fireworks/models/minimax-m3": {
400
374
  "name": "MiniMax-M3",
401
- "api": "anthropic-messages",
402
- "baseUrl": "https://api.fireworks.ai/inference",
403
375
  "reasoning": true,
404
376
  "input": ["text"],
405
377
  "cost": {
@@ -411,16 +383,11 @@
411
383
  "contextWindow": 512000,
412
384
  "maxTokens": 512000,
413
385
  "compat": {
414
- "sendSessionAffinityHeaders": true,
415
- "supportsEagerToolInputStreaming": false,
416
- "supportsCacheControlOnTools": false,
417
- "supportsLongCacheRetention": false
386
+ "supportsReasoningEffort": true
418
387
  }
419
388
  },
420
389
  "accounts/fireworks/models/qwen3p7-plus": {
421
390
  "name": "Qwen 3.7 Plus",
422
- "api": "anthropic-messages",
423
- "baseUrl": "https://api.fireworks.ai/inference",
424
391
  "reasoning": true,
425
392
  "input": ["text", "image"],
426
393
  "cost": {
@@ -432,15 +399,19 @@
432
399
  "contextWindow": 262144,
433
400
  "maxTokens": 65536,
434
401
  "compat": {
435
- "sendSessionAffinityHeaders": true,
436
- "supportsEagerToolInputStreaming": false,
437
- "supportsCacheControlOnTools": false,
438
- "supportsLongCacheRetention": false
402
+ "supportsReasoningEffort": true
439
403
  }
440
404
  },
441
405
  "accounts/fireworks/routers/deepseek-v4-pro": {
442
406
  "name": "DeepSeek V4 Pro (router)",
443
407
  "reasoning": true,
408
+ "thinkingLevelMap": {
409
+ "off": "none",
410
+ "minimal": null,
411
+ "low": "high",
412
+ "medium": "high",
413
+ "xhigh": "max"
414
+ },
444
415
  "input": ["text"],
445
416
  "cost": {
446
417
  "input": 1.74,
@@ -472,8 +443,6 @@
472
443
  },
473
444
  "accounts/fireworks/routers/glm-5p1-fast": {
474
445
  "name": "GLM 5.1 Fast",
475
- "api": "anthropic-messages",
476
- "baseUrl": "https://api.fireworks.ai/inference",
477
446
  "reasoning": true,
478
447
  "input": ["text"],
479
448
  "cost": {
@@ -485,17 +454,19 @@
485
454
  "contextWindow": 202800,
486
455
  "maxTokens": 131072,
487
456
  "compat": {
488
- "sendSessionAffinityHeaders": true,
489
- "supportsEagerToolInputStreaming": false,
490
- "supportsCacheControlOnTools": false,
491
- "supportsLongCacheRetention": false
457
+ "supportsReasoningEffort": true
492
458
  }
493
459
  },
494
460
  "accounts/fireworks/routers/glm-5p2-fast": {
495
461
  "name": "GLM 5.2 Fast",
496
- "api": "anthropic-messages",
497
- "baseUrl": "https://api.fireworks.ai/inference",
498
462
  "reasoning": true,
463
+ "thinkingLevelMap": {
464
+ "off": "none",
465
+ "minimal": null,
466
+ "low": "high",
467
+ "medium": "high",
468
+ "xhigh": "max"
469
+ },
499
470
  "input": ["text"],
500
471
  "cost": {
501
472
  "input": 2.1,
@@ -506,10 +477,7 @@
506
477
  "contextWindow": 1048575,
507
478
  "maxTokens": 131072,
508
479
  "compat": {
509
- "sendSessionAffinityHeaders": true,
510
- "supportsEagerToolInputStreaming": false,
511
- "supportsCacheControlOnTools": false,
512
- "supportsLongCacheRetention": false
480
+ "supportsReasoningEffort": true
513
481
  }
514
482
  },
515
483
  "accounts/fireworks/routers/kimi-k2p5-fast": {
@@ -546,8 +514,6 @@
546
514
  },
547
515
  "accounts/fireworks/routers/kimi-k2p6-fast": {
548
516
  "name": "Kimi K2.6 Fast",
549
- "api": "anthropic-messages",
550
- "baseUrl": "https://api.fireworks.ai/inference",
551
517
  "reasoning": true,
552
518
  "input": ["text", "image"],
553
519
  "cost": {
@@ -559,16 +525,11 @@
559
525
  "contextWindow": 262000,
560
526
  "maxTokens": 262000,
561
527
  "compat": {
562
- "sendSessionAffinityHeaders": true,
563
- "supportsEagerToolInputStreaming": false,
564
- "supportsCacheControlOnTools": false,
565
- "supportsLongCacheRetention": false
528
+ "supportsReasoningEffort": true
566
529
  }
567
530
  },
568
531
  "accounts/fireworks/routers/kimi-k2p6-turbo": {
569
532
  "name": "Kimi K2.6 Turbo",
570
- "api": "anthropic-messages",
571
- "baseUrl": "https://api.fireworks.ai/inference",
572
533
  "reasoning": true,
573
534
  "input": ["text", "image"],
574
535
  "cost": {
@@ -580,16 +541,11 @@
580
541
  "contextWindow": 262000,
581
542
  "maxTokens": 262000,
582
543
  "compat": {
583
- "sendSessionAffinityHeaders": true,
584
- "supportsEagerToolInputStreaming": false,
585
- "supportsCacheControlOnTools": false,
586
- "supportsLongCacheRetention": false
544
+ "supportsReasoningEffort": true
587
545
  }
588
546
  },
589
547
  "accounts/fireworks/routers/kimi-k2p7-code-fast": {
590
548
  "name": "Kimi K2.7 Code Fast",
591
- "api": "anthropic-messages",
592
- "baseUrl": "https://api.fireworks.ai/inference",
593
549
  "reasoning": true,
594
550
  "input": ["text", "image"],
595
551
  "cost": {
@@ -601,10 +557,7 @@
601
557
  "contextWindow": 262000,
602
558
  "maxTokens": 262000,
603
559
  "compat": {
604
- "sendSessionAffinityHeaders": true,
605
- "supportsEagerToolInputStreaming": false,
606
- "supportsCacheControlOnTools": false,
607
- "supportsLongCacheRetention": false
560
+ "supportsReasoningEffort": true
608
561
  }
609
562
  },
610
563
  "accounts/fireworks/routers/minimax-m2p7": {