pi-fireworks-provider 1.3.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.
- package/README.md +24 -1
- package/index.ts +500 -3
- package/package.json +1 -1
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
|
-
- **
|
|
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.
|
|
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",
|