From 1da903983ad72c60995507e813a00bb2bd6faf09 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 11 Jun 2026 21:36:26 +0200 Subject: [PATCH 001/141] fix(coding-agent): skip first-time setup for forks (#5627) --- packages/coding-agent/src/cli/startup-ui.ts | 30 +++++++++++++- .../test/first-time-setup-fork.test.ts | 39 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/first-time-setup-fork.test.ts diff --git a/packages/coding-agent/src/cli/startup-ui.ts b/packages/coding-agent/src/cli/startup-ui.ts index 5a9de203..ec7aa5e7 100644 --- a/packages/coding-agent/src/cli/startup-ui.ts +++ b/packages/coding-agent/src/cli/startup-ui.ts @@ -1,6 +1,6 @@ import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; import { existsSync } from "fs"; -import { ENV_AGENT_DIR, getSettingsPath } from "../config.ts"; +import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getSettingsPath, PACKAGE_NAME } from "../config.ts"; import { areExperimentalFeaturesEnabled } from "../core/experimental.ts"; import { KeybindingsManager } from "../core/keybindings.ts"; import type { SettingsManager } from "../core/settings-manager.ts"; @@ -12,6 +12,24 @@ import { } from "../modes/interactive/components/first-time-setup.ts"; import { detectTerminalBackground, initTheme, setTheme } from "../modes/interactive/theme/theme.ts"; +const OFFICIAL_PACKAGE_NAME = "@earendil-works/pi-coding-agent"; +const OFFICIAL_APP_NAME = "pi"; +const OFFICIAL_CONFIG_DIR_NAME = ".pi"; + +interface DistributionMetadata { + packageName: string; + appName: string; + configDirName: string; +} + +function isOfficialDistribution({ packageName, appName, configDirName }: DistributionMetadata): boolean { + return ( + packageName === OFFICIAL_PACKAGE_NAME && + appName === OFFICIAL_APP_NAME && + configDirName === OFFICIAL_CONFIG_DIR_NAME + ); +} + function createStartupTui(settingsManager: SettingsManager): TUI { initTheme(settingsManager.getTheme()); setKeybindings(KeybindingsManager.create()); @@ -28,11 +46,21 @@ async function clearStartupTui(ui: TUI): Promise { /** * First-time setup runs when all of these hold: + * - this is the official Pi distribution (not a fork/rebrand) * - experimental features are enabled (PI_EXPERIMENTAL=1) * - the default agent directory is used (no custom agent dir override) * - setup was not completed before (settings.json does not exist) */ export function shouldRunFirstTimeSetup(settingsPath: string = getSettingsPath()): boolean { + if ( + !isOfficialDistribution({ + packageName: PACKAGE_NAME, + appName: APP_NAME, + configDirName: CONFIG_DIR_NAME, + }) + ) { + return false; + } if (!areExperimentalFeaturesEnabled()) { return false; } diff --git a/packages/coding-agent/test/first-time-setup-fork.test.ts b/packages/coding-agent/test/first-time-setup-fork.test.ts new file mode 100644 index 00000000..e9b89207 --- /dev/null +++ b/packages/coding-agent/test/first-time-setup-fork.test.ts @@ -0,0 +1,39 @@ +import { mkdtempSync, rmSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../src/config.ts", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as Record), + PACKAGE_NAME: "@example/pi-coding-agent", + }; +}); + +import { shouldRunFirstTimeSetup } from "../src/cli/startup-ui.ts"; + +describe("shouldRunFirstTimeSetup in forked distributions", () => { + const originalPiExperimental = process.env.PI_EXPERIMENTAL; + let tempDir: string; + let settingsPath: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "pi-first-time-setup-fork-")); + settingsPath = join(tempDir, "settings.json"); + process.env.PI_EXPERIMENTAL = "1"; + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + if (originalPiExperimental === undefined) { + delete process.env.PI_EXPERIMENTAL; + } else { + process.env.PI_EXPERIMENTAL = originalPiExperimental; + } + }); + + it("returns false for a forked package", () => { + expect(shouldRunFirstTimeSetup(settingsPath)).toBe(false); + }); +}); From 3f44d3e2eb0e1770157fab22cc842b6f53088605 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 12 Jun 2026 09:00:15 +0200 Subject: [PATCH 002/141] fix(ai): remove stale OpenRouter Kimi free model assertion (#5650) --- packages/ai/test/openai-completions-tool-choice.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 319b18af..28c91795 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -890,11 +890,9 @@ describe("openai-completions tool_choice", () => { }); it("stores OpenRouter Kimi K2.6 reasoning replay compat in built-in metadata", () => { - for (const modelId of ["moonshotai/kimi-k2.6", "moonshotai/kimi-k2.6:free"] as const) { - const model = getModel("openrouter", modelId)!; - expect(model.compat?.supportsDeveloperRole).toBe(false); - expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); - } + const model = getModel("openrouter", "moonshotai/kimi-k2.6")!; + expect(model.compat?.supportsDeveloperRole).toBe(false); + expect(model.compat?.requiresReasoningContentOnAssistantMessages).toBe(true); }); it("stores Xiaomi MiMo reasoning replay compat in built-in metadata", () => { From 1c243365606f6629e21f4a99e03b63f9de422921 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 12:59:32 +0200 Subject: [PATCH 003/141] fix(tui): keep WezTerm Kitty images visible Closes #5618. --- packages/tui/CHANGELOG.md | 4 ++ packages/tui/src/tui.ts | 103 ++++++++++++++++++++++----- packages/tui/test/tui-render.test.ts | 89 ++++++++++++++++++++++- 3 files changed, 178 insertions(+), 18 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ef8a1ba1..da5cd46c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). + ## [0.79.1] - 2026-06-09 ### Added diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 9f6dda4b..2399564d 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -13,24 +13,42 @@ import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth const KITTY_SEQUENCE_PREFIX = "\x1b_G"; -function extractKittyImageIds(line: string): number[] { +interface KittyImageHeader { + ids: number[]; + rows: number; +} + +function parseKittyImageHeader(line: string): KittyImageHeader | undefined { const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX); - if (sequenceStart === -1) return []; + if (sequenceStart === -1) return undefined; const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length; const paramsEnd = line.indexOf(";", paramsStart); - if (paramsEnd === -1) return []; + if (paramsEnd === -1) return undefined; + const ids: number[] = []; + let rows = 1; const params = line.slice(paramsStart, paramsEnd); for (const param of params.split(",")) { const [key, value] = param.split("=", 2); - if (key !== "i" || value === undefined) continue; - const id = Number(value); - if (Number.isInteger(id) && id > 0 && id <= 0xffffffff) { - return [id]; + if (value === undefined) continue; + const numberValue = Number(value); + if (!Number.isInteger(numberValue) || numberValue <= 0 || numberValue > 0xffffffff) continue; + if (key === "i") { + ids.push(numberValue); + } else if (key === "r") { + rows = numberValue; } } - return []; + return { ids, rows }; +} + +function extractKittyImageIds(line: string): number[] { + return parseKittyImageHeader(line)?.ids ?? []; +} + +function extractKittyImageRows(line: string): number { + return parseKittyImageHeader(line)?.rows ?? 1; } /** @@ -1021,14 +1039,41 @@ export class TUI extends Container { return buffer; } - private expandLastChangedForKittyImages(firstChanged: number, lastChanged: number): number { - let expandedLastChanged = lastChanged; - for (let i = firstChanged; i < this.previousLines.length; i++) { - if (extractKittyImageIds(this.previousLines[i]).length > 0) { - expandedLastChanged = Math.max(expandedLastChanged, i); - } + private getKittyImageReservedRows(lines: string[], index: number, maxIndex = lines.length - 1): number { + const rows = extractKittyImageRows(lines[index] ?? ""); + if (rows <= 1) return 1; + + const maxRows = Math.min(rows, maxIndex - index + 1, lines.length - index); + let reservedRows = 1; + while (reservedRows < maxRows) { + const line = lines[index + reservedRows] ?? ""; + if (isImageLine(line) || visibleWidth(line) > 0) break; + reservedRows++; } - return expandedLastChanged; + return reservedRows; + } + + private expandChangedRangeForKittyImages( + firstChanged: number, + lastChanged: number, + newLines: string[], + ): { firstChanged: number; lastChanged: number } { + let expandedFirstChanged = firstChanged; + let expandedLastChanged = lastChanged; + const expandForLines = (lines: string[]): void => { + for (let i = 0; i < lines.length; i++) { + if (extractKittyImageIds(lines[i]).length === 0) continue; + const blockEnd = i + this.getKittyImageReservedRows(lines, i) - 1; + if (i >= firstChanged || (i <= lastChanged && blockEnd >= firstChanged)) { + expandedFirstChanged = Math.min(expandedFirstChanged, i); + expandedLastChanged = Math.max(expandedLastChanged, blockEnd); + } + } + }; + + expandForLines(this.previousLines); + expandForLines(newLines); + return { firstChanged: expandedFirstChanged, lastChanged: expandedLastChanged }; } private deleteChangedKittyImages(firstChanged: number, lastChanged: number): string { @@ -1247,7 +1292,9 @@ export class TUI extends Container { lastChanged = newLines.length - 1; } if (firstChanged !== -1) { - lastChanged = this.expandLastChangedForKittyImages(firstChanged, lastChanged); + const expandedRange = this.expandChangedRangeForKittyImages(firstChanged, lastChanged, newLines); + firstChanged = expandedRange.firstChanged; + lastChanged = expandedRange.lastChanged; } const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0; @@ -1350,9 +1397,31 @@ export class TUI extends Container { const renderEnd = Math.min(lastChanged, newLines.length - 1); for (let i = firstChanged; i <= renderEnd; i++) { if (i > firstChanged) buffer += "\r\n"; - buffer += "\x1b[2K"; // Clear current line const line = newLines[i]; const isImage = isImageLine(line); + const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i, renderEnd) : 1; + if (imageReservedRows > 1) { + const imageStartScreenRow = i - viewportTop; + if (imageStartScreenRow < 0 || imageStartScreenRow + imageReservedRows > height) { + logRedraw( + `kitty image pre-clear would scroll (${imageStartScreenRow} + ${imageReservedRows} > ${height})`, + ); + fullRender(true); + return; + } + + buffer += "\x1b[2K"; + for (let row = 1; row < imageReservedRows; row++) { + buffer += "\r\n\x1b[2K"; + } + buffer += `\x1b[${imageReservedRows - 1}A`; + buffer += line; + buffer += `\x1b[${imageReservedRows - 1}B`; + i += imageReservedRows - 1; + continue; + } + + buffer += "\x1b[2K"; // Clear current line if (!isImage && visibleWidth(line) > width) { // Log all lines to crash file for debugging const crashLogPath = path.join(os.homedir(), ".pi", "agent", "pi-crash.log"); diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index 230a0005..4e31e8f4 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -1,7 +1,14 @@ import assert from "node:assert"; import { describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; -import { deleteKittyImage, encodeKitty } from "../src/terminal-image.ts"; +import { Image } from "../src/components/image.ts"; +import { + deleteKittyImage, + encodeKitty, + resetCapabilitiesCache, + setCapabilities, + setCellDimensions, +} from "../src/terminal-image.ts"; import { type Component, TUI } from "../src/tui.ts"; import { VirtualTerminal } from "./virtual-terminal.ts"; @@ -65,6 +72,86 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num } describe("TUI Kitty image cleanup", () => { + it("clears reserved Kitty image rows before drawing appended image placements", async () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const terminal = new LoggingVirtualTerminal(40, 10); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["before"]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 2 }, + { widthPx: 20, heightPx: 20 }, + ); + const imageLines = image.render(40); + const imageSequence = imageLines[0]; + component.lines = ["before", ...imageLines, "after"]; + tui.requestRender(); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + assert.ok( + writes.includes(`\x1b[2K\r\n\x1b[2K\x1b[1A${imageSequence}\x1b[1B`), + "reserved rows should be cleared before the image placement is drawn", + ); + assert.ok( + !writes.includes(`${imageSequence}\r\n\x1b[2K`), + "reserved row clears must not run after the image placement is drawn", + ); + + tui.stop(); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + + it("falls back to full redraw when Kitty image pre-clear would scroll", async () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const terminal = new LoggingVirtualTerminal(40, 2); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["before"]; + tui.start(); + await terminal.waitForRender(); + const redrawsBeforeImage = tui.fullRedraws; + terminal.clearWrites(); + + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 3 }, + { widthPx: 30, heightPx: 30 }, + ); + component.lines = ["before", ...image.render(40), "after"]; + tui.requestRender(); + await terminal.waitForRender(); + + assert.ok(tui.fullRedraws > redrawsBeforeImage, "unsafe image pre-clear should force a full redraw"); + assert.ok(terminal.getWrites().includes("\x1b[2J"), "fallback should clear and fully redraw"); + + tui.stop(); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + it("deletes changed image ids before drawing moved placements", async () => { const terminal = new LoggingVirtualTerminal(40, 10); const tui = new TUI(terminal); From a455f62f72359f5f2260c16ee3ed653ce968de3d Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Fri, 12 Jun 2026 14:52:46 +0200 Subject: [PATCH 004/141] fix(ai): preserve Anthropic refusal details (#5666) Propagate Anthropic refusal stop_details explanation to errorMessage --- packages/ai/src/providers/anthropic.ts | 31 ++++++---- .../ai/test/anthropic-sse-parsing.test.ts | 58 +++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index efe4bd75..90b46146 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -5,6 +5,7 @@ import type { MessageCreateParamsStreaming, MessageParam, RawMessageStreamEvent, + RefusalStopDetails, } from "@anthropic-ai/sdk/resources/messages.js"; import { calculateCost } from "../models.ts"; import type { @@ -660,7 +661,11 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti } } else if (event.type === "message_delta") { if (event.delta.stop_reason) { - output.stopReason = mapStopReason(event.delta.stop_reason); + const stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details); + output.stopReason = stopReasonResult.stopReason; + if (stopReasonResult.errorMessage) { + output.errorMessage = stopReasonResult.errorMessage; + } } // Only update usage fields if present (not null). // Preserves input_tokens from message_start when proxies omit it in message_delta. @@ -688,7 +693,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti } if (output.stopReason === "aborted" || output.stopReason === "error") { - throw new Error("An unknown error occurred"); + throw new Error(output.errorMessage || "An unknown error occurred"); } stream.push({ type: "done", reason: output.stopReason, message: output }); @@ -1202,22 +1207,28 @@ function convertTools( }); } -function mapStopReason(reason: Anthropic.Messages.StopReason | string): StopReason { +function mapStopReason( + reason: Anthropic.Messages.StopReason | string, + stopDetails?: RefusalStopDetails | null, +): { stopReason: StopReason; errorMessage?: string } { switch (reason) { case "end_turn": - return "stop"; + return { stopReason: "stop" }; case "max_tokens": - return "length"; + return { stopReason: "length" }; case "tool_use": - return "toolUse"; + return { stopReason: "toolUse" }; case "refusal": - return "error"; + return { + stopReason: "error", + errorMessage: stopDetails?.explanation || `The model refused to complete the request`, + }; case "pause_turn": // Stop is good enough -> resubmit - return "stop"; + return { stopReason: "stop" }; case "stop_sequence": - return "stop"; // We don't supply stop sequences, so this should never happen + return { stopReason: "stop" }; // We don't supply stop sequences, so this should never happen case "sensitive": // Content flagged by safety filters (not yet in SDK types) - return "error"; + return { stopReason: "error" }; default: // Handle unknown stop reasons gracefully (API may add new values) throw new Error(`Unhandled stop reason: ${reason}`); diff --git a/packages/ai/test/anthropic-sse-parsing.test.ts b/packages/ai/test/anthropic-sse-parsing.test.ts index 03024966..d8daf7f7 100644 --- a/packages/ai/test/anthropic-sse-parsing.test.ts +++ b/packages/ai/test/anthropic-sse-parsing.test.ts @@ -166,6 +166,64 @@ describe("Anthropic raw SSE parsing", () => { }); }); + it("preserves refusal stop details from message_delta", async () => { + const model = getModel("anthropic", "claude-fable-5"); + const context: Context = { + messages: [{ role: "user", content: "blocked request", timestamp: Date.now() }], + }; + const explanation = + "This request triggered restrictions on violative cyber content and was blocked under Anthropic's Usage Policy. To learn more, provide feedback, or request an exemption based on how you use Claude, visit our help center: https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude."; + const response = createSseResponse([ + { + event: "message_start", + data: JSON.stringify({ + type: "message_start", + message: { + id: "msg_01XFUDYJgAACzvnptvVoYEL", + usage: { + input_tokens: 412, + output_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + }, + }), + }, + { + event: "message_delta", + data: JSON.stringify({ + type: "message_delta", + delta: { + stop_reason: "refusal", + stop_details: { + type: "refusal", + category: "cyber", + explanation, + }, + }, + usage: { + input_tokens: 412, + output_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + }), + }, + { + event: "message_stop", + data: JSON.stringify({ type: "message_stop" }), + }, + ]); + + const stream = streamAnthropic(model, context, { + client: createFakeAnthropicClient(response), + }); + const result = await stream.result(); + + expect(result.stopReason).toBe("error"); + expect(result.errorMessage).toBe(explanation); + }); + it("ignores unknown SSE events after message_stop", async () => { const model = getModel("anthropic", "claude-haiku-4-5"); const context: Context = { From be7d5cf58594fa2c6c82c6d8f051c0ef5b479bd0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 17:07:50 +0200 Subject: [PATCH 005/141] fix(ai): relax Codex SSE header timeout --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/openai-codex-responses.ts | 4 +++- packages/ai/test/openai-codex-stream.test.ts | 12 ++++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 5af75b1e..93fc9a80 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,7 @@ ### Fixed +- Increased the OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). ## [0.79.1] - 2026-06-09 diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 5836ee1b..7fe9e256 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -53,7 +53,9 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth" as const; const DEFAULT_MAX_RETRIES = 0; const BASE_DELAY_MS = 1000; const DEFAULT_MAX_RETRY_DELAY_MS = 60_000; -const DEFAULT_SSE_HEADER_TIMEOUT_MS = 10_000; +// Keep a bounded pre-header timeout so zero-event Codex SSE stalls fail instead of +// leaving callers stuck on "Working..." indefinitely. See #4945. +const DEFAULT_SSE_HEADER_TIMEOUT_MS = 20_000; const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000; const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]); const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009; diff --git a/packages/ai/test/openai-codex-stream.test.ts b/packages/ai/test/openai-codex-stream.test.ts index 9cf0f981..fe4f3ea6 100644 --- a/packages/ai/test/openai-codex-stream.test.ts +++ b/packages/ai/test/openai-codex-stream.test.ts @@ -361,13 +361,21 @@ describe("openai-codex streaming", () => { apiKey: token, transport: "sse", }).result(); + let settled = false; + const observedResultPromise = resultPromise.then((result) => { + settled = true; + return result; + }); await vi.advanceTimersByTimeAsync(0); expect(fetchMock).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(10_000); - const result = await resultPromise; + expect(settled).toBe(false); + + await vi.advanceTimersByTimeAsync(10_000); + const result = await observedResultPromise; expect(result.stopReason).toBe("error"); - expect(result.errorMessage).toBe("Codex SSE response headers timed out after 10000ms"); + expect(result.errorMessage).toBe("Codex SSE response headers timed out after 20000ms"); }); it("aborts SSE body reads after response headers arrive", async () => { From 1fc80f4f6cc3db77824223c7bf3ec83515ae7792 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 17:26:42 +0200 Subject: [PATCH 006/141] fix(coding-agent): preserve custom fallback thinking closes #5552 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/core/model-resolver.ts | 7 +++++-- packages/coding-agent/test/model-resolver.test.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f24e827e..4d3ee3c6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,10 @@ - Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. +### Fixed + +- Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). + ## [0.79.1] - 2026-06-09 ### New Features diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index c9afb7b2..292ccf37 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -340,7 +340,7 @@ export interface ResolveCliModelResult { export function resolveCliModel(options: { cliProvider?: string; cliModel?: string; - cliThinking?: string; + cliThinking?: ThinkingLevel; modelRegistry: ModelRegistry; }): ResolveCliModelResult { const { cliProvider, cliModel, cliThinking, modelRegistry } = options; @@ -470,10 +470,13 @@ export function resolveCliModel(options: { const fallbackModel = buildFallbackModel(provider, fallbackPattern, availableModels); if (fallbackModel) { + const requestedThinking = cliThinking ?? fallbackThinking; + const model = + requestedThinking && requestedThinking !== "off" ? { ...fallbackModel, reasoning: true } : fallbackModel; const fallbackWarning = warning ? `${warning} Model "${fallbackPattern}" not found for provider "${provider}". Using custom model id.` : `Model "${fallbackPattern}" not found for provider "${provider}". Using custom model id.`; - return { model: fallbackModel, thinkingLevel: fallbackThinking, warning: fallbackWarning, error: undefined }; + return { model, thinkingLevel: fallbackThinking, warning: fallbackWarning, error: undefined }; } } diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 4b4caf0f..32861a9e 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -403,6 +403,7 @@ describe("resolveCliModel", () => { expect(result.model?.provider).toBe("neuralwatt"); // The :high suffix must NOT leak into the model id sent to the API expect(result.model?.id).toBe("zai-org/GLM-5.1-FP8"); + expect(result.model?.reasoning).toBe(true); expect(result.thinkingLevel).toBe("high"); }); From 6102dd2084276fd894600d9eabd84679da2ed1da Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 17:56:43 +0200 Subject: [PATCH 007/141] fix(coding-agent): handle missing export themes closes #5596 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 5 +- .../5596-missing-theme-export.test.ts | 89 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4d3ee3c6..eb2fa5a9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). - Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). ## [0.79.1] - 2026-06-09 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 201b8084..af9d32d7 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -33,7 +33,7 @@ import { resetApiProviders, streamSimple, } from "@earendil-works/pi-ai"; -import { theme } from "../modes/interactive/theme/theme.ts"; +import { getThemeByName, theme } from "../modes/interactive/theme/theme.ts"; import { stripFrontmatter } from "../utils/frontmatter.ts"; import { resolvePath } from "../utils/paths.ts"; import { sleep } from "../utils/sleep.ts"; @@ -3017,7 +3017,8 @@ export class AgentSession { * @returns Path to exported file */ async exportToHtml(outputPath?: string): Promise { - const themeName = this.settingsManager.getTheme(); + const configuredThemeName = this.settingsManager.getTheme(); + const themeName = configuredThemeName && getThemeByName(configuredThemeName) ? configuredThemeName : undefined; // Create tool renderer if we have an extension runner (for custom tool HTML rendering) const toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({ diff --git a/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts new file mode 100644 index 00000000..58bc17f8 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts @@ -0,0 +1,89 @@ +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Agent } from "@earendil-works/pi-agent-core"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import { AgentSession } from "../../../src/core/agent-session.ts"; +import { AuthStorage } from "../../../src/core/auth-storage.ts"; +import { convertToLlm } from "../../../src/core/messages.ts"; +import { ModelRegistry } from "../../../src/core/model-registry.ts"; +import { SessionManager } from "../../../src/core/session-manager.ts"; +import { SettingsManager } from "../../../src/core/settings-manager.ts"; +import { initTheme } from "../../../src/modes/interactive/theme/theme.ts"; +import { createTestResourceLoader } from "../../utilities.ts"; + +describe("regression #5596: missing configured theme export", () => { + const cleanups: Array<() => void> = []; + + afterEach(() => { + while (cleanups.length > 0) { + cleanups.pop()?.(); + } + initTheme("dark"); + }); + + it("exports with the active fallback theme when the configured theme is missing", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "pi-5596-")); + const faux = registerFauxProvider({ + models: [{ id: "faux-1", reasoning: false }], + }); + faux.setResponses([fauxAssistantMessage("hello")]); + + const model = faux.getModel(); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey(model.provider, "faux-key"); + const modelRegistry = ModelRegistry.inMemory(authStorage); + modelRegistry.registerProvider(model.provider, { + baseUrl: model.baseUrl, + apiKey: "faux-key", + api: faux.api, + models: faux.models.map((registeredModel) => ({ + id: registeredModel.id, + name: registeredModel.name, + api: registeredModel.api, + reasoning: registeredModel.reasoning, + input: registeredModel.input, + cost: registeredModel.cost, + contextWindow: registeredModel.contextWindow, + maxTokens: registeredModel.maxTokens, + baseUrl: registeredModel.baseUrl, + })), + }); + + const settingsManager = SettingsManager.inMemory({ theme: "missing-theme" }); + const sessionManager = SessionManager.create(tempDir, join(tempDir, "sessions")); + const agent = new Agent({ + getApiKey: () => "faux-key", + initialState: { + model, + systemPrompt: "You are a test assistant.", + tools: [], + }, + convertToLlm, + }); + const session = new AgentSession({ + agent, + sessionManager, + settingsManager, + cwd: tempDir, + modelRegistry, + resourceLoader: createTestResourceLoader(), + }); + cleanups.push(() => { + session.dispose(); + faux.unregister(); + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + await session.prompt("hi"); + initTheme(settingsManager.getTheme()); + + const outputPath = join(tempDir, "export.html"); + await expect(session.exportToHtml(outputPath)).resolves.toBe(outputPath); + expect(existsSync(outputPath)).toBe(true); + expect(settingsManager.getTheme()).toBe("missing-theme"); + }); +}); From 0caca6cf3fe28693b2152b9af31ac3cdb8168f3e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 18:24:55 +0200 Subject: [PATCH 008/141] fix(tui): support slash-separated fuzzy filter tokens --- packages/tui/CHANGELOG.md | 1 + packages/tui/src/fuzzy.ts | 4 ++-- packages/tui/test/fuzzy.test.ts | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index da5cd46c..6405fe69 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed slash-separated fuzzy queries so provider/model completions remain matchable after insertion. - Fixed WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). ## [0.79.1] - 2026-06-09 diff --git a/packages/tui/src/fuzzy.ts b/packages/tui/src/fuzzy.ts index 35df468a..73c10dcf 100644 --- a/packages/tui/src/fuzzy.ts +++ b/packages/tui/src/fuzzy.ts @@ -94,7 +94,7 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch { /** * Filter and sort items by fuzzy match quality (best matches first). - * Supports space-separated tokens: all tokens must match. + * Supports whitespace- and slash-separated tokens: all tokens must match. */ export function fuzzyFilter(items: T[], query: string, getText: (item: T) => string): T[] { if (!query.trim()) { @@ -103,7 +103,7 @@ export function fuzzyFilter(items: T[], query: string, getText: (item: T) => const tokens = query .trim() - .split(/\s+/) + .split(/[\s/]+/) .filter((t) => t.length > 0); if (tokens.length === 0) { diff --git a/packages/tui/test/fuzzy.test.ts b/packages/tui/test/fuzzy.test.ts index 7415e963..af4662af 100644 --- a/packages/tui/test/fuzzy.test.ts +++ b/packages/tui/test/fuzzy.test.ts @@ -102,4 +102,11 @@ describe("fuzzyFilter", () => { assert.ok(result.map((r) => r.name).includes("foo")); assert.ok(result.map((r) => r.name).includes("foobar")); }); + + it("matches slash-separated provider/model queries against reordered text", () => { + const item = { id: "gpt-5.5", provider: "openai-codex" }; + const result = fuzzyFilter([item], "openai-codex/gpt-5.5", (model) => `${model.id} ${model.provider}`); + + assert.deepStrictEqual(result, [item]); + }); }); From 1b2c32c653582628e17622a4c32b3fbdeeff8ce1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 18:32:23 +0200 Subject: [PATCH 009/141] fix(coding-agent): resolve authenticated slash model ids closes #5643 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/model-resolver.ts | 21 ++++++++++ .../coding-agent/test/model-resolver.test.ts | 41 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index eb2fa5a9..0bf10123 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). - Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 292ccf37..0017df00 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -422,6 +422,27 @@ export function resolveCliModel(options: { }); if (model) { + // If provider inference matched an unauthenticated provider/model pair, prefer + // one exact raw model-id match that is authenticated. This keeps + // "provider/model" syntax preferred when usable, but handles models whose + // literal id starts with a known provider name (for example + // commandcode model id "xiaomi/mimo-v2.5-pro"). + if (inferredProvider) { + const rawExactMatches = availableModels.filter( + (m) => m.id.toLowerCase() === cliModel.toLowerCase() && !modelsAreEqual(m, model), + ); + if (rawExactMatches.length > 0 && !modelRegistry.hasConfiguredAuth(model)) { + const authenticatedRawMatches = rawExactMatches.filter((m) => modelRegistry.hasConfiguredAuth(m)); + if (authenticatedRawMatches.length === 1) { + return { + model: authenticatedRawMatches[0], + thinkingLevel: undefined, + warning: undefined, + error: undefined, + }; + } + } + } return { model, thinkingLevel, warning, error: undefined }; } diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 32861a9e..163c3ab5 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -344,6 +344,7 @@ describe("resolveCliModel", () => { }; const registry = { getAll: () => [...allModels, zaiModel, gatewayModel], + hasConfiguredAuth: () => true, } as unknown as Parameters[0]["modelRegistry"]; const result = resolveCliModel({ @@ -356,6 +357,46 @@ describe("resolveCliModel", () => { expect(result.model?.id).toBe("glm-5"); }); + test("prefers an authenticated exact raw model id over an unauthenticated inferred provider", () => { + const commandcodeModel: Model<"anthropic-messages"> = { + id: "xiaomi/mimo-v2.5-pro", + name: "Xiaomi MiMo via Commandcode", + api: "anthropic-messages", + provider: "commandcode", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 8192, + }; + const xiaomiModel: Model<"anthropic-messages"> = { + id: "mimo-v2.5-pro", + name: "Xiaomi MiMo", + api: "anthropic-messages", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com", + reasoning: false, + input: ["text"], + cost: { input: 1, output: 2, cacheRead: 0.1, cacheWrite: 1 }, + contextWindow: 128000, + maxTokens: 8192, + }; + const registry = { + getAll: () => [...allModels, commandcodeModel, xiaomiModel], + hasConfiguredAuth: (model: Model<"anthropic-messages">) => model.provider === "commandcode", + } as unknown as Parameters[0]["modelRegistry"]; + + const result = resolveCliModel({ + cliModel: "xiaomi/mimo-v2.5-pro", + modelRegistry: registry, + }); + + expect(result.error).toBeUndefined(); + expect(result.model?.provider).toBe("commandcode"); + expect(result.model?.id).toBe("xiaomi/mimo-v2.5-pro"); + }); + test("resolves provider-prefixed fuzzy patterns (openrouter/qwen -> openrouter model)", () => { const registry = { getAll: () => allModels, From adf567c1c63228fb545e9abf7b12d7af19a049ee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 18:49:49 +0200 Subject: [PATCH 010/141] fix(coding-agent): rechain fork paths without labels closes #5669 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/session-manager.ts | 12 ++++++++++-- .../test/session-manager/labels.test.ts | 13 +++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0bf10123..2927a100 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). +- Fixed `/fork` to keep session parent chains connected when the forked path contains labels ([#5669](https://github.com/earendil-works/pi/issues/5669)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). - Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index c2bae164..62942480 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -1290,8 +1290,16 @@ export class SessionManager { throw new Error(`Entry ${leafId} not found`); } - // Filter out LabelEntry from path - we'll recreate them from the resolved map - const pathWithoutLabels = path.filter((e) => e.type !== "label"); + // Filter out LabelEntry from path - we'll recreate them from the resolved map. + // Because labels are real tree entries, later entries can be children of labels; + // removing labels requires re-chaining the retained path to avoid orphaned subtrees. + const pathWithoutLabels: SessionEntry[] = []; + let pathParentId: string | null = null; + for (const entry of path) { + if (entry.type === "label") continue; + pathWithoutLabels.push({ ...entry, parentId: pathParentId }); + pathParentId = entry.id; + } const newSessionId = createSessionId(); const timestamp = new Date().toISOString(); diff --git a/packages/coding-agent/test/session-manager/labels.test.ts b/packages/coding-agent/test/session-manager/labels.test.ts index 7500746e..353454c7 100644 --- a/packages/coding-agent/test/session-manager/labels.test.ts +++ b/packages/coding-agent/test/session-manager/labels.test.ts @@ -142,6 +142,19 @@ describe("SessionManager labels", () => { expect(msg2Node?.labelTimestamp).toBe(msg2LabelEntry.timestamp); }); + it("rewires children of removed labels when forking", () => { + const session = SessionManager.inMemory(); + + const msg1Id = session.appendMessage({ role: "user", content: "hello", timestamp: 1 }); + session.appendLabelChange(msg1Id, "checkpoint"); + const modelChangeId = session.appendModelChange("anthropic", "claude-test"); + const msg2Id = session.appendMessage({ role: "user", content: "followup", timestamp: 2 }); + + session.createBranchedSession(msg2Id); + + expect(session.getEntry(modelChangeId)?.parentId).toBe(msg1Id); + }); + it("labels not on path are not preserved in createBranchedSession", () => { const session = SessionManager.inMemory(); From daab056ac16d2aa7447f99205ae6e3d1c19ecfa6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 18:59:11 +0200 Subject: [PATCH 011/141] fix(agent): ignore late tool progress updates closes #5573 --- packages/agent/CHANGELOG.md | 4 + packages/agent/src/agent-loop.ts | 6 ++ packages/agent/src/types.ts | 7 +- packages/agent/test/agent.test.ts | 166 +++++++++++++++++++++++++++++- 4 files changed, 181 insertions(+), 2 deletions(-) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index c1c02515..287f07c3 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed late tool progress callbacks after tool settlement to be ignored instead of emitting stale `tool_execution_update` events ([#5573](https://github.com/earendil-works/pi/issues/5573)). + ## [0.79.1] - 2026-06-09 ## [0.79.0] - 2026-06-08 diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 28f037f5..77bd9568 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -631,6 +631,7 @@ async function executePreparedToolCall( emit: AgentEventSink, ): Promise { const updateEvents: Promise[] = []; + let acceptingUpdates = true; try { const result = await prepared.tool.execute( @@ -638,6 +639,7 @@ async function executePreparedToolCall( prepared.args as never, signal, (partialResult) => { + if (!acceptingUpdates) return; updateEvents.push( Promise.resolve( emit({ @@ -651,14 +653,18 @@ async function executePreparedToolCall( ); }, ); + acceptingUpdates = false; await Promise.all(updateEvents); return { result, isError: false }; } catch (error) { + acceptingUpdates = false; await Promise.all(updateEvents); return { result: createErrorToolResult(error instanceof Error ? error.message : String(error)), isError: true, }; + } finally { + acceptingUpdates = false; } } diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index f24d2496..cb99a79a 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -354,7 +354,12 @@ export interface AgentToolResult { terminate?: boolean; } -/** Callback used by tools to stream partial execution updates. */ +/** + * Callback used by tools to stream partial execution updates. + * + * The callback is scoped to the current `execute()` invocation. Calls made after + * the tool promise settles are ignored. + */ export type AgentToolUpdateCallback = (partialResult: AgentToolResult) => void; /** Tool definition used by the agent runtime. */ diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 82cc58de..4cc51f74 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -1,6 +1,7 @@ import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai"; +import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { Agent } from "../src/index.ts"; +import { Agent, type AgentEvent, type AgentTool, type AgentToolUpdateCallback } from "../src/index.ts"; // Mock stream that mimics AssistantMessageEventStream class MockAssistantStream extends EventStream { @@ -36,6 +37,28 @@ function createAssistantMessage(text: string): AssistantMessage { }; } +type ToolCallContent = Extract; + +function createAssistantToolUseMessage(content: ToolCallContent[]): AssistantMessage { + return { + role: "assistant", + content, + api: "openai-responses", + provider: "openai", + model: "mock", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "toolUse", + timestamp: Date.now(), + }; +} + function createDeferred(): { promise: Promise; resolve: () => void; @@ -242,6 +265,147 @@ describe("Agent", () => { expect(receivedSignal?.aborted).toBe(true); }); + it("should ignore tool updates after the tool execution settles", async () => { + const toolSchema = Type.Object({}); + let delayedUpdate: AgentToolUpdateCallback<{ status: string }> | undefined; + const events: AgentEvent[] = []; + const unhandledRejections: unknown[] = []; + const onUnhandledRejection = (error: unknown) => { + unhandledRejections.push(error); + }; + const tool: AgentTool = { + name: "delayed_tool", + label: "Delayed Tool", + description: "Captures progress callbacks", + parameters: toolSchema, + async execute(_toolCallId, _params, _signal, onUpdate) { + delayedUpdate = onUpdate; + onUpdate?.({ + content: [{ type: "text", text: "running" }], + details: { status: "running" }, + }); + return { + content: [{ type: "text", text: "ok" }], + details: { status: "done" }, + terminate: true, + }; + }, + }; + const agent = new Agent({ + initialState: { tools: [tool] }, + streamFn: () => { + const stream = new MockAssistantStream(); + queueMicrotask(() => { + stream.push({ + type: "done", + reason: "toolUse", + message: createAssistantToolUseMessage([ + { type: "toolCall", id: "call-1", name: "delayed_tool", arguments: {} }, + ]), + }); + }); + return stream; + }, + }); + agent.subscribe((event) => { + events.push(event); + }); + + process.on("unhandledRejection", onUnhandledRejection); + try { + await agent.prompt("run tool"); + const eventCountAfterPrompt = events.length; + + delayedUpdate?.({ + content: [{ type: "text", text: "late" }], + details: { status: "late" }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(events.filter((event) => event.type === "tool_execution_update")).toHaveLength(1); + expect(events).toHaveLength(eventCountAfterPrompt); + expect(unhandledRejections).toEqual([]); + } finally { + process.off("unhandledRejection", onUnhandledRejection); + } + }); + + it("should ignore a settled parallel tool update while another tool is still running", async () => { + const toolSchema = Type.Object({}); + const slowStarted = createDeferred(); + const settledToolEnded = createDeferred(); + const releaseSlow = createDeferred(); + let settledToolUpdate: AgentToolUpdateCallback<{ status: string }> | undefined; + const events: AgentEvent[] = []; + const settledTool: AgentTool = { + name: "settled_tool", + label: "Settled Tool", + description: "Captures progress callbacks", + parameters: toolSchema, + async execute(_toolCallId, _params, _signal, onUpdate) { + settledToolUpdate = onUpdate; + return { + content: [{ type: "text", text: "done" }], + details: { status: "done" }, + terminate: true, + }; + }, + }; + const slowTool: AgentTool = { + name: "slow_tool", + label: "Slow Tool", + description: "Keeps the agent run active", + parameters: toolSchema, + async execute() { + slowStarted.resolve(); + await releaseSlow.promise; + return { + content: [{ type: "text", text: "done" }], + details: { status: "done" }, + terminate: true, + }; + }, + }; + const agent = new Agent({ + initialState: { tools: [settledTool, slowTool] }, + streamFn: () => { + const stream = new MockAssistantStream(); + queueMicrotask(() => { + stream.push({ + type: "done", + reason: "toolUse", + message: createAssistantToolUseMessage([ + { type: "toolCall", id: "call-1", name: "settled_tool", arguments: {} }, + { type: "toolCall", id: "call-2", name: "slow_tool", arguments: {} }, + ]), + }); + }); + return stream; + }, + }); + agent.subscribe((event) => { + events.push(event); + if (event.type === "tool_execution_end" && event.toolCallId === "call-1") { + settledToolEnded.resolve(); + } + }); + + const promptPromise = agent.prompt("run tools"); + await Promise.all([slowStarted.promise, settledToolEnded.promise]); + const eventCountBeforeLateUpdate = events.length; + + settledToolUpdate?.({ + content: [{ type: "text", text: "late" }], + details: { status: "late" }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(events).toHaveLength(eventCountBeforeLateUpdate); + + releaseSlow.resolve(); + await promptPromise; + expect(events.filter((event) => event.type === "tool_execution_update")).toHaveLength(0); + }); + it("should update state with mutators", () => { const agent = new Agent(); From 17721d5e0153ad761fa6155eb90d62b9a5ee3283 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 20:22:53 +0200 Subject: [PATCH 012/141] fix(tui): preserve unordered user list markers (closes #5657) --- packages/coding-agent/CHANGELOG.md | 1 + packages/tui/CHANGELOG.md | 1 + packages/tui/src/components/markdown.ts | 11 +++++++++-- packages/tui/test/markdown.test.ts | 17 ++++++++++++++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2927a100..e770ebdb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed inherited user-message transcript rendering so standalone `+` messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). - Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). - Fixed `/fork` to keep session parent chains connected when the forked path contains labels ([#5669](https://github.com/earendil-works/pi/issues/5669)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 6405fe69..b5e277a9 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Markdown source list marker preservation to include unordered markers, so standalone `+` user messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). - Fixed slash-separated fuzzy queries so provider/model completions remain matchable after insertion. - Fixed WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index ed8ad780..83f15aa5 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -71,7 +71,7 @@ export interface MarkdownTheme { } export interface MarkdownOptions { - /** Preserve source ordered-list markers instead of normalizing them from the list start. */ + /** Preserve source list markers instead of normalizing them. */ preserveOrderedListMarkers?: boolean; } @@ -561,6 +561,11 @@ export class Markdown implements Component { return match ? `${match[1]} ` : undefined; } + private getUnorderedListMarker(item: Tokens.ListItem): string | undefined { + const match = /^(?: {0,3})([-+*])(?:[ \t]+|(?=\r?\n|$))/.exec(item.raw); + return match ? `${match[1]} ` : undefined; + } + /** * Render a list with proper nesting support */ @@ -577,7 +582,9 @@ export class Markdown implements Component { ? this.options.preserveOrderedListMarkers ? (this.getOrderedListMarker(item) ?? `${startNumber + i}. `) : `${startNumber + i}. ` - : "- "; + : this.options.preserveOrderedListMarkers + ? (this.getUnorderedListMarker(item) ?? "- ") + : "- "; const taskMarker = item.task ? `[${item.checked ? "x" : " "}] ` : ""; const marker = bullet + taskMarker; const firstPrefix = indent + this.theme.listBullet(marker); diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index 9c0be05c..dc595b11 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -112,9 +112,9 @@ describe("Markdown component", () => { assert.deepStrictEqual(lines, ["1. alpha", "2. beta", "3. gamma"]); }); - it("should preserve source ordered list markers when configured", () => { + it("should preserve source list markers when configured", () => { const markdown = new Markdown( - " 4. forth\n 3. third\n\n10) ten\n7) seven", + " 4. forth\n 3. third\n\n10) ten\n7) seven\n\n+ plus\n* star\n- minus\n+", 0, 0, defaultMarkdownTheme, @@ -126,7 +126,18 @@ describe("Markdown component", () => { const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); - assert.deepStrictEqual(lines, ["4. forth", "3. third", "", "10) ten", "7) seven"]); + assert.deepStrictEqual(lines, [ + "4. forth", + "3. third", + "", + "10) ten", + "7) seven", + "", + "+ plus", + "* star", + "- minus", + "+", + ]); }); it("should render mixed ordered and unordered nested lists", () => { From a7cdc679e715b5bd36ea11fdb5b26db706746009 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 23:03:10 +0200 Subject: [PATCH 013/141] fix(ai): correct GPT-5 context window metadata closes #5644 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 14 +- packages/ai/src/models.generated.ts | 923 +++++++----------- .../ai/test/openai-model-metadata.test.ts | 15 + 4 files changed, 374 insertions(+), 579 deletions(-) create mode 100644 packages/ai/test/openai-model-metadata.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 93fc9a80..b7b1112d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,7 @@ ### Fixed +- Fixed OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to match current OpenAI limits ([#5644](https://github.com/earendil-works/pi/issues/5644)). - Increased the OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 5e5fff94..a3892cf1 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1363,7 +1363,7 @@ async function generateModels() { candidate.maxTokens = 128000; } if (candidate.provider === "openai" && (candidate.id === "gpt-5.4" || candidate.id === "gpt-5.5")) { - candidate.contextWindow = 272000; + candidate.contextWindow = 1050000; candidate.maxTokens = 128000; } // models.dev reports gpt-5-pro output as 272000 (a duplicate of the input sub-limit); @@ -1630,7 +1630,7 @@ async function generateModels() { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 1050000, maxTokens: 128000, }); } @@ -1757,9 +1757,9 @@ async function generateModels() { // OpenAI Codex (ChatGPT OAuth) models // NOTE: These are not fetched from models.dev; we keep a small, explicit list to avoid aliases. - // Context window is based on observed server limits (400s above ~272k), not marketing numbers. const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; - const CODEX_CONTEXT = 272000; + const CODEX_GPT_54_CONTEXT = 1000000; + const CODEX_STANDARD_CONTEXT = 400000; const CODEX_SPARK_CONTEXT = 128000; const CODEX_MAX_TOKENS = 128000; const codexModels: Model<"openai-codex-responses">[] = [ @@ -1784,7 +1784,7 @@ async function generateModels() { reasoning: true, input: ["text", "image"], cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, + contextWindow: CODEX_GPT_54_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { @@ -1796,7 +1796,7 @@ async function generateModels() { reasoning: true, input: ["text", "image"], cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, + contextWindow: CODEX_STANDARD_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { @@ -1808,7 +1808,7 @@ async function generateModels() { reasoning: true, input: ["text", "image"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, + contextWindow: CODEX_STANDARD_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, ]; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 57da60bc..eeed2e92 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -357,7 +357,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.eu-central-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 11, @@ -497,7 +497,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -1389,7 +1389,7 @@ export const MODELS = { provider: "amazon-bedrock", baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -1878,7 +1878,7 @@ export const MODELS = { baseUrl: "https://api.anthropic.com", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -2919,30 +2919,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.25, - output: 0.69, + input: 0.35, + output: 0.75, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "llama3.1-8b": { - id: "llama3.1-8b", - name: "Llama 3.1 8B", - api: "openai-completions", - provider: "cerebras", - baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.1, - output: 0.1, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32000, - maxTokens: 8000, + maxTokens: 40960, } satisfies Model<"openai-completions">, "zai-glm-4.7": { id: "zai-glm-4.7", @@ -2950,7 +2933,7 @@ export const MODELS = { api: "openai-completions", provider: "cerebras", baseUrl: "https://api.cerebras.ai/v1", - reasoning: false, + reasoning: true, input: ["text"], cost: { input: 2.25, @@ -2959,7 +2942,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 40000, + maxTokens: 40960, } satisfies Model<"openai-completions">, }, "cloudflare-ai-gateway": { @@ -3073,7 +3056,7 @@ export const MODELS = { baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -3722,6 +3705,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 256000, } satisfies Model<"openai-completions">, + "@cf/moonshotai/kimi-k2.7-code": { + id: "@cf/moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, "@cf/nvidia/nemotron-3-120b-a12b": { id: "@cf/nvidia/nemotron-3-120b-a12b", name: "Nemotron 3 Super 120B", @@ -4090,6 +4091,25 @@ export const MODELS = { } satisfies Model<"anthropic-messages">, }, "github-copilot": { + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + api: "openai-completions", + provider: "github-copilot", + baseUrl: "https://api.individual.githubcopilot.com", + headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 10, + output: 50, + cacheRead: 1, + cacheWrite: 12.5, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "claude-haiku-4.5": { id: "claude-haiku-4.5", name: "Claude Haiku 4.5 (latest)", @@ -5022,77 +5042,9 @@ export const MODELS = { } satisfies Model<"google-vertex">, }, "groq": { - "deepseek-r1-distill-llama-70b": { - id: "deepseek-r1-distill-llama-70b", - name: "DeepSeek R1 Distill Llama 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.75, - output: 0.99, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "gemma2-9b-it": { - id: "gemma2-9b-it", - name: "Gemma 2 9B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 0.2, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound": { - id: "groq/compound", - name: "Compound", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "groq/compound-mini": { - id: "groq/compound-mini", - name: "Compound Mini", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "llama-3.1-8b-instant": { id: "llama-3.1-8b-instant", - name: "Llama 3.1 8B Instant", + name: "Llama 3.1 8B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5109,7 +5061,7 @@ export const MODELS = { } satisfies Model<"openai-completions">, "llama-3.3-70b-versatile": { id: "llama-3.3-70b-versatile", - name: "Llama 3.3 70B Versatile", + name: "Llama 3.3 70B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5124,60 +5076,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, - "llama3-70b-8192": { - id: "llama3-70b-8192", - name: "Llama 3 70B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.59, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "llama3-8b-8192": { - id: "llama3-8b-8192", - name: "Llama 3 8B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.05, - output: 0.08, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "meta-llama/llama-4-maverick-17b-128e-instruct": { - id: "meta-llama/llama-4-maverick-17b-128e-instruct", - name: "Llama 4 Maverick 17B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "meta-llama/llama-4-scout-17b-16e-instruct": { id: "meta-llama/llama-4-scout-17b-16e-instruct", - name: "Llama 4 Scout 17B", + name: "Llama 4 Scout 17B 16E", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5192,57 +5093,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 8192, } satisfies Model<"openai-completions">, - "mistral-saba-24b": { - id: "mistral-saba-24b", - name: "Mistral Saba 24B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.79, - output: 0.79, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 32768, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct": { - id: "moonshotai/kimi-k2-instruct", - name: "Kimi K2 Instruct", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2-instruct-0905": { - id: "moonshotai/kimi-k2-instruct-0905", - name: "Kimi K2 Instruct 0905", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: false, - input: ["text"], - cost: { - input: 1, - output: 3, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "openai/gpt-oss-120b": { id: "openai/gpt-oss-120b", name: "GPT OSS 120B", @@ -5294,26 +5144,9 @@ export const MODELS = { contextWindow: 131072, maxTokens: 65536, } satisfies Model<"openai-completions">, - "qwen-qwq-32b": { - id: "qwen-qwq-32b", - name: "Qwen QwQ 32B", - api: "openai-completions", - provider: "groq", - baseUrl: "https://api.groq.com/openai/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.29, - output: 0.39, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "qwen/qwen3-32b": { id: "qwen/qwen3-32b", - name: "Qwen3 32B", + name: "Qwen3-32B", api: "openai-completions", provider: "groq", baseUrl: "https://api.groq.com/openai/v1", @@ -5729,6 +5562,24 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "kimi-coding": { + "k2p7": { + id: "k2p7", + name: "Kimi K2.7 Code", + api: "anthropic-messages", + provider: "kimi-coding", + baseUrl: "https://api.kimi.com/coding", + headers: {"User-Agent":"KimiCLI/1.5"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, "kimi-for-coding": { id: "kimi-for-coding", name: "Kimi For Coding", @@ -6511,6 +6362,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, }, "moonshotai-cn": { "kimi-k2-0711-preview": { @@ -6763,8 +6632,8 @@ export const MODELS = { baseUrl: "https://integrate.api.nvidia.com/v1", headers: {"NVCF-POLL-SECONDS":"3600"}, compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], + reasoning: true, + input: ["text", "image"], cost: { input: 0, output: 0, @@ -6793,44 +6662,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1", - name: "Llama 3.3 Nemotron Super 49B v1", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "nvidia/llama-3.3-nemotron-super-49b-v1.5": { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - name: "Llama 3.3 Nemotron Super 49B v1.5", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "nvidia/nemotron-3-nano-30b-a3b": { id: "nvidia/nemotron-3-nano-30b-a3b", name: "nemotron-3-nano-30b-a3b", @@ -6926,6 +6757,25 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, + "openai/gpt-oss-120b": { + id: "openai/gpt-oss-120b", + name: "GPT-OSS-120B", + api: "openai-completions", + provider: "nvidia", + baseUrl: "https://integrate.api.nvidia.com/v1", + headers: {"NVCF-POLL-SECONDS":"3600"}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 8192, + } satisfies Model<"openai-completions">, "openai/gpt-oss-20b": { id: "openai/gpt-oss-20b", name: "GPT OSS 20B", @@ -6945,25 +6795,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, - "qwen/qwen3-coder-480b-a35b-instruct": { - id: "qwen/qwen3-coder-480b-a35b-instruct", - name: "Qwen3 Coder 480B A35B Instruct", - api: "openai-completions", - provider: "nvidia", - baseUrl: "https://integrate.api.nvidia.com/v1", - headers: {"NVCF-POLL-SECONDS":"3600"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 66536, - } satisfies Model<"openai-completions">, "qwen/qwen3.5-122b-a10b": { id: "qwen/qwen3.5-122b-a10b", name: "Qwen3.5 122B-A10B", @@ -7551,7 +7382,7 @@ export const MODELS = { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"openai-responses">, "gpt-5.4-mini": { @@ -7623,7 +7454,7 @@ export const MODELS = { cacheRead: 0.5, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 1050000, maxTokens: 128000, } satisfies Model<"openai-responses">, "gpt-5.5-pro": { @@ -7815,7 +7646,7 @@ export const MODELS = { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, "gpt-5.4-mini": { @@ -7833,7 +7664,7 @@ export const MODELS = { cacheRead: 0.075, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 400000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, "gpt-5.5": { @@ -7851,7 +7682,7 @@ export const MODELS = { cacheRead: 0.5, cacheWrite: 0, }, - contextWindow: 272000, + contextWindow: 400000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, }, @@ -7882,7 +7713,7 @@ export const MODELS = { baseUrl: "https://opencode.ai/zen", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -8066,7 +7897,7 @@ export const MODELS = { cost: { input: 0.14, output: 0.28, - cacheRead: 0.03, + cacheRead: 0.028, cacheWrite: 0, }, contextWindow: 1000000, @@ -8091,6 +7922,25 @@ export const MODELS = { contextWindow: 200000, maxTokens: 128000, } satisfies Model<"openai-completions">, + "deepseek-v4-pro": { + id: "deepseek-v4-pro", + name: "DeepSeek V4 Pro", + api: "openai-completions", + provider: "opencode", + baseUrl: "https://opencode.ai/zen/v1", + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, + input: ["text"], + cost: { + input: 1.74, + output: 3.84, + cacheRead: 0.145, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 384000, + } satisfies Model<"openai-completions">, "gemini-3-flash": { id: "gemini-3-flash", name: "Gemini 3 Flash", @@ -8724,24 +8574,6 @@ export const MODELS = { contextWindow: 202752, maxTokens: 32768, } satisfies Model<"openai-completions">, - "kimi-k2.5": { - id: "kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, "kimi-k2.6": { id: "kimi-k2.6", name: "Kimi K2.6", @@ -8761,6 +8593,24 @@ export const MODELS = { contextWindow: 262144, maxTokens: 65536, } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, "mimo-v2.5": { id: "mimo-v2.5", name: "MiMo V2.5", @@ -8797,23 +8647,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 128000, } satisfies Model<"openai-completions">, - "minimax-m2.5": { - id: "minimax-m2.5", - name: "MiniMax M2.5", - api: "anthropic-messages", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go", - reasoning: true, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 65536, - } satisfies Model<"anthropic-messages">, "minimax-m2.7": { id: "minimax-m2.7", name: "MiniMax M2.7", @@ -8834,16 +8667,16 @@ export const MODELS = { } satisfies Model<"openai-completions">, "minimax-m3": { id: "minimax-m3", - name: "MiniMax M3", + name: "MiniMax M3 (3x usage)", api: "anthropic-messages", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go", reasoning: true, input: ["text", "image"], cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, + input: 0.1, + output: 0.4, + cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 512000, @@ -9600,13 +9433,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], cost: { - input: 0.0983, - output: 0.1966, - cacheRead: 0.019700000000000002, + input: 0.098, + output: 0.196, + cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 4096, } satisfies Model<"openai-completions">, "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", @@ -9926,12 +9759,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.12, - output: 0.36, + output: 0.35, cacheRead: 0.09, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 8192, + maxTokens: 262144, } satisfies Model<"openai-completions">, "google/gemma-4-31b-it:free": { id: "google/gemma-4-31b-it:free", @@ -10232,9 +10065,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.27, - output: 1.08, - cacheRead: 0.054, + input: 0.25, + output: 1, + cacheRead: 0.049999999999999996, cacheWrite: 0, }, contextWindow: 204800, @@ -10632,19 +10465,18 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262142, } satisfies Model<"openai-completions">, - "moonshotai/kimi-k2.6:free": { - id: "moonshotai/kimi-k2.6:free", - name: "MoonshotAI: Kimi K2.6 (free)", + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "MoonshotAI: Kimi K2.7 Code", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", - compat: {"supportsDeveloperRole":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, input: ["text", "image"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 0.95, + output: 4, + cacheRead: 0.19, cacheWrite: 0, }, contextWindow: 262144, @@ -10820,23 +10652,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 128000, } satisfies Model<"openai-completions">, - "nvidia/nemotron-nano-9b-v2": { - id: "nvidia/nemotron-nano-9b-v2", - name: "NVIDIA: Nemotron Nano 9B V2", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.04, - output: 0.16, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 16384, - } satisfies Model<"openai-completions">, "nvidia/nemotron-nano-9b-v2:free": { id: "nvidia/nemotron-nano-9b-v2:free", name: "NVIDIA: Nemotron Nano 9B V2 (free)", @@ -12558,13 +12373,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.28900000000000003, - output: 2.4, + input: 0.28850000000000003, + output: 3.17, cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 131072, + maxTokens: 262140, } satisfies Model<"openai-completions">, "qwen/qwen3.6-35b-a3b": { id: "qwen/qwen3.6-35b-a3b", @@ -12575,13 +12390,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.14, + input: 0.15, output: 1, - cacheRead: 0, + cacheRead: 0.049999999999999996, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262140, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.6-flash": { id: "qwen/qwen3.6-flash", @@ -12660,10 +12475,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.08, - cacheWrite: 0.5, + input: 0.32, + output: 1.28, + cacheRead: 0.064, + cacheWrite: 0.39999999999999997, }, contextWindow: 1000000, maxTokens: 65536, @@ -12923,23 +12738,6 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, - "z-ai/glm-4-32b": { - id: "z-ai/glm-4-32b", - name: "Z.ai: GLM 4 32B ", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text"], - cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, "z-ai/glm-4.5": { id: "z-ai/glm-4.5", name: "Z.ai: GLM 4.5", @@ -12974,23 +12772,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131070, } satisfies Model<"openai-completions">, - "z-ai/glm-4.5-air:free": { - id: "z-ai/glm-4.5-air:free", - name: "Z.ai: GLM 4.5 Air (free)", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 96000, - } satisfies Model<"openai-completions">, "z-ai/glm-4.5v": { id: "z-ai/glm-4.5v", name: "Z.ai: GLM 4.5V", @@ -13036,11 +12817,11 @@ export const MODELS = { cost: { input: 0.3, output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + cacheRead: 0.055, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 24000, + maxTokens: 32768, } satisfies Model<"openai-completions">, "z-ai/glm-4.7": { id: "z-ai/glm-4.7", @@ -13107,7 +12888,7 @@ export const MODELS = { cacheRead: 0.24, cacheWrite: 0, }, - contextWindow: 202752, + contextWindow: 262144, maxTokens: 131072, } satisfies Model<"openai-completions">, "z-ai/glm-5.1": { @@ -13127,23 +12908,6 @@ export const MODELS = { contextWindow: 202752, maxTokens: 4096, } satisfies Model<"openai-completions">, - "z-ai/glm-5v-turbo": { - id: "z-ai/glm-5v-turbo", - name: "Z.ai: GLM 5V Turbo", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.2, - output: 4, - cacheRead: 0.24, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", name: "Anthropic: Claude Fable Latest", @@ -13299,25 +13063,6 @@ export const MODELS = { } satisfies Model<"openai-completions">, }, "together": { - "MiniMaxAI/MiniMax-M2.5": { - id: "MiniMaxAI/MiniMax-M2.5", - name: "MiniMax-M2.5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.06, - cacheWrite: 0, - }, - contextWindow: 204800, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "MiniMaxAI/MiniMax-M2.7": { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax-M2.7", @@ -13337,28 +13082,9 @@ export const MODELS = { contextWindow: 202752, maxTokens: 131072, } satisfies Model<"openai-completions">, - "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { - id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", - name: "Qwen3 235B A22B Instruct 2507 FP8", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.2, - output: 0.6, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { - id: "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", - name: "Qwen3 Coder 480B A35B Instruct", + "Qwen/Qwen2.5-7B-Instruct-Turbo": { + id: "Qwen/Qwen2.5-7B-Instruct-Turbo", + name: "Qwen 2.5 7B Instruct Turbo", api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", @@ -13366,27 +13092,26 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 2, - output: 2, + input: 0.3, + output: 0.3, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 262144, + contextWindow: 32768, + maxTokens: 32768, } satisfies Model<"openai-completions">, - "Qwen/Qwen3-Coder-Next-FP8": { - id: "Qwen/Qwen3-Coder-Next-FP8", - name: "Qwen3 Coder Next FP8", + "Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { + id: "Qwen/Qwen3-235B-A22B-Instruct-2507-tput", + name: "Qwen3 235B A22B Instruct 2507 FP8", api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, input: ["text"], cost: { - input: 0.5, - output: 1.2, + input: 0.2, + output: 0.6, cacheRead: 0, cacheWrite: 0, }, @@ -13412,6 +13137,25 @@ export const MODELS = { contextWindow: 262144, maxTokens: 130000, } satisfies Model<"openai-completions">, + "Qwen/Qwen3.5-9B": { + id: "Qwen/Qwen3.5-9B", + name: "Qwen3.5 9B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.17, + output: 0.25, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 65536, + } satisfies Model<"openai-completions">, "Qwen/Qwen3.6-Plus": { id: "Qwen/Qwen3.6-Plus", name: "Qwen3.6 Plus", @@ -13437,9 +13181,8 @@ export const MODELS = { api: "openai-completions", provider: "together", baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false}, + reasoning: false, input: ["text"], cost: { input: 2.5, @@ -13450,44 +13193,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 500000, } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3": { - id: "deepseek-ai/DeepSeek-V3", - name: "DeepSeek-V3", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 1.25, - output: 1.25, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "deepseek-ai/DeepSeek-V3-1": { - id: "deepseek-ai/DeepSeek-V3-1", - name: "DeepSeek V3.1", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text"], - cost: { - input: 0.6, - output: 1.7, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, "deepseek-ai/DeepSeek-V4-Pro": { id: "deepseek-ai/DeepSeek-V4-Pro", name: "DeepSeek V4 Pro", @@ -13499,8 +13204,8 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":null}, input: ["text"], cost: { - input: 2.1, - output: 4.4, + input: 1.74, + output: 3.48, cacheRead: 0.2, cacheWrite: 0, }, @@ -13536,8 +13241,8 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], cost: { - input: 0.2, - output: 0.5, + input: 0.39, + output: 0.97, cacheRead: 0, cacheWrite: 0, }, @@ -13562,25 +13267,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, - "moonshotai/Kimi-K2.5": { - id: "moonshotai/Kimi-K2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "together", - baseUrl: "https://api.together.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, - reasoning: true, - thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, - input: ["text", "image"], - cost: { - input: 0.5, - output: 2.8, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 262144, - } satisfies Model<"openai-completions">, "moonshotai/Kimi-K2.6": { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", @@ -13638,6 +13324,44 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, + "openai/gpt-oss-20b": { + id: "openai/gpt-oss-20b", + name: "GPT OSS 20B", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":true,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"openai"}, + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":null}, + input: ["text"], + cost: { + input: 0.05, + output: 0.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "zai-org/GLM-5": { + id: "zai-org/GLM-5", + name: "GLM-5", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 1, + output: 3.2, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 202752, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "zai-org/GLM-5.1": { id: "zai-org/GLM-5.1", name: "GLM-5.1", @@ -14075,7 +13799,7 @@ export const MODELS = { baseUrl: "https://ai-gateway.vercel.sh", compat: {"forceAdaptiveThinking":true}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 10, @@ -15215,40 +14939,6 @@ export const MODELS = { contextWindow: 262114, maxTokens: 262114, } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-thinking-turbo": { - id: "moonshotai/kimi-k2-thinking-turbo", - name: "Kimi K2 Thinking Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: true, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 262114, - maxTokens: 262114, - } satisfies Model<"anthropic-messages">, - "moonshotai/kimi-k2-turbo": { - id: "moonshotai/kimi-k2-turbo", - name: "Kimi K2 Turbo", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - reasoning: false, - input: ["text"], - cost: { - input: 1.15, - output: 8, - cacheRead: 0.15, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 16384, - } satisfies Model<"anthropic-messages">, "moonshotai/kimi-k2.5": { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5", @@ -15283,6 +14973,23 @@ export const MODELS = { contextWindow: 262000, maxTokens: 262000, } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.7-code": { + id: "moonshotai/kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 32768, + } satisfies Model<"anthropic-messages">, "nvidia/nemotron-3-super-120b-a12b": { id: "nvidia/nemotron-3-super-120b-a12b", name: "NVIDIA Nemotron 3 Super 120B A12B", @@ -16749,6 +16456,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi", + baseUrl: "https://api.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-ams": { "mimo-v2-omni": { @@ -16823,6 +16548,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-ams", + baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-cn": { "mimo-v2-omni": { @@ -16897,6 +16640,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-cn", + baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "xiaomi-token-plan-sgp": { "mimo-v2-omni": { @@ -16971,6 +16732,24 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 131072, } satisfies Model<"openai-completions">, + "mimo-v2.5-pro-ultraspeed": { + id: "mimo-v2.5-pro-ultraspeed", + name: "MiMo-V2.5-Pro-UltraSpeed", + api: "openai-completions", + provider: "xiaomi-token-plan-sgp", + baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", + compat: {"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.305, + output: 2.61, + cacheRead: 0.0108, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, }, "zai": { "glm-4.5-air": { diff --git a/packages/ai/test/openai-model-metadata.test.ts b/packages/ai/test/openai-model-metadata.test.ts new file mode 100644 index 00000000..64c593e1 --- /dev/null +++ b/packages/ai/test/openai-model-metadata.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { getModel } from "../src/models.ts"; + +describe("OpenAI model metadata", () => { + it("uses current GPT-5.4 and GPT-5.5 API context windows", () => { + expect(getModel("openai", "gpt-5.4").contextWindow).toBe(1050000); + expect(getModel("openai", "gpt-5.5").contextWindow).toBe(1050000); + }); + + it("uses current OpenAI Codex context windows", () => { + expect(getModel("openai-codex", "gpt-5.4").contextWindow).toBe(1000000); + expect(getModel("openai-codex", "gpt-5.4-mini").contextWindow).toBe(400000); + expect(getModel("openai-codex", "gpt-5.5").contextWindow).toBe(400000); + }); +}); From b4bff7f0d0eb074e56fbf2665a7bc6798567d701 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 23:37:16 +0200 Subject: [PATCH 014/141] fix(coding-agent): avoid project trust prompt for update (#5674) --- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/README.md | 8 +- packages/coding-agent/docs/security.md | 14 ++- packages/coding-agent/docs/settings.md | 6 +- packages/coding-agent/docs/usage.md | 8 +- .../coding-agent/src/core/project-trust.ts | 4 +- .../coding-agent/src/core/trust-manager.ts | 41 ++++--- packages/coding-agent/src/index.ts | 2 +- packages/coding-agent/src/main.ts | 14 ++- .../interactive/components/trust-selector.ts | 14 ++- .../src/modes/interactive/interactive-mode.ts | 6 +- .../coding-agent/src/package-manager-cli.ts | 15 ++- .../test/package-command-paths.test.ts | 66 ++++++++++- .../coding-agent/test/resource-loader.test.ts | 2 +- .../coding-agent/test/trust-manager.test.ts | 108 ++++++------------ 15 files changed, 185 insertions(+), 125 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e770ebdb..1322e247 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,7 +8,9 @@ ### Fixed +- Fixed project trust detection to ignore global `~/.pi/agent` state when running from `$HOME`, and made `pi update` use only saved or explicit project trust without prompting ([#5619](https://github.com/earendil-works/pi/issues/5619)). - Fixed inherited user-message transcript rendering so standalone `+` messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). +>>>>>>> main - Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). - Fixed `/fork` to keep session parent chains connected when the forked path contains labels ([#5669](https://github.com/earendil-works/pi/issues/5669)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 6e5c7059..41a7bbc2 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -291,15 +291,15 @@ See [docs/settings.md](docs/settings.md) for all options. ### Project Trust -On interactive startup, pi asks before trusting a project folder that contains project-local extensions or settings and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. +On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. Before the trust decision, pi loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process. -Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore trust-gated project inputs, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. +Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`. -`pi config` and package commands use the same project trust flow. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. +`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect. @@ -527,7 +527,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -`pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. +`pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. ### Modes diff --git a/packages/coding-agent/docs/security.md b/packages/coding-agent/docs/security.md index 0c6d387a..3a268a8a 100644 --- a/packages/coding-agent/docs/security.md +++ b/packages/coding-agent/docs/security.md @@ -6,14 +6,18 @@ Pi is a local coding agent. It runs with the permissions of the user account tha Project trust controls whether pi loads project-local settings, resources, packages, and extensions. It is not a sandbox and it does not restrict what the model can ask tools to do after you start working in a directory. -Pi considers a project to have trust inputs when it finds any of these from the current working directory: +Pi considers a project to have resources that require trust when it finds any of these from the current working directory: -- `.pi/` in the current directory -- `.agents/skills` in the current directory or an ancestor directory +- `.pi/settings.json` +- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, or `.pi/themes` +- `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md` +- project `.agents/skills` in the current directory or an ancestor directory -When an interactive session starts in a project with configs in `.pi` or `.agents/skills` and no saved decision for the current directory or a parent directory, pi follows `defaultProjectTrust` from global settings. The default value is `"ask"`, which asks whether to trust the project when UI is available. Saved decisions are stored by canonical directory in `~/.pi/agent/trust.json`, and the closest saved decision on the current or parent path applies before the global default. +A bare `.pi` directory does not count as a project resource that requires trust. -Trusting a project allows pi to load trust-gated project inputs, including: +When an interactive session starts in a project with resources that require trust and no saved decision for the current directory or a parent directory, pi follows `defaultProjectTrust` from global settings. The default value is `"ask"`, which asks whether to trust the project when UI is available. Saved decisions are stored by canonical directory in `~/.pi/agent/trust.json`, and the closest saved decision on the current or parent path applies before the global default. + +Trusting a project allows pi to load project resources that require trust, including: - `.pi/settings.json` - `.pi` resources such as extensions, skills, prompt templates, themes, and system prompt files diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 2cf843d1..fccfab06 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -11,13 +11,13 @@ Edit directly or use `/settings` for common options. ## Project Trust -On interactive startup, pi asks before trusting a project folder that contains trust-gated project inputs and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. +On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. -Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore trust-gated project inputs, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. +Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`. -`pi config` and package commands use the same project trust flow. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. +`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect. diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index bb8a4c25..4f8f5954 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -112,15 +112,15 @@ Append to the default prompt without replacing it with `APPEND_SYSTEM.md` in eit ### Project Trust -On interactive startup, pi asks before trusting a project folder that contains project-local extensions or settings and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. +On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions. Before the trust decision, pi loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process. -Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore trust-gated project inputs, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. +Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run. If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.pi/agent/settings.json`, or change it with `/settings`. -`pi config` and package commands use the same project trust flow. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. +`pi config` and package commands use the same project trust flow, except `pi update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them. Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.pi/agent/trust.json` only; the current session is not reloaded, so restart pi for changes to take effect. @@ -153,7 +153,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. +These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. See [Pi Packages](packages.md) for package sources and security notes. diff --git a/packages/coding-agent/src/core/project-trust.ts b/packages/coding-agent/src/core/project-trust.ts index c8b57250..c0892439 100644 --- a/packages/coding-agent/src/core/project-trust.ts +++ b/packages/coding-agent/src/core/project-trust.ts @@ -3,7 +3,7 @@ import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/typ import type { DefaultProjectTrust } from "./settings-manager.ts"; import { getProjectTrustOptions, - hasProjectTrustInputs, + hasTrustRequiringProjectResources, type ProjectTrustOption, type ProjectTrustStore, } from "./trust-manager.ts"; @@ -46,7 +46,7 @@ export async function resolveProjectTrusted(options: ResolveProjectTrustedOption if (options.trustOverride !== undefined) { return options.trustOverride; } - if (!hasProjectTrustInputs(options.cwd)) { + if (!hasTrustRequiringProjectResources(options.cwd)) { return true; } diff --git a/packages/coding-agent/src/core/trust-manager.ts b/packages/coding-agent/src/core/trust-manager.ts index 69f616ae..9c494b47 100644 --- a/packages/coding-agent/src/core/trust-manager.ts +++ b/packages/coding-agent/src/core/trust-manager.ts @@ -1,4 +1,5 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; import { dirname, join } from "node:path"; import lockfile from "proper-lockfile"; import { CONFIG_DIR_NAME } from "../config.ts"; @@ -25,6 +26,16 @@ export interface ProjectTrustOption { type TrustFile = Record; +const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [ + "settings.json", + "extensions", + "skills", + "prompts", + "themes", + "SYSTEM.md", + "APPEND_SYSTEM.md", +] as const; + function normalizeCwd(cwd: string): string { return canonicalizePath(resolvePath(cwd)); } @@ -45,18 +56,14 @@ function findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreE } } -export function getProjectTrustPath(cwd: string): string { - return normalizeCwd(cwd); -} - export function getProjectTrustParentPath(cwd: string): string | undefined { - const trustPath = getProjectTrustPath(cwd); + const trustPath = normalizeCwd(cwd); const parentDir = dirname(trustPath); return parentDir === trustPath ? undefined : parentDir; } export function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] { - const trustPath = getProjectTrustPath(cwd); + const trustPath = normalizeCwd(cwd); const trustOptions: ProjectTrustOption[] = [ { label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath }, ]; @@ -167,18 +174,26 @@ function withTrustFileLock(path: string, fn: () => T): T { } } -export function hasProjectConfigDir(cwd: string): boolean { - return existsSync(join(canonicalizePath(resolvePath(cwd)), CONFIG_DIR_NAME)); -} - -export function hasProjectTrustInputs(cwd: string): boolean { +/** + * Returns true when cwd has project-local resources that must be gated by + * project trust: trust-requiring entries under cwd/.pi, or .agents/skills in + * cwd or one of its ancestors. Returns false when no such project resources + * exist. The user/global ~/.agents/skills directory is always treated as a + * trusted user resource and is ignored here, even when cwd is $HOME. + */ +export function hasTrustRequiringProjectResources(cwd: string): boolean { + const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir())); + const userAgentsSkillsDir = join(homeDir, ".agents", "skills"); let currentDir = canonicalizePath(resolvePath(cwd)); - if (hasProjectConfigDir(currentDir)) { + + const configDir = join(currentDir, CONFIG_DIR_NAME); + if (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) { return true; } while (true) { - if (existsSync(join(currentDir, ".agents", "skills"))) { + const agentsSkillsDir = join(currentDir, ".agents", "skills"); + if (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) { return true; } diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 958c7ebb..3176a167 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -289,7 +289,7 @@ export { withFileMutationQueue, } from "./core/tools/index.ts"; export { - hasProjectTrustInputs, + hasTrustRequiringProjectResources, type ProjectTrustDecision, ProjectTrustStore, type ProjectTrustStoreEntry, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 0bc24685..63d6ec1e 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -41,7 +41,7 @@ import { import { assertValidSessionId, SessionManager } from "./core/session-manager.ts"; import { SettingsManager } from "./core/settings-manager.ts"; import { printTimings, resetTimings, time } from "./core/timings.ts"; -import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts"; import { runMigrations, showDeprecationWarnings } from "./migrations.ts"; import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts"; import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts"; @@ -572,7 +572,9 @@ export async function main(args: string[], options?: MainOptions) { const trustStore = new ProjectTrustStore(agentDir); const sessionCwd = sessionManager.getCwd(); const autoTrustOnReloadCwd = - parsed.projectTrustOverride === undefined && !hasProjectTrustInputs(sessionCwd) ? sessionCwd : undefined; + parsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd) + ? sessionCwd + : undefined; const trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode; const projectTrustByCwd = new Map(); @@ -591,12 +593,14 @@ export async function main(args: string[], options?: MainOptions) { const isInitialRuntime = sessionStartEvent === undefined; const projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = []; const cachedProjectTrust = projectTrustByCwd.get(cwd); - const hasTrustInputs = hasProjectTrustInputs(cwd); + const hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd); const shouldResolveProjectTrust = - parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustInputs; + parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources; const projectTrusted = shouldResolveProjectTrust ? false - : (cachedProjectTrust ?? parsed.projectTrustOverride ?? (!hasTrustInputs || trustStore.get(cwd) === true)); + : (cachedProjectTrust ?? + parsed.projectTrustOverride ?? + (!hasTrustRequiringResources || trustStore.get(cwd) === true)); const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted }); const services = await createAgentSessionServices({ cwd, diff --git a/packages/coding-agent/src/modes/interactive/components/trust-selector.ts b/packages/coding-agent/src/modes/interactive/components/trust-selector.ts index b7b1fe00..92c23288 100644 --- a/packages/coding-agent/src/modes/interactive/components/trust-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/trust-selector.ts @@ -1,7 +1,6 @@ import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui"; import { getProjectTrustOptions, - getProjectTrustPath, type ProjectTrustOption, type ProjectTrustStoreEntry, } from "../../../core/trust-manager.ts"; @@ -19,12 +18,12 @@ export interface TrustSelectorOptions { onCancel: () => void; } -function formatDecision(cwd: string, decision: ProjectTrustStoreEntry | null): string { +function formatDecision(trustPath: string | undefined, decision: ProjectTrustStoreEntry | null): string { if (decision === null) { return "none"; } const label = decision.decision ? "trusted" : "untrusted"; - if (decision.path !== getProjectTrustPath(cwd)) { + if (trustPath !== undefined && decision.path !== trustPath) { return `${label} (inherited from ${decision.path})`; } return `${label} (${decision.path})`; @@ -56,7 +55,14 @@ export class TrustSelectorComponent extends Container { this.addChild(new Text(theme.fg("muted", options.cwd), 1, 0)); this.addChild(new Spacer(1)); this.addChild( - new Text(theme.fg("muted", `Saved decision: ${formatDecision(options.cwd, options.savedDecision)}`), 1, 0), + new Text( + theme.fg( + "muted", + `Saved decision: ${formatDecision(this.trustOptions[0]?.savedPath, options.savedDecision)}`, + ), + 1, + 0, + ), ); this.addChild( new Text(theme.fg("muted", `Current session: ${options.projectTrusted ? "trusted" : "untrusted"}`), 1, 0), diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index d50611af..fc25792e 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -86,7 +86,7 @@ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts"; import type { SourceInfo } from "../../core/source-info.ts"; import { isInstallTelemetryEnabled } from "../../core/telemetry.ts"; import type { TruncationResult } from "../../core/tools/truncate.ts"; -import { hasProjectConfigDir, hasProjectTrustInputs, ProjectTrustStore } from "../../core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts"; import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.ts"; import { copyToClipboard } from "../../utils/clipboard.ts"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; @@ -3271,7 +3271,7 @@ export class InteractiveMode { } private renderProjectTrustWarningIfNeeded(): void { - if (this.settingsManager.isProjectTrusted() || !hasProjectTrustInputs(this.sessionManager.getCwd())) { + if (this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(this.sessionManager.getCwd())) { return; } @@ -4198,7 +4198,7 @@ export class InteractiveMode { if (this.autoTrustOnReloadCwd !== cwd) { return false; } - if (!this.settingsManager.isProjectTrusted() || !hasProjectConfigDir(cwd)) { + if (!this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(cwd)) { return false; } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 94dbff85..0dbe750c 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -18,7 +18,7 @@ import { DefaultPackageManager } from "./core/package-manager.ts"; import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts"; import { DefaultResourceLoader } from "./core/resource-loader.ts"; import { SettingsManager } from "./core/settings-manager.ts"; -import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts"; import { spawnProcess } from "./utils/child-process.ts"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts"; import { @@ -452,13 +452,21 @@ async function createCommandSettingsManager(options: { cwd: string; agentDir: string; projectTrustOverride?: boolean; + useSavedProjectTrustOnly?: boolean; extensionFactories?: ExtensionFactory[]; }): Promise { const settingsManager = SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false }); const projectTrustWarnings: string[] = []; + const trustStore = new ProjectTrustStore(options.agentDir); + if (options.useSavedProjectTrustOnly) { + const savedProjectTrusted = trustStore.get(options.cwd) === true; + settingsManager.setProjectTrusted(options.projectTrustOverride ?? savedProjectTrusted); + return { settingsManager, projectTrustWarnings }; + } + const appMode = getCommandAppMode(); const extensionsResult = - options.projectTrustOverride === undefined && hasProjectTrustInputs(options.cwd) + options.projectTrustOverride === undefined && hasTrustRequiringProjectResources(options.cwd) ? await new DefaultResourceLoader({ cwd: options.cwd, agentDir: options.agentDir, @@ -472,7 +480,7 @@ async function createCommandSettingsManager(options: { const projectTrusted = await resolveProjectTrusted({ cwd: options.cwd, - trustStore: new ProjectTrustStore(options.agentDir), + trustStore, trustOverride: options.projectTrustOverride, defaultProjectTrust: settingsManager.getDefaultProjectTrust(), extensionsResult, @@ -576,6 +584,7 @@ export async function handlePackageCommand( cwd, agentDir, projectTrustOverride: options.projectTrustOverride, + useSavedProjectTrustOnly: options.command === "update", extensionFactories: runtimeOptions.extensionFactories, }); reportProjectTrustWarnings(projectTrustWarnings); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 25a55b35..1cff83f6 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -202,6 +202,69 @@ describe("package commands", () => { } }); + it("does not prompt or ask extensions for project trust during update", async () => { + mkdirSync(join(projectDir, ".pi"), { recursive: true }); + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ defaultProjectTrust: "always" })); + const fakeNpmPath = join(tempDir, "fake-project-npm.cjs"); + const recordPath = join(tempDir, "project-update.json"); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs");fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(process.argv.slice(2)));`, + ); + writeFileSync( + join(projectDir, ".pi", "settings.json"), + JSON.stringify({ packages: ["npm:fake-package"], npmCommand: [originalExecPath, fakeNpmPath] }), + ); + let projectTrustCalled = false; + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + try { + await expect( + main(["update", "--extensions"], { + extensionFactories: [ + (pi) => { + pi.on("project_trust", () => { + projectTrustCalled = true; + return { trusted: "yes" }; + }); + }, + ], + }), + ).resolves.toBeUndefined(); + + expect(projectTrustCalled).toBe(false); + expect(existsSync(recordPath)).toBe(false); + expect(process.exitCode).toBeUndefined(); + } finally { + logSpy.mockRestore(); + } + }); + + it("uses saved project trust during update", async () => { + mkdirSync(join(projectDir, ".pi"), { recursive: true }); + const fakeNpmPath = join(tempDir, "fake-trusted-project-npm.cjs"); + const recordPath = join(tempDir, "trusted-project-update.json"); + writeFileSync( + fakeNpmPath, + `const fs=require("node:fs");fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(process.argv.slice(2)));`, + ); + writeFileSync( + join(projectDir, ".pi", "settings.json"), + JSON.stringify({ packages: ["npm:fake-package"], npmCommand: [originalExecPath, fakeNpmPath] }), + ); + new ProjectTrustStore(agentDir).set(projectDir, true); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + try { + await expect(main(["update", "--extensions"])).resolves.toBeUndefined(); + + expect(existsSync(recordPath)).toBe(true); + expect(process.exitCode).toBeUndefined(); + } finally { + logSpy.mockRestore(); + } + }); + it("lets trust.json override default project trust", async () => { mkdirSync(join(projectDir, ".pi"), { recursive: true }); writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ defaultProjectTrust: "always" })); @@ -223,6 +286,7 @@ describe("package commands", () => { it("blocks local package changes when project is untrusted", async () => { mkdirSync(join(projectDir, ".pi"), { recursive: true }); + writeFileSync(join(projectDir, ".pi", "settings.json"), "{}"); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { diff --git a/packages/coding-agent/test/resource-loader.test.ts b/packages/coding-agent/test/resource-loader.test.ts index 7258b121..72cee79a 100644 --- a/packages/coding-agent/test/resource-loader.test.ts +++ b/packages/coding-agent/test/resource-loader.test.ts @@ -376,7 +376,7 @@ Content`, expect(loader.getSystemPrompt()).toBe("You are a helpful assistant."); }); - it("should skip trust-gated project resources when project is not trusted", async () => { + it("should skip project resources that require trust when project is not trusted", async () => { const piDir = join(cwd, ".pi"); const extensionsDir = join(piDir, "extensions"); const skillDir = join(piDir, "skills", "project-skill"); diff --git a/packages/coding-agent/test/trust-manager.test.ts b/packages/coding-agent/test/trust-manager.test.ts index 2716da36..01500bac 100644 --- a/packages/coding-agent/test/trust-manager.test.ts +++ b/packages/coding-agent/test/trust-manager.test.ts @@ -1,13 +1,8 @@ -import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - getProjectTrustPath, - hasProjectConfigDir, - hasProjectTrustInputs, - ProjectTrustStore, -} from "../src/core/trust-manager.ts"; +import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../src/core/trust-manager.ts"; describe("ProjectTrustStore", () => { let tempDir: string; @@ -26,86 +21,47 @@ describe("ProjectTrustStore", () => { rmSync(tempDir, { recursive: true, force: true }); }); - it("stores decisions per cwd", () => { - const store = new ProjectTrustStore(agentDir); - - expect(store.get(cwd)).toBeNull(); - expect(store.getEntry(cwd)).toBeNull(); - store.set(cwd, true); - expect(store.get(cwd)).toBe(true); - expect(store.getEntry(cwd)).toEqual({ path: getProjectTrustPath(cwd), decision: true }); - store.set(cwd, false); - expect(store.get(cwd)).toBe(false); - expect(store.getEntry(cwd)).toEqual({ path: getProjectTrustPath(cwd), decision: false }); - store.set(cwd, null); - expect(store.get(cwd)).toBeNull(); - expect(store.getEntry(cwd)).toBeNull(); - }); - - it("inherits the closest saved decision from parent directories", () => { - const store = new ProjectTrustStore(agentDir); - const parentDir = join(tempDir, "trusted-parent"); - const childDir = join(parentDir, "project"); - const grandchildDir = join(childDir, "nested"); - mkdirSync(grandchildDir, { recursive: true }); - - store.set(parentDir, true); - expect(store.get(childDir)).toBe(true); - expect(store.getEntry(childDir)).toEqual({ path: getProjectTrustPath(parentDir), decision: true }); - expect(store.get(grandchildDir)).toBe(true); - expect(store.getEntry(grandchildDir)).toEqual({ path: getProjectTrustPath(parentDir), decision: true }); - - store.set(childDir, false); - expect(store.get(grandchildDir)).toBe(false); - expect(store.getEntry(grandchildDir)).toEqual({ path: getProjectTrustPath(childDir), decision: false }); - }); - - it("can clear a child override to inherit parent trust", () => { + it("stores decisions and inherits from parent directories", () => { const store = new ProjectTrustStore(agentDir); const parentDir = join(tempDir, "trusted-parent"); const childDir = join(parentDir, "project"); mkdirSync(childDir, { recursive: true }); + expect(store.get(childDir)).toBeNull(); store.set(parentDir, true); - store.set(childDir, false); - expect(store.getEntry(childDir)).toEqual({ path: getProjectTrustPath(childDir), decision: false }); - - store.setMany([ - { path: parentDir, decision: true }, - { path: childDir, decision: null }, - ]); expect(store.get(childDir)).toBe(true); - expect(store.getEntry(childDir)).toEqual({ path: getProjectTrustPath(parentDir), decision: true }); + store.set(childDir, false); + expect(store.get(childDir)).toBe(false); + store.set(childDir, null); + expect(store.get(childDir)).toBe(true); }); - it("fails loudly without overwriting malformed trust stores", () => { - const trustPath = join(agentDir, "trust.json"); - writeFileSync(trustPath, "{not json", "utf-8"); - const store = new ProjectTrustStore(agentDir); + it("detects trust-requiring project resources", () => { + const originalHome = process.env.HOME; + process.env.HOME = tempDir; + try { + mkdirSync(join(tempDir, ".pi", "agent"), { recursive: true }); + mkdirSync(join(tempDir, ".agents", "skills"), { recursive: true }); + expect(hasTrustRequiringProjectResources(tempDir)).toBe(false); + expect(hasTrustRequiringProjectResources(cwd)).toBe(false); - expect(() => store.get(cwd)).toThrow(/Failed to read trust store/); - expect(() => store.set(cwd, true)).toThrow(/Failed to read trust store/); - expect(readFileSync(trustPath, "utf-8")).toBe("{not json"); - }); + writeFileSync(join(tempDir, ".pi", "settings.json"), "{}"); + expect(hasTrustRequiringProjectResources(tempDir)).toBe(true); + rmSync(join(tempDir, ".pi", "settings.json"), { force: true }); - it("detects project trust inputs", () => { - expect(hasProjectConfigDir(cwd)).toBe(false); - expect(hasProjectTrustInputs(cwd)).toBe(false); + mkdirSync(join(cwd, ".pi"), { recursive: true }); + writeFileSync(join(cwd, ".pi", "settings.json"), "{}"); + expect(hasTrustRequiringProjectResources(cwd)).toBe(true); - mkdirSync(join(cwd, ".pi"), { recursive: true }); - expect(hasProjectConfigDir(cwd)).toBe(true); - expect(hasProjectTrustInputs(cwd)).toBe(true); - rmSync(join(cwd, ".pi"), { recursive: true, force: true }); - - writeFileSync(join(cwd, "AGENTS.md"), "Project instructions"); - expect(hasProjectTrustInputs(cwd)).toBe(false); - rmSync(join(cwd, "AGENTS.md"), { force: true }); - - writeFileSync(join(cwd, "CLAUDE.md"), "Legacy project instructions"); - expect(hasProjectTrustInputs(cwd)).toBe(false); - rmSync(join(cwd, "CLAUDE.md"), { force: true }); - - mkdirSync(join(cwd, ".agents", "skills"), { recursive: true }); - expect(hasProjectTrustInputs(cwd)).toBe(true); + rmSync(join(cwd, ".pi"), { recursive: true, force: true }); + mkdirSync(join(cwd, ".agents", "skills"), { recursive: true }); + expect(hasTrustRequiringProjectResources(cwd)).toBe(true); + } finally { + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + } }); }); From 7a3cb6312e94c9276546d93b65b2524fc513e7ff Mon Sep 17 00:00:00 2001 From: Michael Yu Date: Sat, 13 Jun 2026 06:01:04 +0800 Subject: [PATCH 015/141] fix(ai): normalize generated model costs (#5634) --- packages/ai/scripts/generate-models.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index a3892cf1..c74a6afc 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -372,6 +372,10 @@ function normalizeNvidiaModelId(modelId: string): string { return modelId.toLowerCase().replaceAll("_", "."); } +function roundCost(value: number): number { + return Number(value.toFixed(6)); +} + async function fetchNvidiaNimModelIds(): Promise> { try { console.log("Fetching models from NVIDIA NIM API..."); @@ -417,10 +421,10 @@ async function fetchOpenRouterModels(): Promise[]> { } // Convert pricing from $/token to $/million tokens - const inputCost = parseFloat(model.pricing?.prompt || "0") * 1_000_000; - const outputCost = parseFloat(model.pricing?.completion || "0") * 1_000_000; - const cacheReadCost = parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000; - const cacheWriteCost = parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000; + const inputCost = roundCost(parseFloat(model.pricing?.prompt || "0") * 1_000_000); + const outputCost = roundCost(parseFloat(model.pricing?.completion || "0") * 1_000_000); + const cacheReadCost = roundCost(parseFloat(model.pricing?.input_cache_read || "0") * 1_000_000); + const cacheWriteCost = roundCost(parseFloat(model.pricing?.input_cache_write || "0") * 1_000_000); const normalizedModel: Model = { id: modelKey, @@ -476,10 +480,10 @@ async function fetchAiGatewayModels(): Promise[]> { input.push("image"); } - const inputCost = toNumber(model.pricing?.input) * 1_000_000; - const outputCost = toNumber(model.pricing?.output) * 1_000_000; - const cacheReadCost = toNumber(model.pricing?.input_cache_read) * 1_000_000; - const cacheWriteCost = toNumber(model.pricing?.input_cache_write) * 1_000_000; + const inputCost = roundCost(toNumber(model.pricing?.input) * 1_000_000); + const outputCost = roundCost(toNumber(model.pricing?.output) * 1_000_000); + const cacheReadCost = roundCost(toNumber(model.pricing?.input_cache_read) * 1_000_000); + const cacheWriteCost = roundCost(toNumber(model.pricing?.input_cache_write) * 1_000_000); models.push({ id: model.id, From 121f0edbf48ca0a93fb0941fb1d5ed2ce6f81407 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 00:01:14 +0200 Subject: [PATCH 016/141] fix(ai): detect parenthesized context overflow errors closes #5677 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/utils/overflow.ts | 5 +++-- packages/ai/test/overflow.test.ts | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b7b1112d..167908b6 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -6,6 +6,7 @@ ### Fixed +- Fixed OpenAI-compatible context overflow detection for parenthesized `maximum context length (N)` errors ([#5677](https://github.com/earendil-works/pi/issues/5677)). - Fixed OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to match current OpenAI limits ([#5644](https://github.com/earendil-works/pi/issues/5644)). - Increased the OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). diff --git a/packages/ai/src/utils/overflow.ts b/packages/ai/src/utils/overflow.ts index e3a9b37a..623b873a 100644 --- a/packages/ai/src/utils/overflow.ts +++ b/packages/ai/src/utils/overflow.ts @@ -12,6 +12,7 @@ import type { AssistantMessage } from "../types.ts"; * - Anthropic: "413 {\"error\":{\"type\":\"request_too_large\",\"message\":\"Request exceeds the maximum size\"}}" * - OpenAI: "Your input exceeds the context window of this model" * - OpenAI/LiteLLM: "Requested token count exceeds the model's maximum context length of 131072 tokens" + * - OpenAI-compatible: "Input length (265330) exceeds model's maximum context length (262144)." * - Google: "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)" * - xAI: "This model's maximum prompt length is 131072 but the request contains 537812 tokens" * - Groq: "Please reduce the length of the messages or completion" @@ -36,7 +37,7 @@ const OVERFLOW_PATTERNS = [ /request_too_large/i, // Anthropic request byte-size overflow (HTTP 413) /input is too long for requested model/i, // Amazon Bedrock /exceeds the context window/i, // OpenAI (Completions & Responses API) - /exceeds (?:the )?(?:model'?s )?maximum context length of [\d,]+ tokens?/i, // OpenAI-compatible proxies (LiteLLM) + /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, // OpenAI-compatible proxies (LiteLLM) /input token count.*exceeds the maximum/i, // Google (Gemini) /maximum prompt length is \d+/i, // xAI (Grok) /reduce the length of the messages/i, // Groq @@ -85,7 +86,7 @@ const NON_OVERFLOW_PATTERNS = [ * * **Reliable detection (returns error with detectable message):** * - Anthropic: "prompt is too long: X tokens > Y maximum" or "request_too_large" - * - OpenAI (Completions & Responses): "exceeds the context window" or "exceeds the model's maximum context length of X tokens" + * - OpenAI (Completions & Responses): "exceeds the context window", "exceeds the model's maximum context length of X tokens", or "exceeds model's maximum context length (X)" * - Google Gemini: "input token count exceeds the maximum" * - xAI (Grok): "maximum prompt length is X but request contains Y" * - Groq: "reduce the length of the messages" diff --git a/packages/ai/test/overflow.test.ts b/packages/ai/test/overflow.test.ts index 346d2006..62108911 100644 --- a/packages/ai/test/overflow.test.ts +++ b/packages/ai/test/overflow.test.ts @@ -49,6 +49,13 @@ describe("isContextOverflow", () => { expect(isContextOverflow(message, 131072)).toBe(true); }); + it("detects OpenAI-compatible parenthesized maximum context length errors", () => { + const message = createErrorMessage( + "Error: 400 Input length (265330) exceeds model's maximum context length (262144).", + ); + expect(isContextOverflow(message, 262144)).toBe(true); + }); + it("detects OpenRouter Poolside maximum allowed input length errors", () => { const message = createErrorMessage( "Provider returned error: Input length 131393 exceeds the maximum allowed input length of 131040 tokens.", From e320f09618ab3ddbb0f5f9d57769c9e458582e1d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 00:20:01 +0200 Subject: [PATCH 017/141] docs: update unreleased changelogs --- packages/ai/CHANGELOG.md | 5 ++++- packages/coding-agent/CHANGELOG.md | 21 ++++++++++++++++++--- packages/tui/CHANGELOG.md | 2 ++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 167908b6..7c6f84bf 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,13 +2,16 @@ ## [Unreleased] -- When Amazon Bedrock rejects an unsupported data retention mode, the error now links the AWS data retention documentation ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). +### Added + +- Added AWS data retention documentation links to Amazon Bedrock unsupported data retention mode validation errors ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). ### Fixed - Fixed OpenAI-compatible context overflow detection for parenthesized `maximum context length (N)` errors ([#5677](https://github.com/earendil-works/pi/issues/5677)). - Fixed OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to match current OpenAI limits ([#5644](https://github.com/earendil-works/pi/issues/5644)). - Increased the OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). +- Fixed Anthropic refusal stops to preserve provider `stop_details` explanations in error messages ([#5666](https://github.com/earendil-works/pi/pull/5666) by [@rwachtler](https://github.com/rwachtler)). - Fixed Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). ## [0.79.1] - 2026-06-09 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1322e247..c6c5a1c4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,19 +2,34 @@ ## [Unreleased] +### New Features + +- **Clearer Bedrock validation guidance** - Amazon Bedrock data retention validation errors now link to AWS data retention documentation. See [Amazon Bedrock](docs/providers.md#amazon-bedrock). + ### Added -- Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. +- Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json` ([#5587](https://github.com/earendil-works/pi/pull/5587) by [@vegarsti](https://github.com/vegarsti)). +- Added AWS data retention documentation links to inherited Amazon Bedrock unsupported data retention mode validation errors ([#5561](https://github.com/earendil-works/pi/pull/5561) by [@unexge](https://github.com/unexge)). ### Fixed - Fixed project trust detection to ignore global `~/.pi/agent` state when running from `$HOME`, and made `pi update` use only saved or explicit project trust without prompting ([#5619](https://github.com/earendil-works/pi/issues/5619)). +- Fixed experimental first-time setup to skip forked sessions instead of rerunning the setup prompts ([#5627](https://github.com/earendil-works/pi/pull/5627) by [@vegarsti](https://github.com/vegarsti)). +- Fixed inherited OpenAI-compatible context overflow detection for parenthesized `maximum context length (N)` errors ([#5677](https://github.com/earendil-works/pi/issues/5677)). +- Fixed inherited OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to match current OpenAI limits ([#5644](https://github.com/earendil-works/pi/issues/5644)). +- Fixed inherited Anthropic refusal stops to preserve provider `stop_details` explanations in error messages ([#5666](https://github.com/earendil-works/pi/pull/5666) by [@rwachtler](https://github.com/rwachtler)). +- Increased the inherited OpenAI Codex Responses SSE response-header timeout to 20 seconds to reduce false-positive stalls while retaining the bounded wait introduced for zero-event hangs ([#4945](https://github.com/earendil-works/pi/issues/4945)). +- Fixed inherited Claude Fable 5 thinking-off requests to omit Anthropic's unsupported `thinking.type: "disabled"` payload ([#5567](https://github.com/earendil-works/pi/pull/5567) by [@tmustier](https://github.com/tmustier)). +- Fixed inherited late tool progress callbacks after tool settlement to be ignored instead of emitting stale `tool_execution_update` events ([#5573](https://github.com/earendil-works/pi/issues/5573)). - Fixed inherited user-message transcript rendering so standalone `+` messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). ->>>>>>> main +- Fixed inherited slash-separated fuzzy queries so provider/model completions remain matchable after insertion. +- Fixed inherited WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). +- Fixed inherited editor wrapping for CJK text to break at character boundaries instead of leaving large trailing gaps ([#5585](https://github.com/earendil-works/pi/pull/5585) by [@haoqixu](https://github.com/haoqixu)). +- Fixed inherited loose Markdown list rendering to preserve blank-line separation between list items ([#5562](https://github.com/earendil-works/pi/pull/5562) by [@Perlence](https://github.com/Perlence)). - Fixed `--model` resolution for authenticated custom model IDs whose slash prefix matches an unauthenticated built-in provider ([#5643](https://github.com/earendil-works/pi/issues/5643)). - Fixed `/fork` to keep session parent chains connected when the forked path contains labels ([#5669](https://github.com/earendil-works/pi/issues/5669)). - Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). -- Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). +- Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5560](https://github.com/earendil-works/pi/pull/5560) by [@haoqixu](https://github.com/haoqixu)). ## [0.79.1] - 2026-06-09 diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index b5e277a9..edec3f28 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -7,6 +7,8 @@ - Fixed Markdown source list marker preservation to include unordered markers, so standalone `+` user messages no longer render as `-` ([#5657](https://github.com/earendil-works/pi/issues/5657)). - Fixed slash-separated fuzzy queries so provider/model completions remain matchable after insertion. - Fixed WezTerm inline Kitty image rendering so reserved row clears do not erase all but the top strip of tool image previews ([#5618](https://github.com/earendil-works/pi/issues/5618)). +- Fixed editor wrapping for CJK text to break at character boundaries instead of leaving large trailing gaps ([#5585](https://github.com/earendil-works/pi/pull/5585) by [@haoqixu](https://github.com/haoqixu)). +- Fixed loose Markdown list rendering to preserve blank-line separation between list items ([#5562](https://github.com/earendil-works/pi/pull/5562) by [@Perlence](https://github.com/Perlence)). ## [0.79.1] - 2026-06-09 From f21f3c4bbdd3868ce2a7a68019d7920b838f663b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 00:25:15 +0200 Subject: [PATCH 018/141] Release v0.79.2 --- package-lock.json | 26 +- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 380 +++++++++--------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 +- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 240 insertions(+), 240 deletions(-) diff --git a/package-lock.json b/package-lock.json index b32c59fe..5cdd129d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6170,10 +6170,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.1", + "version": "0.79.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6207,7 +6207,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.1", + "version": "0.79.2", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6252,12 +6252,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.79.2", + "@earendil-works/pi-ai": "^0.79.2", + "@earendil-works/pi-tui": "^0.79.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6296,32 +6296,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.1", + "version": "0.79.2", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.1" + "version": "0.79.2" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.1", + "version": "0.79.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.1", + "version": "1.9.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.1", + "version": "0.79.2", "dependencies": { "ms": "2.1.3" }, @@ -6357,7 +6357,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.1", + "version": "0.79.2", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 287f07c3..feba85dc 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.2] - 2026-06-12 ### Fixed diff --git a/packages/agent/package.json b/packages/agent/package.json index afd0368c..0c94a352 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.1", + "version": "0.79.2", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 7c6f84bf..eadcd674 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.2] - 2026-06-12 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 9a54de2f..bbbddd43 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.1", + "version": "0.79.2", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index eeed2e92..aa587f10 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -8830,8 +8830,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.7999999999999999, - output: 3.1999999999999997, + input: 0.8, + output: 3.2, cacheRead: 0, cacheWrite: 0, }, @@ -8864,7 +8864,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.7999999999999999, + input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, @@ -8900,7 +8900,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 1.25, }, contextWindow: 200000, @@ -9244,8 +9244,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -9295,8 +9295,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.20020000000000002, - output: 0.8000999999999999, + input: 0.2002, + output: 0.8001, cacheRead: 0, cacheWrite: 0, }, @@ -9312,7 +9312,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.77, cacheRead: 0.135, cacheWrite: 0, @@ -9330,7 +9330,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.21, - output: 0.7899999999999999, + output: 0.79, cacheRead: 0.13, cacheWrite: 0, }, @@ -9364,7 +9364,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.5, - output: 2.1500000000000004, + output: 2.15, cacheRead: 0.35, cacheWrite: 0, }, @@ -9489,7 +9489,7 @@ export const MODELS = { input: 0.3, output: 2.5, cacheRead: 0.03, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65535, @@ -9503,10 +9503,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.01, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65535, @@ -9520,10 +9520,10 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.01, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65535, @@ -9590,8 +9590,8 @@ export const MODELS = { cost: { input: 0.5, output: 3, - cacheRead: 0.049999999999999996, - cacheWrite: 0.08333333333333334, + cacheRead: 0.05, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9607,8 +9607,8 @@ export const MODELS = { cost: { input: 0.25, output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, + cacheRead: 0.025, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9624,8 +9624,8 @@ export const MODELS = { cost: { input: 0.25, output: 1.5, - cacheRead: 0.024999999999999998, - cacheWrite: 0.08333333333333334, + cacheRead: 0.025, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9641,7 +9641,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0.375, }, contextWindow: 1048576, @@ -9658,7 +9658,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0.375, }, contextWindow: 1048756, @@ -9676,7 +9676,7 @@ export const MODELS = { input: 1.5, output: 9, cacheRead: 0.15, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -9690,7 +9690,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.049999999999999996, + input: 0.05, output: 0.15, cacheRead: 0, cacheWrite: 0, @@ -9792,9 +9792,9 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.09999999999999999, - cacheRead: 0.049999999999999996, + input: 0.05, + output: 0.1, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 131072, @@ -9812,7 +9812,7 @@ export const MODELS = { cost: { input: 0.25, output: 0.75, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 128000, @@ -9895,8 +9895,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.4, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -9929,7 +9929,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.32, cacheRead: 0, cacheWrite: 0, @@ -9980,7 +9980,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -9997,7 +9997,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2.2, cacheRead: 0, cacheWrite: 0, @@ -10049,8 +10049,8 @@ export const MODELS = { input: ["text"], cost: { input: 0.15, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + output: 0.9, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 204800, @@ -10067,7 +10067,7 @@ export const MODELS = { cost: { input: 0.25, output: 1, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 204800, @@ -10100,7 +10100,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.3, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0.03, cacheWrite: 0, }, @@ -10116,7 +10116,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0, @@ -10133,8 +10133,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.19999999999999998, - output: 0.19999999999999998, + input: 0.2, + output: 0.2, cacheRead: 0.02, cacheWrite: 0, }, @@ -10150,8 +10150,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, + input: 0.1, + output: 0.1, cacheRead: 0.01, cacheWrite: 0, }, @@ -10186,7 +10186,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 128000, @@ -10203,7 +10203,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 131072, @@ -10220,7 +10220,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -10235,7 +10235,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0, @@ -10269,7 +10269,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0, @@ -10303,7 +10303,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.6, cacheRead: 0.02, cacheWrite: 0, @@ -10338,7 +10338,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.075, - output: 0.19999999999999998, + output: 0.2, cacheRead: 0, cacheWrite: 0, }, @@ -10356,7 +10356,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 65536, @@ -10371,7 +10371,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0, @@ -10388,7 +10388,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.5700000000000001, + input: 0.57, output: 2.3, cacheRead: 0, cacheWrite: 0, @@ -10457,9 +10457,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.6799999999999999, + input: 0.68, output: 3.41, - cacheRead: 0.33999999999999997, + cacheRead: 0.34, cacheWrite: 0, }, contextWindow: 262144, @@ -10508,8 +10508,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.4, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -10525,8 +10525,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, + input: 0.05, + output: 0.2, cacheRead: 0, cacheWrite: 0, }, @@ -10577,7 +10577,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.09, - output: 0.44999999999999996, + output: 0.45, cacheRead: 0, cacheWrite: 0, }, @@ -10797,9 +10797,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, + input: 0.4, + output: 1.6, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 1047576, @@ -10814,9 +10814,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, + input: 0.1, + output: 0.4, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 1047576, @@ -10969,7 +10969,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -10984,8 +10984,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, + input: 0.05, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -11088,7 +11088,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -11248,7 +11248,7 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0, @@ -11693,7 +11693,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.1, cacheRead: 0, cacheWrite: 0, @@ -11711,7 +11711,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.36, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -11729,7 +11729,7 @@ export const MODELS = { cost: { input: 0.26, output: 0.78, - cacheRead: 0.052000000000000005, + cacheRead: 0.052, cacheWrite: 0.325, }, contextWindow: 1000000, @@ -11778,7 +11778,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.24, cacheRead: 0, cacheWrite: 0, @@ -11795,8 +11795,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.45499999999999996, - output: 1.8199999999999998, + input: 0.455, + output: 1.82, cacheRead: 0, cacheWrite: 0, }, @@ -11813,7 +11813,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.09, - output: 0.09999999999999999, + output: 0.1, cacheRead: 0, cacheWrite: 0, }, @@ -11829,9 +11829,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, - cacheRead: 0.09999999999999999, + input: 0.1, + output: 0.1, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 262144, @@ -11881,7 +11881,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.08, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0.08, cacheWrite: 0, }, @@ -11914,9 +11914,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, - cacheRead: 0.049999999999999996, + input: 0.05, + output: 0.4, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 131072, @@ -11932,7 +11932,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.22, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0, cacheWrite: 0, }, @@ -11983,7 +11983,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.11, - output: 0.7999999999999999, + output: 0.8, cacheRead: 0.07, cacheWrite: 0, }, @@ -12118,7 +12118,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.88, cacheRead: 0.11, cacheWrite: 0, @@ -12186,8 +12186,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.10400000000000001, - output: 0.41600000000000004, + input: 0.104, + output: 0.416, cacheRead: 0, cacheWrite: 0, }, @@ -12273,7 +12273,7 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -12305,7 +12305,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.15, cacheRead: 0, cacheWrite: 0, @@ -12357,7 +12357,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.3, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0, cacheWrite: 0.375, }, @@ -12373,7 +12373,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.28850000000000003, + input: 0.2885, output: 3.17, cacheRead: 0, cacheWrite: 0, @@ -12392,7 +12392,7 @@ export const MODELS = { cost: { input: 0.15, output: 1, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -12478,7 +12478,7 @@ export const MODELS = { input: 0.32, output: 1.28, cacheRead: 0.064, - cacheWrite: 0.39999999999999997, + cacheWrite: 0.4, }, contextWindow: 1000000, maxTokens: 65536, @@ -12492,8 +12492,8 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, + input: 0.1, + output: 0.1, cacheRead: 0, cacheWrite: 0, }, @@ -12560,7 +12560,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0, @@ -12579,7 +12579,7 @@ export const MODELS = { cost: { input: 0.063, output: 0.21, - cacheRead: 0.020999999999999998, + cacheRead: 0.021, cacheWrite: 0, }, contextWindow: 262144, @@ -12594,7 +12594,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.16999999999999998, + input: 0.17, output: 0.43, cacheRead: 0, cacheWrite: 0, @@ -12611,8 +12611,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, - output: 0.39999999999999997, + input: 0.4, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -12647,7 +12647,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -12664,7 +12664,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -12681,7 +12681,7 @@ export const MODELS = { cost: { input: 1, output: 2, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 256000, @@ -12696,7 +12696,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0, @@ -12782,7 +12782,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.6, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0.11, cacheWrite: 0, }, @@ -12816,7 +12816,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.3, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0.055, cacheWrite: 0, }, @@ -12832,7 +12832,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 1.75, cacheRead: 0.08, cacheWrite: 0, @@ -12850,7 +12850,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.06, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -12936,7 +12936,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 1.25, }, contextWindow: 200000, @@ -12988,7 +12988,7 @@ export const MODELS = { input: 1.5, output: 9, cacheRead: 0.15, - cacheWrite: 0.08333333333333334, + cacheWrite: 0.083333, }, contextWindow: 1048576, maxTokens: 65536, @@ -13004,7 +13004,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0.375, }, contextWindow: 1048576, @@ -13019,9 +13019,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.6799999999999999, + input: 0.68, output: 3.41, - cacheRead: 0.33999999999999997, + cacheRead: 0.34, cacheWrite: 0, }, contextWindow: 262144, @@ -13477,7 +13477,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 4, cacheRead: 0, cacheWrite: 0, @@ -13547,7 +13547,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -13647,7 +13647,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 4, cacheRead: 0, cacheWrite: 0, @@ -13664,8 +13664,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.001, cacheWrite: 0.125, }, @@ -13681,7 +13681,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2.4, cacheRead: 0.04, cacheWrite: 0.5, @@ -13699,7 +13699,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.6, - output: 3.5999999999999996, + output: 3.6, cacheRead: 0, cacheWrite: 0, }, @@ -13717,7 +13717,7 @@ export const MODELS = { cost: { input: 0.5, output: 3, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 0.625, }, contextWindow: 1000000, @@ -13749,8 +13749,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, + input: 0.4, + output: 1.6, cacheRead: 0.08, cacheWrite: 0.5, }, @@ -13783,7 +13783,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.7999999999999999, + input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1, @@ -13821,7 +13821,7 @@ export const MODELS = { cost: { input: 1, output: 5, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 1.25, }, contextWindow: 200000, @@ -14014,7 +14014,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.25, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0, cacheWrite: 0, }, @@ -14032,7 +14032,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 256000, @@ -14217,8 +14217,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, + input: 0.1, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -14253,7 +14253,7 @@ export const MODELS = { cost: { input: 0.5, output: 3, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 1000000, @@ -14270,7 +14270,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -14321,7 +14321,7 @@ export const MODELS = { cost: { input: 2, output: 12, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -14371,7 +14371,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.14, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -14389,7 +14389,7 @@ export const MODELS = { cost: { input: 0.25, output: 0.75, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 128000, @@ -14541,7 +14541,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.24, - output: 0.9700000000000001, + output: 0.97, cacheRead: 0, cacheWrite: 0, }, @@ -14557,7 +14557,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.16999999999999998, + input: 0.17, output: 0.66, cacheRead: 0, cacheWrite: 0, @@ -14711,7 +14711,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.3, - output: 0.8999999999999999, + output: 0.9, cacheRead: 0, cacheWrite: 0, }, @@ -14727,7 +14727,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0, cacheWrite: 0, @@ -14744,7 +14744,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -14761,7 +14761,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -14778,8 +14778,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.09999999999999999, - output: 0.09999999999999999, + input: 0.1, + output: 0.1, cacheRead: 0, cacheWrite: 0, }, @@ -14812,7 +14812,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, + input: 0.4, output: 2, cacheRead: 0, cacheWrite: 0, @@ -14863,7 +14863,7 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0, @@ -14914,7 +14914,7 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.5700000000000001, + input: 0.57, output: 2.3, cacheRead: 0, cacheWrite: 0, @@ -14950,7 +14950,7 @@ export const MODELS = { cost: { input: 0.6, output: 3, - cacheRead: 0.09999999999999999, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 262114, @@ -15033,7 +15033,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.6, cacheRead: 0, cacheWrite: 0, @@ -15051,7 +15051,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.06, - output: 0.22999999999999998, + output: 0.23, cacheRead: 0, cacheWrite: 0, }, @@ -15101,9 +15101,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.39999999999999997, - output: 1.5999999999999999, - cacheRead: 0.09999999999999999, + input: 0.4, + output: 1.6, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 1047576, @@ -15118,9 +15118,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.09999999999999999, - output: 0.39999999999999997, - cacheRead: 0.024999999999999998, + input: 0.1, + output: 0.4, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 1047576, @@ -15222,7 +15222,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -15237,8 +15237,8 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.049999999999999996, - output: 0.39999999999999997, + input: 0.05, + output: 0.4, cacheRead: 0.005, cacheWrite: 0, }, @@ -15307,7 +15307,7 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.024999999999999998, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, @@ -15501,7 +15501,7 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0, @@ -15589,8 +15589,8 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.049999999999999996, - output: 0.19999999999999998, + input: 0.05, + output: 0.2, cacheRead: 0, cacheWrite: 0, }, @@ -15776,7 +15776,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0, @@ -15793,9 +15793,9 @@ export const MODELS = { reasoning: false, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.5, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 1000000, @@ -15810,9 +15810,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 0.5, - cacheRead: 0.049999999999999996, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 1000000, @@ -15829,7 +15829,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -15846,7 +15846,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -15863,7 +15863,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -15880,7 +15880,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -15897,7 +15897,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -15914,7 +15914,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 2000000, @@ -15931,7 +15931,7 @@ export const MODELS = { cost: { input: 1.25, output: 2.5, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -15948,7 +15948,7 @@ export const MODELS = { cost: { input: 1, output: 2, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 256000, @@ -15963,7 +15963,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.09999999999999999, + input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0, @@ -15982,7 +15982,7 @@ export const MODELS = { cost: { input: 1, output: 3, - cacheRead: 0.19999999999999998, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1000000, @@ -16048,7 +16048,7 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.19999999999999998, + input: 0.2, output: 1.1, cacheRead: 0.03, cacheWrite: 0, @@ -16066,7 +16066,7 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.6, - output: 1.7999999999999998, + output: 1.8, cacheRead: 0.11, cacheWrite: 0, }, @@ -16100,8 +16100,8 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.3, - output: 0.8999999999999999, - cacheRead: 0.049999999999999996, + output: 0.9, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 128000, @@ -16151,7 +16151,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.07, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0, cacheWrite: 0, }, @@ -16168,7 +16168,7 @@ export const MODELS = { input: ["text"], cost: { input: 0.06, - output: 0.39999999999999997, + output: 0.4, cacheRead: 0.01, cacheWrite: 0, }, @@ -16185,8 +16185,8 @@ export const MODELS = { input: ["text"], cost: { input: 1, - output: 3.1999999999999997, - cacheRead: 0.19999999999999998, + output: 3.2, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 202800, diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c6c5a1c4..dc0f9fa0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.2] - 2026-06-12 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index dc287898..8ad2516c 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.1", + "version": "0.79.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.1", + "version": "0.79.2", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 4b1dd16b..c898bca6 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.1", + "version": "0.79.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index fba74f25..cff3a11f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.1", + "version": "0.79.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index b71b386c..96e72e79 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.1", + "version": "0.79.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.1", + "version": "0.79.2", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 51448000..416616c4 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.1", + "version": "0.79.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 360c0def..936fc58b 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.1", + "version": "1.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.1", + "version": "1.9.2", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index f285b6ce..4d8f4398 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.1", + "version": "1.9.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index ee75d24f..4d58693f 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.1", + "version": "0.79.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.1", + "version": "0.79.2", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index fab9f145..1c39c1eb 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.1", + "version": "0.79.2", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 9132d489..3718cee9 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.2", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.79.2", + "@earendil-works/pi-ai": "^0.79.2", + "@earendil-works/pi-tui": "^0.79.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -473,11 +473,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.1.tgz", + "version": "0.79.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.1", + "@earendil-works/pi-ai": "^0.79.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -487,8 +487,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.1.tgz", + "version": "0.79.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.2.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -510,8 +510,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.1.tgz", + "version": "0.79.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index b9381465..73774712 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.1", + "version": "0.79.2", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.1", - "@earendil-works/pi-ai": "^0.79.1", - "@earendil-works/pi-tui": "^0.79.1", + "@earendil-works/pi-agent-core": "^0.79.2", + "@earendil-works/pi-ai": "^0.79.2", + "@earendil-works/pi-tui": "^0.79.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index edec3f28..2b348d0b 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.2] - 2026-06-12 ### Fixed diff --git a/packages/tui/package.json b/packages/tui/package.json index 94ba86ad..0b1d76a3 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.1", + "version": "0.79.2", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 032c01c1e11d5171c6ceb6afb83a4715bfa6e4ae Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 00:25:18 +0200 Subject: [PATCH 019/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index feba85dc..ec983000 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.2] - 2026-06-12 ### Fixed diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index eadcd674..754e1edd 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.2] - 2026-06-12 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index dc0f9fa0..4fa1b007 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.2] - 2026-06-12 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 2b348d0b..1edd68d0 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.2] - 2026-06-12 ### Fixed From aa3a5233adad808309bdfe3d54ef94bbd6c80168 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 11:02:38 +0200 Subject: [PATCH 020/141] fix(ai): restore Codex context limits --- packages/ai/CHANGELOG.md | 4 + packages/ai/scripts/generate-models.ts | 14 +- packages/ai/src/models.generated.ts | 151 +++++++++--------- .../ai/test/openai-model-metadata.test.ts | 15 -- 4 files changed, 86 insertions(+), 98 deletions(-) delete mode 100644 packages/ai/test/openai-model-metadata.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 754e1edd..0b0e1e16 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Restored OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to the observed 272k-token Codex backend limit, avoiding a billing hazard from sending prompts above Codex's accepted limit (reported by [@trethore](https://github.com/trethore)). + ## [0.79.2] - 2026-06-12 ### Added diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index c74a6afc..1eaf9fe1 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1367,7 +1367,7 @@ async function generateModels() { candidate.maxTokens = 128000; } if (candidate.provider === "openai" && (candidate.id === "gpt-5.4" || candidate.id === "gpt-5.5")) { - candidate.contextWindow = 1050000; + candidate.contextWindow = 272000; candidate.maxTokens = 128000; } // models.dev reports gpt-5-pro output as 272000 (a duplicate of the input sub-limit); @@ -1634,7 +1634,7 @@ async function generateModels() { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 1050000, + contextWindow: 272000, maxTokens: 128000, }); } @@ -1761,9 +1761,9 @@ async function generateModels() { // OpenAI Codex (ChatGPT OAuth) models // NOTE: These are not fetched from models.dev; we keep a small, explicit list to avoid aliases. + // Context window is based on observed server limits (400s above ~272k), not marketing numbers. const CODEX_BASE_URL = "https://chatgpt.com/backend-api"; - const CODEX_GPT_54_CONTEXT = 1000000; - const CODEX_STANDARD_CONTEXT = 400000; + const CODEX_CONTEXT = 272000; const CODEX_SPARK_CONTEXT = 128000; const CODEX_MAX_TOKENS = 128000; const codexModels: Model<"openai-codex-responses">[] = [ @@ -1788,7 +1788,7 @@ async function generateModels() { reasoning: true, input: ["text", "image"], cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 }, - contextWindow: CODEX_GPT_54_CONTEXT, + contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { @@ -1800,7 +1800,7 @@ async function generateModels() { reasoning: true, input: ["text", "image"], cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, - contextWindow: CODEX_STANDARD_CONTEXT, + contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, { @@ -1812,7 +1812,7 @@ async function generateModels() { reasoning: true, input: ["text", "image"], cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, - contextWindow: CODEX_STANDARD_CONTEXT, + contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, ]; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index aa587f10..0b59d572 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3945,24 +3945,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 32768, } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/kimi-k2p5": { - id: "accounts/fireworks/models/kimi-k2p5", - name: "Kimi K2.5", - api: "anthropic-messages", - provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/kimi-k2p6": { id: "accounts/fireworks/models/kimi-k2p6", name: "Kimi K2.6", @@ -3981,23 +3963,23 @@ export const MODELS = { contextWindow: 262000, maxTokens: 262000, } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/minimax-m2p5": { - id: "accounts/fireworks/models/minimax-m2p5", - name: "MiniMax-M2.5", + "accounts/fireworks/models/kimi-k2p7-code": { + id: "accounts/fireworks/models/kimi-k2p7-code", + name: "Kimi K2.7 Code", api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { - input: 0.3, - output: 1.2, - cacheRead: 0.03, + input: 0.95, + output: 4, + cacheRead: 0.19, cacheWrite: 0, }, - contextWindow: 196608, - maxTokens: 196608, + contextWindow: 262000, + maxTokens: 262000, } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/minimax-m2p7": { id: "accounts/fireworks/models/minimax-m2p7", @@ -4017,9 +3999,27 @@ export const MODELS = { contextWindow: 196608, maxTokens: 196608, } satisfies Model<"anthropic-messages">, - "accounts/fireworks/models/qwen3p6-plus": { - id: "accounts/fireworks/models/qwen3p6-plus", - name: "Qwen 3.6 Plus", + "accounts/fireworks/models/minimax-m3": { + id: "accounts/fireworks/models/minimax-m3", + name: "MiniMax-M3", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 512000, + maxTokens: 512000, + } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/qwen3p7-plus": { + id: "accounts/fireworks/models/qwen3p7-plus", + name: "Qwen 3.7 Plus", api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", @@ -4027,9 +4027,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.5, - output: 3, - cacheRead: 0.1, + input: 0.4, + output: 1.6, + cacheRead: 0.08, cacheWrite: 0, }, contextWindow: 262144, @@ -4089,6 +4089,24 @@ export const MODELS = { contextWindow: 262000, maxTokens: 262000, } satisfies Model<"anthropic-messages">, + "accounts/fireworks/routers/kimi-k2p7-code-fast": { + id: "accounts/fireworks/routers/kimi-k2p7-code-fast", + name: "Kimi K2.7 Code Fast", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262000, + maxTokens: 262000, + } satisfies Model<"anthropic-messages">, }, "github-copilot": { "claude-fable-5": { @@ -7382,7 +7400,7 @@ export const MODELS = { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 1050000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-responses">, "gpt-5.4-mini": { @@ -7454,7 +7472,7 @@ export const MODELS = { cacheRead: 0.5, cacheWrite: 0, }, - contextWindow: 1050000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-responses">, "gpt-5.5-pro": { @@ -7646,7 +7664,7 @@ export const MODELS = { cacheRead: 0.25, cacheWrite: 0, }, - contextWindow: 1000000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, "gpt-5.4-mini": { @@ -7664,7 +7682,7 @@ export const MODELS = { cacheRead: 0.075, cacheWrite: 0, }, - contextWindow: 400000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, "gpt-5.5": { @@ -7682,7 +7700,7 @@ export const MODELS = { cacheRead: 0.5, cacheWrite: 0, }, - contextWindow: 400000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, }, @@ -7705,25 +7723,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 32000, } satisfies Model<"openai-completions">, - "claude-fable-5": { - id: "claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "opencode", - baseUrl: "https://opencode.ai/zen", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, "claude-haiku-4-5": { id: "claude-haiku-4-5", name: "Claude Haiku 4.5", @@ -13082,6 +13081,25 @@ export const MODELS = { contextWindow: 202752, maxTokens: 131072, } satisfies Model<"openai-completions">, + "MiniMaxAI/MiniMax-M3": { + id: "MiniMaxAI/MiniMax-M3", + name: "MiniMax-M3", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text", "image"], + cost: { + input: 0.3, + output: 1.2, + cacheRead: 0.06, + cacheWrite: 0, + }, + contextWindow: 524288, + maxTokens: 250000, + } satisfies Model<"openai-completions">, "Qwen/Qwen2.5-7B-Instruct-Turbo": { id: "Qwen/Qwen2.5-7B-Instruct-Turbo", name: "Qwen 2.5 7B Instruct Turbo", @@ -13791,25 +13809,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"anthropic-messages">, - "anthropic/claude-fable-5": { - id: "anthropic/claude-fable-5", - name: "Claude Fable 5", - api: "anthropic-messages", - provider: "vercel-ai-gateway", - baseUrl: "https://ai-gateway.vercel.sh", - compat: {"forceAdaptiveThinking":true}, - reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, - input: ["text", "image"], - cost: { - input: 10, - output: 50, - cacheRead: 1, - cacheWrite: 12.5, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"anthropic-messages">, "anthropic/claude-haiku-4.5": { id: "anthropic/claude-haiku-4.5", name: "Claude Haiku 4.5", diff --git a/packages/ai/test/openai-model-metadata.test.ts b/packages/ai/test/openai-model-metadata.test.ts deleted file mode 100644 index 64c593e1..00000000 --- a/packages/ai/test/openai-model-metadata.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getModel } from "../src/models.ts"; - -describe("OpenAI model metadata", () => { - it("uses current GPT-5.4 and GPT-5.5 API context windows", () => { - expect(getModel("openai", "gpt-5.4").contextWindow).toBe(1050000); - expect(getModel("openai", "gpt-5.5").contextWindow).toBe(1050000); - }); - - it("uses current OpenAI Codex context windows", () => { - expect(getModel("openai-codex", "gpt-5.4").contextWindow).toBe(1000000); - expect(getModel("openai-codex", "gpt-5.4-mini").contextWindow).toBe(400000); - expect(getModel("openai-codex", "gpt-5.5").contextWindow).toBe(400000); - }); -}); From 57b6bdce715119934926210de2815dd986cb3fe0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 11:07:31 +0200 Subject: [PATCH 021/141] docs(coding-agent): update Codex context limit changelog --- packages/coding-agent/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4fa1b007..0fc04af7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed inherited OpenAI GPT-5.4/GPT-5.5 and OpenAI Codex GPT-5.4/GPT-5.4 mini/GPT-5.5 context window metadata to use the observed 272k-token Codex backend limit, avoiding a billing hazard from prompts above Codex's accepted limit (reported by [@trethore](https://github.com/trethore)). + ## [0.79.2] - 2026-06-12 ### New Features From f2585c4c824149ed953f7fcda16b34c9d25fb433 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 11:11:11 +0200 Subject: [PATCH 022/141] Release v0.79.3 --- package-lock.json | 26 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +-- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++++--------- packages/coding-agent/package.json | 8 +++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 19 files changed, 50 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5cdd129d..11022066 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6170,10 +6170,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.2", + "version": "0.79.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.2", + "@earendil-works/pi-ai": "^0.79.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6207,7 +6207,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.2", + "version": "0.79.3", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6252,12 +6252,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.2", + "version": "0.79.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.2", - "@earendil-works/pi-ai": "^0.79.2", - "@earendil-works/pi-tui": "^0.79.2", + "@earendil-works/pi-agent-core": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-tui": "^0.79.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6296,32 +6296,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.2", + "version": "0.79.3", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.2" + "version": "0.79.3" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.2", + "version": "0.79.3", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.2", + "version": "1.9.3", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.2", + "version": "0.79.3", "dependencies": { "ms": "2.1.3" }, @@ -6357,7 +6357,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.2", + "version": "0.79.3", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index ec983000..2c73f2d6 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 diff --git a/packages/agent/package.json b/packages/agent/package.json index 0c94a352..b8a17c29 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.2", + "version": "0.79.3", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.2", + "@earendil-works/pi-ai": "^0.79.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0b0e1e16..b332dc01 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index bbbddd43..806d19ca 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.2", + "version": "0.79.3", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0fc04af7..36e685c5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 8ad2516c..906cc651 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.2", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.2", + "version": "0.79.3", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index c898bca6..b2082217 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.2", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index cff3a11f..3e80ae34 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.2", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 96e72e79..cbd6cfec 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.2", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.2", + "version": "0.79.3", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 416616c4..a339c43a 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.2", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 936fc58b..04da2c15 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.2", + "version": "1.9.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.2", + "version": "1.9.3", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 4d8f4398..60573f2a 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.2", + "version": "1.9.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 4d58693f..df12962d 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.2", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.2", + "version": "0.79.3", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 1c39c1eb..b47c9e68 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.2", + "version": "0.79.3", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 3718cee9..e660e9d7 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.2", + "version": "0.79.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.2", + "version": "0.79.3", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.2", - "@earendil-works/pi-ai": "^0.79.2", - "@earendil-works/pi-tui": "^0.79.2", + "@earendil-works/pi-agent-core": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-tui": "^0.79.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -473,11 +473,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.2.tgz", + "version": "0.79.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.3.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.2", + "@earendil-works/pi-ai": "^0.79.3", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -487,8 +487,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.2.tgz", + "version": "0.79.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.3.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -510,8 +510,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.2", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.2.tgz", + "version": "0.79.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.3.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 73774712..851b80a3 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.2", + "version": "0.79.3", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.2", - "@earendil-works/pi-ai": "^0.79.2", - "@earendil-works/pi-tui": "^0.79.2", + "@earendil-works/pi-agent-core": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-tui": "^0.79.3", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 1edd68d0..da464e1c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 diff --git a/packages/tui/package.json b/packages/tui/package.json index 0b1d76a3..a808a640 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.2", + "version": "0.79.3", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From b15148fe376ca79fe66c1c22a64542b9a9d2a2b5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 11:11:14 +0200 Subject: [PATCH 023/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 2c73f2d6..f176ad06 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b332dc01..03604c9b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 36e685c5..7f677ad9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index da464e1c..69b0529a 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 From 6f29450e1339b37582796775749de3957936cd8d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 11:17:38 +0200 Subject: [PATCH 024/141] fix(ai): update adaptive thinking model expectations --- packages/ai/test/anthropic-adaptive-thinking-models.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts index da042e2f..18023e11 100644 --- a/packages/ai/test/anthropic-adaptive-thinking-models.test.ts +++ b/packages/ai/test/anthropic-adaptive-thinking-models.test.ts @@ -5,9 +5,8 @@ import type { Api, Model } from "../src/types.ts"; const EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS = [ "anthropic/claude-fable-5", "anthropic/claude-opus-4-8", - "opencode/claude-fable-5", + "cloudflare-ai-gateway/claude-fable-5", "opencode/claude-opus-4-8", - "vercel-ai-gateway/anthropic/claude-fable-5", "vercel-ai-gateway/anthropic/claude-opus-4.8", ]; From f315d81491813682d02d659e55788fe227b3db2c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 15:43:00 +0200 Subject: [PATCH 025/141] meta: Update weekend policy in contributing to be less confusing --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24f0f825..d2c79b9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ If you use an agent, run it from the `pi-mono` root directory so it picks up `AG All issues and PRs from new contributors are auto-closed by default. -Issues submitted Friday through Sunday are not reviewed. If something is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx +Issues submitted Friday through Sunday are not guaranteed to be reviewed. If something is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar below will not be reopened or receive a reply. @@ -76,9 +76,9 @@ Ask on [Discord](https://discord.com/invite/nKXTsAcmbT). pi receives more issues than the maintainers can responsibly review in real time. Many reports do not meet the quality bar in this guide or do not follow CONTRIBUTING.md. Some are slung at the repository mindlessly via an agent instead of being reviewed and shaped by the person submitting them. Auto-closing creates a buffer so maintainers can review the tracker on their own schedule and reopen the issues that meet the quality bar. -### Why are weekend issues not reviewed? +### Why are weekend issues lower priority? -Maintainers need uninterrupted time away from the issue tracker. Issues submitted Friday through Sunday are auto-closed and are not part of the Monday review queue. If a problem is urgent, ask on Discord and include the short version, a repro, and the relevant logs. +We triage the tracker during working hours. That means more issues can accumulate over the weekend. Anything submitted Friday through Sunday may be missed or given lower priority in the Monday review queue. If a problem is urgent, ask on Discord and include the short version, a repro, and the relevant logs. ### Why do some issues get no reply? From 9e9fc7947871a913946f727854ae0a57fbce1863 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 21:24:22 +0200 Subject: [PATCH 026/141] fix(coding-agent): treat uppercase config values as literals closes #5661 --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/docs/models.md | 4 +- packages/coding-agent/docs/providers.md | 4 +- .../coding-agent/src/core/model-registry.ts | 80 +--------- .../src/core/resolve-config-value.ts | 5 - packages/coding-agent/src/migrations.ts | 139 +--------------- .../test/config-value-migration.test.ts | 150 ++++++++++-------- .../coding-agent/test/model-registry.test.ts | 52 ++++-- .../5661-uppercase-header-values.test.ts | 91 +++++++++++ 9 files changed, 226 insertions(+), 303 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7f677ad9..6b17d85a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). + ## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 39b56aa1..830eaacd 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -161,13 +161,11 @@ The `apiKey` and `headers` fields support command execution, environment interpo "apiKey": "$$literal-dollar-prefix" "apiKey": "$!literal-bang-prefix" ``` -- **Literal value:** Used directly +- **Literal value:** Used directly. Plain uppercase strings such as `MY_API_KEY` are literals; use `$MY_API_KEY` for environment variables. ```json "apiKey": "sk-..." ``` -Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup. - For `models.json`, shell commands are resolved at request time. pi intentionally does not apply built-in TTL, stale reuse, or recovery logic for arbitrary commands. Different commands need different caching and failure strategies, and pi cannot infer the right one. If your command is slow, expensive, rate-limited, or should keep using a previous value on transient failures, wrap it in your own script or command that implements the caching or TTL behavior you want. diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 185405cd..f139aa30 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -124,13 +124,13 @@ The `key` field supports command execution, environment interpolation, and liter { "type": "api_key", "key": "$$literal-dollar-prefix" } { "type": "api_key", "key": "$!literal-bang-prefix" } ``` -- **Literal value:** Used directly +- **Literal value:** Used directly. Plain uppercase strings such as `MY_API_KEY` are literals; use `$MY_API_KEY` for environment variables. ```json { "type": "api_key", "key": "sk-ant-..." } { "type": "api_key", "key": "public" } ``` -Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup. OAuth credentials are also stored here after `/login` and managed automatically. +OAuth credentials are also stored here after `/login` and managed automatically. ## Cloud Providers diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 00c81576..ff172966 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -25,7 +25,6 @@ import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import type { TLocalizedValidationError } from "typebox/error"; import { getAgentDir } from "../config.ts"; -import { warnDeprecation } from "../utils/deprecation.ts"; import { stripJsonComments } from "../utils/json.ts"; import { normalizePath } from "../utils/paths.ts"; import type { AuthStatus, AuthStorage } from "./auth-storage.ts"; @@ -35,7 +34,6 @@ import { getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, - isLegacyEnvVarNameConfigValue, resolveConfigValueOrThrow, resolveConfigValueUncached, resolveHeadersOrThrow, @@ -237,77 +235,6 @@ interface ProviderRequestConfig { authHeader?: boolean; } -function migrateLegacyRegisterProviderConfigValue(providerName: string, field: string, value: string): string { - if (!isLegacyEnvVarNameConfigValue(value)) return value; - warnDeprecation( - `registerProvider("${providerName}") ${field} value "${value}" is treated as a legacy environment variable reference. This will no longer be detected as an environment variable reference in a future release. Pass "$${value}" instead.`, - ); - return `$${value}`; -} - -function migrateLegacyRegisterProviderHeaders( - providerName: string, - field: string, - headers: Record | undefined, -): Record | undefined { - if (!headers) return undefined; - let migratedHeaders: Record | undefined; - for (const [key, value] of Object.entries(headers)) { - const migratedValue = migrateLegacyRegisterProviderConfigValue(providerName, `${field} header "${key}"`, value); - if (migratedValue === value) continue; - migratedHeaders ??= { ...headers }; - migratedHeaders[key] = migratedValue; - } - return migratedHeaders ?? headers; -} - -function migrateLegacyRegisterProviderConfigValues( - providerName: string, - config: ProviderConfigInput, -): ProviderConfigInput { - let migratedConfig: ProviderConfigInput | undefined; - - const setMigratedConfigValue = ( - key: TKey, - value: ProviderConfigInput[TKey], - ) => { - migratedConfig ??= { ...config }; - migratedConfig[key] = value; - }; - - if (config.apiKey) { - const apiKey = migrateLegacyRegisterProviderConfigValue(providerName, "apiKey", config.apiKey); - if (apiKey !== config.apiKey) { - setMigratedConfigValue("apiKey", apiKey); - } - } - - const headers = migrateLegacyRegisterProviderHeaders(providerName, "headers", config.headers); - if (headers !== config.headers) { - setMigratedConfigValue("headers", headers); - } - - if (config.models) { - let models: ProviderConfigInput["models"] | undefined; - for (let index = 0; index < config.models.length; index++) { - const model = config.models[index]; - const modelHeaders = migrateLegacyRegisterProviderHeaders( - providerName, - `model "${model.id}" headers`, - model.headers, - ); - if (modelHeaders === model.headers) continue; - models ??= [...config.models]; - models[index] = { ...model, headers: modelHeaders }; - } - if (models) { - setMigratedConfigValue("models", models); - } - } - - return migratedConfig ?? config; -} - export type ResolvedRequestAuth = | { ok: true; @@ -869,10 +796,9 @@ export class ModelRegistry { * If provider has oauth: registers OAuth provider for /login support. */ registerProvider(providerName: string, config: ProviderConfigInput): void { - const migratedConfig = migrateLegacyRegisterProviderConfigValues(providerName, config); - this.validateProviderConfig(providerName, migratedConfig); - this.applyProviderConfig(providerName, migratedConfig); - this.upsertRegisteredProvider(providerName, migratedConfig); + this.validateProviderConfig(providerName, config); + this.applyProviderConfig(providerName, config); + this.upsertRegisteredProvider(providerName, config); } /** diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 119e379d..6c2263f6 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -10,7 +10,6 @@ import { getShellConfig } from "../utils/shell.ts"; const commandResultCache = new Map(); const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; const ENV_VAR_NAME_PREFIX_RE = /^[A-Za-z_][A-Za-z0-9_]*/; -const LEGACY_ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/; type TemplatePart = { type: "literal"; value: string } | { type: "env"; name: string }; @@ -136,10 +135,6 @@ export function isConfigValueConfigured(config: string): boolean { return getMissingConfigValueEnvVarNames(config).length === 0; } -export function isLegacyEnvVarNameConfigValue(config: string): boolean { - return LEGACY_ENV_VAR_NAME_RE.test(config); -} - /** * Resolve a config value (API key, header value, etc.) to an actual value. * - If starts with "!", executes the rest as a shell command and uses stdout (cached) diff --git a/packages/coding-agent/src/migrations.ts b/packages/coding-agent/src/migrations.ts index 5cce43b8..39aeea04 100644 --- a/packages/coding-agent/src/migrations.ts +++ b/packages/coding-agent/src/migrations.ts @@ -3,12 +3,10 @@ */ import chalk from "chalk"; -import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.ts"; import { migrateKeybindingsConfig } from "./core/keybindings.ts"; -import { isLegacyEnvVarNameConfigValue } from "./core/resolve-config-value.ts"; -import { stripJsonComments } from "./utils/json.ts"; const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration"; @@ -74,140 +72,6 @@ export function migrateAuthToAuthJson(): string[] { return providers; } -interface ConfigValueMigration { - location: string; - from: string; - to: string; -} - -function migrateLegacyEnvVarString(value: string): string | undefined { - return isLegacyEnvVarNameConfigValue(value) ? `$${value}` : undefined; -} - -function migrateStringProperty( - record: Record, - key: string, - location: string, - migrations: ConfigValueMigration[], -): boolean { - const value = record[key]; - if (typeof value !== "string") return false; - const migrated = migrateLegacyEnvVarString(value); - if (migrated === undefined) return false; - record[key] = migrated; - migrations.push({ location, from: value, to: migrated }); - return true; -} - -function migrateHeadersConfig(headers: unknown, location: string, migrations: ConfigValueMigration[]): boolean { - if (typeof headers !== "object" || headers === null || Array.isArray(headers)) return false; - const headerRecord = headers as Record; - let migrated = false; - for (const [key, value] of Object.entries(headerRecord)) { - if (typeof value !== "string") continue; - const migratedValue = migrateLegacyEnvVarString(value); - if (migratedValue === undefined) continue; - headerRecord[key] = migratedValue; - migrations.push({ location: `${location}[${JSON.stringify(key)}]`, from: value, to: migratedValue }); - migrated = true; - } - return migrated; -} - -function migrateAuthJsonConfigValues(agentDir: string): ConfigValueMigration[] { - const authPath = join(agentDir, "auth.json"); - if (!existsSync(authPath)) return []; - - try { - const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return []; - const authData = parsed as Record; - - const migrations: ConfigValueMigration[] = []; - for (const [provider, credential] of Object.entries(authData)) { - if (typeof credential !== "object" || credential === null || Array.isArray(credential)) continue; - const credentialRecord = credential as Record; - if (credentialRecord.type !== "api_key") continue; - migrateStringProperty(credentialRecord, "key", `auth.json[${JSON.stringify(provider)}].key`, migrations); - } - - if (migrations.length === 0) return []; - writeFileSync(authPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8"); - chmodSync(authPath, 0o600); - return migrations; - } catch { - return []; - } -} - -function migrateModelsJsonConfigValues(agentDir: string): ConfigValueMigration[] { - const modelsPath = join(agentDir, "models.json"); - if (!existsSync(modelsPath)) return []; - - try { - const parsed = JSON.parse(stripJsonComments(readFileSync(modelsPath, "utf-8"))) as unknown; - if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return []; - const modelsData = parsed as Record; - const providers = modelsData.providers; - if (typeof providers !== "object" || providers === null || Array.isArray(providers)) return []; - - const migrations: ConfigValueMigration[] = []; - for (const [provider, providerConfig] of Object.entries(providers)) { - if (typeof providerConfig !== "object" || providerConfig === null || Array.isArray(providerConfig)) continue; - const providerRecord = providerConfig as Record; - const providerLocation = `models.json.providers[${JSON.stringify(provider)}]`; - migrateStringProperty(providerRecord, "apiKey", `${providerLocation}.apiKey`, migrations); - migrateHeadersConfig(providerRecord.headers, `${providerLocation}.headers`, migrations); - - if (Array.isArray(providerRecord.models)) { - for (let index = 0; index < providerRecord.models.length; index++) { - const modelConfig = providerRecord.models[index]; - if (typeof modelConfig !== "object" || modelConfig === null || Array.isArray(modelConfig)) continue; - const modelRecord = modelConfig as Record; - const modelKey = typeof modelRecord.id === "string" ? JSON.stringify(modelRecord.id) : String(index); - migrateHeadersConfig(modelRecord.headers, `${providerLocation}.models[${modelKey}].headers`, migrations); - } - } - - const modelOverrides = providerRecord.modelOverrides; - if (typeof modelOverrides === "object" && modelOverrides !== null && !Array.isArray(modelOverrides)) { - for (const [modelId, modelOverride] of Object.entries(modelOverrides)) { - if (typeof modelOverride !== "object" || modelOverride === null || Array.isArray(modelOverride)) - continue; - const modelOverrideRecord = modelOverride as Record; - migrateHeadersConfig( - modelOverrideRecord.headers, - `${providerLocation}.modelOverrides[${JSON.stringify(modelId)}].headers`, - migrations, - ); - } - } - } - - if (migrations.length === 0) return []; - writeFileSync(modelsPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf-8"); - return migrations; - } catch { - return []; - } -} - -function migrateExplicitEnvVarConfigValues(): void { - const agentDir = getAgentDir(); - const migrations = [...migrateAuthJsonConfigValues(agentDir), ...migrateModelsJsonConfigValues(agentDir)]; - if (migrations.length === 0) return; - - const details = migrations.map((migration) => ` - ${migration.location}: ${migration.from} -> ${migration.to}`); - console.log( - chalk.yellow( - [ - "Warning: Migrated API key/header environment references to explicit $ENV_VAR syntax. Plain strings will be treated as literals.", - ...details, - ].join("\n"), - ), - ); -} - /** * Migrate sessions from ~/.pi/agent/*.jsonl to proper session directories. * @@ -443,7 +307,6 @@ export function runMigrations(cwd: string): { deprecationWarnings: string[]; } { const migratedAuthProviders = migrateAuthToAuthJson(); - migrateExplicitEnvVarConfigValues(); migrateSessionsFromAgentRoot(); migrateToolsToBin(); migrateKeybindingsConfigFile(); diff --git a/packages/coding-agent/test/config-value-migration.test.ts b/packages/coding-agent/test/config-value-migration.test.ts index d0bb8125..8a273b53 100644 --- a/packages/coding-agent/test/config-value-migration.test.ts +++ b/packages/coding-agent/test/config-value-migration.test.ts @@ -37,7 +37,7 @@ describe("config value env var syntax migration", () => { } } - it("rewrites legacy uppercase auth.json API key values to explicit env references", () => { + it("leaves uppercase auth.json API key values unchanged", () => { const agentDir = createAgentDir(); fs.writeFileSync( path.join(agentDir, "auth.json"), @@ -61,19 +61,17 @@ describe("config value env var syntax migration", () => { string, Record >; - expect(migrated.anthropic.key).toBe("$ANTHROPIC_API_KEY"); + expect(migrated.anthropic.key).toBe("ANTHROPIC_API_KEY"); expect(migrated.openai.key).toBe("$OPENAI_API_KEY"); expect(migrated.opencode.key).toBe("public"); expect(migrated.github.access).toBe("ACCESS_TOKEN"); - const logMessage = String(logSpy.mock.calls[0]?.[0] ?? ""); - expect(logMessage).toContain("explicit $ENV_VAR syntax"); - expect(logMessage).toContain('auth.json["anthropic"].key: ANTHROPIC_API_KEY -> $ANTHROPIC_API_KEY'); + expect(logSpy).not.toHaveBeenCalled(); }); it.each([ ["malformed", '{\n "providers": {\n'], ["blank", ""], - ])("does not throw on %s models.json during config migration", (_name, content) => { + ])("does not throw on %s models.json during migrations", (_name, content) => { const agentDir = createAgentDir(); const modelsPath = path.join(agentDir, "models.json"); fs.writeFileSync(modelsPath, content, "utf-8"); @@ -87,71 +85,93 @@ describe("config value env var syntax migration", () => { expect(loadError).toContain(`File: ${modelsPath}`); }); - it("rewrites legacy uppercase models.json API key and header values", () => { + it("leaves uppercase models.json API key and header values unchanged", async () => { const agentDir = createAgentDir(); - fs.writeFileSync( - path.join(agentDir, "models.json"), - `${JSON.stringify( - { - providers: { - "custom-provider": { - baseUrl: "https://example.com/v1", - apiKey: "CUSTOM_API_KEY", - api: "openai-completions", - headers: { - "x-api-key": "HEADER_API_KEY", - "x-literal": "literal", - }, - models: [ - { - id: "model-a", - headers: { "x-model-key": "MODEL_API_KEY" }, + const envKeys = ["CUSTOM_API_KEY", "HEADER_API_KEY", "MODEL_API_KEY", "OVERRIDE_API_KEY"]; + const savedEnv: Record = {}; + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + process.env[key] = `env-${key}`; + } + + try { + fs.writeFileSync( + path.join(agentDir, "models.json"), + `${JSON.stringify( + { + providers: { + "custom-provider": { + baseUrl: "https://example.com/v1", + apiKey: "CUSTOM_API_KEY", + api: "openai-completions", + headers: { + "x-api-key": "HEADER_API_KEY", + "x-literal": "literal", + }, + models: [ + { + id: "model-a", + headers: { "x-model-key": "MODEL_API_KEY" }, + }, + ], + modelOverrides: { + "model-b": { headers: { "x-override-key": "OVERRIDE_API_KEY" } }, }, - ], - modelOverrides: { - "model-b": { headers: { "x-override-key": "OVERRIDE_API_KEY" } }, }, }, }, + null, + 2, + )}\n`, + "utf-8", + ); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + withAgentDir(agentDir, () => runMigrations(agentDir)); + + const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "models.json"), "utf-8")) as { + providers: Record< + string, + { + apiKey?: string; + headers?: Record; + models?: Array<{ headers?: Record }>; + modelOverrides?: Record }>; + } + >; + }; + const provider = migrated.providers["custom-provider"]!; + expect(provider.apiKey).toBe("CUSTOM_API_KEY"); + expect(provider.headers?.["x-api-key"]).toBe("HEADER_API_KEY"); + expect(provider.headers?.["x-literal"]).toBe("literal"); + expect(provider.models?.[0]?.headers?.["x-model-key"]).toBe("MODEL_API_KEY"); + expect(provider.modelOverrides?.["model-b"]?.headers?.["x-override-key"]).toBe("OVERRIDE_API_KEY"); + expect(logSpy).not.toHaveBeenCalled(); + + const registry = ModelRegistry.create( + AuthStorage.create(path.join(agentDir, "auth.json")), + path.join(agentDir, "models.json"), + ); + const model = registry.find("custom-provider", "model-a"); + expect(model).toBeDefined(); + expect(await registry.getApiKeyForProvider("custom-provider")).toBe("CUSTOM_API_KEY"); + expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({ + ok: true, + apiKey: "CUSTOM_API_KEY", + headers: { + "x-api-key": "HEADER_API_KEY", + "x-literal": "literal", + "x-model-key": "MODEL_API_KEY", }, - null, - 2, - )}\n`, - "utf-8", - ); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - - withAgentDir(agentDir, () => runMigrations(agentDir)); - - const migrated = JSON.parse(fs.readFileSync(path.join(agentDir, "models.json"), "utf-8")) as { - providers: Record< - string, - { - apiKey?: string; - headers?: Record; - models?: Array<{ headers?: Record }>; - modelOverrides?: Record }>; + }); + } finally { + for (const key of envKeys) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; } - >; - }; - const provider = migrated.providers["custom-provider"]!; - expect(provider.apiKey).toBe("$CUSTOM_API_KEY"); - expect(provider.headers?.["x-api-key"]).toBe("$HEADER_API_KEY"); - expect(provider.headers?.["x-literal"]).toBe("literal"); - expect(provider.models?.[0]?.headers?.["x-model-key"]).toBe("$MODEL_API_KEY"); - expect(provider.modelOverrides?.["model-b"]?.headers?.["x-override-key"]).toBe("$OVERRIDE_API_KEY"); - const logMessage = String(logSpy.mock.calls[0]?.[0] ?? ""); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].apiKey: CUSTOM_API_KEY -> $CUSTOM_API_KEY', - ); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].headers["x-api-key"]: HEADER_API_KEY -> $HEADER_API_KEY', - ); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].models["model-a"].headers["x-model-key"]: MODEL_API_KEY -> $MODEL_API_KEY', - ); - expect(logMessage).toContain( - 'models.json.providers["custom-provider"].modelOverrides["model-b"].headers["x-override-key"]: OVERRIDE_API_KEY -> $OVERRIDE_API_KEY', - ); + } + } }); }); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 95b70fbd..3cba5264 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -7,7 +7,6 @@ import { getOAuthProvider } from "@earendil-works/pi-ai/oauth"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; import { clearApiKeyCache, ModelRegistry, type ProviderConfigInput } from "../src/core/model-registry.ts"; -import { clearDeprecationWarningsForTests } from "../src/utils/deprecation.ts"; describe("ModelRegistry", () => { let tempDir: string; @@ -19,7 +18,6 @@ describe("ModelRegistry", () => { mkdirSync(tempDir, { recursive: true }); modelsJsonPath = join(tempDir, "models.json"); authStorage = AuthStorage.create(join(tempDir, "auth.json")); - clearDeprecationWarningsForTests(); }); afterEach(() => { @@ -27,7 +25,6 @@ describe("ModelRegistry", () => { rmSync(tempDir, { recursive: true }); } clearApiKeyCache(); - clearDeprecationWarningsForTests(); vi.restoreAllMocks(); }); @@ -896,26 +893,55 @@ describe("ModelRegistry", () => { expect(registry.getProviderDisplayName("oauth-provider")).toBe("OAuth Provider"); }); - test("registerProvider warns and temporarily treats uppercase apiKey as an env reference", async () => { - const originalEnv = process.env.CUSTOM_NAME; - process.env.CUSTOM_NAME = "legacy-env-key"; + test("registerProvider treats uppercase apiKey and headers as literals", async () => { + const envKeys = ["CUSTOM_NAME", "BEARER", "MODEL_TOKEN"]; + const savedEnv: Record = {}; + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + process.env[key] = `env-${key}`; + } const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); try { const registry = ModelRegistry.create(authStorage, modelsJsonPath); - registry.registerProvider("legacy-provider", { + registry.registerProvider("literal-provider", { ...providerConfig("https://provider.test/v1", [{ id: "demo-model" }], "openai-completions"), apiKey: "CUSTOM_NAME", + headers: { Authorization: "BEARER" }, + models: [ + { + id: "demo-model", + name: "demo-model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 100000, + maxTokens: 8000, + headers: { "x-model-token": "MODEL_TOKEN" }, + }, + ], }); - expect(await registry.getApiKeyForProvider("legacy-provider")).toBe("legacy-env-key"); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Pass "$CUSTOM_NAME" instead')); + expect(await registry.getApiKeyForProvider("literal-provider")).toBe("CUSTOM_NAME"); + const model = registry.find("literal-provider", "demo-model"); + expect(model).toBeDefined(); + expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({ + ok: true, + apiKey: "CUSTOM_NAME", + headers: { + Authorization: "BEARER", + "x-model-token": "MODEL_TOKEN", + }, + }); + expect(warnSpy).not.toHaveBeenCalled(); } finally { - if (originalEnv === undefined) { - delete process.env.CUSTOM_NAME; - } else { - process.env.CUSTOM_NAME = originalEnv; + for (const key of envKeys) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } } } }); diff --git a/packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts b/packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts new file mode 100644 index 00000000..e625a3c6 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5661-uppercase-header-values.test.ts @@ -0,0 +1,91 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { ENV_AGENT_DIR } from "../../../src/config.ts"; +import { AuthStorage } from "../../../src/core/auth-storage.ts"; +import { ModelRegistry } from "../../../src/core/model-registry.ts"; +import { runMigrations } from "../../../src/migrations.ts"; +import { createHarness } from "../harness.ts"; + +describe("regression #5661: uppercase models.json header values", () => { + const cleanups: Array<() => void> = []; + + afterEach(() => { + while (cleanups.length > 0) { + cleanups.pop()?.(); + } + }); + + function withAgentDir(agentDir: string, fn: () => void): void { + const previousAgentDir = process.env[ENV_AGENT_DIR]; + process.env[ENV_AGENT_DIR] = agentDir; + try { + fn(); + } finally { + if (previousAgentDir === undefined) { + delete process.env[ENV_AGENT_DIR]; + } else { + process.env[ENV_AGENT_DIR] = previousAgentDir; + } + } + } + + it("keeps uppercase header strings as literals during startup migrations", async () => { + const harness = await createHarness({ withConfiguredAuth: false }); + cleanups.push(harness.cleanup); + + const envKeys = ["CUSTOM_API_KEY", "BEARER"]; + const savedEnv: Record = {}; + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + process.env[key] = `env-${key}`; + } + cleanups.push(() => { + for (const key of envKeys) { + if (savedEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = savedEnv[key]; + } + } + }); + + const modelsPath = join(harness.tempDir, "models.json"); + writeFileSync( + modelsPath, + `${JSON.stringify( + { + providers: { + "my-provider": { + baseUrl: "https://example.com/v1", + apiKey: "CUSTOM_API_KEY", + api: "openai-completions", + headers: { Authorization: "BEARER" }, + models: [{ id: "my-model" }], + }, + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + + withAgentDir(harness.tempDir, () => runMigrations(harness.tempDir)); + + const migrated = JSON.parse(readFileSync(modelsPath, "utf-8")) as { + providers: Record }>; + }; + expect(migrated.providers["my-provider"]?.apiKey).toBe("CUSTOM_API_KEY"); + expect(migrated.providers["my-provider"]?.headers?.Authorization).toBe("BEARER"); + + const registry = ModelRegistry.create(AuthStorage.create(join(harness.tempDir, "auth.json")), modelsPath); + const model = registry.find("my-provider", "my-model"); + expect(model).toBeDefined(); + expect(await registry.getApiKeyAndHeaders(model!)).toMatchObject({ + ok: true, + apiKey: "CUSTOM_API_KEY", + headers: { Authorization: "BEARER" }, + }); + }); +}); From 21a904f4b7a117b253dac05f27e39724ad9e2385 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 13 Jun 2026 23:59:45 +0200 Subject: [PATCH 027/141] fix(ai): disable OpenCode long cache retention for rejecting routes closes #5702 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/scripts/generate-models.ts | 16 +++++++ packages/ai/src/models.generated.ts | 56 +++++++++++++++++++----- packages/ai/test/cache-retention.test.ts | 40 +++++++++++++++++ packages/coding-agent/CHANGELOG.md | 1 + 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 03604c9b..71da18c3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenCode/OpenCode Go completion models that reject `prompt_cache_retention` to omit long-retention cache fields when `cacheRetention` is `long` ([#5702](https://github.com/earendil-works/pi/issues/5702)). + ## [0.79.3] - 2026-06-13 ### Fixed diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 1eaf9fe1..63c54a56 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -187,6 +187,15 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([ "gpt-5.5", ]); +const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([ + "opencode:deepseek-v4-flash", + "opencode:deepseek-v4-pro", + "opencode:kimi-k2.5", + "opencode:kimi-k2.6", + "opencode:minimax-m2.7", + "opencode-go:kimi-k2.6", +]); + function mergeThinkingLevelMap(model: Model, map: NonNullable["thinkingLevelMap"]>): void { model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map }; } @@ -1070,6 +1079,13 @@ async function loadModelsDevData(): Promise[]> { if (api === "openai-completions") { compat = { ...(compat ?? {}), maxTokensField: "max_tokens" }; + if ( + OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS.has( + `${variant.provider}:${modelId}`, + ) + ) { + compat = { ...compat, supportsLongCacheRetention: false }; + } } models.push({ diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 0b59d572..35295e5d 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7889,7 +7889,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -7927,7 +7927,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -8343,7 +8343,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -8361,7 +8361,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text", "image"], cost: { @@ -8415,7 +8415,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, input: ["text"], cost: { @@ -8579,7 +8579,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens"}, + compat: {"thinkingFormat":"deepseek","supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsLongCacheRetention":false}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, input: ["text", "image"], @@ -10473,13 +10473,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.95, - output: 4, - cacheRead: 0.19, + input: 0.75, + output: 3.5, + cacheRead: 0.16, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", @@ -16823,6 +16823,24 @@ export const MODELS = { contextWindow: 200000, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", name: "GLM-5V-Turbo", @@ -16915,6 +16933,24 @@ export const MODELS = { contextWindow: 200000, maxTokens: 131072, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "glm-5v-turbo": { id: "glm-5v-turbo", name: "GLM-5V-Turbo", diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 8828ba86..6e2c1a5b 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; @@ -13,6 +14,11 @@ class PayloadCaptured extends Error { } } +interface OpenAICompletionsCachePayload { + prompt_cache_key?: string; + prompt_cache_retention?: string; +} + function stopAfterPayload(capture: (payload: TPayload) => void): (payload: unknown) => never { return (payload: unknown): never => { capture(payload as TPayload); @@ -455,5 +461,39 @@ describe("Cache Retention (PI_CACHE_RETENTION)", () => { expect(capturedPayload.prompt_cache_key).toBeUndefined(); expect(capturedPayload.prompt_cache_retention).toBeUndefined(); }); + + it.each([ + MODELS.opencode["deepseek-v4-flash"], + MODELS.opencode["deepseek-v4-pro"], + MODELS.opencode["kimi-k2.5"], + MODELS.opencode["kimi-k2.6"], + MODELS.opencode["minimax-m2.7"], + MODELS["opencode-go"]["kimi-k2.6"], + ] as const)("should omit long cache retention for $provider/$id", async (metadata) => { + const model = metadata as Model<"openai-completions">; + let capturedPayload: OpenAICompletionsCachePayload | undefined; + + try { + const s = streamOpenAICompletions(model, context, { + apiKey: "fake-key", + cacheRetention: "long", + sessionId: "session-opencode-long-cache-unsupported", + onPayload: stopAfterPayload((payload) => { + capturedPayload = payload; + }), + }); + + for await (const event of s) { + if (event.type === "error") break; + } + } catch { + // Expected to fail + } + + expect(model.compat?.supportsLongCacheRetention).toBe(false); + expect(capturedPayload).toBeDefined(); + expect(capturedPayload?.prompt_cache_key).toBeUndefined(); + expect(capturedPayload?.prompt_cache_retention).toBeUndefined(); + }); }); }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6b17d85a..3cb2c685 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). - Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). ## [0.79.3] - 2026-06-13 From 5be8c31fa7a26f26cdc78b021e64ff0b7541d57a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 00:57:56 +0200 Subject: [PATCH 028/141] meta: add extension disclaimer to bug reporting --- .github/ISSUE_TEMPLATE/bug.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index cf50ffde..43cb73c5 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -11,6 +11,8 @@ body: Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice. + **Important:** before reporting an issue in core, please validate first with `pi -ne` that this is not caused by an extension you loaded. + - type: textarea id: description attributes: From 2fbdff9dab1a1652aa9f97e9508b90b4ae44a7ce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 01:31:32 +0200 Subject: [PATCH 029/141] fix(coding-agent): fix pnpm self-update bin-dir closes #5689 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/config.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3cb2c685..152ee9f0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). - Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). - Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 65fb220e..75b2efaf 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -109,19 +109,27 @@ function getSelfUpdateCommandForMethod( switch (method) { case "bun-binary": return undefined; - case "pnpm": + case "pnpm": { + const match = readCommandOutput("pnpm", ["root", "-g"]) + ? undefined + : /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir()); + const binDirArgs = match + ? [`--config.global-bin-dir=${process.env.PNPM_HOME || dirname(dirname(match[1]))}`] + : []; return makeSelfUpdateCommand( makeSelfUpdateCommandStep("pnpm", [ "install", "-g", "--ignore-scripts", "--config.minimumReleaseAge=0", + ...binDirArgs, updatePackageName, ]), updatePackageName === installedPackageName ? undefined - : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), + : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]), ); + } case "yarn": return makeSelfUpdateCommand( makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), @@ -205,7 +213,9 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC } case "pnpm": { const root = readCommandOutput("pnpm", ["root", "-g"]); - return root ? [root, dirname(root)] : []; + if (root) return [root, dirname(root)]; + const match = /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(getPackageDir()); + return match ? [match[1]] : []; } case "yarn": { const dir = readCommandOutput("yarn", ["global", "dir"]); From c48f656f88697b23c65cce1e658893eda02c4c12 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 01:37:58 +0200 Subject: [PATCH 030/141] fix(coding-agent): handle npm package semver ranges closes #5695 --- package-lock.json | 10 +++- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/npm-shrinkwrap.json | 13 +++++ packages/coding-agent/package.json | 2 + .../coding-agent/src/core/package-manager.ts | 53 ++++++++++++------- .../coding-agent/src/utils/version-check.ts | 34 ++---------- .../coding-agent/test/package-manager.test.ts | 38 ++++++++----- .../coding-agent/test/version-check.test.ts | 1 + 8 files changed, 90 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 11022066..4ee5233f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2337,6 +2337,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript/native-preview": { "version": "7.0.0-dev.20260120.1", "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260120.1.tgz", @@ -4700,7 +4707,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6269,6 +6275,7 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" @@ -6283,6 +6290,7 @@ "@types/ms": "2.1.0", "@types/node": "24.12.4", "@types/proper-lockfile": "4.1.4", + "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", "vitest": "3.2.4" diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 152ee9f0..71de722b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). +- Fixed npm package specs that use ranges or tags (for example `@^1.2.7`) so installed package resources still load instead of being treated as mismatched exact pins ([#5695](https://github.com/earendil-works/pi/issues/5695)). - Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). - Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index e660e9d7..74eff303 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -23,6 +23,7 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" @@ -1636,6 +1637,18 @@ } ] }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 851b80a3..ff4522ce 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -50,6 +50,7 @@ "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", + "semver": "7.8.0", "typebox": "1.1.38", "undici": "8.3.0", "yaml": "2.9.0" @@ -70,6 +71,7 @@ "@types/ms": "2.1.0", "@types/node": "24.12.4", "@types/proper-lockfile": "4.1.4", + "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", "vitest": "3.2.4" diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 5120c9ad..ebdf974b 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -27,6 +27,7 @@ import type { Readable } from "node:stream"; import { globSync } from "glob"; import ignore from "ignore"; import { minimatch } from "minimatch"; +import { maxSatisfying, rcompare, satisfies, valid, validRange } from "semver"; import { CONFIG_DIR_NAME } from "../config.ts"; import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts"; import { type GitSource, parseGitUrl } from "../utils/git.ts"; @@ -44,6 +45,14 @@ function isOfflineModeEnabled(): boolean { return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes"; } +function isExactNpmVersion(version: string | undefined): boolean { + return valid(version ?? "") !== null; +} + +function getNpmVersionRange(version: string | undefined): string | undefined { + return version ? (validRange(version) ?? undefined) : undefined; +} + export interface PathMetadata { source: string; scope: SourceScope; @@ -119,6 +128,8 @@ type NpmSource = { type: "npm"; spec: string; name: string; + version?: string; + range?: string; pinned: boolean; }; @@ -1113,8 +1124,8 @@ export class DefaultPackageManager implements PackageManager { } try { - const latestVersion = await this.getLatestNpmVersion(source.name); - return latestVersion !== installedVersion; + const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range); + return targetVersion !== installedVersion; } catch { // Preserve existing update behavior when version lookup fails. return true; @@ -1128,7 +1139,7 @@ export class DefaultPackageManager implements PackageManager { const sourceLabel = sources.length === 1 ? sources[0].source : `${scope} npm packages`; const message = sources.length === 1 ? `Updating ${sources[0].source}...` : `Updating ${scope} npm packages...`; - const specs = sources.map((entry) => `${entry.parsed.name}@latest`); + const specs = sources.map((entry) => (entry.parsed.version ? entry.parsed.spec : `${entry.parsed.name}@latest`)); await this.withProgress("update", sourceLabel, message, async () => { await this.installNpmBatch(specs, scope); @@ -1241,8 +1252,7 @@ export class DefaultPackageManager implements PackageManager { if (parsed.type === "npm") { let installedPath = this.getNpmInstallPath(parsed, scope); const needsInstall = - !existsSync(installedPath) || - (parsed.pinned && !(await this.installedNpmMatchesPinnedVersion(parsed, installedPath))); + !existsSync(installedPath) || !(await this.installedNpmMatchesConfiguredVersion(parsed, installedPath)); if (needsInstall) { const installed = await installMissing(); if (!installed) continue; @@ -1394,7 +1404,9 @@ export class DefaultPackageManager implements PackageManager { type: "npm", spec, name, - pinned: Boolean(version), + version, + range: getNpmVersionRange(version), + pinned: isExactNpmVersion(version), }; } @@ -1411,18 +1423,12 @@ export class DefaultPackageManager implements PackageManager { return { type: "local", path: source }; } - private async installedNpmMatchesPinnedVersion(source: NpmSource, installedPath: string): Promise { + private async installedNpmMatchesConfiguredVersion(source: NpmSource, installedPath: string): Promise { const installedVersion = this.getInstalledNpmVersion(installedPath); if (!installedVersion) { return false; } - - const { version: pinnedVersion } = this.parseNpmSpec(source.spec); - if (!pinnedVersion) { - return true; - } - - return installedVersion === pinnedVersion; + return source.range ? satisfies(installedVersion, source.range) : true; } private async npmHasAvailableUpdate(source: NpmSource, installedPath: string): Promise { @@ -1436,8 +1442,8 @@ export class DefaultPackageManager implements PackageManager { } try { - const latestVersion = await this.getLatestNpmVersion(source.name); - return latestVersion !== installedVersion; + const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range); + return targetVersion !== installedVersion; } catch { return false; } @@ -1455,16 +1461,25 @@ export class DefaultPackageManager implements PackageManager { } } - private async getLatestNpmVersion(packageName: string): Promise { + private async getLatestNpmVersion(packageSpec: string, range?: string): Promise { const npmCommand = this.getNpmCommand(); const stdout = await this.runCommandCapture( npmCommand.command, - [...npmCommand.args, "view", packageName, "version", "--json"], + [...npmCommand.args, "view", packageSpec, "version", "--json"], { cwd: this.cwd, timeoutMs: NETWORK_TIMEOUT_MS }, ); const raw = stdout.trim(); if (!raw) throw new Error("Empty response from npm view"); - return JSON.parse(raw); + const parsed = JSON.parse(raw) as unknown; + if (typeof parsed === "string") { + return parsed; + } + if (Array.isArray(parsed)) { + const versions = parsed.filter((value): value is string => typeof value === "string" && value.length > 0); + const latest = range ? maxSatisfying(versions, range) : [...versions].sort(rcompare)[0]; + if (latest) return latest; + } + throw new Error("Unexpected response from npm view"); } private async gitHasAvailableUpdate(installedPath: string): Promise { diff --git a/packages/coding-agent/src/utils/version-check.ts b/packages/coding-agent/src/utils/version-check.ts index aec25bdb..a6a4f469 100644 --- a/packages/coding-agent/src/utils/version-check.ts +++ b/packages/coding-agent/src/utils/version-check.ts @@ -1,3 +1,4 @@ +import { compare, valid } from "semver"; import { getPiUserAgent } from "./pi-user-agent.ts"; const LATEST_VERSION_URL = "https://pi.dev/api/latest-version"; @@ -9,40 +10,13 @@ export interface LatestPiRelease { note?: string; } -interface ParsedVersion { - major: number; - minor: number; - patch: number; - prerelease?: string; -} - -function parsePackageVersion(version: string): ParsedVersion | undefined { - const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/); - if (!match) { - return undefined; - } - return { - major: Number.parseInt(match[1], 10), - minor: Number.parseInt(match[2], 10), - patch: Number.parseInt(match[3], 10), - prerelease: match[4], - }; -} - export function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined { - const left = parsePackageVersion(leftVersion); - const right = parsePackageVersion(rightVersion); + const left = valid(leftVersion.trim()); + const right = valid(rightVersion.trim()); if (!left || !right) { return undefined; } - - if (left.major !== right.major) return left.major - right.major; - if (left.minor !== right.minor) return left.minor - right.minor; - if (left.patch !== right.patch) return left.patch - right.patch; - if (left.prerelease === right.prerelease) return 0; - if (!left.prerelease) return 1; - if (!right.prerelease) return -1; - return left.prerelease.localeCompare(right.prerelease); + return compare(left, right); } export function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean { diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index cd1f4532..9b1b0d7f 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -1128,8 +1128,17 @@ Content`, }); it("should parse package source types from docs examples", () => { - expect((packageManager as any).parseSource("npm:@scope/pkg@1.2.3").type).toBe("npm"); - expect((packageManager as any).parseSource("npm:pkg").type).toBe("npm"); + const parseNpm = (source: string) => { + const parsed = (packageManager as any).parseSource(source); + if (parsed.type !== "npm") { + throw new Error(`Expected npm source: ${source}`); + } + return parsed; + }; + + expect(parseNpm("npm:@scope/pkg@1.2.3").pinned).toBe(true); + expect(parseNpm("npm:@scope/pkg@^1.2.3").pinned).toBe(false); + expect(parseNpm("npm:pkg").pinned).toBe(false); expect((packageManager as any).parseSource("git:github.com/user/repo@v1").type).toBe("git"); expect((packageManager as any).parseSource("https://github.com/user/repo@v1").type).toBe("git"); @@ -2052,25 +2061,27 @@ export default function(api) { api.registerTool({ name: "test", description: "te }); describe("offline mode and network timeouts", () => { - it("should update project npm packages using @latest when newer version is available", async () => { + it("should update npm range packages using the configured spec", async () => { const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(installedPath, { recursive: true }); writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.0.0" })); - settingsManager.setProjectPackages(["npm:example"]); + settingsManager.setProjectPackages(["npm:example@^1.0.0"]); - const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.2.3"'); + const runCommandCaptureSpy = vi + .spyOn(packageManager as any, "runCommandCapture") + .mockResolvedValue('["1.0.0","1.2.0"]'); const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); await packageManager.update("npm:example"); expect(runCommandCaptureSpy).toHaveBeenCalledWith( "npm", - ["view", "example", "version", "--json"], + ["view", "example@^1.0.0", "version", "--json"], expect.objectContaining({ cwd: tempDir, timeoutMs: expect.any(Number) }), ); expect(runCommandSpy).toHaveBeenCalledWith( "npm", - ["install", "example@latest", "--prefix", join(tempDir, ".pi", "npm"), "--legacy-peer-deps"], + ["install", "example@^1.0.0", "--prefix", join(tempDir, ".pi", "npm"), "--legacy-peer-deps"], undefined, ); }); @@ -2078,17 +2089,19 @@ export default function(api) { api.registerTool({ name: "test", description: "te it("should skip project npm update when installed version matches latest", async () => { const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(installedPath, { recursive: true }); - writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.2.3" })); - settingsManager.setProjectPackages(["npm:example"]); + writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.3.1" })); + settingsManager.setProjectPackages(["npm:example@^1.0.0"]); - const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.2.3"'); + const runCommandCaptureSpy = vi + .spyOn(packageManager as any, "runCommandCapture") + .mockResolvedValue('["1.0.0","1.3.1","1.0.2"]'); const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); await packageManager.update("npm:example"); expect(runCommandCaptureSpy).toHaveBeenCalledWith( "npm", - ["view", "example", "version", "--json"], + ["view", "example@^1.0.0", "version", "--json"], expect.objectContaining({ cwd: tempDir, timeoutMs: expect.any(Number) }), ); expect(runCommandSpy).not.toHaveBeenCalled(); @@ -2298,11 +2311,12 @@ export default function(api) { api.registerTool({ name: "test", description: "te }); it("should not run npm view during resolve for installed unpinned packages", async () => { + process.env.PI_OFFLINE = "1"; const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(join(installedPath, "extensions"), { recursive: true }); writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.0.0" })); writeFileSync(join(installedPath, "extensions", "index.ts"), "export default function() {};"); - settingsManager.setProjectPackages(["npm:example"]); + settingsManager.setProjectPackages(["npm:example@^1.0.0"]); const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture"); diff --git a/packages/coding-agent/test/version-check.test.ts b/packages/coding-agent/test/version-check.test.ts index 70197fa5..b294c627 100644 --- a/packages/coding-agent/test/version-check.test.ts +++ b/packages/coding-agent/test/version-check.test.ts @@ -29,6 +29,7 @@ describe("version checks", () => { expect(comparePackageVersions("0.70.6", "0.70.5")).toBeGreaterThan(0); expect(comparePackageVersions("0.70.5", "0.70.5")).toBe(0); expect(comparePackageVersions("0.70.4", "0.70.5")).toBeLessThan(0); + expect(comparePackageVersions("5.0.0-beta.20", "5.0.0-beta.9")).toBeGreaterThan(0); expect(isNewerPackageVersion("0.70.5", "0.70.5")).toBe(false); expect(isNewerPackageVersion("0.70.6", "0.70.5")).toBe(true); }); From 3fcfb7abf77784bfe52567f7670870834efb65d1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 02:05:11 +0200 Subject: [PATCH 031/141] docs(coding-agent): document extension resource lifecycle --- packages/coding-agent/docs/extensions.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index cb02960f..7038351d 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -216,6 +216,12 @@ export default async function (pi: ExtensionAPI) { This pattern makes the fetched models available during normal startup and to `pi --list-models`. +### Long-lived resources and shutdown + +Extension factories may run in invocations that never start a session. Do not start background resources such as processes, sockets, file watchers, or timers from the factory. + +Defer background resource startup until `session_start` or the command/tool/event that needs the resource. Register an idempotent `session_shutdown` handler to close any session-scoped resources you start. + ### Extension Styles **Single file** - simplest, for small extensions: @@ -471,7 +477,7 @@ pi.on("session_tree", async (event, ctx) => { #### session_shutdown -Fired before an extension runtime is torn down. +Fired before a started session runtime is torn down. Use this to clean up resources opened from `session_start` or other session-scoped hooks. ```typescript pi.on("session_shutdown", async (event, ctx) => { From f0989800cbd577979b6f38c3cc87bdc0514bff39 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Sun, 14 Jun 2026 07:14:42 +0200 Subject: [PATCH 032/141] feat: detect first-run terminal theme (#5385) --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/cli/startup-ui.ts | 34 +-- .../src/modes/interactive/interactive-mode.ts | 59 +++-- .../src/modes/interactive/theme/theme.ts | 85 +++---- .../coding-agent/test/theme-detection.test.ts | 86 +++++-- packages/tui/CHANGELOG.md | 4 + packages/tui/src/index.ts | 2 + packages/tui/src/terminal-colors.ts | 62 +++++ packages/tui/src/tui.ts | 64 +++++ packages/tui/test/terminal-colors.test.ts | 233 ++++++++++++++++++ 10 files changed, 532 insertions(+), 101 deletions(-) create mode 100644 packages/tui/src/terminal-colors.ts create mode 100644 packages/tui/test/terminal-colors.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 71de722b..23d7b939 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added first-run interactive theme detection from the terminal background. + ### Fixed - Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). diff --git a/packages/coding-agent/src/cli/startup-ui.ts b/packages/coding-agent/src/cli/startup-ui.ts index ec7aa5e7..93841304 100644 --- a/packages/coding-agent/src/cli/startup-ui.ts +++ b/packages/coding-agent/src/cli/startup-ui.ts @@ -10,7 +10,7 @@ import { FirstTimeSetupComponent, type FirstTimeSetupResult, } from "../modes/interactive/components/first-time-setup.ts"; -import { detectTerminalBackground, initTheme, setTheme } from "../modes/interactive/theme/theme.ts"; +import { detectTerminalBackgroundTheme, initTheme, setTheme } from "../modes/interactive/theme/theme.ts"; const OFFICIAL_PACKAGE_NAME = "@earendil-works/pi-coding-agent"; const OFFICIAL_APP_NAME = "pi"; @@ -123,19 +123,25 @@ export async function showFirstTimeSetup(settingsManager: SettingsManager): Prom resolve(); }; - const component = new FirstTimeSetupComponent({ - detectedTheme: detectTerminalBackground().theme, - onThemePreview: (themeName) => { - setTheme(themeName); - ui.invalidate(); - ui.requestRender(); - }, - onSubmit: (result) => void finish(result), - onCancel: () => void finish(undefined), - }); - ui.addChild(component); - ui.setFocus(component); - ui.start(); + const showSetup = async () => { + ui.start(); + const detection = await detectTerminalBackgroundTheme({ ui, timeoutMs: 100 }); + setTheme(detection.theme); + const component = new FirstTimeSetupComponent({ + detectedTheme: detection.theme, + onThemePreview: (themeName) => { + setTheme(themeName); + ui.requestRender(); + }, + onSubmit: (result) => void finish(result), + onCancel: () => void finish(undefined), + }); + ui.addChild(component); + ui.setFocus(component); + ui.requestRender(); + }; + + void showSetup(); }); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index fc25792e..b3424a15 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -126,6 +126,7 @@ import { TrustSelectorComponent } from "./components/trust-selector.ts"; import { UserMessageComponent } from "./components/user-message.ts"; import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; import { + detectTerminalBackgroundTheme, getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, @@ -428,6 +429,25 @@ export class InteractiveMode { initTheme(this.settingsManager.getTheme(), true); } + private async detectThemeIfUnset(): Promise { + if (this.settingsManager.getTheme()) { + return; + } + + const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); + const result = setTheme(detection.theme, true); + if (!result.success) { + return; + } + + if (detection.confidence === "high") { + this.settingsManager.setTheme(detection.theme); + await this.settingsManager.flush(); + } + this.updateEditorBorderColor(); + this.ui.requestRender(); + } + private getAutocompleteSourceTag(sourceInfo?: SourceInfo): string | undefined { if (!sourceInfo) { return undefined; @@ -629,9 +649,28 @@ export class InteractiveMode { console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`)); } - // Add header container as first child + // Add header container as first child. Populate it after detectThemeIfUnset. this.ui.addChild(this.headerContainer); + this.ui.addChild(this.chatContainer); + this.ui.addChild(this.pendingMessagesContainer); + this.ui.addChild(this.statusContainer); + this.renderWidgets(); // Initialize with default spacer + this.ui.addChild(this.widgetContainerAbove); + this.ui.addChild(this.editorContainer); + this.ui.addChild(this.widgetContainerBelow); + this.ui.addChild(this.footer); + this.ui.setFocus(this.editor); + + this.setupKeyHandlers(); + this.setupEditorSubmitHandler(); + + // Start the UI before initializing extensions so session_start handlers can use interactive dialogs + this.ui.start(); + this.isInitialized = true; + + await this.detectThemeIfUnset(); + // Add header with keybindings from config (unless silenced) if (this.options.verbose || !this.settingsManager.getQuietStartup()) { const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`); @@ -692,23 +731,7 @@ export class InteractiveMode { this.builtInHeader = new Text("", 0, 0); this.headerContainer.addChild(this.builtInHeader); } - - this.ui.addChild(this.chatContainer); - this.ui.addChild(this.pendingMessagesContainer); - this.ui.addChild(this.statusContainer); - this.renderWidgets(); // Initialize with default spacer - this.ui.addChild(this.widgetContainerAbove); - this.ui.addChild(this.editorContainer); - this.ui.addChild(this.widgetContainerBelow); - this.ui.addChild(this.footer); - this.ui.setFocus(this.editor); - - this.setupKeyHandlers(); - this.setupEditorSubmitHandler(); - - // Start the UI before initializing extensions so session_start handlers can use interactive dialogs - this.ui.start(); - this.isInitialized = true; + this.ui.requestRender(); // Initialize extensions first so resources are shown before messages await this.rebindCurrentSession(); diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 8bdf4816..778f8028 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -4,6 +4,7 @@ import { type EditorTheme, getCapabilities, type MarkdownTheme, + type RgbColor, type SelectListTheme, type SettingsListTheme, } from "@earendil-works/pi-tui"; @@ -624,12 +625,6 @@ export function getThemeByName(name: string): Theme | undefined { export type TerminalTheme = "dark" | "light"; -export interface RgbColor { - r: number; - g: number; - b: number; -} - export interface TerminalThemeDetection { theme: TerminalTheme; source: "terminal background" | "COLORFGBG" | "fallback"; @@ -641,6 +636,15 @@ export interface TerminalThemeDetectionOptions { env?: NodeJS.ProcessEnv; } +export interface TerminalBackgroundThemeDetector { + queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise; +} + +export interface TerminalBackgroundThemeDetectionOptions extends TerminalThemeDetectionOptions { + ui: TerminalBackgroundThemeDetector; + timeoutMs: number; +} + function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { const parts = colorfgbg.split(";"); for (let i = parts.length - 1; i >= 0; i--) { @@ -668,50 +672,7 @@ export function getThemeForRgbColor(rgb: RgbColor): TerminalTheme { return getRgbColorLuminance(rgb) >= 0.5 ? "light" : "dark"; } -function parseOscHexChannel(channel: string): number | undefined { - if (!/^[0-9a-f]+$/i.test(channel)) { - return undefined; - } - const max = 16 ** channel.length - 1; - if (max <= 0) { - return undefined; - } - return Math.round((parseInt(channel, 16) / max) * 255); -} - -export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { - const match = data.match(/^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i); - if (!match) { - return undefined; - } - - const value = match[1].trim(); - if (value.startsWith("#")) { - const hex = value.slice(1); - if (/^[0-9a-f]{6}$/i.test(hex)) { - return hexToRgb(value); - } - if (/^[0-9a-f]{12}$/i.test(hex)) { - const r = parseOscHexChannel(hex.slice(0, 4)); - const g = parseOscHexChannel(hex.slice(4, 8)); - const b = parseOscHexChannel(hex.slice(8, 12)); - return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; - } - return undefined; - } - - const rgbValue = value.replace(/^rgba?:/i, ""); - const [red, green, blue] = rgbValue.split("/"); - if (red === undefined || green === undefined || blue === undefined) { - return undefined; - } - const r = parseOscHexChannel(red); - const g = parseOscHexChannel(green); - const b = parseOscHexChannel(blue); - return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; -} - -export function detectTerminalBackground(options: TerminalThemeDetectionOptions = {}): TerminalThemeDetection { +export function detectTerminalBackgroundFromEnv(options: TerminalThemeDetectionOptions = {}): TerminalThemeDetection { const env = options.env ?? process.env; const colorfgbg = env.COLORFGBG || ""; const bg = getColorFgBgBackgroundIndex(colorfgbg); @@ -732,8 +693,30 @@ export function detectTerminalBackground(options: TerminalThemeDetectionOptions }; } +export async function detectTerminalBackgroundTheme({ + ui, + timeoutMs, + env, +}: TerminalBackgroundThemeDetectionOptions): Promise { + try { + const rgb = await ui.queryTerminalBackgroundColor({ timeoutMs }); + if (rgb) { + return { + theme: getThemeForRgbColor(rgb), + source: "terminal background", + detail: `OSC 11 background rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`, + confidence: "high", + }; + } + } catch { + // Fall back to environment-based detection when the terminal query fails. + } + + return detectTerminalBackgroundFromEnv({ env }); +} + export function getDefaultTheme(): string { - return detectTerminalBackground().theme; + return detectTerminalBackgroundFromEnv().theme; } // ============================================================================ diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts index 7bb280b2..4ad0cf56 100644 --- a/packages/coding-agent/test/theme-detection.test.ts +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -1,24 +1,24 @@ -import { resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui"; +import { type RgbColor, resetCapabilitiesCache, setCapabilities } from "@earendil-works/pi-tui"; import { afterEach, describe, expect, it } from "vitest"; import { - detectTerminalBackground, + detectTerminalBackgroundFromEnv, + detectTerminalBackgroundTheme, getThemeByName, getThemeForRgbColor, - parseOsc11BackgroundColor, } from "../src/modes/interactive/theme/theme.ts"; afterEach(() => { resetCapabilitiesCache(); }); -describe("detectTerminalBackground", () => { +describe("detectTerminalBackgroundFromEnv", () => { it("uses the COLORFGBG background color index", () => { - expect(detectTerminalBackground({ env: { COLORFGBG: "0;15" } })).toMatchObject({ + expect(detectTerminalBackgroundFromEnv({ env: { COLORFGBG: "0;15" } })).toMatchObject({ theme: "light", source: "COLORFGBG", confidence: "high", }); - expect(detectTerminalBackground({ env: { COLORFGBG: "15;0" } })).toMatchObject({ + expect(detectTerminalBackgroundFromEnv({ env: { COLORFGBG: "15;0" } })).toMatchObject({ theme: "dark", source: "COLORFGBG", confidence: "high", @@ -26,11 +26,11 @@ describe("detectTerminalBackground", () => { }); it("uses the last COLORFGBG field as the background", () => { - expect(detectTerminalBackground({ env: { COLORFGBG: "0;7;15" } }).theme).toBe("light"); + expect(detectTerminalBackgroundFromEnv({ env: { COLORFGBG: "0;7;15" } }).theme).toBe("light"); }); it("defaults to dark without terminal background hints", () => { - expect(detectTerminalBackground({ env: {} })).toMatchObject({ + expect(detectTerminalBackgroundFromEnv({ env: {} })).toMatchObject({ theme: "dark", source: "fallback", confidence: "low", @@ -38,6 +38,65 @@ describe("detectTerminalBackground", () => { }); }); +describe("detectTerminalBackgroundTheme", () => { + it("uses the queried terminal background before environment hints", async () => { + let queriedTimeoutMs: number | undefined; + const detection = await detectTerminalBackgroundTheme({ + env: { COLORFGBG: "15;0" }, + timeoutMs: 250, + ui: { + async queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise { + queriedTimeoutMs = timeoutMs; + return { r: 250, g: 250, b: 250 }; + }, + }, + }); + + expect(queriedTimeoutMs).toBe(250); + expect(detection).toMatchObject({ + theme: "light", + source: "terminal background", + confidence: "high", + }); + }); + + it("falls back to environment hints when the terminal query returns no color", async () => { + const detection = await detectTerminalBackgroundTheme({ + env: { COLORFGBG: "15;0" }, + timeoutMs: 250, + ui: { + async queryTerminalBackgroundColor(): Promise { + return undefined; + }, + }, + }); + + expect(detection).toMatchObject({ + theme: "dark", + source: "COLORFGBG", + confidence: "high", + }); + }); + + it("falls back to environment hints when the terminal query fails", async () => { + const detection = await detectTerminalBackgroundTheme({ + env: { COLORFGBG: "0;15" }, + timeoutMs: 250, + ui: { + async queryTerminalBackgroundColor(): Promise { + throw new Error("terminal write failed"); + }, + }, + }); + + expect(detection).toMatchObject({ + theme: "light", + source: "COLORFGBG", + confidence: "high", + }); + }); +}); + describe("theme color mode", () => { it("uses terminal capabilities", () => { setCapabilities({ images: null, trueColor: false, hyperlinks: false }); @@ -54,16 +113,7 @@ describe("theme color mode", () => { }); }); -describe("parseOsc11BackgroundColor", () => { - it("parses 16-bit OSC 11 rgb responses", () => { - expect(parseOsc11BackgroundColor("\x1b]11;rgb:0000/8000/ffff\x07")).toEqual({ r: 0, g: 128, b: 255 }); - }); - - it("parses OSC 11 hex responses", () => { - expect(parseOsc11BackgroundColor("\x1b]11;#ffffff\x1b\\")).toEqual({ r: 255, g: 255, b: 255 }); - expect(parseOsc11BackgroundColor("\x1b]11;#000000\x07")).toEqual({ r: 0, g: 0, b: 0 }); - }); - +describe("theme detection from RGB", () => { it("classifies RGB colors by luminance", () => { expect(getThemeForRgbColor({ r: 8, g: 8, b: 8 })).toBe("dark"); expect(getThemeForRgbColor({ r: 250, g: 250, b: 250 })).toBe("light"); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 69b0529a..b1108dee 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added terminal background color query support for OSC 11 replies. + ## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index fe0fed00..47bb5240 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -61,6 +61,8 @@ export { export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer.ts"; // Terminal interface and implementations export { ProcessTerminal, type Terminal } from "./terminal.ts"; +// Terminal colors +export { parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; // Terminal image support export { allocateImageId, diff --git a/packages/tui/src/terminal-colors.ts b/packages/tui/src/terminal-colors.ts new file mode 100644 index 00000000..1c383a51 --- /dev/null +++ b/packages/tui/src/terminal-colors.ts @@ -0,0 +1,62 @@ +export interface RgbColor { + r: number; + g: number; + b: number; +} + +function hexToRgb(hex: string): RgbColor { + const normalized = hex.startsWith("#") ? hex.slice(1) : hex; + const r = parseInt(normalized.slice(0, 2), 16); + const g = parseInt(normalized.slice(2, 4), 16); + const b = parseInt(normalized.slice(4, 6), 16); + return { r, g, b }; +} + +function parseOscHexChannel(channel: string): number | undefined { + if (!/^[0-9a-f]+$/i.test(channel)) { + return undefined; + } + const max = 16 ** channel.length - 1; + if (max <= 0) { + return undefined; + } + return Math.round((parseInt(channel, 16) / max) * 255); +} + +const OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN = /^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i; + +export function isOsc11BackgroundColorResponse(data: string): boolean { + return OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN.test(data); +} + +export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { + const match = data.match(OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN); + if (!match) { + return undefined; + } + + const value = match[1].trim(); + if (value.startsWith("#")) { + const hex = value.slice(1); + if (/^[0-9a-f]{6}$/i.test(hex)) { + return hexToRgb(value); + } + if (/^[0-9a-f]{12}$/i.test(hex)) { + const r = parseOscHexChannel(hex.slice(0, 4)); + const g = parseOscHexChannel(hex.slice(4, 8)); + const b = parseOscHexChannel(hex.slice(8, 12)); + return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; + } + return undefined; + } + + const rgbValue = value.replace(/^rgba?:/i, ""); + const [red, green, blue] = rgbValue.split("/"); + if (red === undefined || green === undefined || blue === undefined) { + return undefined; + } + const r = parseOscHexChannel(red); + const g = parseOscHexChannel(green); + const b = parseOscHexChannel(blue); + return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; +} diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 2399564d..2b5d05cd 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -8,6 +8,7 @@ import * as path from "node:path"; import { performance } from "node:perf_hooks"; import { isKeyRelease, matchesKey } from "./keys.ts"; import type { Terminal } from "./terminal.ts"; +import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts"; import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts"; @@ -82,6 +83,11 @@ export interface Component { type InputListenerResult = { consume?: boolean; data?: string } | undefined; type InputListener = (data: string) => InputListenerResult; +type PendingOsc11BackgroundQuery = { + settled: boolean; + resolve: ((rgb: RgbColor | undefined) => void) | undefined; + timer: NodeJS.Timeout | undefined; +}; /** * Interface for components that can receive focus and display a hardware cursor. @@ -303,6 +309,8 @@ export class TUI extends Container { private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves private fullRedrawCount = 0; private stopped = false; + private pendingOsc11BackgroundReplies = 0; + private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = []; // Overlay stack for modal components rendered on top of base content private focusOrderCounter = 0; @@ -720,6 +728,10 @@ export class TUI extends Container { } private handleInput(data: string): void { + if (this.consumeOsc11BackgroundResponse(data)) { + return; + } + if (this.inputListeners.size > 0) { let current = data; for (const listener of this.inputListeners) { @@ -788,6 +800,30 @@ export class TUI extends Container { } } + private consumeOsc11BackgroundResponse(data: string): boolean { + if (this.pendingOsc11BackgroundReplies <= 0) { + return false; + } + + if (!isOsc11BackgroundColorResponse(data)) { + return false; + } + + const rgb = parseOsc11BackgroundColor(data); + this.pendingOsc11BackgroundReplies -= 1; + const query = this.pendingOsc11BackgroundQueries.shift(); + if (query && !query.settled) { + query.settled = true; + if (query.timer) { + clearTimeout(query.timer); + query.timer = undefined; + } + query.resolve?.(rgb); + query.resolve = undefined; + } + return true; + } + private consumeCellSizeResponse(data: string): boolean { // Response format: ESC [ 6 ; height ; width t const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/); @@ -1561,4 +1597,32 @@ export class TUI extends Container { this.terminal.hideCursor(); } } + + /** + * Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`). + * @param timeoutMs Query timeout in milliseconds. + * @returns Promise containing the parsed RGB color, or undefined if it times out or fails to parse. + */ + queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise { + return new Promise((resolve) => { + const query: PendingOsc11BackgroundQuery = { + settled: false, + resolve, + timer: undefined, + }; + + query.timer = setTimeout(() => { + if (query.settled) { + return; + } + query.settled = true; + query.timer = undefined; + query.resolve?.(undefined); + query.resolve = undefined; + }, timeoutMs); + this.pendingOsc11BackgroundQueries.push(query); + this.pendingOsc11BackgroundReplies += 1; + this.terminal.write("\x1b]11;?\x07"); + }); + } } diff --git a/packages/tui/test/terminal-colors.test.ts b/packages/tui/test/terminal-colors.test.ts new file mode 100644 index 00000000..9ac2a4ee --- /dev/null +++ b/packages/tui/test/terminal-colors.test.ts @@ -0,0 +1,233 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { type Component, parseOsc11BackgroundColor, type Terminal, TUI } from "../src/index.ts"; + +class TestTerminal implements Terminal { + private inputHandler?: (data: string) => void; + private resizeHandler?: () => void; + private readonly columnCount: number; + private readonly rowCount: number; + readonly writes: string[] = []; + + constructor(columnCount = 80, rowCount = 24) { + this.columnCount = columnCount; + this.rowCount = rowCount; + } + + start(onInput: (data: string) => void, onResize: () => void): void { + this.inputHandler = onInput; + this.resizeHandler = onResize; + } + + stop(): void { + this.inputHandler = undefined; + this.resizeHandler = undefined; + } + + async drainInput(_maxMs?: number, _idleMs?: number): Promise {} + + write(data: string): void { + this.writes.push(data); + } + + get columns(): number { + return this.columnCount; + } + + get rows(): number { + return this.rowCount; + } + + get kittyProtocolActive(): boolean { + return false; + } + + moveBy(_lines: number): void {} + + hideCursor(): void {} + + showCursor(): void {} + + clearLine(): void {} + + clearFromCursor(): void {} + + clearScreen(): void {} + + setTitle(_title: string): void {} + + setProgress(_active: boolean): void {} + + sendInput(data: string): void { + this.inputHandler?.(data); + } + + sendResize(): void { + this.resizeHandler?.(); + } +} + +class InputRecorder implements Component { + readonly inputs: string[] = []; + + render(_width: number): string[] { + return []; + } + + handleInput(data: string): void { + this.inputs.push(data); + } + + invalidate(): void {} +} + +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe("parseOsc11BackgroundColor", () => { + it("parses 16-bit OSC 11 rgb responses", () => { + assert.deepStrictEqual(parseOsc11BackgroundColor("\x1b]11;rgb:0000/8000/ffff\x07"), { + r: 0, + g: 128, + b: 255, + }); + }); + + it("parses OSC 11 hex responses", () => { + assert.deepStrictEqual(parseOsc11BackgroundColor("\x1b]11;#ffffff\x1b\\"), { r: 255, g: 255, b: 255 }); + assert.deepStrictEqual(parseOsc11BackgroundColor("\x1b]11;#000000\x07"), { r: 0, g: 0, b: 0 }); + }); + + it("rejects non-strict OSC 11 responses", () => { + assert.strictEqual(parseOsc11BackgroundColor(`x\x1b]11;#ffffff\x07`), undefined); + assert.strictEqual(parseOsc11BackgroundColor("\x1b]10;#ffffff\x07"), undefined); + assert.strictEqual(parseOsc11BackgroundColor("\x1b]11;#ffffff\x07x"), undefined); + }); +}); + +describe("TUI.queryTerminalBackgroundColor", () => { + it("writes OSC 11 query and resolves with the parsed RGB reply", async () => { + const terminal = new TestTerminal(); + const tui = new TUI(terminal); + tui.start(); + try { + const query = tui.queryTerminalBackgroundColor({ timeoutMs: 1000 }); + assert.ok(terminal.writes.includes("\x1b]11;?\x07")); + + terminal.sendInput("\x1b]11;#ffffff\x07"); + + assert.deepStrictEqual(await query, { r: 255, g: 255, b: 255 }); + } finally { + tui.stop(); + } + }); + + it("consumes OSC 11 replies before input listeners and focused component dispatch", async () => { + const terminal = new TestTerminal(); + const tui = new TUI(terminal); + const component = new InputRecorder(); + const listenerInputs: string[] = []; + tui.addChild(component); + tui.setFocus(component); + tui.addInputListener((data) => { + listenerInputs.push(data); + return undefined; + }); + tui.start(); + try { + const query = tui.queryTerminalBackgroundColor({ timeoutMs: 1000 }); + + terminal.sendInput("\x1b]11;#000000\x07"); + + assert.deepStrictEqual(await query, { r: 0, g: 0, b: 0 }); + assert.deepStrictEqual(listenerInputs, []); + assert.deepStrictEqual(component.inputs, []); + } finally { + tui.stop(); + } + }); + + it("consumes unparseable strict OSC 11 replies and resolves undefined", async () => { + const terminal = new TestTerminal(); + const tui = new TUI(terminal); + const component = new InputRecorder(); + const listenerInputs: string[] = []; + tui.addChild(component); + tui.setFocus(component); + tui.addInputListener((data) => { + listenerInputs.push(data); + return undefined; + }); + tui.start(); + try { + const query = tui.queryTerminalBackgroundColor({ timeoutMs: 1000 }); + + terminal.sendInput("\x1b]11;not-a-color\x07"); + + assert.strictEqual(await query, undefined); + assert.deepStrictEqual(listenerInputs, []); + assert.deepStrictEqual(component.inputs, []); + } finally { + tui.stop(); + } + }); + + it("dispatches non-matching input normally while waiting for an OSC 11 reply", async () => { + const terminal = new TestTerminal(); + const tui = new TUI(terminal); + const component = new InputRecorder(); + const listenerInputs: string[] = []; + tui.addChild(component); + tui.setFocus(component); + tui.addInputListener((data) => { + listenerInputs.push(data); + return undefined; + }); + tui.start(); + try { + let settled = false; + const query = tui.queryTerminalBackgroundColor({ timeoutMs: 1000 }).then((rgb) => { + settled = true; + return rgb; + }); + + terminal.sendInput("x"); + await Promise.resolve(); + + assert.strictEqual(settled, false); + assert.deepStrictEqual(listenerInputs, ["x"]); + assert.deepStrictEqual(component.inputs, ["x"]); + + terminal.sendInput("\x1b]11;#ffffff\x07"); + assert.deepStrictEqual(await query, { r: 255, g: 255, b: 255 }); + } finally { + tui.stop(); + } + }); + + it("keeps consuming a late OSC 11 reply after timeout", async () => { + const terminal = new TestTerminal(); + const tui = new TUI(terminal); + const component = new InputRecorder(); + const listenerInputs: string[] = []; + tui.addChild(component); + tui.setFocus(component); + tui.addInputListener((data) => { + listenerInputs.push(data); + return undefined; + }); + tui.start(); + try { + const query = tui.queryTerminalBackgroundColor({ timeoutMs: 1 }); + await wait(5); + + assert.strictEqual(await query, undefined); + + terminal.sendInput("\x1b]11;#ffffff\x07"); + + assert.deepStrictEqual(listenerInputs, []); + assert.deepStrictEqual(component.inputs, []); + } finally { + tui.stop(); + } + }); +}); From 11b5403fade1502a9a58a9cd4e9f983a3d1d734e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 10:52:31 +0200 Subject: [PATCH 033/141] fix(coding-agent): exit after package commands closes #5687 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/main.ts | 1 + packages/coding-agent/test/package-command-paths.test.ts | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 23d7b939..6e63d2f2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed package commands such as `pi list`, `pi install`, and `pi update` to terminate after completing even if an extension leaves background handles open ([#5687](https://github.com/earendil-works/pi/issues/5687)). - Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). - Fixed npm package specs that use ranges or tags (for example `@^1.2.7`) so installed package resources still load instead of being treated as mismatched exact pins ([#5695](https://github.com/earendil-works/pi/issues/5695)). - Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 63d6ec1e..00f3b208 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -467,6 +467,7 @@ export async function main(args: string[], options?: MainOptions) { } if (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) { + process.exit(process.exitCode ?? 0); return; } diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 1cff83f6..4cc1ce5e 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -37,12 +37,21 @@ describe("package commands", () => { originalExitCode = process.exitCode; originalExecPath = process.execPath; process.exitCode = undefined; + vi.spyOn(process, "exit").mockImplementation(((code?: string | number | null) => { + if (code === undefined || code === null || Number(code) === 0) { + process.exitCode = undefined; + } else { + process.exitCode = code; + } + return undefined as never; + }) as typeof process.exit); process.env[ENV_AGENT_DIR] = agentDir; process.chdir(projectDir); }); afterEach(() => { vi.unstubAllGlobals(); + vi.restoreAllMocks(); process.chdir(originalCwd); process.exitCode = originalExitCode; if (originalAgentDir === undefined) { From 6b40c99a5982b206d2acbaacc5d7ef641c2a98c3 Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Sun, 14 Jun 2026 08:02:58 -0500 Subject: [PATCH 034/141] feat(examples): Wrap question extension text instead of truncating (#5708) --- .../examples/extensions/question.ts | 57 +++++++++----- .../examples/extensions/questionnaire.ts | 77 ++++++++++++------- 2 files changed, 88 insertions(+), 46 deletions(-) diff --git a/packages/coding-agent/examples/extensions/question.ts b/packages/coding-agent/examples/extensions/question.ts index eb1af51a..1192b490 100644 --- a/packages/coding-agent/examples/extensions/question.ts +++ b/packages/coding-agent/examples/extensions/question.ts @@ -5,7 +5,15 @@ */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui"; +import { + Editor, + type EditorTheme, + Key, + matchesKey, + Text, + visibleWidth, + wrapTextWithAnsi, +} from "@earendil-works/pi-tui"; import { Type } from "typebox"; interface OptionWithDesc { @@ -139,10 +147,27 @@ export default function question(pi: ExtensionAPI) { if (cachedLines) return cachedLines; const lines: string[] = []; - const add = (s: string) => lines.push(truncateToWidth(s, width)); + const renderWidth = Math.max(1, width); - add(theme.fg("accent", "─".repeat(width))); - add(theme.fg("text", ` ${params.question}`)); + function addWrapped(text: string) { + lines.push(...wrapTextWithAnsi(text, renderWidth)); + } + + function addWrappedWithPrefix(prefix: string, text: string) { + const prefixWidth = visibleWidth(prefix); + if (prefixWidth >= renderWidth) { + addWrapped(prefix + text); + return; + } + const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth); + const continuationPrefix = " ".repeat(prefixWidth); + for (let i = 0; i < wrapped.length; i++) { + lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`); + } + } + + lines.push(theme.fg("accent", "─".repeat(renderWidth))); + addWrappedWithPrefix(" ", theme.fg("text", params.question)); lines.push(""); for (let i = 0; i < allOptions.length; i++) { @@ -150,36 +175,32 @@ export default function question(pi: ExtensionAPI) { const selected = i === optionIndex; const isOther = opt.isOther === true; const prefix = selected ? theme.fg("accent", "> ") : " "; + const label = `${i + 1}. ${opt.label}${isOther && editMode ? " ✎" : ""}`; + const color = selected || (isOther && editMode) ? "accent" : "text"; - if (isOther && editMode) { - add(prefix + theme.fg("accent", `${i + 1}. ${opt.label} ✎`)); - } else if (selected) { - add(prefix + theme.fg("accent", `${i + 1}. ${opt.label}`)); - } else { - add(` ${theme.fg("text", `${i + 1}. ${opt.label}`)}`); - } + addWrappedWithPrefix(prefix, theme.fg(color, label)); // Show description if present if (opt.description) { - add(` ${theme.fg("muted", opt.description)}`); + addWrappedWithPrefix(" ", theme.fg("muted", opt.description)); } } if (editMode) { lines.push(""); - add(theme.fg("muted", " Your answer:")); - for (const line of editor.render(width - 2)) { - add(` ${line}`); + addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:")); + for (const line of editor.render(Math.max(1, renderWidth - 2))) { + lines.push(` ${line}`); } } lines.push(""); if (editMode) { - add(theme.fg("dim", " Enter to submit • Esc to go back")); + addWrappedWithPrefix(" ", theme.fg("dim", "Enter to submit • Esc to go back")); } else { - add(theme.fg("dim", " ↑↓ navigate • Enter to select • Esc to cancel")); + addWrappedWithPrefix(" ", theme.fg("dim", "↑↓ navigate • Enter to select • Esc to cancel")); } - add(theme.fg("accent", "─".repeat(width))); + lines.push(theme.fg("accent", "─".repeat(renderWidth))); cachedLines = lines; return lines; diff --git a/packages/coding-agent/examples/extensions/questionnaire.ts b/packages/coding-agent/examples/extensions/questionnaire.ts index 653bd864..3a546ac3 100644 --- a/packages/coding-agent/examples/extensions/questionnaire.ts +++ b/packages/coding-agent/examples/extensions/questionnaire.ts @@ -6,7 +6,15 @@ */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui"; +import { + Editor, + type EditorTheme, + Key, + matchesKey, + Text, + visibleWidth, + wrapTextWithAnsi, +} from "@earendil-works/pi-tui"; import { Type } from "typebox"; // Types @@ -259,13 +267,28 @@ export default function questionnaire(pi: ExtensionAPI) { if (cachedLines) return cachedLines; const lines: string[] = []; + const renderWidth = Math.max(1, width); const q = currentQuestion(); const opts = currentOptions(); - // Helper to add truncated line - const add = (s: string) => lines.push(truncateToWidth(s, width)); + function addWrapped(text: string) { + lines.push(...wrapTextWithAnsi(text, renderWidth)); + } - add(theme.fg("accent", "─".repeat(width))); + function addWrappedWithPrefix(prefix: string, text: string) { + const prefixWidth = visibleWidth(prefix); + if (prefixWidth >= renderWidth) { + addWrapped(prefix + text); + return; + } + const wrapped = wrapTextWithAnsi(text, renderWidth - prefixWidth); + const continuationPrefix = " ".repeat(prefixWidth); + for (let i = 0; i < wrapped.length; i++) { + lines.push(`${i === 0 ? prefix : continuationPrefix}${wrapped[i]}`); + } + } + + lines.push(theme.fg("accent", "─".repeat(renderWidth))); // Tab bar (multi-question only) if (isMulti) { @@ -287,7 +310,7 @@ export default function questionnaire(pi: ExtensionAPI) { ? theme.bg("selectedBg", theme.fg("text", submitText)) : theme.fg(canSubmit ? "success" : "dim", submitText); tabs.push(`${submitStyled} →`); - add(` ${tabs.join("")}`); + addWrappedWithPrefix(" ", tabs.join("")); lines.push(""); } @@ -298,54 +321,52 @@ export default function questionnaire(pi: ExtensionAPI) { const selected = i === optionIndex; const isOther = opt.isOther === true; const prefix = selected ? theme.fg("accent", "> ") : " "; - const color = selected ? "accent" : "text"; - // Mark "Type something" differently when in input mode - if (isOther && inputMode) { - add(prefix + theme.fg("accent", `${i + 1}. ${opt.label} ✎`)); - } else { - add(prefix + theme.fg(color, `${i + 1}. ${opt.label}`)); - } + const label = `${i + 1}. ${opt.label}${isOther && inputMode ? " ✎" : ""}`; + const color = selected || (isOther && inputMode) ? "accent" : "text"; + + addWrappedWithPrefix(prefix, theme.fg(color, label)); if (opt.description) { - add(` ${theme.fg("muted", opt.description)}`); + addWrappedWithPrefix(" ", theme.fg("muted", opt.description)); } } } // Content if (inputMode && q) { - add(theme.fg("text", ` ${q.prompt}`)); + addWrappedWithPrefix(" ", theme.fg("text", q.prompt)); lines.push(""); // Show options for reference renderOptions(); lines.push(""); - add(theme.fg("muted", " Your answer:")); - for (const line of editor.render(width - 2)) { - add(` ${line}`); + addWrappedWithPrefix(" ", theme.fg("muted", "Your answer:")); + for (const line of editor.render(Math.max(1, renderWidth - 2))) { + lines.push(` ${line}`); } lines.push(""); - add(theme.fg("dim", " Enter to submit • Esc to cancel")); + addWrappedWithPrefix(" ", theme.fg("dim", "Enter to submit • Esc to cancel")); } else if (currentTab === questions.length) { - add(theme.fg("accent", theme.bold(" Ready to submit"))); + addWrappedWithPrefix(" ", theme.fg("accent", theme.bold("Ready to submit"))); lines.push(""); for (const question of questions) { const answer = answers.get(question.id); if (answer) { const prefix = answer.wasCustom ? "(wrote) " : ""; - add(`${theme.fg("muted", ` ${question.label}: `)}${theme.fg("text", prefix + answer.label)}`); + const summary = `${theme.fg("muted", `${question.label}: `)}${theme.fg("text", prefix + answer.label)}`; + addWrappedWithPrefix(" ", summary); } } lines.push(""); if (allAnswered()) { - add(theme.fg("success", " Press Enter to submit")); + addWrappedWithPrefix(" ", theme.fg("success", "Press Enter to submit")); } else { const missing = questions .filter((q) => !answers.has(q.id)) .map((q) => q.label) .join(", "); - add(theme.fg("warning", ` Unanswered: ${missing}`)); + addWrappedWithPrefix(" ", theme.fg("warning", `Unanswered: ${missing}`)); } } else if (q) { - add(theme.fg("text", ` ${q.prompt}`)); + addWrappedWithPrefix(" ", theme.fg("text", q.prompt)); lines.push(""); renderOptions(); } @@ -353,11 +374,11 @@ export default function questionnaire(pi: ExtensionAPI) { lines.push(""); if (!inputMode) { const help = isMulti - ? " Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel" - : " ↑↓ navigate • Enter select • Esc cancel"; - add(theme.fg("dim", help)); + ? "Tab/←→ navigate • ↑↓ select • Enter confirm • Esc cancel" + : "↑↓ navigate • Enter select • Esc cancel"; + addWrappedWithPrefix(" ", theme.fg("dim", help)); } - add(theme.fg("accent", "─".repeat(width))); + lines.push(theme.fg("accent", "─".repeat(renderWidth))); cachedLines = lines; return lines; @@ -400,7 +421,7 @@ export default function questionnaire(pi: ExtensionAPI) { let text = theme.fg("toolTitle", theme.bold("questionnaire ")); text += theme.fg("muted", `${count} question${count !== 1 ? "s" : ""}`); if (labels) { - text += theme.fg("dim", ` (${truncateToWidth(labels, 40)})`); + text += theme.fg("dim", ` (${labels})`); } return new Text(text, 0, 0); }, From d683a581b74734306196a249af7afa991e240707 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 16:11:31 +0200 Subject: [PATCH 035/141] meta: Update CONTRIBUTING.md for clearer language --- CONTRIBUTING.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2c79b9f..7a5c57a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,13 +2,21 @@ This guide exists to save both sides time. +## Philosophy + +First things first: **pi's core is minimal**. + +If your feature does not belong in the core, it should be an extension. PRs that bloat the core will likely be rejected. + +Pi's core exists to be minimal and to be extensible so that it can be influenced and manipulated by extensions. Even hook points for extensions however should be well considered and discussed to avoid adding unmaintainable bloat and complex interactions. + ## The One Rule **You must understand your code.** If you cannot explain what your changes do and how they interact with the rest of the system, your PR will be closed. Using AI to write code is fine. Submitting AI-generated slop without understanding it is not. -If you use an agent, run it from the `pi-mono` root directory so it picks up `AGENTS.md` automatically. Your agent must follow the rules and guidelines in that file. +If you use an agent, run it from the `pi` root directory so it picks up `AGENTS.md` automatically. Your agent must follow the rules and guidelines in that file. ## Contribution Gate @@ -32,7 +40,7 @@ If you open an issue, you must use one of the two GitHub issue templates. If you open an issue, keep it short, concrete, and worth reading. - Keep it concise. If it does not fit on one screen, it is too long. -- Write in your own voice. +- Write in your own voice (do not use an LLM to generate text, if you must, follow up with a clearly AI labeled comment). - State the bug or request clearly. - Explain why it matters. - If you want to implement the change yourself, say so. @@ -62,10 +70,6 @@ Do not edit `CHANGELOG.md`. Changelog entries are added by maintainers. If you are adding a new provider to `packages/ai`, see `AGENTS.md` for required tests. -## Philosophy - -pi's core is minimal. If your feature does not belong in the core, it should be an extension. PRs that bloat the core will likely be rejected. - ## Questions? Ask on [Discord](https://discord.com/invite/nKXTsAcmbT). From 93b3b7c1fe8e10795dd2171db04e3a5eb4c84360 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 14 Jun 2026 23:16:59 +0200 Subject: [PATCH 036/141] fix(tui): preserve WezTerm Kitty images on full redraw Reserve visible Kitty image rows before drawing placements in full redraws, while preserving the first-row path for taller-than-viewport images so #4415 stays fixed. Closes #5618. --- packages/tui/CHANGELOG.md | 4 ++ packages/tui/src/tui.ts | 15 ++++- packages/tui/test/tui-render.test.ts | 89 ++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index b1108dee..91b52474 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,6 +6,10 @@ - Added terminal background color query support for OSC 11 replies. +### Fixed + +- Fixed WezTerm inline Kitty image rendering during full redraw fallbacks so image padding rows are reserved before the placement is drawn without regressing tall-image placement ([#5618](https://github.com/earendil-works/pi/issues/5618), [#4415](https://github.com/earendil-works/pi/issues/4415)). + ## [0.79.3] - 2026-06-13 ## [0.79.2] - 2026-06-12 diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 2b5d05cd..1fff6545 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -1244,7 +1244,20 @@ export class TUI extends Container { } for (let i = 0; i < newLines.length; i++) { if (i > 0) buffer += "\r\n"; - buffer += newLines[i]; + const line = newLines[i]; + const isImage = isImageLine(line); + const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i) : 1; + if (imageReservedRows > 1 && imageReservedRows <= height) { + for (let row = 1; row < imageReservedRows; row++) { + buffer += "\r\n"; + } + buffer += `\x1b[${imageReservedRows - 1}A`; + buffer += line; + buffer += `\x1b[${imageReservedRows - 1}B`; + i += imageReservedRows - 1; + continue; + } + buffer += line; } buffer += "\x1b[?2026l"; // End synchronized output this.terminal.write(buffer); diff --git a/packages/tui/test/tui-render.test.ts b/packages/tui/test/tui-render.test.ts index 4e31e8f4..ab038ac7 100644 --- a/packages/tui/test/tui-render.test.ts +++ b/packages/tui/test/tui-render.test.ts @@ -152,6 +152,95 @@ describe("TUI Kitty image cleanup", () => { } }); + it("reserves Kitty image rows before drawing during full redraw fallbacks", async () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const terminal = new LoggingVirtualTerminal(40, 5); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["l0", "l1", "l2", "l3", "l4"]; + tui.start(); + await terminal.waitForRender(); + const redrawsBeforeImage = tui.fullRedraws; + terminal.clearWrites(); + + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 3 }, + { widthPx: 30, heightPx: 30 }, + ); + const imageLines = image.render(40); + const imageSequence = imageLines[0]; + component.lines = ["l0", "l1", "l2", "l3", "l4", ...imageLines, "after"]; + tui.requestRender(); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + assert.ok(tui.fullRedraws > redrawsBeforeImage, "scrolling image append should force a full redraw"); + assert.ok( + writes.includes(`\r\n\r\n\x1b[2A${imageSequence}\x1b[2B`), + "full redraw should reserve visible image rows before drawing the placement", + ); + assert.ok( + !writes.includes(`${imageSequence}\r\n\x1b[0m`), + "full redraw must not write reserved padding rows after drawing the placement", + ); + + tui.stop(); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + + it("does not use cursor-up placement for Kitty images taller than the viewport", async () => { + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true }); + setCellDimensions({ widthPx: 10, heightPx: 10 }); + try { + const terminal = new LoggingVirtualTerminal(40, 5); + const tui = new TUI(terminal); + const component = new TestComponent(); + tui.addChild(component); + + component.lines = ["before"]; + tui.start(); + await terminal.waitForRender(); + terminal.clearWrites(); + + const image = new Image( + "AAAA", + "image/png", + { fallbackColor: (value) => value }, + { maxWidthCells: 6 }, + { widthPx: 60, heightPx: 60 }, + ); + const imageLines = image.render(40); + const imageSequence = imageLines[0]; + assert.ok(imageLines.length > terminal.rows, "test image should exceed the viewport height"); + + component.lines = ["before", ...imageLines, "after"]; + tui.requestRender(true); + await terminal.waitForRender(); + + const writes = terminal.getWrites(); + assert.ok(writes.includes(imageSequence), "image placement should be drawn"); + assert.ok( + !writes.includes(`\x1b[${imageLines.length - 1}A${imageSequence}`), + "taller-than-viewport images must keep the #4461 first-row placement path", + ); + + tui.stop(); + } finally { + resetCapabilitiesCache(); + setCellDimensions({ widthPx: 9, heightPx: 18 }); + } + }); + it("deletes changed image ids before drawing moved placements", async () => { const terminal = new LoggingVirtualTerminal(40, 10); const tui = new TUI(terminal); From b5e13bcd8ab8f436f751684817201f1e4a326223 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 00:02:31 +0200 Subject: [PATCH 037/141] docs(coding-agent): clarify active tools docs closes #5729 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/extensions.md | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6e63d2f2..6afe9ad9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed extensions documentation to clarify that `pi.getActiveTools()` returns active tool names while `pi.getAllTools()` returns tool metadata ([#5729](https://github.com/earendil-works/pi/issues/5729)). - Fixed package commands such as `pi list`, `pi install`, and `pi update` to terminate after completing even if an extension leaves background handles open ([#5687](https://github.com/earendil-works/pi/issues/5687)). - Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). - Fixed npm package specs that use ranges or tags (for example `@^1.2.7`) so installed package resources still load instead of being treated as mismatched exact pins ([#5695](https://github.com/earendil-works/pi/issues/5695)). diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 7038351d..ab3188ce 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -1534,21 +1534,21 @@ const result = await pi.exec("git", ["status"], { signal, timeout: 5000 }); ### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names) -Manage active tools. This works for both built-in tools and dynamically registered tools. +Manage active tools. This works for both built-in tools and dynamically registered tools. `pi.getActiveTools()` returns the active tool names as `string[]`; `pi.getAllTools()` returns metadata for all configured tools. ```typescript -const active = pi.getActiveTools(); +const active = pi.getActiveTools(); // ["read", "bash", ...] const all = pi.getAllTools(); -// [{ +// all = [{ // name: "read", // description: "Read file contents...", // parameters: ..., // promptGuidelines: ["Use read to examine files instead of cat or sed."], // sourceInfo: { path: "", source: "builtin", scope: "temporary", origin: "top-level" } // }, ...] -const names = all.map(t => t.name); const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin"); const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk"); +pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool pi.setActiveTools(["read", "bash"]); // Switch to read-only ``` From ba0ec61563b731ecf28d01f9d6395e568ffe28c9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 00:47:35 +0200 Subject: [PATCH 038/141] fix(coding-agent): restore terminal on SIGTERM closes #5724 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/interactive/interactive-mode.ts | 6 +- ...-signal-shutdown-extension-cleanup.test.ts | 1 - .../5724-sigterm-signal-exit.test.ts | 92 +++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6afe9ad9..e697d433 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed SIGTERM/SIGHUP interactive shutdown to keep signal handlers installed until terminal cleanup completes, preventing `signal-exit` from re-sending the signal and leaving the terminal in raw/Kitty keyboard mode ([#5724](https://github.com/earendil-works/pi/issues/5724)). - Fixed extensions documentation to clarify that `pi.getActiveTools()` returns active tool names while `pi.getAllTools()` returns tool metadata ([#5729](https://github.com/earendil-works/pi/issues/5729)). - Fixed package commands such as `pi list`, `pi install`, and `pi update` to terminate after completing even if an extension leaves background handles open ([#5687](https://github.com/earendil-works/pi/issues/5687)). - Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index b3424a15..56ef9098 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3362,7 +3362,9 @@ export class InteractiveMode { private async shutdown(options?: { fromSignal?: boolean }): Promise { if (this.isShuttingDown) return; this.isShuttingDown = true; - this.unregisterSignalHandlers(); + // Keep signal handlers registered until terminal cleanup has completed. + // `signal-exit` checks the listener list during the same SIGTERM/SIGHUP + // dispatch and re-sends the signal if only its own listeners remain. if (options?.fromSignal) { // Signal-triggered shutdown (SIGTERM/SIGHUP). Emit extension cleanup @@ -5742,7 +5744,6 @@ export class InteractiveMode { } stop(): void { - this.unregisterSignalHandlers(); if (this.settingsManager.getShowTerminalProgress()) { this.ui.terminal.setProgress(false); } @@ -5760,5 +5761,6 @@ export class InteractiveMode { this.ui.stop(); this.isInitialized = false; } + this.unregisterSignalHandlers(); } } diff --git a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts index 30810fe8..33a960e2 100644 --- a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts +++ b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts @@ -116,7 +116,6 @@ describe("InteractiveMode.shutdown ordering (#5080)", () => { expect(order).toEqual(["dispose", "drainInput", "stop"]); expect(context.isShuttingDown).toBe(true); - expect(context.unregisterSignalHandlers).toHaveBeenCalledTimes(1); }); test("interactive quit stops the TUI before emitting session_shutdown", async () => { diff --git a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts new file mode 100644 index 00000000..d170f1cf --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts @@ -0,0 +1,92 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; +import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts"; + +// Regression for https://github.com/earendil-works/pi/issues/5724 +// +// `proper-lockfile` installs `signal-exit`, whose signal listener re-sends +// SIGTERM/SIGHUP when it observes no other process listeners during the same +// signal dispatch. InteractiveMode must therefore keep its signal handlers +// registered until async terminal cleanup has completed. + +type ShutdownThis = { + isShuttingDown: boolean; + unregisterSignalHandlers: () => void; + runtimeHost: { dispose: () => Promise }; + ui: { terminal: { drainInput: (ms: number) => Promise } }; + stop: () => void; +}; + +type InteractiveModePrototypeWithShutdown = { + shutdown(this: ShutdownThis, options?: { fromSignal?: boolean }): Promise; +}; + +const interactiveModePrototype = InteractiveMode.prototype as unknown; + +class ProcessExitError extends Error {} + +function deferred(): { promise: Promise; resolve: () => void } { + let resolve: (() => void) | undefined; + const promise = new Promise((res) => { + resolve = res; + }); + return { + promise, + resolve: () => resolve?.(), + }; +} + +async function callShutdown(context: ShutdownThis, options?: { fromSignal?: boolean }): Promise { + try { + await (interactiveModePrototype as InteractiveModePrototypeWithShutdown).shutdown.call(context, options); + } catch (error) { + if (!(error instanceof ProcessExitError)) throw error; + } +} + +describe("InteractiveMode SIGTERM shutdown with signal-exit (#5724)", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("keeps signal handlers registered while signal-triggered cleanup is pending", async () => { + vi.spyOn(process, "exit").mockImplementation((() => { + throw new ProcessExitError(); + }) as typeof process.exit); + + const order: string[] = []; + const dispose = deferred(); + const context: ShutdownThis = { + isShuttingDown: false, + unregisterSignalHandlers: vi.fn(() => { + order.push("unregister"); + }), + runtimeHost: { + dispose: vi.fn(() => { + order.push("dispose"); + return dispose.promise; + }), + }, + ui: { + terminal: { + drainInput: vi.fn(async () => { + order.push("drainInput"); + }), + }, + }, + stop: vi.fn(() => { + order.push("stop"); + }), + }; + + const shutdownPromise = callShutdown(context, { fromSignal: true }); + await Promise.resolve(); + + expect(order).toEqual(["dispose"]); + expect(context.unregisterSignalHandlers).not.toHaveBeenCalled(); + + dispose.resolve(); + await shutdownPromise; + + expect(order).toEqual(["dispose", "drainInput", "stop"]); + }); +}); From 5b6058c3a7e9669650925683ce4c0380f652bd4d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 01:06:15 +0200 Subject: [PATCH 039/141] fix(tui): align overlays over CJK wide cells closes #5297 --- packages/tui/CHANGELOG.md | 1 + packages/tui/src/utils.ts | 2 +- .../regression-overlay-cjk-boundary.test.ts | 68 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 packages/tui/test/regression-overlay-cjk-boundary.test.ts diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 91b52474..9ab6749b 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed overlay compositing over CJK wide characters so borders stay aligned when an overlay starts inside a full-width cell ([#5297](https://github.com/earendil-works/pi/issues/5297)). - Fixed WezTerm inline Kitty image rendering during full redraw fallbacks so image padding rows are reserved before the placement is drawn without regressing tall-image placement ([#5618](https://github.com/earendil-works/pi/issues/5618), [#4415](https://github.com/earendil-works/pi/issues/4415)). ## [0.79.3] - 2026-06-13 diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index bf228ce0..b074a3a2 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -1156,7 +1156,7 @@ export function extractSegments( for (const { segment } of graphemeSegmenter.segment(line.slice(i, textEnd))) { const w = graphemeWidth(segment); - if (currentCol < beforeEnd) { + if (currentCol < beforeEnd && currentCol + w <= beforeEnd) { if (pendingAnsiBefore) { before += pendingAnsiBefore; pendingAnsiBefore = ""; diff --git a/packages/tui/test/regression-overlay-cjk-boundary.test.ts b/packages/tui/test/regression-overlay-cjk-boundary.test.ts new file mode 100644 index 00000000..2ae1f5ed --- /dev/null +++ b/packages/tui/test/regression-overlay-cjk-boundary.test.ts @@ -0,0 +1,68 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { TUI } from "../src/tui.ts"; +import { extractSegments, sliceByColumn, visibleWidth } from "../src/utils.ts"; +import { VirtualTerminal } from "./virtual-terminal.ts"; + +type TuiComposite = { + compositeLineAt( + baseLine: string, + overlayLine: string, + startCol: number, + overlayWidth: number, + totalWidth: number, + ): string; +}; + +function compositeLineAt( + baseLine: string, + overlayLine: string, + startCol: number, + overlayWidth: number, + totalWidth: number, +): string { + const tui = new TUI(new VirtualTerminal(totalWidth, 10)) as unknown as TuiComposite; + return tui.compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth); +} + +describe("overlay CJK boundary regression", () => { + it("excludes a wide grapheme from before when overlay starts inside it", () => { + const segments = extractSegments("abcd让EFGH", 5, 9, 11, true); + + assert.strictEqual(segments.before, "abcd"); + assert.strictEqual(segments.beforeWidth, 4); + assert.strictEqual(visibleWidth(segments.before), segments.beforeWidth); + assert.strictEqual(segments.after, "H"); + assert.strictEqual(segments.afterWidth, 1); + }); + + it("keeps ASCII before-segment behavior at the same boundary", () => { + const segments = extractSegments("abcdG EFGH", 5, 9, 11, true); + + assert.strictEqual(segments.before, "abcdG"); + assert.strictEqual(segments.beforeWidth, 5); + assert.strictEqual(visibleWidth(segments.before), segments.beforeWidth); + }); + + it("composites an overlay at the requested column when it starts inside a wide grapheme", () => { + const out = compositeLineAt("abcd让EFGH", "│XX│", 5, 4, 20); + const prefix = sliceByColumn(out, 0, 5, true); + const overlay = sliceByColumn(out, 5, 4, true); + + assert.strictEqual(out.includes("让"), false); + assert.strictEqual(visibleWidth(out), 20); + assert.strictEqual(visibleWidth(prefix), 5); + assert.strictEqual(visibleWidth(overlay), 4); + assert.strictEqual(overlay.includes("│XX│"), true); + }); + + it("composites an overlay when it starts at a wide grapheme boundary", () => { + const out = compositeLineAt("abcd让EFGH", "│XX│", 4, 4, 20); + const overlay = sliceByColumn(out, 4, 4, true); + + assert.strictEqual(out.includes("让"), false); + assert.strictEqual(visibleWidth(out), 20); + assert.strictEqual(visibleWidth(overlay), 4); + assert.strictEqual(overlay.includes("│XX│"), true); + }); +}); From 24053eab6681be055fce2663b1956bc74413caa7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 01:12:40 +0200 Subject: [PATCH 040/141] fix(tui): update tab overlay boundary expectation --- packages/tui/test/tab-width.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/tui/test/tab-width.test.ts b/packages/tui/test/tab-width.test.ts index 2797f89f..427a77db 100644 --- a/packages/tui/test/tab-width.test.ts +++ b/packages/tui/test/tab-width.test.ts @@ -16,8 +16,13 @@ describe("tab width accounting", () => { const text = "out 192M\t.pi/skill-tests/results-ha"; const segments = extractSegments(text, 10, 13, 10, true); - assert.strictEqual(segments.before, "out 192M\t"); - assert.strictEqual(segments.beforeWidth, 11); + assert.strictEqual(segments.before, "out 192M"); + assert.strictEqual(segments.beforeWidth, 8); assert.strictEqual(visibleWidth(segments.before), segments.beforeWidth); + + const tabFits = extractSegments(text, 11, 13, 10, true); + assert.strictEqual(tabFits.before, "out 192M\t"); + assert.strictEqual(tabFits.beforeWidth, 11); + assert.strictEqual(visibleWidth(tabFits.before), tabFits.beforeWidth); }); }); From bb959aae017eedc8edaa91d01d0475d483ea9371 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 01:25:27 +0200 Subject: [PATCH 041/141] fix(coding-agent): wrap tree help on narrow terminals closes #5055 --- packages/coding-agent/CHANGELOG.md | 1 + .../interactive/components/tree-selector.ts | 118 ++++++++++++++---- .../coding-agent/test/tree-selector.test.ts | 26 +++- 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e697d433..39db7fdd 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `/tree` help rendering to show compact wrapped controls instead of truncating them on narrow terminals ([#5055](https://github.com/earendil-works/pi/issues/5055)). - Fixed SIGTERM/SIGHUP interactive shutdown to keep signal handlers installed until terminal cleanup completes, preventing `signal-exit` from re-sending the signal and leaving the terminal in raw/Kitty keyboard mode ([#5724](https://github.com/earendil-works/pi/issues/5724)). - Fixed extensions documentation to clarify that `pi.getActiveTools()` returns active tool names while `pi.getAllTools()` returns tool metadata ([#5729](https://github.com/earendil-works/pi/issues/5729)). - Fixed package commands such as `pi list`, `pi install`, and `pi update` to terminate after completing even if an extension leaves background handles open ([#5687](https://github.com/earendil-works/pi/issues/5687)). diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index 990e705d..7c2cbf3e 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -4,15 +4,17 @@ import { type Focusable, getKeybindings, Input, + type Keybinding, Spacer, Text, - TruncatedText, truncateToWidth, + visibleWidth, + wrapTextWithAnsi, } from "@earendil-works/pi-tui"; import type { SessionTreeNode } from "../../../core/session-manager.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; -import { keyHint, keyText } from "./keybinding-hints.ts"; +import { formatKeyText, keyHint } from "./keybinding-hints.ts"; /** Gutter info: position (displayIndent where connector was) and whether to show │ */ interface GutterInfo { @@ -1075,6 +1077,98 @@ class SearchLine implements Component { handleInput(_keyData: string): void {} } +/** Component that renders tree help as semantic rows with chunk-aware wrapping */ +class TreeHelp implements Component { + invalidate(): void {} + + render(width: number): string[] { + const items = TREE_HELP_ITEMS.map(({ keys, label, labelFirst }) => { + const text = formatHelpKeys(keys); + if (!text) return label; + return labelFirst ? `${label} ${text}` : `${text} ${label}`; + }); + + const availableWidth = Math.max(1, width); + const indent = " "; + const separator = " · "; + const lines: string[] = []; + let currentLine = ""; + + for (const item of items) { + const candidate = currentLine + ? `${currentLine}${separator}${item}` + : visibleWidth(`${indent}${item}`) <= availableWidth + ? `${indent}${item}` + : item; + if (!currentLine || visibleWidth(candidate) <= availableWidth) { + currentLine = candidate; + continue; + } + + lines.push(...wrapTextWithAnsi(currentLine.trimEnd(), availableWidth)); + currentLine = visibleWidth(`${indent}${item}`) <= availableWidth ? `${indent}${item}` : item; + } + + if (currentLine) { + lines.push(...wrapTextWithAnsi(currentLine.trimEnd(), availableWidth)); + } + + return lines.map((line) => theme.fg("muted", line)); + } +} + +const TREE_HELP_ITEMS: Array<{ keys: Keybinding[]; label: string; labelFirst?: boolean }> = [ + { keys: ["tui.select.up", "tui.select.down"], label: "move" }, + { keys: ["tui.editor.cursorLeft", "tui.editor.cursorRight"], label: "page" }, + { keys: ["app.tree.foldOrUp", "app.tree.unfoldOrDown"], label: "branch" }, + { keys: ["app.tree.editLabel"], label: "label" }, + { keys: ["app.tree.toggleLabelTimestamp"], label: "label time" }, + { + keys: [ + "app.tree.filter.default", + "app.tree.filter.noTools", + "app.tree.filter.userOnly", + "app.tree.filter.labeledOnly", + "app.tree.filter.all", + ], + label: "filters", + labelFirst: true, + }, + { keys: ["app.tree.filter.cycleForward", "app.tree.filter.cycleBackward"], label: "cycle", labelFirst: true }, +]; + +function formatHelpKeys(keybindings: Keybinding[]): string { + const keys: string[] = []; + for (const keybinding of keybindings) { + const key = getKeybindings().getKeys(keybinding)[0]; + if (key !== undefined) keys.push(key); + } + if (keys.length === 0) return ""; + + return formatKeyText(compactRawKeys(keys)) + .replace(/\bpageUp\b/g, "pgup") + .replace(/\bpageDown\b/g, "pgdn") + .replace(/\bup\b/g, "↑") + .replace(/\bdown\b/g, "↓") + .replace(/\bleft\b/g, "←") + .replace(/\bright\b/g, "→"); +} + +function compactRawKeys(keys: string[]): string { + if (keys.length === 1) return keys[0]!; + + const parts = keys.map((key) => { + const separatorIndex = key.lastIndexOf("+"); + return separatorIndex === -1 + ? { prefix: "", suffix: key } + : { prefix: key.slice(0, separatorIndex + 1), suffix: key.slice(separatorIndex + 1) }; + }); + const prefix = parts[0]!.prefix; + return prefix && parts.every((part) => part.prefix === prefix) + ? `${prefix}${parts.map((part) => part.suffix).join("/")}` + : keys.join("/"); +} + /** Label input component shown when editing a label */ class LabelInput implements Component, Focusable { private input: Input; @@ -1181,25 +1275,7 @@ export class TreeSelectorComponent extends Container implements Focusable { this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); this.addChild(new Text(theme.bold(" Session Tree"), 1, 0)); - const filterKeys = [ - keyText("app.tree.filter.default"), - keyText("app.tree.filter.noTools"), - keyText("app.tree.filter.userOnly"), - keyText("app.tree.filter.labeledOnly"), - keyText("app.tree.filter.all"), - ].join("/"); - const cycleKeys = `${keyText("app.tree.filter.cycleForward")}/${keyText("app.tree.filter.cycleBackward")}`; - const branchKeys = `${keyText("app.tree.foldOrUp")}/${keyText("app.tree.unfoldOrDown")}`; - this.addChild( - new TruncatedText( - theme.fg( - "muted", - ` ↑/↓: move. ←/→: page. ${branchKeys}: fold/branch. ${keyText("app.tree.editLabel")}: label. ${filterKeys}: filters (${cycleKeys} cycle). ${keyText("app.tree.toggleLabelTimestamp")}: label time`, - ), - 0, - 0, - ), - ); + this.addChild(new TreeHelp()); this.addChild(new SearchLine(this.treeList)); this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); diff --git a/packages/coding-agent/test/tree-selector.test.ts b/packages/coding-agent/test/tree-selector.test.ts index 8f423dc5..4281b986 100644 --- a/packages/coding-agent/test/tree-selector.test.ts +++ b/packages/coding-agent/test/tree-selector.test.ts @@ -1,4 +1,5 @@ -import { setKeybindings } from "@earendil-works/pi-tui"; +import { stripVTControlCharacters } from "node:util"; +import { setKeybindings, visibleWidth } from "@earendil-works/pi-tui"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import { KeybindingsManager } from "../src/core/keybindings.ts"; import type { @@ -248,6 +249,29 @@ describe("TreeSelectorComponent", () => { }); }); + describe("help", () => { + test("renders semantic help rows without truncating narrow terminal controls", () => { + const entries = [userMessage("user-1", null, "hello"), assistantMessage("asst-1", "user-1", "hi")]; + const tree = buildTree(entries); + const selector = new TreeSelectorComponent( + tree, + "asst-1", + 24, + () => {}, + () => {}, + ); + + const plainLines = selector.render(30).map(stripVTControlCharacters); + const plain = plainLines.join("\n"); + expect(plain).toContain("branch"); + expect(plain).toContain("filters"); + expect(plain).toContain("cycle"); + expect(plain).toContain("label time"); + expect(plain).not.toContain("..."); + expect(plainLines.every((line) => visibleWidth(line) <= 30)).toBe(true); + }); + }); + describe("label timestamps", () => { test("toggles label timestamps for labeled nodes", () => { const entries = [userMessage("user-1", null, "hello"), assistantMessage("asst-1", "user-1", "hi")]; From a85196817051df96f8f63aed4799371270cd1ef1 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Mon, 15 Jun 2026 09:16:13 +0200 Subject: [PATCH 042/141] docs(coding-agent): reorder containerization patterns --- README.md | 2 +- .../coding-agent/docs/containerization.md | 70 +++++++++---------- packages/coding-agent/docs/index.md | 2 +- packages/coding-agent/docs/security.md | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 563b6858..295dce24 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,9 @@ Pi does not include a built-in permission system for restricting filesystem, pro If you need stronger boundaries, containerize or sandbox Pi. See [packages/coding-agent/docs/containerization.md](packages/coding-agent/docs/containerization.md) for three patterns: -- **OpenShell**: run the whole `pi` process in a policy-controlled sandbox. - **Gondolin extension**: keep `pi` and provider auth on the host while routing built-in tools and `!` commands into a local Linux micro-VM. - **Plain Docker**: run the whole `pi` process in a local container for simple isolation. +- **OpenShell**: run the whole `pi` process in a policy-controlled sandbox. ## Contributing diff --git a/packages/coding-agent/docs/containerization.md b/packages/coding-agent/docs/containerization.md index a3a96bee..33f2df3d 100644 --- a/packages/coding-agent/docs/containerization.md +++ b/packages/coding-agent/docs/containerization.md @@ -10,46 +10,12 @@ There are two general options. You can either | Pattern | What is isolated | Best for | Notes | | --- | --- | --- | --- | -| OpenShell | Whole `pi` process in a policy-controlled sandbox | Local or remote managed sandbox | Requires an OpenShell gateway | | Gondolin extension | Built-in tools and `!` commands | Local micro-VM isolation while keeping auth on host | See [`examples/extensions/gondolin/`](../examples/extensions/gondolin/). | | Plain Docker | Whole `pi` process in a local container | Simple local isolation | Provider API keys enter the container. | +| OpenShell | Whole `pi` process in a policy-controlled sandbox | Local or remote managed sandbox | Requires an OpenShell gateway | Extensions run wherever the `pi` process runs. If you run host `pi` with a tool-routing extension, other custom extension tools still run on the host unless they also delegate their operations. -## OpenShell - -Use [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) when you want a policy-controlled sandbox with filesystem, process, network, credential, and inference controls. -OpenShell can run sandboxes through a local gateway backed by Docker, Podman, or a VM runtime, or through a remote Kubernetes gateway. - -Every sandbox requires an active gateway. -Register and select one before creating a sandbox: - -```bash -openshell gateway add --name -openshell gateway select -``` - -Launch `pi` inside an OpenShell sandbox: - -```bash -openshell sandbox create --name pi-sandbox --from pi -- pi -``` - -In this pattern, the whole `pi` process runs inside the sandbox. -Built-in tools, `!` commands, and extension tools execute inside the OpenShell boundary. - -If the gateway is remote, project files are not bind-mounted from the host, meaning writes in the sandbox are not reflected on your machine. -Clone the repository inside the sandbox or use OpenShell file transfer commands: - -```bash -openshell sandbox upload pi-sandbox ./repo /workspace -openshell sandbox download pi-sandbox /workspace/repo ./repo-out -``` - -OpenShell providers can keep raw model API keys outside the sandbox. -When inference routing is configured, code inside the sandbox can call `https://inference.local`, and the gateway injects the configured provider credentials upstream. -Configure Pi to use the corresponding OpenAI-compatible or Anthropic-compatible endpoint if you want model traffic to use this route. - ## Gondolin [Gondolin](https://github.com/earendil-works/gondolin) is a local Linux micro-VM. @@ -109,3 +75,37 @@ docker run --rm -it \ The `-v "$PWD:/workspace"` mounts your current directory into the container at /workspace such that reads and writes in `/workspace` inside Docker directly affect your host files, like in the Gondolin example. Use a named volume for `/root/.pi/agent` if you want container-local settings and sessions. Mounting your host `~/.pi/agent` exposes host auth and session files to the container. + +## OpenShell + +Use [NVIDIA OpenShell](https://docs.nvidia.com/openshell/about/overview) when you want a policy-controlled sandbox with filesystem, process, network, credential, and inference controls. +OpenShell can run sandboxes through a local gateway backed by Docker, Podman, or a VM runtime, or through a remote Kubernetes gateway. + +Every sandbox requires an active gateway. +Register and select one before creating a sandbox: + +```bash +openshell gateway add --name +openshell gateway select +``` + +Launch `pi` inside an OpenShell sandbox: + +```bash +openshell sandbox create --name pi-sandbox --from pi -- pi +``` + +In this pattern, the whole `pi` process runs inside the sandbox. +Built-in tools, `!` commands, and extension tools execute inside the OpenShell boundary. + +If the gateway is remote, project files are not bind-mounted from the host, meaning writes in the sandbox are not reflected on your machine. +Clone the repository inside the sandbox or use OpenShell file transfer commands: + +```bash +openshell sandbox upload pi-sandbox ./repo /workspace +openshell sandbox download pi-sandbox /workspace/repo ./repo-out +``` + +OpenShell providers can keep raw model API keys outside the sandbox. +When inference routing is configured, code inside the sandbox can call `https://inference.local`, and the gateway injects the configured provider credentials upstream. +Configure Pi to use the corresponding OpenAI-compatible or Anthropic-compatible endpoint if you want model traffic to use this route. diff --git a/packages/coding-agent/docs/index.md b/packages/coding-agent/docs/index.md index 71995831..b5ee017e 100644 --- a/packages/coding-agent/docs/index.md +++ b/packages/coding-agent/docs/index.md @@ -42,7 +42,7 @@ For the full first-run flow, see [Quickstart](quickstart.md). - [Using Pi](usage.md) - interactive mode, slash commands, context files, and CLI reference. - [Providers](providers.md) - subscription and API-key setup for built-in providers. - [Security](security.md) - project trust, sandbox boundaries, and vulnerability reporting. -- [Containerization](containerization.md) - sandbox pi with OpenShell, Gondolin, or Docker. +- [Containerization](containerization.md) - sandbox pi with Gondolin, Docker, or OpenShell. - [Settings](settings.md) - global and project settings. - [Keybindings](keybindings.md) - default shortcuts and custom keybindings. - [Sessions](sessions.md) - session management, branching, and tree navigation. diff --git a/packages/coding-agent/docs/security.md b/packages/coding-agent/docs/security.md index 3a268a8a..29828e03 100644 --- a/packages/coding-agent/docs/security.md +++ b/packages/coding-agent/docs/security.md @@ -42,7 +42,7 @@ For untrusted repositories, generated code you do not intend to monitor closely, Common patterns are documented in [Containerization](containerization.md): -- run the whole `pi` process inside OpenShell or Docker +- run the whole `pi` process inside a container/sandbox - run host pi while routing built-in tool execution into a Gondolin micro-VM - mount only the workspace paths the agent should access - avoid mounting host `~/.pi/agent` unless the container should access host sessions, settings, and credentials From 0be5bb6c968ded1698793d0a3e7e60a0294050e2 Mon Sep 17 00:00:00 2001 From: Bucky <16464422+theBucky@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:21:14 +0800 Subject: [PATCH 043/141] fix(ai): price anthropic 1h cache writes at 2x input (#5738) anthropic 1h cache writes were billed at the 5m rate; split the bucket and price the 1h portion correctly --- packages/ai/src/models.ts | 5 +- packages/ai/src/providers/anthropic.ts | 1 + packages/ai/src/types.ts | 2 + .../anthropic-cache-write-1h-cost.test.ts | 86 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 packages/ai/test/anthropic-cache-write-1h-cost.test.ts diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index e14a6c2f..9d9fa519 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -37,10 +37,13 @@ export function getModels( } export function calculateCost(model: Model, usage: Usage): Usage["cost"] { + // Anthropic charges 2x base input for 1h cache writes. + const longWrite = usage.cacheWrite1h ?? 0; + const shortWrite = usage.cacheWrite - longWrite; usage.cost.input = (model.cost.input / 1000000) * usage.input; usage.cost.output = (model.cost.output / 1000000) * usage.output; usage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead; - usage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite; + usage.cost.cacheWrite = (model.cost.cacheWrite * shortWrite + model.cost.input * 2 * longWrite) / 1000000; usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite; return usage.cost; } diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 90b46146..9a64cbd8 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -535,6 +535,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti output.usage.output = event.message.usage.output_tokens || 0; output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0; output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0; + output.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0; // Anthropic doesn't provide total_tokens, compute from components output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 897e87d9..b7d0fda0 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -267,6 +267,8 @@ export interface Usage { output: number; cacheRead: number; cacheWrite: number; + /** Subset of `cacheWrite` written with 1h retention. Only Anthropic reports this split. */ + cacheWrite1h?: number; totalTokens: number; cost: { input: number; diff --git a/packages/ai/test/anthropic-cache-write-1h-cost.test.ts b/packages/ai/test/anthropic-cache-write-1h-cost.test.ts new file mode 100644 index 00000000..f9523b40 --- /dev/null +++ b/packages/ai/test/anthropic-cache-write-1h-cost.test.ts @@ -0,0 +1,86 @@ +import type Anthropic from "@anthropic-ai/sdk"; +import { describe, expect, it } from "vitest"; +import { getModel } from "../src/models.ts"; +import { streamAnthropic } from "../src/providers/anthropic.ts"; +import type { Context } from "../src/types.ts"; + +function createSseResponse(events: Array<{ event: string; data: string }>): Response { + const body = events.map(({ event, data }) => `event: ${event}\ndata: ${data}\n`).join("\n"); + return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } }); +} + +function createFakeAnthropicClient(response: Response): Anthropic { + return { + messages: { create: () => ({ asResponse: async () => response }) }, + } as unknown as Anthropic; +} + +function eventsWithCacheCreation( + cacheCreation: Record | undefined, +): Array<{ event: string; data: string }> { + const startUsage: Record = { + input_tokens: 100, + output_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 1_000_000, + }; + if (cacheCreation) startUsage.cache_creation = cacheCreation; + return [ + { + event: "message_start", + data: JSON.stringify({ type: "message_start", message: { id: "msg_test", usage: startUsage } }), + }, + { + event: "content_block_start", + data: JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }), + }, + { + event: "content_block_delta", + data: JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hi" } }), + }, + { event: "content_block_stop", data: JSON.stringify({ type: "content_block_stop", index: 0 }) }, + { + event: "message_delta", + data: JSON.stringify({ + type: "message_delta", + delta: { stop_reason: "end_turn" }, + usage: { + input_tokens: 100, + output_tokens: 5, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 1_000_000, + }, + }), + }, + { event: "message_stop", data: JSON.stringify({ type: "message_stop" }) }, + ]; +} + +// claude-opus-4-8: input 5, cacheWrite (5m) 6.25 per Mtok. 1h write = 2x input = 10. +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +describe("Anthropic 1h cache write cost", () => { + it("prices the 1h portion at 2x input and the rest at the 5m rate", async () => { + const model = getModel("anthropic", "claude-opus-4-8"); + const response = createSseResponse( + eventsWithCacheCreation({ ephemeral_5m_input_tokens: 600_000, ephemeral_1h_input_tokens: 400_000 }), + ); + const result = await streamAnthropic(model, context, { client: createFakeAnthropicClient(response) }).result(); + + expect(result.usage.cacheWrite).toBe(1_000_000); + expect(result.usage.cacheWrite1h).toBe(400_000); + // 600k * 6.25/Mtok + 400k * 10/Mtok = 3.75 + 4.0 = 7.75 + expect(result.usage.cost.cacheWrite).toBeCloseTo(7.75, 10); + }); + + it("falls back to the 5m rate when no breakdown is reported", async () => { + const model = getModel("anthropic", "claude-opus-4-8"); + const response = createSseResponse(eventsWithCacheCreation(undefined)); + const result = await streamAnthropic(model, context, { client: createFakeAnthropicClient(response) }).result(); + + expect(result.usage.cacheWrite).toBe(1_000_000); + expect(result.usage.cacheWrite1h ?? 0).toBe(0); + // 1M * 6.25/Mtok = 6.25 + expect(result.usage.cost.cacheWrite).toBeCloseTo(6.25, 10); + }); +}); From 28b3af5d0fd3c688706ad718979726a9d290967c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Jun 2026 07:25:09 +0000 Subject: [PATCH 044/141] chore: approve contributor Mearman --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 0513093f..f38c892d 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -237,3 +237,5 @@ davidlifschitz pr vdxz pr dangooddd pr + +Mearman pr From 408ac103ec698a60aa84524ea4b028f1660e98d1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 09:46:59 +0200 Subject: [PATCH 045/141] fix(ai): update Copilot Claude thinking metadata closes #4637 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 14 ++++ packages/ai/src/models.generated.ts | 66 +++++++++++++++++-- .../ai/test/github-copilot-anthropic.test.ts | 12 +++- 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 71da18c3..b73499de 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed GitHub Copilot Claude adaptive-thinking effort metadata to match manually checked Copilot model capabilities ([#4637](https://github.com/earendil-works/pi/issues/4637)). - Fixed OpenCode/OpenCode Go completion models that reject `prompt_cache_retention` to omit long-retention cache fields when `cacheRetention` is `long` ([#5702](https://github.com/earendil-works/pi/issues/5702)). ## [0.79.3] - 2026-06-13 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 63c54a56..dfb8f061 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -196,6 +196,14 @@ const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new "opencode-go:kimi-k2.6", ]); +// Checked manually against the authenticated GitHub Copilot /models endpoint on 2026-06-15. +// Keep this to narrow corrections over models.dev metadata instead of snapshotting Copilot's catalog. +const GITHUB_COPILOT_THINKING_LEVEL_OVERRIDES = { + "claude-opus-4.7": { minimal: "low" }, + "claude-opus-4.8": { minimal: "low" }, + "claude-sonnet-4.6": { minimal: "low", xhigh: "max" }, +} satisfies Record["thinkingLevelMap"]>>; + function mergeThinkingLevelMap(model: Model, map: NonNullable["thinkingLevelMap"]>): void { model.thinkingLevelMap = { ...model.thinkingLevelMap, ...map }; } @@ -358,6 +366,12 @@ function applyThinkingLevelMetadata(model: Model): void { // Ring reasons by default. Only high/xhigh have documented explicit effort controls. mergeThinkingLevelMap(model, ANT_LING_RING_THINKING_LEVEL_MAP); } + if (model.provider === "github-copilot") { + const override = GITHUB_COPILOT_THINKING_LEVEL_OVERRIDES[model.id]; + if (override) { + mergeThinkingLevelMap(model, override); + } + } } function getAnthropicMessagesCompat(provider: string, modelId: string): AnthropicMessagesCompat | undefined { diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 35295e5d..b1d877fa 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4194,7 +4194,7 @@ export const MODELS = { headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 5, @@ -4214,7 +4214,7 @@ export const MODELS = { headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, compat: {"forceAdaptiveThinking":true,"supportsTemperature":false}, reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh"}, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text", "image"], cost: { input: 5, @@ -4272,6 +4272,7 @@ export const MODELS = { headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, compat: {"forceAdaptiveThinking":true}, reasoning: true, + thinkingLevelMap: {"minimal":"low","xhigh":"max"}, input: ["text", "image"], cost: { input: 3, @@ -4831,6 +4832,42 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"google-generative-ai">, + "gemma-4-E2B-it": { + id: "gemma-4-E2B-it", + name: "Gemma 4 E2B IT", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">, + "gemma-4-E4B-it": { + id: "gemma-4-E4B-it", + name: "Gemma 4 E4B IT", + api: "google-generative-ai", + provider: "google", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + reasoning: true, + thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 8192, + } satisfies Model<"google-generative-ai">, }, "google-vertex": { "gemini-1.5-flash": { @@ -9432,13 +9469,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], cost: { - input: 0.098, - output: 0.196, + input: 0.09, + output: 0.18, cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 4096, + maxTokens: 65536, } satisfies Model<"openai-completions">, "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", @@ -13304,6 +13341,25 @@ export const MODELS = { contextWindow: 262144, maxTokens: 131000, } satisfies Model<"openai-completions">, + "moonshotai/Kimi-K2.7-Code": { + id: "moonshotai/Kimi-K2.7-Code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "together", + baseUrl: "https://api.together.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"supportsLongCacheRetention":false,"thinkingFormat":"together"}, + reasoning: true, + thinkingLevelMap: {"minimal":null,"low":null,"medium":null}, + input: ["text"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "nvidia/nemotron-3-ultra-550b-a55b": { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra 550B A55B", diff --git a/packages/ai/test/github-copilot-anthropic.test.ts b/packages/ai/test/github-copilot-anthropic.test.ts index ee3de3e8..ace2bd8f 100644 --- a/packages/ai/test/github-copilot-anthropic.test.ts +++ b/packages/ai/test/github-copilot-anthropic.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { getModel } from "../src/models.ts"; +import { getModel, getSupportedThinkingLevels } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import type { Context } from "../src/types.ts"; @@ -54,6 +54,16 @@ describe("Copilot Claude via Anthropic Messages", () => { messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], }; + it("applies Copilot-specific adaptive thinking effort overrides", () => { + const opus47 = getModel("github-copilot", "claude-opus-4.7"); + expect(opus47.thinkingLevelMap).toMatchObject({ minimal: "low", xhigh: "xhigh" }); + expect(getSupportedThinkingLevels(opus47)).toContain("xhigh"); + + const sonnet46 = getModel("github-copilot", "claude-sonnet-4.6"); + expect(sonnet46.thinkingLevelMap).toMatchObject({ minimal: "low", xhigh: "max" }); + expect(getSupportedThinkingLevels(sonnet46)).toContain("xhigh"); + }); + it("uses Bearer auth, Copilot headers, and valid Anthropic Messages payload", async () => { const model = getModel("github-copilot", "claude-sonnet-4.6"); expect(model.api).toBe("anthropic-messages"); From 3fa4095629164a71f517a68fe5fa56662355bfcf Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Mon, 15 Jun 2026 08:55:45 +0100 Subject: [PATCH 046/141] fix: drain stdout before resolving when a child holds the pipe past exit (#5753) --- packages/coding-agent/src/core/tools/bash.ts | 3 + .../coding-agent/src/utils/child-process.ts | 28 +++++-- .../regressions/5208-late-bash-output.test.ts | 29 +++++++ .../5303-bash-output-truncation.test.ts | 79 +++++++++++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5208-late-bash-output.test.ts create mode 100644 packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index d2bfacb9..e56291bb 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -289,6 +289,7 @@ export function createBashToolDefinition( const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command; const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook); const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" }); + let acceptingOutput = true; let updateTimer: NodeJS.Timeout | undefined; let updateDirty = false; let lastUpdateAt = 0; @@ -334,11 +335,13 @@ export function createBashToolDefinition( } const handleData = (data: Buffer) => { + if (!acceptingOutput) return; output.append(data); scheduleOutputUpdate(); }; const finishOutput = async () => { + acceptingOutput = false; output.finish(); clearUpdateTimer(); emitOutputUpdate(); diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 3d97d3da..b152444d 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -38,10 +38,13 @@ export function spawnProcessSync( /** * Wait for a child process to terminate without hanging on inherited stdio handles. * - * On Windows, daemonized descendants can inherit the child's stdout/stderr pipe - * handles. In that case the child emits `exit`, but `close` can hang forever even - * though the original process is already gone. We wait briefly for stdio to end, - * then forcibly stop tracking the inherited handles. + * A short-lived child can `exit` while a detached descendant keeps its stdout/stderr + * pipe open. We must not resolve and destroy the streams on a fixed deadline measured + * from `exit`, or output still being written past that deadline is silently lost + * (earendil-works/pi#5303). Instead, after `exit` we wait for the pipes to fall idle: + * the grace timer is re-armed on every chunk, so an actively writing descendant keeps + * us reading, while a quiet inherited handle (e.g. a Windows daemonized descendant + * that never lets `close` fire) still releases us after the grace elapses. */ export function waitForChildProcess(child: ChildProcess): Promise { return new Promise((resolve, reject) => { @@ -62,6 +65,8 @@ export function waitForChildProcess(child: ChildProcess): Promise child.removeListener("close", onClose); child.stdout?.removeListener("end", onStdoutEnd); child.stderr?.removeListener("end", onStderrEnd); + child.stdout?.removeListener("data", onData); + child.stderr?.removeListener("data", onData); }; const finalize = (code: number | null) => { @@ -80,6 +85,17 @@ export function waitForChildProcess(child: ChildProcess): Promise } }; + const armIdleTimer = () => { + if (postExitTimer) clearTimeout(postExitTimer); + postExitTimer = setTimeout(() => finalize(exitCode), EXIT_STDIO_GRACE_MS); + }; + + const onData = () => { + // Output is still arriving after exit; defer finalizing so we don't + // destroy the stream mid-write and truncate the tail. + if (exited && !settled) armIdleTimer(); + }; + const onStdoutEnd = () => { stdoutEnded = true; maybeFinalizeAfterExit(); @@ -102,7 +118,7 @@ export function waitForChildProcess(child: ChildProcess): Promise exitCode = code; maybeFinalizeAfterExit(); if (!settled) { - postExitTimer = setTimeout(() => finalize(code), EXIT_STDIO_GRACE_MS); + armIdleTimer(); } }; @@ -112,6 +128,8 @@ export function waitForChildProcess(child: ChildProcess): Promise child.stdout?.once("end", onStdoutEnd); child.stderr?.once("end", onStderrEnd); + child.stdout?.on("data", onData); + child.stderr?.on("data", onData); child.once("error", onError); child.once("exit", onExit); child.once("close", onClose); diff --git a/packages/coding-agent/test/suite/regressions/5208-late-bash-output.test.ts b/packages/coding-agent/test/suite/regressions/5208-late-bash-output.test.ts new file mode 100644 index 00000000..cb78a7f6 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5208-late-bash-output.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { type BashOperations, createBashTool } from "../../../src/core/tools/bash.ts"; + +function getTextOutput(result: { content?: Array<{ type: string; text?: string }> }): string { + return ( + result.content + ?.filter((block) => block.type === "text") + .map((block) => block.text ?? "") + .join("\n") ?? "" + ); +} + +describe("regression #5208: late bash output callbacks", () => { + it("ignores output callbacks after bash operations resolve", async () => { + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + onData(Buffer.from("before\n", "utf-8")); + setTimeout(() => onData(Buffer.from("late\n", "utf-8")), 0); + return { exitCode: 0 }; + }, + }; + const bash = createBashTool(process.cwd(), { operations }); + + const result = await bash.execute("test-call-late-output", { command: "late-output" }); + await new Promise((resolve) => setTimeout(resolve, 20)); + + expect(getTextOutput(result).trim()).toBe("before"); + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts b/packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts new file mode 100644 index 00000000..aeaa49fc --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5303-bash-output-truncation.test.ts @@ -0,0 +1,79 @@ +import type { ChildProcessByStdio } from "node:child_process"; +import type { Readable } from "node:stream"; +import { afterEach, describe, expect, it } from "vitest"; +import { spawnProcess, waitForChildProcess } from "../../../src/utils/child-process.ts"; + +/** + * Regression test for https://github.com/earendil-works/pi/issues/5303 + * + * waitForChildProcess armed a fixed 100ms timer on `exit` and destroyed the + * stdio streams when it fired. When a short-lived detached descendant kept the + * stdout pipe open, `close` never fired, so that timer was the only thing that + * resolved the wait, and any output written more than 100ms after exit was + * binned. In practice every git commit whose pre-commit hook runs lint-staged + * came back truncated mid-listr2 output, read by the model as a hang. + * + * The fix re-arms the grace on each chunk, so an actively writing pipe keeps us + * reading while a genuinely idle held-open handle still releases after the + * grace elapses. Both behaviours are covered below. + */ +describe.skipIf(process.platform === "win32")("issue #5303 bash output truncation past exit", () => { + let child: ChildProcessByStdio | undefined; + + afterEach(() => { + if (child?.pid) { + try { + process.kill(-child.pid, "SIGKILL"); + } catch { + // Already gone. + } + } + child = undefined; + }); + + it("captures output emitted after exit while a detached child holds stdout open", async () => { + // The shell exits immediately, but a backgrounded subshell keeps the stdout + // pipe open and emits ticks every 50ms, the last well past the 100ms grace. + const command = 'printf "HEAD\\n"; ( for i in 1 2 3 4 5 6; do sleep 0.05; printf "TICK$i\\n"; done ) &'; + child = spawnProcess("/bin/sh", ["-c", command], { + stdio: ["ignore", "pipe", "pipe"], + detached: true, + }) as ChildProcessByStdio; + + let output = ""; + child.stdout.on("data", (chunk: Buffer) => { + output += chunk.toString(); + }); + + const exitCode = await waitForChildProcess(child); + + expect(exitCode).toBe(0); + expect(output).toContain("HEAD"); + expect(output).toContain("TICK6"); + }); + + it("resolves promptly when a detached child holds stdout open but stays quiet", async () => { + // The shell exits, but a backgrounded sleeper inherits the stdout pipe and + // keeps it open for a long time without writing. `close` never fires, so we + // must still release via the idle grace rather than hang on the open handle. + const command = 'printf "DONE\\n"; ( sleep 30 ) &'; + child = spawnProcess("/bin/sh", ["-c", command], { + stdio: ["ignore", "pipe", "pipe"], + detached: true, + }) as ChildProcessByStdio; + + let output = ""; + child.stdout.on("data", (chunk: Buffer) => { + output += chunk.toString(); + }); + + const start = Date.now(); + const exitCode = await waitForChildProcess(child); + const elapsed = Date.now() - start; + + expect(exitCode).toBe(0); + expect(output).toContain("DONE"); + // Must not wait for the 30s sleeper; the idle grace releases us in well under a second. + expect(elapsed).toBeLessThan(2000); + }); +}); From 8a7ad60f03766e685aa73a0203c64804020b9c2c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 10:11:22 +0200 Subject: [PATCH 047/141] feat(coding-agent): add binary release checksums closes #5739 --- .github/workflows/build-binaries.yml | 27 +++++++++++++-------------- packages/coding-agent/CHANGELOG.md | 1 + 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 4a081581..355ad1cd 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -59,28 +59,27 @@ jobs: run: | cd packages/coding-agent/binaries + release_assets=( + pi-darwin-arm64.tar.gz + pi-darwin-x64.tar.gz + pi-linux-x64.tar.gz + pi-linux-arm64.tar.gz + pi-windows-x64.zip + pi-windows-arm64.zip + ) + sha256sum "${release_assets[@]}" > SHA256SUMS + release_assets+=(SHA256SUMS) + if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then gh release edit "${RELEASE_TAG}" \ --title "${RELEASE_TAG}" \ --notes-file /tmp/release-notes.md - gh release upload "${RELEASE_TAG}" \ - pi-darwin-arm64.tar.gz \ - pi-darwin-x64.tar.gz \ - pi-linux-x64.tar.gz \ - pi-linux-arm64.tar.gz \ - pi-windows-x64.zip \ - pi-windows-arm64.zip \ - --clobber + gh release upload "${RELEASE_TAG}" "${release_assets[@]}" --clobber else gh release create "${RELEASE_TAG}" \ --title "${RELEASE_TAG}" \ --notes-file /tmp/release-notes.md \ - pi-darwin-arm64.tar.gz \ - pi-darwin-x64.tar.gz \ - pi-linux-x64.tar.gz \ - pi-linux-arm64.tar.gz \ - pi-windows-x64.zip \ - pi-windows-arm64.zip + "${release_assets[@]}" fi publish-npm: diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 39db7fdd..594e4fd0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added `SHA256SUMS` integrity files to standalone binary GitHub release assets ([#5739](https://github.com/earendil-works/pi/issues/5739)). - Added first-run interactive theme detection from the terminal background. ### Fixed From b1ad469be4239d2c970d413bab98df76d59f3ed4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 10:23:19 +0200 Subject: [PATCH 048/141] docs: audit changelog entries --- packages/ai/CHANGELOG.md | 1 + packages/coding-agent/CHANGELOG.md | 13 ++++++++++++- packages/tui/CHANGELOG.md | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b73499de..19766189 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Anthropic 1-hour prompt-cache write cost accounting to price 1-hour cache writes at 2x input instead of the 5-minute cache-write rate ([#5738](https://github.com/earendil-works/pi/pull/5738) by [@theBucky](https://github.com/theBucky)). - Fixed GitHub Copilot Claude adaptive-thinking effort metadata to match manually checked Copilot model capabilities ([#4637](https://github.com/earendil-works/pi/issues/4637)). - Fixed OpenCode/OpenCode Go completion models that reject `prompt_cache_retention` to omit long-retention cache fields when `cacheRetention` is `long` ([#5702](https://github.com/earendil-works/pi/issues/5702)). diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 594e4fd0..ac772d06 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,20 +2,31 @@ ## [Unreleased] +### New Features + +- **Automatic first-run theme selection** - pi detects the terminal background on first run and defaults to the `dark` or `light` theme. See [Selecting a Theme](docs/themes.md#selecting-a-theme). +- **Standalone binary integrity checksums** - GitHub release assets now include `SHA256SUMS` files for verifying standalone binary downloads. See [Quickstart Install](docs/quickstart.md#install). + ### Added - Added `SHA256SUMS` integrity files to standalone binary GitHub release assets ([#5739](https://github.com/earendil-works/pi/issues/5739)). -- Added first-run interactive theme detection from the terminal background. +- Added first-run interactive theme detection from the terminal background ([#5385](https://github.com/earendil-works/pi/pull/5385) by [@vegarsti](https://github.com/vegarsti)). ### Fixed +- Fixed bash tool output collection to keep draining stdout/stderr after the child exits while descendants still write, avoiding truncated late output ([#5753](https://github.com/earendil-works/pi/pull/5753) by [@Mearman](https://github.com/Mearman)). - Fixed `/tree` help rendering to show compact wrapped controls instead of truncating them on narrow terminals ([#5055](https://github.com/earendil-works/pi/issues/5055)). - Fixed SIGTERM/SIGHUP interactive shutdown to keep signal handlers installed until terminal cleanup completes, preventing `signal-exit` from re-sending the signal and leaving the terminal in raw/Kitty keyboard mode ([#5724](https://github.com/earendil-works/pi/issues/5724)). - Fixed extensions documentation to clarify that `pi.getActiveTools()` returns active tool names while `pi.getAllTools()` returns tool metadata ([#5729](https://github.com/earendil-works/pi/issues/5729)). +- Fixed question and questionnaire extension examples to wrap long prompt, option, and help text instead of truncating it ([#5708](https://github.com/earendil-works/pi/pull/5708) by [@xl0](https://github.com/xl0)). - Fixed package commands such as `pi list`, `pi install`, and `pi update` to terminate after completing even if an extension leaves background handles open ([#5687](https://github.com/earendil-works/pi/issues/5687)). - Fixed `pi update` for pnpm global installs whose configured `global-bin-dir` no longer matches the active pnpm home ([#5689](https://github.com/earendil-works/pi/issues/5689)). - Fixed npm package specs that use ranges or tags (for example `@^1.2.7`) so installed package resources still load instead of being treated as mismatched exact pins ([#5695](https://github.com/earendil-works/pi/issues/5695)). +- Fixed inherited Anthropic 1-hour prompt-cache write cost accounting to price 1-hour cache writes at 2x input instead of the 5-minute cache-write rate ([#5738](https://github.com/earendil-works/pi/pull/5738) by [@theBucky](https://github.com/theBucky)). +- Fixed inherited GitHub Copilot Claude adaptive-thinking effort metadata to match manually checked Copilot model capabilities ([#4637](https://github.com/earendil-works/pi/issues/4637)). - Fixed inherited OpenCode/OpenCode Go completion model metadata to omit long-retention cache fields for routes that reject `prompt_cache_retention` ([#5702](https://github.com/earendil-works/pi/issues/5702)). +- Fixed inherited overlay compositing over CJK wide characters so borders stay aligned when an overlay starts inside a full-width cell ([#5297](https://github.com/earendil-works/pi/issues/5297)). +- Fixed inherited WezTerm inline Kitty image rendering during full redraw fallbacks so image padding rows are reserved before the placement is drawn without regressing tall-image placement ([#5618](https://github.com/earendil-works/pi/issues/5618), [#4415](https://github.com/earendil-works/pi/issues/4415)). - Fixed custom provider config so plain uppercase API key and header values remain literals instead of being treated as legacy environment references; use explicit `$ENV_VAR` syntax for environment variables ([#5661](https://github.com/earendil-works/pi/issues/5661)). ## [0.79.3] - 2026-06-13 diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9ab6749b..1de1cd15 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added terminal background color query support for OSC 11 replies. +- Added terminal background color query support for OSC 11 replies ([#5385](https://github.com/earendil-works/pi/pull/5385) by [@vegarsti](https://github.com/vegarsti)). ### Fixed From bba6af2c71c5004546007cde59995d15efe8465b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 10:28:21 +0200 Subject: [PATCH 049/141] Release v0.79.4 --- package-lock.json | 26 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +-- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++++--------- packages/coding-agent/package.json | 8 +++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 19 files changed, 50 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ee5233f..4063babb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6176,10 +6176,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.3", + "version": "0.79.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6213,7 +6213,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.3", + "version": "0.79.4", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6258,12 +6258,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.3", + "version": "0.79.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.3", - "@earendil-works/pi-ai": "^0.79.3", - "@earendil-works/pi-tui": "^0.79.3", + "@earendil-works/pi-agent-core": "^0.79.4", + "@earendil-works/pi-ai": "^0.79.4", + "@earendil-works/pi-tui": "^0.79.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6304,32 +6304,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.3", + "version": "0.79.4", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.3" + "version": "0.79.4" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.3", + "version": "0.79.4", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.3", + "version": "1.9.4", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.3", + "version": "0.79.4", "dependencies": { "ms": "2.1.3" }, @@ -6365,7 +6365,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.3", + "version": "0.79.4", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index f176ad06..4699f57f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.4] - 2026-06-15 ## [0.79.3] - 2026-06-13 diff --git a/packages/agent/package.json b/packages/agent/package.json index b8a17c29..36812b0f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.3", + "version": "0.79.4", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 19766189..6f46bf15 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.4] - 2026-06-15 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index 806d19ca..8278c9b9 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.3", + "version": "0.79.4", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ac772d06..ca4276b9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.4] - 2026-06-15 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 906cc651..5d512449 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.3", + "version": "0.79.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.3", + "version": "0.79.4", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index b2082217..404ac822 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.3", + "version": "0.79.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 3e80ae34..f5e5cdeb 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.3", + "version": "0.79.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index cbd6cfec..c7cae39d 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.3", + "version": "0.79.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.3", + "version": "0.79.4", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index a339c43a..ba925197 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.3", + "version": "0.79.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 04da2c15..84bc5c66 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.3", + "version": "1.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.3", + "version": "1.9.4", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 60573f2a..e2962554 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.3", + "version": "1.9.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index df12962d..555b18ba 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.3", + "version": "0.79.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.3", + "version": "0.79.4", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index b47c9e68..db2ac17e 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.3", + "version": "0.79.4", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 74eff303..438d1319 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.3", + "version": "0.79.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.3", + "version": "0.79.4", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.3", - "@earendil-works/pi-ai": "^0.79.3", - "@earendil-works/pi-tui": "^0.79.3", + "@earendil-works/pi-agent-core": "^0.79.4", + "@earendil-works/pi-ai": "^0.79.4", + "@earendil-works/pi-tui": "^0.79.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.3.tgz", + "version": "0.79.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.4.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.3", + "@earendil-works/pi-ai": "^0.79.4", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.3.tgz", + "version": "0.79.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.4.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -511,8 +511,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.3.tgz", + "version": "0.79.4", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.4.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index ff4522ce..e67d13dd 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.3", + "version": "0.79.4", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.3", - "@earendil-works/pi-ai": "^0.79.3", - "@earendil-works/pi-tui": "^0.79.3", + "@earendil-works/pi-agent-core": "^0.79.4", + "@earendil-works/pi-ai": "^0.79.4", + "@earendil-works/pi-tui": "^0.79.4", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 1de1cd15..89e3e742 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.4] - 2026-06-15 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index a808a640..749a3933 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.3", + "version": "0.79.4", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 1aa3c02d56635ec40e7c8448d7eff35022e95740 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 10:28:24 +0200 Subject: [PATCH 050/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 4699f57f..64f79b1b 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.4] - 2026-06-15 ## [0.79.3] - 2026-06-13 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6f46bf15..ff08f5b7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.4] - 2026-06-15 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ca4276b9..621b0fe1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.4] - 2026-06-15 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 89e3e742..5998b2b1 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.4] - 2026-06-15 ### Added From 0369bdb8f4f3ec3b9c67342c9bdd14f92fe87dd3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 17:08:59 +0200 Subject: [PATCH 051/141] fix(ai): add Moonshot CN Kimi K2.7 metadata closes #5760 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/scripts/generate-models.ts | 34 +++++++++++-- packages/ai/src/models.generated.ts | 20 ++++++++ .../ai/src/providers/openai-completions.ts | 6 ++- .../openai-completions-tool-choice.test.ts | 48 +++++++++++++++++++ packages/ai/test/supports-xhigh.test.ts | 9 ++++ packages/coding-agent/CHANGELOG.md | 4 ++ 7 files changed, 120 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ff08f5b7..243b729f 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). + ## [0.79.4] - 2026-06-15 ### Fixed diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index dfb8f061..07d0559d 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -68,6 +68,8 @@ const KIMI_STATIC_HEADERS = { "User-Agent": "KimiCLI/1.5", } as const; +const MOONSHOT_CN_MIRRORED_MODEL_IDS = new Set(["kimi-k2.7-code", "kimi-k2.7-code-highspeed"]); + const TOGETHER_BASE_URL = "https://api.together.ai/v1"; const TOGETHER_BASE_COMPAT: OpenAICompletionsCompat = { supportsStore: false, @@ -347,6 +349,15 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) { mergeThinkingLevelMap(model, { minimal: "low" }); } + if ( + (model.provider === "moonshotai" || model.provider === "moonshotai-cn") && + (model.id === "kimi-k2.7-code" || model.id === "kimi-k2.7-code-highspeed") + ) { + // Kimi K2.7 Code is always-thinking. Official docs say + // `thinking: { type: "disabled" }` is rejected, and callers can omit + // the thinking parameter to use the enabled default. + mergeThinkingLevelMap(model, { off: null }); + } if (model.provider === "openrouter" && model.id.startsWith("inception/mercury-2")) { // Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling. // Mark "off" unsupported so the openai-completions provider omits the reasoning param @@ -1262,12 +1273,27 @@ async function loadModelsDevData(): Promise[]> { supportsStrictMode: false, thinkingFormat: "deepseek", }; + const getMoonshotProviderModels = (key: "moonshotai" | "moonshotai-cn"): Record => { + const providerModels = data[key]?.models as Record | undefined; + return providerModels ? { ...providerModels } : {}; + }; + const moonshotModels = { + moonshotai: getMoonshotProviderModels("moonshotai"), + "moonshotai-cn": getMoonshotProviderModels("moonshotai-cn"), + }; + + // models.dev can lag the CN catalog while the global Moonshot catalog already + // has the model. Mirror selected current model IDs into moonshotai-cn until + // upstream CN metadata catches up. + for (const modelId of MOONSHOT_CN_MIRRORED_MODEL_IDS) { + const model = moonshotModels.moonshotai[modelId]; + if (model && !moonshotModels["moonshotai-cn"][modelId]) { + moonshotModels["moonshotai-cn"][modelId] = model; + } + } for (const { key, provider, baseUrl } of moonshotVariants) { - if (!data[key]?.models) continue; - - for (const [modelId, model] of Object.entries(data[key].models)) { - const m = model as ModelsDevModel; + for (const [modelId, m] of Object.entries(moonshotModels[key])) { if (m.tool_call !== true) continue; models.push({ diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index b1d877fa..cf18edc2 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -6425,6 +6425,7 @@ export const MODELS = { baseUrl: "https://api.moonshot.ai/v1", compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { input: 0.95, @@ -6563,6 +6564,25 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "kimi-k2.7-code": { + id: "kimi-k2.7-code", + name: "Kimi K2.7 Code", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.19, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, }, "nvidia": { "meta/llama-3.1-70b-instruct": { diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 0f87c897..140726ed 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -564,7 +564,11 @@ function buildParams( preserve_thinking: true, }; } else if (compat.thinkingFormat === "deepseek" && model.reasoning) { - (params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" }; + if (options?.reasoningEffort) { + (params as any).thinking = { type: "enabled" }; + } else if (model.thinkingLevelMap?.off !== null) { + (params as any).thinking = { type: "disabled" }; + } if (options?.reasoningEffort && compat.supportsReasoningEffort) { (params as any).reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 28c91795..9cf84496 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1118,6 +1118,54 @@ describe("openai-completions tool_choice", () => { expect(params.reasoning_effort).toBeUndefined(); }); + it("omits disabled thinking for Moonshot Kimi K2.7 Code models", async () => { + const cases = [getModel("moonshotai", "kimi-k2.7-code"), getModel("moonshotai-cn", "kimi-k2.7-code")]; + + for (const model of cases) { + expect(model).toBeDefined(); + let payload: unknown; + + await streamSimple( + model!, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { thinking?: unknown; reasoning_effort?: string }; + expect(params.thinking).toBeUndefined(); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("keeps disabled thinking for Moonshot Kimi K2.6 when thinking is off", async () => { + const model = getModel("moonshotai-cn", "kimi-k2.6")!; + let payload: unknown; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { thinking?: unknown; reasoning_effort?: string }; + expect(params.thinking).toEqual({ type: "disabled" }); + expect(params.reasoning_effort).toBeUndefined(); + }); + it("sends max_tokens for OpenCode completions models", async () => { const cases = [getModel("opencode-go", "kimi-k2.6")!, getModel("opencode", "grok-build-0.1")!] as const; diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index ec6dc87b..9f5363cb 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -69,6 +69,15 @@ describe("getSupportedThinkingLevels", () => { expect(getSupportedThinkingLevels(model!)).toEqual(["off", "high"]); }); + it("excludes thinking off for Moonshot Kimi K2.7 Code models", () => { + const cases = [getModel("moonshotai", "kimi-k2.7-code"), getModel("moonshotai-cn", "kimi-k2.7-code")]; + + for (const model of cases) { + expect(model).toBeDefined(); + expect(getSupportedThinkingLevels(model!)).toEqual(["minimal", "low", "medium", "high"]); + } + }); + it("includes only high for OpenCode Grok Build", () => { const model = getModel("opencode", "grok-build-0.1"); expect(model).toBeDefined(); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 621b0fe1..05e39136 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed inherited Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). + ## [0.79.4] - 2026-06-15 ### New Features From 431d88f4fcd4d5c897a2e99c14deef0716139ff9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 17:36:39 +0200 Subject: [PATCH 052/141] meta: Link to rfcs --- CONTRIBUTING.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a5c57a8..b0c38322 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,3 +95,8 @@ AI can help group duplicates, summarize reports, and spot missing information. I ### Is this hostile to contributors? No. It is a guardrail against burnout and tracker spam. Short, concrete, reproducible issues are welcome. Thoughtful contributions are welcome. Automated slop, entitlement, and large volumes of low-effort reports are not. + +## Where can I learn about plans? + +Earendil uses RFCs to discuss larger changes. Not all of them are public, but +quite a few are. They can be found at [rfc.earendil.com](https://rfc.earendil.com/keyword/pi/). diff --git a/README.md b/README.md index 295dce24..dc31b930 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ If you need stronger boundaries, containerize or sandbox Pi. See [packages/codin ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.md](AGENTS.md) for project-specific rules (for both humans and agents). +See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and [AGENTS.md](AGENTS.md) for project-specific rules (for both humans and agents). Longer term plans for Pi can also be found in [RFCs](https://rfc.earendil.com/keyword/pi/). ## Development From bee8e9c80529a0424fcffb64c3a3052895f2aac6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 15 Jun 2026 23:21:44 +0200 Subject: [PATCH 053/141] feat(coding-agent): mark experimental sessions in footer --- .../coding-agent/src/modes/interactive/components/footer.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/coding-agent/src/modes/interactive/components/footer.ts b/packages/coding-agent/src/modes/interactive/components/footer.ts index f108ae5e..aa537127 100644 --- a/packages/coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/coding-agent/src/modes/interactive/components/footer.ts @@ -1,6 +1,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path"; import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import type { AgentSession } from "../../../core/agent-session.ts"; +import { areExperimentalFeaturesEnabled } from "../../../core/experimental.ts"; import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.ts"; import { theme } from "../theme/theme.ts"; @@ -159,6 +160,9 @@ export class FooterComponent implements Component { contextPercentStr = contextPercentDisplay; } statsParts.push(contextPercentStr); + if (areExperimentalFeaturesEnabled()) { + statsParts.push(`${theme.fg("dim", "•")} ${theme.bold(theme.fg("warning", "xp"))}`); + } let statsLeft = statsParts.join(" "); From 7cfd1af395a1454c960c3a5178d9c0c58bbaafa6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 00:39:24 +0200 Subject: [PATCH 054/141] fix(coding-agent): keep empty session selector open closes #5747 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/interactive/components/session-selector.ts | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 05e39136..72a9b722 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the session selector to stay open and show the all-sessions empty state when both current-folder and all-scope session lists are empty ([#5747](https://github.com/earendil-works/pi/issues/5747)). - Fixed inherited Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). ## [0.79.4] - 2026-06-15 diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index 74141e5e..a92f0762 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -694,7 +694,6 @@ export class SessionSelectorComponent extends Container implements Focusable { private allSessions: SessionInfo[] | null = null; private currentSessionsLoader: SessionsLoader; private allSessionsLoader: SessionsLoader; - private onCancel: () => void; private requestRender: () => void; private renameSession?: (sessionPath: string, currentName: string | undefined) => Promise; private currentLoading = false; @@ -751,7 +750,6 @@ export class SessionSelectorComponent extends Container implements Focusable { this.keybindings = options?.keybindings ?? KeybindingsManager.create(); this.currentSessionsLoader = currentSessionsLoader; this.allSessionsLoader = allSessionsLoader; - this.onCancel = onCancel; this.requestRender = requestRender; this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender); const renameSession = options?.renameSession; @@ -948,10 +946,6 @@ export class SessionSelectorComponent extends Container implements Focusable { this.header.setLoading(false); this.sessionList.setSessions(sessions, showCwd); this.requestRender(); - - if (scope === "all" && sessions.length === 0 && (this.currentSessions?.length ?? 0) === 0) { - this.onCancel(); - } } catch (err) { if (scope === "current") { this.currentLoading = false; From b0c8f65ffa832cefc73a5db96378fe1337f6665e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 01:03:12 +0200 Subject: [PATCH 055/141] fix(ai): update Google Vertex Gemini models closes #5761 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 229 ++++++--------------- packages/ai/src/models.generated.ts | 223 +++++++++----------- packages/ai/src/providers/google-vertex.ts | 3 +- packages/ai/src/providers/google.ts | 3 +- packages/coding-agent/CHANGELOG.md | 1 + 6 files changed, 162 insertions(+), 298 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 243b729f..3203ecfe 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). ## [0.79.4] - 2026-06-15 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 07d0559d..26e581bb 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -123,6 +123,7 @@ const TOGETHER_TOGGLE_REASONING_LEVEL_MAP = { const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1"; const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh"; +const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com"; const NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1"; const NVIDIA_HEADERS = { "NVCF-POLL-SECONDS": "3600", @@ -270,7 +271,8 @@ function isGemini3ProModel(modelId: string): boolean { } function isGemini3FlashModel(modelId: string): boolean { - return /gemini-3(?:\.\d+)?-flash/.test(modelId.toLowerCase()); + const id = modelId.toLowerCase(); + return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; } function isGemma4Model(modelId: string): boolean { @@ -624,6 +626,13 @@ async function loadModelsDevData(): Promise[]> { for (const [modelId, model] of Object.entries(data.google.models)) { const m = model as ModelsDevModel; if (m.tool_call !== true) continue; + let source = m; + if (modelId === "gemini-flash-latest") { + source = (data.google.models["gemini-3.5-flash"] as ModelsDevModel | undefined) ?? m; + } + if (modelId === "gemini-flash-lite-latest") { + source = (data.google.models["gemini-3.1-flash-lite"] as ModelsDevModel | undefined) ?? m; + } models.push({ id: modelId, @@ -631,16 +640,57 @@ async function loadModelsDevData(): Promise[]> { api: "google-generative-ai", provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: m.reasoning === true, - input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], + reasoning: source.reasoning === true, + input: source.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, + input: source.cost?.input || 0, + output: source.cost?.output || 0, + cacheRead: source.cost?.cache_read || 0, + cacheWrite: source.cost?.cache_write || 0, }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, + contextWindow: source.limit?.context || 4096, + maxTokens: source.limit?.output || 4096, + }); + } + } + + // Process Google Vertex Gemini models. The google-vertex models.dev catalog also includes + // Claude, OpenAI, and other MaaS models that do not use the @google/genai Gemini streaming + // path implemented by our google-vertex provider. + if (data["google-vertex"]?.models) { + for (const [modelId, model] of Object.entries(data["google-vertex"].models)) { + const m = model as ModelsDevModel; + if (m.tool_call !== true) continue; + if (!modelId.startsWith("gemini-")) continue; + if (modelId === "gemini-3.1-flash-lite-preview") continue; + let source = m; + if (modelId === "gemini-flash-latest") { + source = (data["google-vertex"].models["gemini-3.5-flash"] as ModelsDevModel | undefined) ?? m; + } + if (modelId === "gemini-flash-lite-latest") { + source = (data["google-vertex"].models["gemini-3.1-flash-lite"] as ModelsDevModel | undefined) ?? m; + } + + // models.dev reports Vertex cache_read/cache_write values for Gemini 2.5 Flash that + // do not match the official Gemini API standard pricing table. pi only accounts + // cachedContentTokenCount as cacheRead. + const cacheRead = modelId === "gemini-2.5-flash" ? 0.03 : source.cost?.cache_read || 0; + models.push({ + id: modelId, + name: m.name || modelId, + api: "google-vertex", + provider: "google-vertex", + baseUrl: VERTEX_BASE_URL, + reasoning: source.reasoning === true, + input: source.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], + cost: { + input: source.cost?.input || 0, + output: source.cost?.output || 0, + cacheRead, + cacheWrite: 0, + }, + contextWindow: source.limit?.context || 4096, + maxTokens: source.limit?.output || 4096, }); } } @@ -1968,167 +2018,6 @@ async function generateModels() { }); } - const VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com"; - const vertexModels: Model<"google-vertex">[] = [ - { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 64000, - }, - { - id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 0.15, output: 0.6, cacheRead: 0.0375, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 8192, - }, - { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.075, output: 0.3, cacheRead: 0.01875, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 1048576, - maxTokens: 65536, - }, - { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 1.25, output: 5, cacheRead: 0.3125, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 8192, - }, - { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 0.075, output: 0.3, cacheRead: 0.01875, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 8192, - }, - { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: VERTEX_BASE_URL, - reasoning: false, - input: ["text", "image"], - cost: { input: 0.0375, output: 0.15, cacheRead: 0.01, cacheWrite: 0 }, - contextWindow: 1000000, - maxTokens: 8192, - }, - ]; - allModels.push(...vertexModels); - // Azure Foundry deploys these with larger context windows than OpenAI's own API, // which caps gpt-5.4/gpt-5.5 at 272k. See models-sold-directly-by-azure docs. const AZURE_CONTEXT_WINDOW_OVERRIDES: Record = { diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index cf18edc2..34575eb0 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4769,11 +4769,12 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { - input: 0.3, - output: 2.5, - cacheRead: 0.075, + input: 1.5, + output: 9, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 1048576, @@ -4786,10 +4787,11 @@ export const MODELS = { provider: "google", baseUrl: "https://generativelanguage.googleapis.com/v1beta", reasoning: true, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { - input: 0.1, - output: 0.4, + input: 0.25, + output: 1.5, cacheRead: 0.025, cacheWrite: 0, }, @@ -4870,94 +4872,9 @@ export const MODELS = { } satisfies Model<"google-generative-ai">, }, "google-vertex": { - "gemini-1.5-flash": { - id: "gemini-1.5-flash", - name: "Gemini 1.5 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-1.5-flash-8b": { - id: "gemini-1.5-flash-8b", - name: "Gemini 1.5 Flash-8B (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.0375, - output: 0.15, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-1.5-pro": { - id: "gemini-1.5-pro", - name: "Gemini 1.5 Pro (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 5, - cacheRead: 0.3125, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-2.0-flash": { - id: "gemini-2.0-flash", - name: "Gemini 2.0 Flash (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.0375, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 8192, - } satisfies Model<"google-vertex">, - "gemini-2.0-flash-lite": { - id: "gemini-2.0-flash-lite", - name: "Gemini 2.0 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.075, - output: 0.3, - cacheRead: 0.01875, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, "gemini-2.5-flash": { id: "gemini-2.5-flash", - name: "Gemini 2.5 Flash (Vertex)", + name: "Gemini 2.5 Flash", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", @@ -4974,24 +4891,7 @@ export const MODELS = { } satisfies Model<"google-vertex">, "gemini-2.5-flash-lite": { id: "gemini-2.5-flash-lite", - name: "Gemini 2.5 Flash Lite (Vertex)", - api: "google-vertex", - provider: "google-vertex", - baseUrl: "https://{location}-aiplatform.googleapis.com", - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.1, - output: 0.4, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 1048576, - maxTokens: 65536, - } satisfies Model<"google-vertex">, - "gemini-2.5-flash-lite-preview-09-2025": { - id: "gemini-2.5-flash-lite-preview-09-2025", - name: "Gemini 2.5 Flash Lite Preview 09-25 (Vertex)", + name: "Gemini 2.5 Flash-Lite", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", @@ -5008,7 +4908,7 @@ export const MODELS = { } satisfies Model<"google-vertex">, "gemini-2.5-pro": { id: "gemini-2.5-pro", - name: "Gemini 2.5 Pro (Vertex)", + name: "Gemini 2.5 Pro", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", @@ -5025,7 +4925,7 @@ export const MODELS = { } satisfies Model<"google-vertex">, "gemini-3-flash-preview": { id: "gemini-3-flash-preview", - name: "Gemini 3 Flash Preview (Vertex)", + name: "Gemini 3 Flash Preview", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", @@ -5041,27 +4941,27 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-vertex">, - "gemini-3-pro-preview": { - id: "gemini-3-pro-preview", - name: "Gemini 3 Pro Preview (Vertex)", + "gemini-3.1-flash-lite": { + id: "gemini-3.1-flash-lite", + name: "Gemini 3.1 Flash Lite", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", reasoning: true, - thinkingLevelMap: {"off":null,"minimal":null,"low":"LOW","medium":null,"high":"HIGH"}, + thinkingLevelMap: {"off":null}, input: ["text", "image"], cost: { - input: 2, - output: 12, - cacheRead: 0.2, + input: 0.25, + output: 1.5, + cacheRead: 0.025, cacheWrite: 0, }, - contextWindow: 1000000, - maxTokens: 64000, + contextWindow: 1048576, + maxTokens: 65536, } satisfies Model<"google-vertex">, "gemini-3.1-pro-preview": { id: "gemini-3.1-pro-preview", - name: "Gemini 3.1 Pro Preview (Vertex)", + name: "Gemini 3.1 Pro Preview", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", @@ -5079,7 +4979,7 @@ export const MODELS = { } satisfies Model<"google-vertex">, "gemini-3.1-pro-preview-customtools": { id: "gemini-3.1-pro-preview-customtools", - name: "Gemini 3.1 Pro Preview Custom Tools (Vertex)", + name: "Gemini 3.1 Pro Preview Custom Tools", api: "google-vertex", provider: "google-vertex", baseUrl: "https://{location}-aiplatform.googleapis.com", @@ -5095,6 +4995,60 @@ export const MODELS = { contextWindow: 1048576, maxTokens: 65536, } satisfies Model<"google-vertex">, + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-flash-latest": { + id: "gemini-flash-latest", + name: "Gemini Flash Latest", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, + "gemini-flash-lite-latest": { + id: "gemini-flash-lite-latest", + name: "Gemini Flash-Lite Latest", + api: "google-vertex", + provider: "google-vertex", + baseUrl: "https://{location}-aiplatform.googleapis.com", + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.5, + cacheRead: 0.025, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 65536, + } satisfies Model<"google-vertex">, }, "groq": { "llama-3.1-8b-instant": { @@ -9489,13 +9443,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], cost: { - input: 0.09, - output: 0.18, + input: 0.098, + output: 0.196, cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 65536, + maxTokens: 4096, } satisfies Model<"openai-completions">, "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", @@ -12333,7 +12287,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -15065,6 +15019,23 @@ export const MODELS = { contextWindow: 256000, maxTokens: 32768, } satisfies Model<"anthropic-messages">, + "moonshotai/kimi-k2.7-code-highspeed": { + id: "moonshotai/kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code High Speed", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, "nvidia/nemotron-3-super-120b-a12b": { id: "nvidia/nemotron-3-super-120b-a12b", name: "NVIDIA Nemotron 3 Super 120B A12B", diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index 6feeabb6..de476c00 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -490,7 +490,8 @@ function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { } function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-flash/.test(model.id.toLowerCase()); + const id = model.id.toLowerCase(); + return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; } function getDisabledThinkingConfig(model: Model<"google-vertex">): ThinkingConfig { diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index d3f6f623..a270792a 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -406,7 +406,8 @@ function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { } function isGemini3FlashModel(model: Model<"google-generative-ai">): boolean { - return /gemini-3(?:\.\d+)?-flash/.test(model.id.toLowerCase()); + const id = model.id.toLowerCase(); + return /gemini-3(?:\.\d+)?-flash/.test(id) || id === "gemini-flash-latest" || id === "gemini-flash-lite-latest"; } function getDisabledThinkingConfig(model: Model<"google-generative-ai">): ThinkingConfig { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 72a9b722..dfcf6226 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed inherited Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed the session selector to stay open and show the all-sessions empty state when both current-folder and all-scope session lists are empty ([#5747](https://github.com/earendil-works/pi/issues/5747)). - Fixed inherited Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). From f8a77f47f2499febe56a71c3563415a0cae2616c Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Tue, 16 Jun 2026 09:37:37 +0200 Subject: [PATCH 056/141] feat(coding-agent): add Vercel AI Gateway attribution (#5798) --- .../coding-agent/src/core/provider-attribution.ts | 12 ++++++++++++ .../test/sdk-openrouter-attribution.test.ts | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/packages/coding-agent/src/core/provider-attribution.ts b/packages/coding-agent/src/core/provider-attribution.ts index 91265c92..97ad1cfa 100644 --- a/packages/coding-agent/src/core/provider-attribution.ts +++ b/packages/coding-agent/src/core/provider-attribution.ts @@ -7,6 +7,7 @@ const NVIDIA_NIM_HOST = "integrate.api.nvidia.com"; const CLOUDFLARE_API_HOST = "api.cloudflare.com"; const CLOUDFLARE_AI_GATEWAY_HOST = "gateway.ai.cloudflare.com"; const OPENCODE_HOST = "opencode.ai"; +const VERCEL_GATEWAY_HOST = "ai-gateway.vercel.sh"; function matchesHost(baseUrl: string, expectedHost: string): boolean { try { @@ -33,6 +34,10 @@ function isCloudflareModel(model: Model): boolean { ); } +function isVercelGatewayModel(model: Model): boolean { + return model.provider === "vercel-ai-gateway" || matchesHost(model.baseUrl, VERCEL_GATEWAY_HOST); +} + function getDefaultAttributionHeaders( model: Model, settingsManager: SettingsManager, @@ -61,6 +66,13 @@ function getDefaultAttributionHeaders( }; } + if (isVercelGatewayModel(model)) { + return { + "http-referer": "https://pi.dev", + "x-title": "pi", + }; + } + return undefined; } diff --git a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts index 9f035189..b03e5bb6 100644 --- a/packages/coding-agent/test/sdk-openrouter-attribution.test.ts +++ b/packages/coding-agent/test/sdk-openrouter-attribution.test.ts @@ -196,6 +196,13 @@ describe("createAgentSession provider attribution headers", () => { expect(headers?.["X-OpenRouter-Categories"]).toBe("provider-category"); }); + it("adds default attribution headers for Vercel AI Gateway models", async () => { + const headers = await captureHeaders(createModel("vercel-ai-gateway", "https://ai-gateway.vercel.sh/v1")); + + expect(headers?.["http-referer"]).toBe("https://pi.dev"); + expect(headers?.["x-title"]).toBe("pi"); + }); + it("adds default attribution headers for direct NVIDIA NIM endpoints", async () => { const headers = await captureHeaders(createModel("custom-nim", "https://integrate.api.nvidia.com/v1")); From 75b0d723c0778a7c7d9543b5d960f3fc80c455fe Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 14:29:07 +0200 Subject: [PATCH 057/141] fix(ai): support Z.AI GLM-5.2 effort levels closes #5770 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 11 +++ packages/ai/src/models.generated.ts | 72 +++++++++++++---- .../ai/src/providers/openai-completions.ts | 12 ++- .../openai-completions-tool-choice.test.ts | 80 +++++++++++++++++++ 5 files changed, 159 insertions(+), 17 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 3203ecfe..76bbc3d7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Z.AI GLM-5.2 thinking requests to send `reasoning_effort` with the provider's `high`/`max` effort mapping ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 26e581bb..8cc890ee 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -157,6 +157,13 @@ const NVIDIA_NIM_UNSUPPORTED_MODELS = new Set([ "upstage/solar-10.7b-instruct", ]); const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]); +const ZAI_GLM52_THINKING_LEVEL_MAP = { + minimal: null, + low: "high", + medium: "high", + high: "high", + xhigh: "max", +} as const; const EAGER_TOOL_INPUT_STREAMING_UNSUPPORTED_ANTHROPIC_MODELS = new Set([ "github-copilot:claude-haiku-4.5", "github-copilot:claude-sonnet-4", @@ -894,6 +901,8 @@ async function loadModelsDevData(): Promise[]> { if (m.tool_call !== true) continue; const supportsImage = m.modalities?.input?.includes("image"); + const isGlm52 = modelId === "glm-5.2"; + models.push({ id: modelId, name: m.name || modelId, @@ -901,6 +910,7 @@ async function loadModelsDevData(): Promise[]> { provider, baseUrl, reasoning: m.reasoning === true, + ...(isGlm52 ? { thinkingLevelMap: ZAI_GLM52_THINKING_LEVEL_MAP } : {}), input: supportsImage ? ["text", "image"] : ["text"], cost: { input: m.cost?.input || 0, @@ -911,6 +921,7 @@ async function loadModelsDevData(): Promise[]> { compat: { supportsDeveloperRole: false, thinkingFormat: "zai", + ...(isGlm52 ? { supportsReasoningEffort: true } : {}), ...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}), }, contextWindow: m.limit?.context || 4096, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 34575eb0..d7e2f9f3 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3921,7 +3921,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.6, - cacheRead: 0.015, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 131072, @@ -6390,6 +6390,25 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "kimi-k2.7-code-highspeed": { + id: "kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code HighSpeed", + api: "openai-completions", + provider: "moonshotai", + baseUrl: "https://api.moonshot.ai/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, }, "moonshotai-cn": { "kimi-k2-0711-preview": { @@ -6537,6 +6556,25 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"openai-completions">, + "kimi-k2.7-code-highspeed": { + id: "kimi-k2.7-code-highspeed", + name: "Kimi K2.7 Code HighSpeed", + api: "openai-completions", + provider: "moonshotai-cn", + baseUrl: "https://api.moonshot.cn/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek"}, + reasoning: true, + thinkingLevelMap: {"off":null}, + input: ["text", "image"], + cost: { + input: 1.9, + output: 8, + cacheRead: 0.38, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, }, "nvidia": { "meta/llama-3.1-70b-instruct": { @@ -12298,13 +12336,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.39, - output: 2.34, + input: 0.385, + output: 2.45, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 65536, + contextWindow: 256000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "qwen/qwen3.5-9b": { id: "qwen/qwen3.5-9b", @@ -12587,13 +12625,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.063, - output: 0.21, - cacheRead: 0.021, + input: 0.066, + output: 0.26, + cacheRead: 0.029, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "thedrummer/rocinante-12b": { id: "thedrummer/rocinante-12b", @@ -12774,13 +12812,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.125, + input: 0.13, output: 0.85, - cacheRead: 0.06, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 131070, + maxTokens: 98304, } satisfies Model<"openai-completions">, "z-ai/glm-4.5v": { id: "z-ai/glm-4.5v", @@ -13214,8 +13252,8 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 2.5, - output: 7.5, + input: 1.25, + output: 3.75, cacheRead: 0, cacheWrite: 0, }, @@ -16876,8 +16914,9 @@ export const MODELS = { api: "openai-completions", provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0, @@ -16986,8 +17025,9 @@ export const MODELS = { api: "openai-completions", provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", - compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","supportsReasoningEffort":true,"zaiToolStream":true}, reasoning: true, + thinkingLevelMap: {"minimal":null,"low":"high","medium":"high","high":"high","xhigh":"max"}, input: ["text"], cost: { input: 0, diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 140726ed..c2d5cff9 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -554,8 +554,18 @@ function buildParams( } if (compat.thinkingFormat === "zai" && model.reasoning) { - const zaiParams = params as typeof params & { thinking?: { type: "enabled" | "disabled" } }; + const zaiParams = params as Omit & { + thinking?: { type: "enabled" | "disabled" }; + reasoning_effort?: string; + }; zaiParams.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" }; + if (options?.reasoningEffort && compat.supportsReasoningEffort) { + const mappedEffort = model.thinkingLevelMap?.[options.reasoningEffort]; + const effort = mappedEffort === undefined ? options.reasoningEffort : mappedEffort; + if (typeof effort === "string") { + zaiParams.reasoning_effort = effort; + } + } } else if (compat.thinkingFormat === "qwen" && model.reasoning) { (params as any).enable_thinking = !!options?.reasoningEffort; } else if (compat.thinkingFormat === "qwen-chat-template" && model.reasoning) { diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 9cf84496..8ca86025 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -257,6 +257,86 @@ describe("openai-completions tool_choice", () => { expect(getModel("zai", "glm-4.5-air")?.compat?.zaiToolStream).toBeUndefined(); }); + it("stores z.ai GLM-5.2 effort metadata", () => { + for (const provider of ["zai", "zai-coding-cn"] as const) { + const model = getModel(provider, "glm-5.2")!; + expect(model.compat?.supportsReasoningEffort).toBe(true); + expect(model.thinkingLevelMap).toEqual({ + minimal: null, + low: "high", + medium: "high", + high: "high", + xhigh: "max", + }); + } + }); + + it("maps z.ai GLM-5.2 thinking levels to reasoning_effort", async () => { + const model = getModel("zai", "glm-5.2")!; + const cases = [ + { reasoning: "low", effort: "high" }, + { reasoning: "medium", effort: "high" }, + { reasoning: "high", effort: "high" }, + { reasoning: "xhigh", effort: "max" }, + ] as const; + + for (const testCase of cases) { + let payload: unknown; + + await streamSimple( + model, + { + messages: [ + { + role: "user", + content: "Hi", + timestamp: Date.now(), + }, + ], + }, + { + apiKey: "test", + reasoning: testCase.reasoning, + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { thinking?: unknown; reasoning_effort?: string }; + expect(params.thinking).toEqual({ type: "enabled" }); + expect(params.reasoning_effort).toBe(testCase.effort); + } + }); + + it("omits z.ai GLM-5.2 reasoning_effort when thinking is off", async () => { + const model = getModel("zai", "glm-5.2")!; + let payload: unknown; + + await streamSimple( + model, + { + messages: [ + { + role: "user", + content: "Hi", + timestamp: Date.now(), + }, + ], + }, + { + apiKey: "test", + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + const params = (payload ?? mockState.lastParams) as { thinking?: unknown; reasoning_effort?: string }; + expect(params.thinking).toEqual({ type: "disabled" }); + expect(params.reasoning_effort).toBeUndefined(); + }); + it("omits tool_stream for unsupported z.ai models", async () => { const model = getModel("zai", "glm-4.5-air")!; const tools: Tool[] = [ From 06d8c54de297d6b0ddb44c249eca9f988ddeaaee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 15:10:23 +0200 Subject: [PATCH 058/141] fix(coding-agent): avoid Windows pi update exit assertion closes #5805 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/main.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index dfcf6226..11cb9900 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed successful `pi update` on Windows to exit naturally instead of calling `process.exit(0)`, avoiding a Node.js/libuv assertion after version-check network requests ([#5805](https://github.com/earendil-works/pi/issues/5805)). - Fixed inherited Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed the session selector to stay open and show the all-sessions empty state when both current-folder and all-scope session lists are empty ([#5747](https://github.com/earendil-works/pi/issues/5747)). - Fixed inherited Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 00f3b208..42ce1ce5 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -467,7 +467,15 @@ export async function main(args: string[], options?: MainOptions) { } if (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) { - process.exit(process.exitCode ?? 0); + const exitCode = process.exitCode ?? 0; + if (process.platform === "win32" && exitCode === 0 && args[0] === "update") { + // We normally prefer process.exit(0) for package commands so bad extensions cannot keep + // one-shot commands alive. On Windows, Node can assert after fetch() if process.exit(0) + // runs during teardown; let successful `pi update` drain naturally instead. + // https://github.com/nodejs/node/issues/56645 + return; + } + process.exit(exitCode); return; } From 3039f3e17d09e23fb2b097b67cf2f5eb92e5aadb Mon Sep 17 00:00:00 2001 From: 4h9fbZ <176179231+4h9fbZ@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:14:17 +0200 Subject: [PATCH 059/141] fix(tui): restore cursorUp start-of-line jump when input is non-empty (#5789) --- packages/tui/src/components/editor.ts | 9 ++++++++- packages/tui/test/editor.test.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 724dd460..bedd0105 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -390,6 +390,10 @@ export class Editor implements Component, Focusable { } } + private isEditorEmpty(): boolean { + return this.state.lines.length === 1 && this.state.lines[0] === ""; + } + private isOnFirstVisualLine(): boolean { const visualLines = this.buildVisualLineMap(this.lastWidth); const currentVisualLine = this.findCurrentVisualLine(visualLines); @@ -803,7 +807,10 @@ export class Editor implements Component, Focusable { // Arrow key navigation (with history support) if (kb.matches(data, "tui.editor.cursorUp")) { - if (this.isOnFirstVisualLine() && this.history.length > 0) { + if ( + this.isOnFirstVisualLine() && + (this.isEditorEmpty() || this.historyIndex > -1 || this.state.cursorCol === 0) + ) { this.navigateHistory(-1); } else if (this.isOnFirstVisualLine()) { // Already at top - jump to start of line diff --git a/packages/tui/test/editor.test.ts b/packages/tui/test/editor.test.ts index 2c8a7b62..0f33370e 100644 --- a/packages/tui/test/editor.test.ts +++ b/packages/tui/test/editor.test.ts @@ -79,7 +79,7 @@ describe("Editor component", () => { assert.strictEqual(editor.getText(), "first"); }); - it("restores draft on Down arrow after browsing history", () => { + it("jumps to start before entering history from a non-empty draft", () => { const editor = new Editor(createTestTUI(), defaultEditorTheme); editor.addToHistory("prompt"); @@ -87,12 +87,16 @@ describe("Editor component", () => { editor.handleInput("\x1b[D"); editor.handleInput("\x1b[D"); - editor.handleInput("\x1b[A"); // Up - shows "prompt" + editor.handleInput("\x1b[A"); // Up - jumps to start before history browsing + assert.strictEqual(editor.getText(), "draft"); + assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 }); + + editor.handleInput("\x1b[A"); // Up at start - shows "prompt" assert.strictEqual(editor.getText(), "prompt"); editor.handleInput("\x1b[B"); // Down - restores draft assert.strictEqual(editor.getText(), "draft"); - assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 3 }); + assert.deepStrictEqual(editor.getCursor(), { line: 0, col: 0 }); }); it("navigates forward through history with Down arrow", () => { @@ -104,6 +108,7 @@ describe("Editor component", () => { editor.setText("draft"); // Go to oldest + editor.handleInput("\x1b[A"); // start of draft editor.handleInput("\x1b[A"); // third editor.handleInput("\x1b[A"); // second editor.handleInput("\x1b[A"); // first From 7f29e7a3697dc77416818faea4f4af29a13ce079 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 17:19:08 +0200 Subject: [PATCH 060/141] feat: add provider-scoped environment overrides (#5807) --- packages/ai/CHANGELOG.md | 4 + packages/ai/README.md | 19 ++++ packages/ai/src/env-api-keys.ts | 90 ++++++------------ packages/ai/src/providers/amazon-bedrock.ts | 92 ++++++++++++------- packages/ai/src/providers/anthropic.ts | 17 ++-- .../src/providers/azure-openai-responses.ts | 15 ++- packages/ai/src/providers/cloudflare.ts | 9 +- packages/ai/src/providers/google-vertex.ts | 19 +++- .../src/providers/openai-codex-responses.ts | 39 ++++---- .../ai/src/providers/openai-completions.ts | 15 +-- packages/ai/src/providers/openai-responses.ts | 15 +-- packages/ai/src/providers/simple-options.ts | 1 + packages/ai/src/stream.ts | 2 +- packages/ai/src/types.ts | 9 ++ packages/ai/src/utils/node-http-proxy.ts | 55 +++++------ packages/ai/src/utils/oauth/anthropic.ts | 3 +- packages/ai/src/utils/oauth/openai-codex.ts | 3 +- packages/ai/src/utils/provider-env.ts | 52 +++++++++++ .../test/bedrock-endpoint-resolution.test.ts | 33 ++++++- packages/ai/test/node-http-proxy.test.ts | 11 +++ .../openai-completions-empty-tools.test.ts | 25 +++++ packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/docs/providers.md | 22 ++++- .../src/bun/restore-sandbox-env.ts | 4 + .../coding-agent/src/core/agent-session.ts | 17 +++- .../coding-agent/src/core/auth-storage.ts | 11 ++- .../core/compaction/branch-summarization.ts | 5 +- .../src/core/compaction/compaction.ts | 13 ++- .../coding-agent/src/core/model-registry.ts | 20 +++- .../src/core/resolve-config-value.ts | 40 ++++---- packages/coding-agent/src/core/sdk.ts | 2 + .../coding-agent/test/auth-storage.test.ts | 28 ++++++ .../coding-agent/test/model-registry.test.ts | 32 +++++++ 33 files changed, 511 insertions(+), 215 deletions(-) create mode 100644 packages/ai/src/utils/provider-env.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 76bbc3d7..e4d9ced0 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added provider-scoped `StreamOptions.env` overrides for provider configuration, including Cloudflare endpoint placeholders, Azure OpenAI, Google Vertex, Amazon Bedrock, cache retention, and proxy environment lookups ([#5728](https://github.com/earendil-works/pi/issues/5728)). + ### Fixed - Fixed Z.AI GLM-5.2 thinking requests to send `reasoning_effort` with the provider's `high`/`max` effort mapping ([#5770](https://github.com/earendil-works/pi/issues/5770)). diff --git a/packages/ai/README.md b/packages/ai/README.md index 4190fcb8..a7b8028e 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -38,6 +38,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Browser Usage](#browser-usage) - [Browser Compatibility Notes](#browser-compatibility-notes) - [Environment Variables](#environment-variables-nodejs-only) + - [Provider-Scoped Environment Overrides](#provider-scoped-environment-overrides) - [Checking Environment Variables](#checking-environment-variables) - [OAuth Providers](#oauth-providers) - [Vertex AI](#vertex-ai) @@ -1145,6 +1146,24 @@ const response = await complete(model, context, { }); ``` +### Provider-Scoped Environment Overrides + +Pass `env` in stream options to scope provider configuration to a request. Values in `env` are used before process environment variables for API key discovery and provider configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`. + +```typescript +const model = getModel('cloudflare-ai-gateway', 'workers-ai/@cf/moonshotai/kimi-k2.6'); + +const response = await complete(model, context, { + env: { + CLOUDFLARE_API_KEY: '...', + CLOUDFLARE_ACCOUNT_ID: 'account-id', + CLOUDFLARE_GATEWAY_ID: 'gateway-id' + } +}); +``` + +Use this when one process needs different provider settings per request, or when ambient environment variables should not leak into a provider call. + ### Checking Environment Variables ```typescript diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 291d2288..7bd955e1 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -23,44 +23,17 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import type { KnownProvider } from "./types.ts"; - -let _procEnvCache: Map | null = null; - -/** - * Fallback for https://github.com/oven-sh/bun/issues/27802 - * Bun compiled binaries have an empty `process.env` inside sandbox - * environments on Linux. We can recover the env from `/proc/self/environ`. - */ -function getProcEnv(key: string): string | undefined { - if (!process.versions?.bun) return undefined; - if (typeof process === "undefined") return undefined; - - // If process.env already has entries, the bug is not triggered. - if (Object.keys(process.env).length > 0) return undefined; - - if (_procEnvCache === null) { - _procEnvCache = new Map(); - try { - const { readFileSync } = require("node:fs") as typeof import("node:fs"); - const data = readFileSync("/proc/self/environ", "utf-8"); - for (const entry of data.split("\0")) { - const idx = entry.indexOf("="); - if (idx > 0) { - _procEnvCache.set(entry.slice(0, idx), entry.slice(idx + 1)); - } - } - } catch { - // /proc/self/environ may not be readable. - } - } - - return _procEnvCache.get(key); -} +import type { KnownProvider, ProviderEnv } from "./types.ts"; +import { getProviderEnvValue } from "./utils/provider-env.ts"; let cachedVertexAdcCredentialsExists: boolean | null = null; -function hasVertexAdcCredentials(): boolean { +function hasVertexAdcCredentials(env?: ProviderEnv): boolean { + const explicitCredentialsPath = env?.GOOGLE_APPLICATION_CREDENTIALS; + if (explicitCredentialsPath) { + return _existsSync ? _existsSync(explicitCredentialsPath) : false; + } + if (cachedVertexAdcCredentialsExists === null) { // If node modules haven't loaded yet (async import race at startup), // return false WITHOUT caching so the next call retries once they're ready. @@ -75,7 +48,7 @@ function hasVertexAdcCredentials(): boolean { } // Check GOOGLE_APPLICATION_CREDENTIALS env var first (standard way) - const gacPath = process.env.GOOGLE_APPLICATION_CREDENTIALS || getProcEnv("GOOGLE_APPLICATION_CREDENTIALS"); + const gacPath = getProviderEnvValue("GOOGLE_APPLICATION_CREDENTIALS", env); if (gacPath) { cachedVertexAdcCredentialsExists = _existsSync(gacPath); } else { @@ -143,13 +116,13 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined { * credential sources such as AWS profiles, AWS IAM credentials, and Google * Application Default Credentials. */ -export function findEnvKeys(provider: KnownProvider): string[] | undefined; -export function findEnvKeys(provider: string): string[] | undefined; -export function findEnvKeys(provider: string): string[] | undefined { +export function findEnvKeys(provider: KnownProvider, env?: ProviderEnv): string[] | undefined; +export function findEnvKeys(provider: string, env?: ProviderEnv): string[] | undefined; +export function findEnvKeys(provider: string, env?: ProviderEnv): string[] | undefined { const envVars = getApiKeyEnvVars(provider); if (!envVars) return undefined; - const found = envVars.filter((envVar) => !!process.env[envVar] || !!getProcEnv(envVar)); + const found = envVars.filter((envVar) => !!getProviderEnvValue(envVar, env)); return found.length > 0 ? found : undefined; } @@ -158,25 +131,22 @@ export function findEnvKeys(provider: string): string[] | undefined { * * Will not return API keys for providers that require OAuth tokens. */ -export function getEnvApiKey(provider: KnownProvider): string | undefined; -export function getEnvApiKey(provider: string): string | undefined; -export function getEnvApiKey(provider: string): string | undefined { - const envKeys = findEnvKeys(provider); +export function getEnvApiKey(provider: KnownProvider, env?: ProviderEnv): string | undefined; +export function getEnvApiKey(provider: string, env?: ProviderEnv): string | undefined; +export function getEnvApiKey(provider: string, env?: ProviderEnv): string | undefined { + const envKeys = findEnvKeys(provider, env); if (envKeys?.[0]) { - return process.env[envKeys[0]] || getProcEnv(envKeys[0]); + return getProviderEnvValue(envKeys[0], env); } // Vertex AI supports either an explicit API key or Application Default Credentials. // Auth is configured via `gcloud auth application-default login`. if (provider === "google-vertex") { - const hasCredentials = hasVertexAdcCredentials(); + const hasCredentials = hasVertexAdcCredentials(env); const hasProject = !!( - process.env.GOOGLE_CLOUD_PROJECT || - process.env.GCLOUD_PROJECT || - getProcEnv("GOOGLE_CLOUD_PROJECT") || - getProcEnv("GCLOUD_PROJECT") + getProviderEnvValue("GOOGLE_CLOUD_PROJECT", env) || getProviderEnvValue("GCLOUD_PROJECT", env) ); - const hasLocation = !!(process.env.GOOGLE_CLOUD_LOCATION || getProcEnv("GOOGLE_CLOUD_LOCATION")); + const hasLocation = !!getProviderEnvValue("GOOGLE_CLOUD_LOCATION", env); if (hasCredentials && hasProject && hasLocation) { return ""; @@ -192,18 +162,12 @@ export function getEnvApiKey(provider: string): string | undefined { // 5. AWS_CONTAINER_CREDENTIALS_FULL_URI - ECS task roles (full URI) // 6. AWS_WEB_IDENTITY_TOKEN_FILE - IRSA (IAM Roles for Service Accounts) if ( - process.env.AWS_PROFILE || - (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) || - process.env.AWS_BEARER_TOKEN_BEDROCK || - process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || - process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI || - process.env.AWS_WEB_IDENTITY_TOKEN_FILE || - getProcEnv("AWS_PROFILE") || - (getProcEnv("AWS_ACCESS_KEY_ID") && getProcEnv("AWS_SECRET_ACCESS_KEY")) || - getProcEnv("AWS_BEARER_TOKEN_BEDROCK") || - getProcEnv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") || - getProcEnv("AWS_CONTAINER_CREDENTIALS_FULL_URI") || - getProcEnv("AWS_WEB_IDENTITY_TOKEN_FILE") + getProviderEnvValue("AWS_PROFILE", env) || + (getProviderEnvValue("AWS_ACCESS_KEY_ID", env) && getProviderEnvValue("AWS_SECRET_ACCESS_KEY", env)) || + getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", env) || + getProviderEnvValue("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", env) || + getProviderEnvValue("AWS_CONTAINER_CREDENTIALS_FULL_URI", env) || + getProviderEnvValue("AWS_WEB_IDENTITY_TOKEN_FILE", env) ) { return ""; } diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index a4ace1c2..5d327f4b 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -1,3 +1,4 @@ +import type { Agent as HttpsAgent } from "node:https"; import { BedrockRuntimeClient, type BedrockRuntimeClientConfig, @@ -23,6 +24,8 @@ import { } from "@aws-sdk/client-bedrock-runtime"; import { NodeHttpHandler } from "@smithy/node-http-handler"; import type { BuildMiddleware, DocumentType, MetadataBearer } from "@smithy/types"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; import { calculateCost } from "../models.ts"; import type { Api, @@ -31,6 +34,7 @@ import type { Context, ImageContent, Model, + ProviderEnv, SimpleStreamOptions, StopReason, StreamFunction, @@ -45,7 +49,8 @@ import type { } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { parseStreamingJson } from "../utils/json-parse.ts"; -import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.ts"; +import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts"; import { transformMessages } from "./transform-messages.ts"; @@ -119,18 +124,18 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt const blocks = output.content as Block[]; const config: BedrockRuntimeClientConfig = { - profile: options.profile, + profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env), }; const configuredRegion = getConfiguredBedrockRegion(options); - const hasConfiguredProfile = hasConfiguredBedrockProfile(); + const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE")); const endpointRegion = getStandardBedrockEndpointRegion(model.baseUrl); const useExplicitEndpoint = shouldUseExplicitBedrockEndpoint( model.baseUrl, configuredRegion, - hasConfiguredProfile, + hasAmbientConfiguredProfile, ); - // Only pin standard AWS Bedrock runtime endpoints when no region/profile is configured. + // Only pin standard AWS Bedrock runtime endpoints when no region or ambient AWS_PROFILE is configured. // This preserves custom endpoints (VPC/proxy) from #3402 without forcing built-in // catalog defaults such as us-east-1 to override AWS_REGION/AWS_PROFILE. if (useExplicitEndpoint) { @@ -138,8 +143,10 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt } // Resolve bearer token for Bedrock API key auth. - const bearerToken = options.bearerToken || process.env.AWS_BEARER_TOKEN_BEDROCK || undefined; - const useBearerToken = bearerToken !== undefined && process.env.AWS_BEDROCK_SKIP_AUTH !== "1"; + const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1"; + const bearerToken = + options.bearerToken || getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) || undefined; + const useBearerToken = bearerToken !== undefined && !skipAuth; // in Node.js/Bun environment only if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { @@ -153,25 +160,33 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt config.region = configuredRegion; } else if (endpointRegion && useExplicitEndpoint) { config.region = endpointRegion; - } else if (!hasConfiguredProfile) { + } else if (!hasAmbientConfiguredProfile) { config.region = "us-east-1"; } // Support proxies that don't need authentication - if (process.env.AWS_BEDROCK_SKIP_AUTH === "1") { + if (skipAuth) { config.credentials = { accessKeyId: "dummy-access-key", secretAccessKey: "dummy-secret-key", }; } - const proxyAgents = createHttpProxyAgentsForTarget(model.baseUrl); - if (proxyAgents) { + const credentials = getConfiguredBedrockCredentials(options.env); + if (!skipAuth && credentials) { + config.credentials = credentials; + } + + const proxyUrl = resolveHttpProxyUrlForTarget(model.baseUrl, options.env); + if (proxyUrl) { // Bedrock runtime uses NodeHttp2Handler by default since v3.798.0, which is based // on `http2` module and has no support for http agent. // Use NodeHttpHandler to support HTTP(S) proxy agents. - config.requestHandler = new NodeHttpHandler(proxyAgents); - } else if (process.env.AWS_BEDROCK_FORCE_HTTP1 === "1") { + config.requestHandler = new NodeHttpHandler({ + httpAgent: new HttpProxyAgent(proxyUrl), + httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, + }); + } else if (getProviderEnvValue("AWS_BEDROCK_FORCE_HTTP1", options.env) === "1") { // Some custom endpoints require HTTP/1.1 instead of HTTP/2 config.requestHandler = new NodeHttpHandler(); } @@ -192,12 +207,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt if (options.headers && Object.keys(options.headers).length > 0) { addCustomHeadersMiddleware(client, options.headers); } - const cacheRetention = resolveCacheRetention(options.cacheRetention); + const cacheRetention = resolveCacheRetention(options.cacheRetention, options.env); const inferenceMaxTokens = options.maxTokens ?? (isAnthropicClaudeModel(model) ? model.maxTokens : undefined); let commandInput = { modelId: model.id, - messages: convertMessages(context, model, cacheRetention), - system: buildSystemPrompt(context.systemPrompt, model, cacheRetention), + messages: convertMessages(context, model, cacheRetention, options.env), + system: buildSystemPrompt(context.systemPrompt, model, cacheRetention, options.env), inferenceConfig: { ...(inferenceMaxTokens !== undefined && { maxTokens: inferenceMaxTokens }), ...(options.temperature !== undefined && { temperature: options.temperature }), @@ -578,11 +593,11 @@ function mapThinkingLevelToEffort( * Resolve cache retention preference. * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. */ -function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention { +function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { if (cacheRetention) { return cacheRetention; } - if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") { + if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { return "long"; } return "short"; @@ -617,14 +632,14 @@ function isAnthropicClaudeModel(model: Model<"bedrock-converse-stream">): boolea * As a last resort, set AWS_BEDROCK_FORCE_CACHE=1 to enable cache points. * Amazon Nova models have automatic caching and don't need explicit cache points. */ -function supportsPromptCaching(model: Model<"bedrock-converse-stream">): boolean { +function supportsPromptCaching(model: Model<"bedrock-converse-stream">, env?: ProviderEnv): boolean { const candidates = getModelMatchCandidates(model.id, model.name); const hasClaudeRef = candidates.some((s) => s.includes("claude")); if (!hasClaudeRef) { // Application inference profiles don't contain the model name in the ARN. // Allow users to force cache points via environment variable. - if (typeof process !== "undefined" && process.env.AWS_BEDROCK_FORCE_CACHE === "1") return true; + if (getProviderEnvValue("AWS_BEDROCK_FORCE_CACHE", env) === "1") return true; return false; } // Claude 4.x models (opus-4, sonnet-4, haiku-4) @@ -652,13 +667,14 @@ function buildSystemPrompt( systemPrompt: string | undefined, model: Model<"bedrock-converse-stream">, cacheRetention: CacheRetention, + env?: ProviderEnv, ): SystemContentBlock[] | undefined { if (!systemPrompt) return undefined; const blocks: SystemContentBlock[] = [{ text: sanitizeSurrogates(systemPrompt) }]; // Add cache point for supported Claude models when caching is enabled - if (cacheRetention !== "none" && supportsPromptCaching(model)) { + if (cacheRetention !== "none" && supportsPromptCaching(model, env)) { blocks.push({ cachePoint: { type: CachePointType.DEFAULT, ...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}) }, }); @@ -699,6 +715,7 @@ function convertMessages( context: Context, model: Model<"bedrock-converse-stream">, cacheRetention: CacheRetention, + env?: ProviderEnv, ): Message[] { const result: Message[] = []; const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId); @@ -844,7 +861,7 @@ function convertMessages( } // Add cache point to the last user message for supported Claude models when caching is enabled - if (cacheRetention !== "none" && supportsPromptCaching(model) && result.length > 0) { + if (cacheRetention !== "none" && supportsPromptCaching(model, env) && result.length > 0) { const lastMessage = result[result.length - 1]; if (lastMessage.role === ConversationRole.USER && lastMessage.content) { (lastMessage.content as ContentBlock[]).push({ @@ -906,19 +923,26 @@ function mapStopReason(reason: string | undefined): StopReason { } function getConfiguredBedrockRegion(options: BedrockOptions): string | undefined { - if (typeof process === "undefined") { - return options.region; - } - - return options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || undefined; + return ( + options.region || + getProviderEnvValue("AWS_REGION", options.env) || + getProviderEnvValue("AWS_DEFAULT_REGION", options.env) || + undefined + ); } -function hasConfiguredBedrockProfile(): boolean { - if (typeof process === "undefined") { - return false; +function getConfiguredBedrockCredentials(env?: ProviderEnv): BedrockRuntimeClientConfig["credentials"] | undefined { + const accessKeyId = getProviderEnvValue("AWS_ACCESS_KEY_ID", env); + const secretAccessKey = getProviderEnvValue("AWS_SECRET_ACCESS_KEY", env); + if (!accessKeyId || !secretAccessKey) { + return undefined; } - - return Boolean(process.env.AWS_PROFILE); + const sessionToken = getProviderEnvValue("AWS_SESSION_TOKEN", env); + return { + accessKeyId, + secretAccessKey, + ...(sessionToken ? { sessionToken } : {}), + }; } function getStandardBedrockEndpointRegion(baseUrl: string | undefined): string | undefined { @@ -938,14 +962,14 @@ function getStandardBedrockEndpointRegion(baseUrl: string | undefined): string | function shouldUseExplicitBedrockEndpoint( baseUrl: string, configuredRegion: string | undefined, - hasConfiguredProfile: boolean, + hasAmbientConfiguredProfile: boolean, ): boolean { const endpointRegion = getStandardBedrockEndpointRegion(baseUrl); if (!endpointRegion) { return true; } - return !configuredRegion && !hasConfiguredProfile; + return !configuredRegion && !hasAmbientConfiguredProfile; } function isGovCloudBedrockTarget(model: Model<"bedrock-converse-stream">, options: BedrockOptions): boolean { diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 9a64cbd8..6ffab7cb 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -17,6 +17,7 @@ import type { ImageContent, Message, Model, + ProviderEnv, SimpleStreamOptions, StopReason, StreamFunction, @@ -30,6 +31,7 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; import { resolveCloudflareBaseUrl } from "./cloudflare.ts"; @@ -41,11 +43,11 @@ import { transformMessages } from "./transform-messages.ts"; * Resolve cache retention preference. * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. */ -function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention { +function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { if (cacheRetention) { return cacheRetention; } - if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") { + if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { return "long"; } return "short"; @@ -54,8 +56,9 @@ function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention function getCacheControl( model: Model<"anthropic-messages">, cacheRetention?: CacheRetention, + env?: ProviderEnv, ): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } { - const retention = resolveCacheRetention(cacheRetention); + const retention = resolveCacheRetention(cacheRetention, env); if (retention === "none") { return { retention }; } @@ -494,7 +497,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti }); } - const cacheRetention = options?.cacheRetention ?? resolveCacheRetention(); + const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; const created = createClient( @@ -505,6 +508,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti options?.headers, copilotDynamicHeaders, cacheSessionId, + options?.env, ); client = created.client; isOAuth = created.isOAuthToken; @@ -794,6 +798,7 @@ function createClient( optionsHeaders?: Record, dynamicHeaders?: Record, sessionId?: string, + env?: ProviderEnv, ): { client: Anthropic; isOAuthToken: boolean } { // Adaptive thinking models have interleaved thinking built in, so skip the beta header. const needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true; @@ -809,7 +814,7 @@ function createClient( const client = new Anthropic({ apiKey: null, authToken: null, - baseURL: resolveCloudflareBaseUrl(model), + baseURL: resolveCloudflareBaseUrl(model, env), dangerouslyAllowBrowser: true, defaultHeaders: mergeHeaders( { @@ -902,7 +907,7 @@ function buildParams( isOAuthToken: boolean, options?: AnthropicOptions, ): MessageCreateParamsStreaming { - const { cacheControl } = getCacheControl(model, options?.cacheRetention); + const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env); const compat = getAnthropicCompat(model); const params: MessageCreateParamsStreaming = { model: model.id, diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index ecb4c7e6..db1d3fb7 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -12,6 +12,7 @@ import type { } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; import { buildBaseOptions } from "./simple-options.ts"; @@ -36,7 +37,9 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?: if (options?.azureDeploymentName) { return options.azureDeploymentName; } - const mappedDeployment = parseDeploymentNameMap(process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).get(model.id); + const mappedDeployment = parseDeploymentNameMap( + getProviderEnvValue("AZURE_OPENAI_DEPLOYMENT_NAME_MAP", options?.env), + ).get(model.id); return mappedDeployment || model.id; } @@ -198,10 +201,14 @@ function resolveAzureConfig( model: Model<"azure-openai-responses">, options?: AzureOpenAIResponsesOptions, ): { baseUrl: string; apiVersion: string } { - const apiVersion = options?.azureApiVersion || process.env.AZURE_OPENAI_API_VERSION || DEFAULT_AZURE_API_VERSION; + const apiVersion = + options?.azureApiVersion || + getProviderEnvValue("AZURE_OPENAI_API_VERSION", options?.env) || + DEFAULT_AZURE_API_VERSION; - const baseUrl = options?.azureBaseUrl?.trim() || process.env.AZURE_OPENAI_BASE_URL?.trim() || undefined; - const resourceName = options?.azureResourceName || process.env.AZURE_OPENAI_RESOURCE_NAME; + const baseUrl = + options?.azureBaseUrl?.trim() || getProviderEnvValue("AZURE_OPENAI_BASE_URL", options?.env)?.trim() || undefined; + const resourceName = options?.azureResourceName || getProviderEnvValue("AZURE_OPENAI_RESOURCE_NAME", options?.env); let resolvedBaseUrl = baseUrl; diff --git a/packages/ai/src/providers/cloudflare.ts b/packages/ai/src/providers/cloudflare.ts index cd0a8159..98546419 100644 --- a/packages/ai/src/providers/cloudflare.ts +++ b/packages/ai/src/providers/cloudflare.ts @@ -1,4 +1,5 @@ -import type { Api, Model } from "../types.ts"; +import type { Api, Model, ProviderEnv } from "../types.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; /** Workers AI direct endpoint. */ export const CLOUDFLARE_WORKERS_AI_BASE_URL = @@ -20,12 +21,12 @@ export function isCloudflareProvider(provider: string): boolean { return provider === "cloudflare-workers-ai" || provider === "cloudflare-ai-gateway"; } -/** Substitute `{VAR}` placeholders in a Cloudflare baseUrl from process.env. */ -export function resolveCloudflareBaseUrl(model: Model): string { +/** Substitute `{VAR}` placeholders in a Cloudflare baseUrl from provider env or process.env. */ +export function resolveCloudflareBaseUrl(model: Model, env?: ProviderEnv): string { const url = model.baseUrl; if (!url.includes("{")) return url; const baseUrl = url.replace(/\{([A-Z_][A-Z0-9_]*)\}/g, (_match, name: string) => { - const value = process.env[name]; + const value = getProviderEnvValue(name, env); if (!value) { throw new Error(`${name} is required for provider ${model.provider} but is not set.`); } diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index de476c00..aa971959 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -14,6 +14,7 @@ import type { Context, Model, ThinkingLevel as PiThinkingLevel, + ProviderEnv, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -23,6 +24,7 @@ import type { ToolCall, } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; import type { GoogleThinkingLevel } from "./google-shared.ts"; import { @@ -91,7 +93,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOpt // Create the client using either a Vertex API key, if provided, or ADC with project and location const client = apiKey ? createClientWithApiKey(model, apiKey, options?.headers) - : createClient(model, resolveProject(options), resolveLocation(options), options?.headers); + : createClient(model, resolveProject(options), resolveLocation(options), options?.headers, options?.env); let params = buildParams(model, context, options); const nextParams = await options?.onPayload?.(params, model); if (nextParams !== undefined) { @@ -333,12 +335,15 @@ function createClient( project: string, location: string, optionsHeaders?: Record, + env?: ProviderEnv, ): GoogleGenAI { + const googleAuthOptions = buildGoogleAuthOptions(env); return new GoogleGenAI({ vertexai: true, project, location, apiVersion: API_VERSION, + ...(googleAuthOptions ? { googleAuthOptions } : {}), httpOptions: buildHttpOptions(model, optionsHeaders), }); } @@ -394,6 +399,11 @@ function baseUrlIncludesApiVersion(baseUrl: string): boolean { } } +function buildGoogleAuthOptions(env?: ProviderEnv): { keyFilename: string } | undefined { + const keyFilename = getProviderEnvValue("GOOGLE_APPLICATION_CREDENTIALS", env); + return keyFilename ? { keyFilename } : undefined; +} + function resolveApiKey(options?: GoogleVertexOptions): string | undefined { const apiKey = options?.apiKey?.trim(); if (!apiKey || apiKey === GCP_VERTEX_CREDENTIALS_MARKER || isPlaceholderApiKey(apiKey)) { @@ -407,7 +417,10 @@ function isPlaceholderApiKey(apiKey: string): boolean { } function resolveProject(options?: GoogleVertexOptions): string { - const project = options?.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT; + const project = + options?.project || + getProviderEnvValue("GOOGLE_CLOUD_PROJECT", options?.env) || + getProviderEnvValue("GCLOUD_PROJECT", options?.env); if (!project) { throw new Error( "Vertex AI requires a project ID. Set GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT or pass project in options.", @@ -417,7 +430,7 @@ function resolveProject(options?: GoogleVertexOptions): string { } function resolveLocation(options?: GoogleVertexOptions): string { - const location = options?.location || process.env.GOOGLE_CLOUD_LOCATION; + const location = options?.location || getProviderEnvValue("GOOGLE_CLOUD_LOCATION", options?.env); if (!location) { throw new Error("Vertex AI requires a location. Set GOOGLE_CLOUD_LOCATION or pass location in options."); } diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 7fe9e256..19a2f5d7 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -27,6 +27,7 @@ import type { AssistantMessage, Context, Model, + ProviderEnv, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -40,6 +41,7 @@ import { } from "../utils/diagnostics.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; +import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts"; import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts"; import { buildBaseOptions } from "./simple-options.ts"; @@ -814,19 +816,13 @@ type WebSocketConstructor = new ( ) => WebSocketLike; let _cachedWebsocket: WebSocketConstructor | null = null; -async function getWebSocketConstructor(): Promise { - if (_cachedWebsocket) return _cachedWebsocket; +async function getWebSocketConstructor(env?: ProviderEnv): Promise { + if (!env && _cachedWebsocket) return _cachedWebsocket; // bun doesn't respect http proxy envs, ref: https://github.com/oven-sh/bun/issues/15489 // TODO: remove this when bun supports proxy envs in websocket. - if ( - process?.versions?.bun && - (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy) - ) { - const m = await dynamicImport("proxy-from-env"); - const getProxyForUrl = (m as { getProxyForUrl: (url: string | object | URL) => string }).getProxyForUrl; - - _cachedWebsocket = class extends WebSocket { + if (typeof process !== "undefined" && process.versions?.bun) { + const WebSocketWithProxy = class extends WebSocket { constructor(url: string | URL, options?: string | string[] | Record) { let _opts: Record = {}; if (Array.isArray(options) || typeof options === "string") { @@ -835,11 +831,17 @@ async function getWebSocketConstructor(): Promise { _opts = { ...options }; } - const proxy = getProxyForUrl(url.toString().replace(/^wss:/, "https:").replace(/^ws:/, "http:")); - super(url, { ..._opts, ...(proxy ? { proxy } : {}) } as any); + const proxyUrl = resolveHttpProxyUrlForTarget( + url.toString().replace(/^wss:/, "https:").replace(/^ws:/, "http:"), + env, + ); + super(url, { ..._opts, ...(proxyUrl ? { proxy: proxyUrl.toString() } : {}) } as any); } }; - return _cachedWebsocket; + if (!env) { + _cachedWebsocket = WebSocketWithProxy; + } + return WebSocketWithProxy; } const ctor = (globalThis as { WebSocket?: unknown }).WebSocket; @@ -894,8 +896,9 @@ async function connectWebSocket( headers: Headers, signal?: AbortSignal, connectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS, + env?: ProviderEnv, ): Promise { - const WebSocketCtor = await getWebSocketConstructor(); + const WebSocketCtor = await getWebSocketConstructor(env); if (!WebSocketCtor) { throw new Error("WebSocket transport is not available in this runtime"); } @@ -972,6 +975,7 @@ async function acquireWebSocket( sessionId: string | undefined, signal?: AbortSignal, connectTimeoutMs?: number, + env?: ProviderEnv, ): Promise<{ socket: WebSocketLike; entry?: CachedWebSocketConnection; @@ -979,7 +983,7 @@ async function acquireWebSocket( release: (options?: { keep?: boolean }) => void; }> { if (!sessionId) { - const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs); + const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env); return { socket, reused: false, @@ -1011,7 +1015,7 @@ async function acquireWebSocket( }; } if (cached.busy) { - const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs); + const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env); return { socket, reused: false, @@ -1026,7 +1030,7 @@ async function acquireWebSocket( } } - const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs); + const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs, env); const entry: CachedWebSocketConnection = { socket, busy: true }; websocketSessionCache.set(sessionId, entry); return { @@ -1312,6 +1316,7 @@ async function processWebSocketStream( options?.sessionId, options?.signal, websocketConnectTimeoutMs, + options?.env, ); let keepConnection = true; const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto"; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index c2d5cff9..61b03631 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -19,6 +19,7 @@ import type { Message, Model, OpenAICompletionsCompat, + ProviderEnv, SimpleStreamOptions, StopReason, StreamFunction, @@ -32,6 +33,7 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; import { parseStreamingJson } from "../utils/json-parse.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts"; import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; @@ -98,11 +100,11 @@ type ChatCompletionToolWithCacheControl = OpenAI.Chat.Completions.ChatCompletion cache_control?: OpenAICompatCacheControl; }; -function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention { +function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { if (cacheRetention) { return cacheRetention; } - if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") { + if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { return "long"; } return "short"; @@ -140,9 +142,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA throw new Error(`No API key for provider: ${model.provider}`); } const compat = getCompat(model); - const cacheRetention = resolveCacheRetention(options?.cacheRetention); + const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; - const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat); + const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat, options?.env); let params = buildParams(model, context, options, compat, cacheRetention); const nextParams = await options?.onPayload?.(params, model); if (nextParams !== undefined) { @@ -454,6 +456,7 @@ function createClient( optionsHeaders?: Record, sessionId?: string, compat: ResolvedOpenAICompletionsCompat = getCompat(model), + env?: ProviderEnv, ) { const headers = { ...model.headers }; if (model.provider === "github-copilot") { @@ -487,7 +490,7 @@ function createClient( return new OpenAI({ apiKey, - baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl, + baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders, }); @@ -498,7 +501,7 @@ function buildParams( context: Context, options?: OpenAICompletionsOptions, compat: ResolvedOpenAICompletionsCompat = getCompat(model), - cacheRetention: CacheRetention = resolveCacheRetention(options?.cacheRetention), + cacheRetention: CacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env), ) { const messages = convertMessages(model, context, compat); const cacheControl = getCompatCacheControl(compat, cacheRetention); diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 9afeff29..014233d5 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -8,6 +8,7 @@ import type { Context, Model, OpenAIResponsesCompat, + ProviderEnv, SimpleStreamOptions, StreamFunction, StreamOptions, @@ -15,6 +16,7 @@ import type { } from "../types.ts"; import { AssistantMessageEventStream } from "../utils/event-stream.ts"; import { headersToRecord } from "../utils/headers.ts"; +import { getProviderEnvValue } from "../utils/provider-env.ts"; import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts"; import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts"; @@ -27,11 +29,11 @@ const OPENAI_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode" * Resolve cache retention preference. * Defaults to "short" and uses PI_CACHE_RETENTION for backward compatibility. */ -function resolveCacheRetention(cacheRetention?: CacheRetention): CacheRetention { +function resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention { if (cacheRetention) { return cacheRetention; } - if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") { + if (getProviderEnvValue("PI_CACHE_RETENTION", env) === "long") { return "long"; } return "short"; @@ -111,9 +113,9 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } - const cacheRetention = resolveCacheRetention(options?.cacheRetention); + const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; - const client = createClient(model, context, apiKey, options?.headers, cacheSessionId); + const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, options?.env); let params = buildParams(model, context, options); const nextParams = await options?.onPayload?.(params, model); if (nextParams !== undefined) { @@ -185,6 +187,7 @@ function createClient( apiKey: string, optionsHeaders?: Record, sessionId?: string, + env?: ProviderEnv, ) { const compat = getCompat(model); const headers = { ...model.headers }; @@ -220,7 +223,7 @@ function createClient( return new OpenAI({ apiKey, - baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl, + baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model, env) : model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders, }); @@ -229,7 +232,7 @@ function createClient( function buildParams(model: Model<"openai-responses">, context: Context, options?: OpenAIResponsesOptions) { const messages = convertResponsesMessages(model, context, OPENAI_TOOL_CALL_PROVIDERS); - const cacheRetention = resolveCacheRetention(options?.cacheRetention); + const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env); const compat = getCompat(model); const params: ResponseCreateParamsStreaming = { model: model.id, diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index f1709062..773e8d95 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -17,6 +17,7 @@ export function buildBaseOptions(_model: Model, options?: SimpleStreamOptio maxRetries: options?.maxRetries, maxRetryDelayMs: options?.maxRetryDelayMs, metadata: options?.metadata, + env: options?.env, }; } diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 3ae631a1..3f333d9e 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -24,7 +24,7 @@ function withEnvApiKey( options: TOptions | undefined, ): TOptions | undefined { if (hasExplicitApiKey(options?.apiKey)) return options; - const apiKey = getEnvApiKey(model.provider); + const apiKey = getEnvApiKey(model.provider, options?.env); if (!apiKey) return options; return { ...options, apiKey } as TOptions; } diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index b7d0fda0..cedac6e8 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -79,6 +79,9 @@ export type CacheRetention = "none" | "short" | "long"; export type Transport = "sse" | "websocket" | "websocket-cached" | "auto"; +/** Provider-scoped environment overrides. Values take precedence over process.env. */ +export type ProviderEnv = Record; + export interface ProviderResponse { status: number; headers: Record; @@ -153,6 +156,12 @@ export interface StreamOptions { * For example, Anthropic uses `user_id` for abuse tracking and rate limiting. */ metadata?: Record; + /** + * Provider-scoped environment values. These take precedence over process.env for + * provider configuration such as regional settings, endpoint placeholders, and + * proxy variables. + */ + env?: ProviderEnv; } export type ProviderStreamOptions = StreamOptions & Record; diff --git a/packages/ai/src/utils/node-http-proxy.ts b/packages/ai/src/utils/node-http-proxy.ts index 7b5f4750..81ff0089 100644 --- a/packages/ai/src/utils/node-http-proxy.ts +++ b/packages/ai/src/utils/node-http-proxy.ts @@ -1,7 +1,5 @@ -import type { Agent as HttpAgent } from "node:http"; -import type { Agent as HttpsAgent } from "node:https"; -import { HttpProxyAgent } from "http-proxy-agent"; -import { HttpsProxyAgent } from "https-proxy-agent"; +import type { ProviderEnv } from "../types.ts"; +import { getProviderEnvValue } from "./provider-env.ts"; const DEFAULT_PROXY_PORTS: Record = { ftp: 21, @@ -12,16 +10,16 @@ const DEFAULT_PROXY_PORTS: Record = { wss: 443, }; -export interface NodeHttpProxyAgents { - httpAgent: HttpAgent; - httpsAgent: HttpsAgent; -} - -export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = - "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; - -function getProxyEnv(key: string): string { - return process.env[key.toLowerCase()] || process.env[key.toUpperCase()] || ""; +function getProxyEnv(key: string, env?: ProviderEnv): string { + const lowercaseKey = key.toLowerCase(); + const uppercaseKey = key.toUpperCase(); + return ( + env?.[lowercaseKey] || + env?.[uppercaseKey] || + getProviderEnvValue(lowercaseKey) || + getProviderEnvValue(uppercaseKey) || + "" + ); } function parseProxyTargetUrl(targetUrl: string | URL): URL | undefined { @@ -36,8 +34,8 @@ function parseProxyTargetUrl(targetUrl: string | URL): URL | undefined { } } -function shouldProxyHostname(hostname: string, port: number): boolean { - const noProxy = getProxyEnv("no_proxy").toLowerCase(); +function shouldProxyHostname(hostname: string, port: number, env?: ProviderEnv): boolean { + const noProxy = getProxyEnv("no_proxy", env).toLowerCase(); if (!noProxy) { return true; } @@ -68,7 +66,7 @@ function shouldProxyHostname(hostname: string, port: number): boolean { }); } -function getProxyForUrl(targetUrl: string | URL): string { +function getProxyForUrl(targetUrl: string | URL, env?: ProviderEnv): string { const parsedUrl = parseProxyTargetUrl(targetUrl); if (!parsedUrl?.protocol || !parsedUrl.host) { return ""; @@ -77,19 +75,22 @@ function getProxyForUrl(targetUrl: string | URL): string { const protocol = parsedUrl.protocol.split(":", 1)[0]!; const hostname = parsedUrl.host.replace(/:\d*$/, ""); const port = Number.parseInt(parsedUrl.port, 10) || DEFAULT_PROXY_PORTS[protocol] || 0; - if (!shouldProxyHostname(hostname, port)) { + if (!shouldProxyHostname(hostname, port, env)) { return ""; } - let proxy = getProxyEnv(`${protocol}_proxy`) || getProxyEnv("all_proxy"); + let proxy = getProxyEnv(`${protocol}_proxy`, env) || getProxyEnv("all_proxy", env); if (proxy && !proxy.includes("://")) { proxy = `${protocol}://${proxy}`; } return proxy; } -export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | undefined { - const proxy = getProxyForUrl(targetUrl); +export const UNSUPPORTED_PROXY_PROTOCOL_MESSAGE = + "Unsupported proxy protocol. SOCKS and PAC proxy URLs are not supported; use an HTTP or HTTPS proxy URL."; + +export function resolveHttpProxyUrlForTarget(targetUrl: string | URL, env?: ProviderEnv): URL | undefined { + const proxy = getProxyForUrl(targetUrl, env); if (!proxy) { return undefined; } @@ -109,15 +110,3 @@ export function resolveHttpProxyUrlForTarget(targetUrl: string | URL): URL | und return proxyUrl; } - -export function createHttpProxyAgentsForTarget(targetUrl: string | URL): NodeHttpProxyAgents | undefined { - const proxyUrl = resolveHttpProxyUrlForTarget(targetUrl); - if (!proxyUrl) { - return undefined; - } - - return { - httpAgent: new HttpProxyAgent(proxyUrl), - httpsAgent: new HttpsProxyAgent(proxyUrl) as unknown as HttpsAgent, - }; -} diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts index feeb34fa..1b3e4244 100644 --- a/packages/ai/src/utils/oauth/anthropic.ts +++ b/packages/ai/src/utils/oauth/anthropic.ts @@ -6,6 +6,7 @@ */ import type { Server } from "node:http"; +import { getProviderEnvValue } from "../provider-env.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; import { generatePKCE } from "./pkce.ts"; import type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from "./types.ts"; @@ -28,7 +29,7 @@ const decode = (s: string) => atob(s); const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"); const AUTHORIZE_URL = "https://claude.ai/oauth/authorize"; const TOKEN_URL = "https://platform.claude.com/v1/oauth/token"; -const CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1"; +const CALLBACK_HOST = getProviderEnvValue("PI_OAUTH_CALLBACK_HOST") || "127.0.0.1"; const CALLBACK_PORT = 53692; const CALLBACK_PATH = "/callback"; const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`; diff --git a/packages/ai/src/utils/oauth/openai-codex.ts b/packages/ai/src/utils/oauth/openai-codex.ts index 2f769a84..a5103c39 100644 --- a/packages/ai/src/utils/oauth/openai-codex.ts +++ b/packages/ai/src/utils/oauth/openai-codex.ts @@ -17,6 +17,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } +import { getProviderEnvValue } from "../provider-env.ts"; import { pollOAuthDeviceCodeFlow } from "./device-code.ts"; import { oauthErrorHtml, oauthSuccessHtml } from "./oauth-page.ts"; import { generatePKCE } from "./pkce.ts"; @@ -47,7 +48,7 @@ type OAuthToken = { access: string; refresh: string; expires: number }; type TokenOperation = "exchange" | "refresh"; function getCallbackHost(): string { - return typeof process !== "undefined" ? process.env.PI_OAUTH_CALLBACK_HOST || "127.0.0.1" : "127.0.0.1"; + return getProviderEnvValue("PI_OAUTH_CALLBACK_HOST") || "127.0.0.1"; } type DeviceAuthInfo = { diff --git a/packages/ai/src/utils/provider-env.ts b/packages/ai/src/utils/provider-env.ts new file mode 100644 index 00000000..db496067 --- /dev/null +++ b/packages/ai/src/utils/provider-env.ts @@ -0,0 +1,52 @@ +import type { ProviderEnv } from "../types.ts"; + +let procEnvCache: Map | null = null; + +/** + * Fallback for https://github.com/oven-sh/bun/issues/27802. + * Bun compiled binaries can expose an empty process.env inside Linux sandboxes + * even though /proc/self/environ contains the environment. + * + * This intentionally duplicates restoreSandboxEnv() in + * packages/coding-agent/src/bun/restore-sandbox-env.ts. The ai package can be + * used directly, without going through that entrypoint, so provider env lookup + * must not depend on process.env having been patched. + */ +function getBunSandboxEnvValue(name: string): string | undefined { + if (typeof process === "undefined" || !process.versions?.bun || Object.keys(process.env).length > 0) { + return undefined; + } + + if (procEnvCache === null) { + procEnvCache = new Map(); + try { + const { readFileSync } = require("node:fs") as { + readFileSync(path: string, encoding: BufferEncoding): string; + }; + const data = readFileSync("/proc/self/environ", "utf-8"); + for (const entry of data.split("\0")) { + const idx = entry.indexOf("="); + if (idx > 0) { + procEnvCache.set(entry.slice(0, idx), entry.slice(idx + 1)); + } + } + } catch { + // /proc/self/environ may not exist or may not be readable. + } + } + + return procEnvCache.get(name); +} + +/** + * Resolve a provider env value from scoped overrides, normal process.env, then + * the duplicated Bun sandbox fallback for direct pi-ai consumers. + */ +export function getProviderEnvValue(name: string, env?: ProviderEnv): string | undefined { + return ( + env?.[name] || + (typeof process !== "undefined" ? process.env[name] : undefined) || + getBunSandboxEnvValue(name) || + undefined + ); +} diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 412c62e0..18be2476 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -45,7 +45,7 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => { }); import { getModel } from "../src/models.ts"; -import { streamBedrock } from "../src/providers/amazon-bedrock.ts"; +import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts"; import type { Context, Model } from "../src/types.ts"; const context: Context = { @@ -83,8 +83,12 @@ afterEach(() => { } }); -async function captureClientConfig(model: Model<"bedrock-converse-stream">): Promise> { - await streamBedrock(model, context, { cacheRetention: "none" }).result(); +async function captureClientConfig( + model: Model<"bedrock-converse-stream">, + options: BedrockOptions = {}, +): Promise> { + bedrockMock.constructorCalls.length = 0; + await streamBedrock(model, context, { cacheRetention: "none", ...options }).result(); expect(bedrockMock.constructorCalls).toHaveLength(1); return bedrockMock.constructorCalls[0]; } @@ -115,6 +119,29 @@ describe("bedrock endpoint resolution", () => { expect(config.region).toBe("eu-central-1"); }); + it("handles missing regions for explicit, scoped, and ambient profiles", async () => { + const model = getModel("amazon-bedrock", "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"); + + let config = await captureClientConfig(model, { profile: "bedrock-profile" }); + + expect(config.profile).toBe("bedrock-profile"); + expect(config.endpoint).toBe("https://bedrock-runtime.eu-central-1.amazonaws.com"); + expect(config.region).toBe("eu-central-1"); + + config = await captureClientConfig(model, { env: { AWS_PROFILE: "scoped-bedrock-profile" } }); + + expect(config.profile).toBe("scoped-bedrock-profile"); + expect(config.endpoint).toBe("https://bedrock-runtime.eu-central-1.amazonaws.com"); + expect(config.region).toBe("eu-central-1"); + + process.env.AWS_PROFILE = "ambient-bedrock-profile"; + config = await captureClientConfig(model); + + expect(config.profile).toBe("ambient-bedrock-profile"); + expect(config.endpoint).toBeUndefined(); + expect(config.region).toBeUndefined(); + }); + it("still passes custom Bedrock endpoints through to the SDK client", async () => { process.env.AWS_REGION = "us-west-2"; const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-opus-4-8"); diff --git a/packages/ai/test/node-http-proxy.test.ts b/packages/ai/test/node-http-proxy.test.ts index f4c9b735..a077a928 100644 --- a/packages/ai/test/node-http-proxy.test.ts +++ b/packages/ai/test/node-http-proxy.test.ts @@ -54,6 +54,17 @@ describe("node HTTP proxy resolution", () => { ); }); + it("prefers scoped proxy env aliases before process env aliases", () => { + resetProxyEnv(); + process.env.https_proxy = "http://process-proxy.example:8080"; + + expect( + resolveHttpProxyUrlForTarget("https://bedrock-runtime.us-east-1.amazonaws.com", { + HTTPS_PROXY: "http://scoped-proxy.example:8080", + })?.toString(), + ).toBe("http://scoped-proxy.example:8080/"); + }); + it("rejects SOCKS and PAC proxy URLs explicitly", () => { resetProxyEnv(); process.env.HTTPS_PROXY = "socks5://proxy.example:1080"; diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index a743351c..be233670 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -163,6 +163,31 @@ describe("openai-completions empty tools handling", () => { expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test"); }); + it("uses provider env before process.env for Cloudflare AI Gateway base URL", async () => { + process.env.CLOUDFLARE_ACCOUNT_ID = "process-account"; + process.env.CLOUDFLARE_GATEWAY_ID = "process-gateway"; + const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + env: { + CLOUDFLARE_ACCOUNT_ID: "provider-account", + CLOUDFLARE_GATEWAY_ID: "provider-gateway", + }, + }, + ).result(); + + const clientOptions = mockState.lastClientOptions as { baseURL?: string }; + expect(clientOptions.baseURL).toBe( + "https://gateway.ai.cloudflare.com/v1/provider-account/provider-gateway/compat", + ); + }); + it("preserves inline upstream Authorization for Cloudflare AI Gateway BYOK requests", async () => { process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 11cb9900..9cee0caf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `auth.json` API key `env` values so provider-specific environment overrides can be scoped to Pi and propagated to inherited provider configuration ([#5728](https://github.com/earendil-works/pi/issues/5728)). + ### Fixed - Fixed successful `pi update` on Windows to exit naturally instead of calling `process.exit(0)`, avoiding a Node.js/libuv assertion after version-check network requests ([#5805](https://github.com/earendil-works/pi/issues/5805)). diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index f139aa30..86f1de06 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -104,6 +104,24 @@ Store credentials in `~/.pi/agent/auth.json`: The file is created with `0600` permissions (user read/write only). Auth file credentials take priority over environment variables. +API key credentials can also include provider-scoped environment values. These values are used before process environment variables when resolving the credential key, provider/model headers, and provider configuration such as Cloudflare account IDs, Azure OpenAI settings, Vertex project/location, Bedrock settings, `PI_CACHE_RETENTION`, and `HTTP_PROXY`/`HTTPS_PROXY`. + +```json +{ + "cloudflare-ai-gateway": { + "type": "api_key", + "key": "$CLOUDFLARE_API_KEY", + "env": { + "CLOUDFLARE_API_KEY": "...", + "CLOUDFLARE_ACCOUNT_ID": "account-id", + "CLOUDFLARE_GATEWAY_ID": "gateway-id" + } + } +} +``` + +Use this when pi should use different provider settings than the project shell environment. + ### Key Resolution The `key` field supports command execution, environment interpolation, and literals: @@ -194,7 +212,7 @@ export AWS_BEDROCK_FORCE_HTTP1=1 ### Cloudflare AI Gateway -`CLOUDFLARE_API_KEY` can be set via `/login`. The account ID and gateway slug must be set as environment variables. +`CLOUDFLARE_API_KEY` can be set via `/login`. The account ID and gateway slug can be set as environment variables or in the API key credential's `env` object in `auth.json`. ```bash export CLOUDFLARE_API_KEY=... # or use /login @@ -218,7 +236,7 @@ For normal pi usage, prefer unified billing or stored BYOK. Inline BYOK requires ### Cloudflare Workers AI -`CLOUDFLARE_API_KEY` can be set via `/login`. `CLOUDFLARE_ACCOUNT_ID` must be set as an environment variable. +`CLOUDFLARE_API_KEY` can be set via `/login`. `CLOUDFLARE_ACCOUNT_ID` can be set as an environment variable or in the API key credential's `env` object in `auth.json`. ```bash export CLOUDFLARE_API_KEY=... # or use /login diff --git a/packages/coding-agent/src/bun/restore-sandbox-env.ts b/packages/coding-agent/src/bun/restore-sandbox-env.ts index b175a494..445ddfd6 100644 --- a/packages/coding-agent/src/bun/restore-sandbox-env.ts +++ b/packages/coding-agent/src/bun/restore-sandbox-env.ts @@ -4,6 +4,10 @@ * Bun compiled binaries have an empty `process.env` when running inside * sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover * the environment from `/proc/self/environ`. + * + * Keep this in sync with getBunSandboxEnvValue() in + * packages/ai/src/utils/provider-env.ts. The ai package duplicates the lookup + * for direct consumers that do not go through this coding-agent entrypoint. */ import { readFileSync } from "node:fs"; diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index af9d32d7..54de3103 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -357,6 +357,7 @@ export class AgentSession { private async _getRequiredRequestAuth(model: Model): Promise<{ apiKey: string; headers?: Record; + env?: Record; }> { const result = await this._modelRegistry.getApiKeyAndHeaders(model); if (!result.ok) { @@ -366,7 +367,7 @@ export class AgentSession { throw new Error(result.error); } if (result.apiKey) { - return { apiKey: result.apiKey, headers: result.headers }; + return { apiKey: result.apiKey, headers: result.headers, env: result.env }; } const isOAuth = this._modelRegistry.isUsingOAuth(model); @@ -383,13 +384,14 @@ export class AgentSession { private async _getCompactionRequestAuth(model: Model): Promise<{ apiKey?: string; headers?: Record; + env?: Record; }> { if (this.agent.streamFn === streamSimple) { return this._getRequiredRequestAuth(model); } const result = await this._modelRegistry.getApiKeyAndHeaders(model); - return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {}; + return result.ok ? { apiKey: result.apiKey, headers: result.headers, env: result.env } : {}; } /** @@ -1649,7 +1651,7 @@ export class AgentSession { throw new Error(formatNoModelSelectedMessage()); } - const { apiKey, headers } = await this._getCompactionRequestAuth(this.model); + const { apiKey, headers, env } = await this._getCompactionRequestAuth(this.model); const pathEntries = this.sessionManager.getBranch(); const settings = this.settingsManager.getCompactionSettings(); @@ -1708,6 +1710,7 @@ export class AgentSession { this._compactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, + env, ); summary = result.summary; firstKeptEntryId = result.firstKeptEntryId; @@ -1898,6 +1901,7 @@ export class AgentSession { let apiKey: string | undefined; let headers: Record | undefined; + let env: Record | undefined; if (this.agent.streamFn === streamSimple) { const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model); if (!authResult.ok || !authResult.apiKey) { @@ -1912,8 +1916,9 @@ export class AgentSession { } apiKey = authResult.apiKey; headers = authResult.headers; + env = authResult.env; } else { - ({ apiKey, headers } = await this._getCompactionRequestAuth(this.model)); + ({ apiKey, headers, env } = await this._getCompactionRequestAuth(this.model)); } const pathEntries = this.sessionManager.getBranch(); @@ -1981,6 +1986,7 @@ export class AgentSession { this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn, + env, ); summary = compactResult.summary; firstKeptEntryId = compactResult.firstKeptEntryId; @@ -2784,12 +2790,13 @@ export class AgentSession { let summaryDetails: unknown; if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) { const model = this.model!; - const { apiKey, headers } = await this._getRequiredRequestAuth(model); + const { apiKey, headers, env } = await this._getRequiredRequestAuth(model); const branchSummarySettings = this.settingsManager.getBranchSummarySettings(); const result = await generateBranchSummary(entriesToSummarize, { model, apiKey, headers, + env, signal: this._branchSummaryAbortController.signal, customInstructions, replaceInstructions, diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 9a0564ec..3394a063 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -24,6 +24,7 @@ import { resolveConfigValue } from "./resolve-config-value.ts"; export type ApiKeyCredential = { type: "api_key"; key: string; + env?: Record; }; export type OAuthCredential = { @@ -303,6 +304,14 @@ export class AuthStorage { return this.data[provider] ?? undefined; } + /** + * Get provider-scoped environment values for an API key credential. + */ + getProviderEnv(provider: string): Record | undefined { + const cred = this.data[provider]; + return cred?.type === "api_key" && cred.env ? { ...cred.env } : undefined; + } + /** * Set credential for a provider. */ @@ -471,7 +480,7 @@ export class AuthStorage { const cred = this.data[providerId]; if (cred?.type === "api_key") { - return resolveConfigValue(cred.key); + return resolveConfigValue(cred.key, cred.env); } if (cred?.type === "oauth") { diff --git a/packages/coding-agent/src/core/compaction/branch-summarization.ts b/packages/coding-agent/src/core/compaction/branch-summarization.ts index f6ce3d67..3378eb19 100644 --- a/packages/coding-agent/src/core/compaction/branch-summarization.ts +++ b/packages/coding-agent/src/core/compaction/branch-summarization.ts @@ -69,6 +69,8 @@ export interface GenerateBranchSummaryOptions { apiKey: string; /** Request headers for the model */ headers?: Record; + /** Provider-scoped environment values for the model */ + env?: Record; /** Abort signal for cancellation */ signal: AbortSignal; /** Optional custom instructions for summarization */ @@ -290,6 +292,7 @@ export async function generateBranchSummary( model, apiKey, headers, + env, signal, customInstructions, replaceInstructions, @@ -335,7 +338,7 @@ export async function generateBranchSummary( // request behavior (timeouts, retries, attribution headers) stays consistent // without running through agent state/events. const context = { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }; - const requestOptions: SimpleStreamOptions = { apiKey, headers, signal, maxTokens: 2048 }; + const requestOptions: SimpleStreamOptions = { apiKey, headers, env, signal, maxTokens: 2048 }; const response = streamFn ? await (await streamFn(model, context, requestOptions)).result() : await completeSimple(model, context, requestOptions); diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index eae19794..07f2f9fe 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -528,10 +528,11 @@ function createSummarizationOptions( maxTokens: number, apiKey: string | undefined, headers: Record | undefined, + env: Record | undefined, signal: AbortSignal | undefined, thinkingLevel: ThinkingLevel | undefined, ): SimpleStreamOptions { - const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers }; + const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers, env }; if (model.reasoning && thinkingLevel && thinkingLevel !== "off") { options.reasoning = thinkingLevel; } @@ -566,6 +567,7 @@ export async function generateSummary( previousSummary?: string, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, + env?: Record, ): Promise { const maxTokens = Math.min( Math.floor(0.8 * reserveTokens), @@ -598,7 +600,7 @@ export async function generateSummary( }, ]; - const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel); + const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel); const response = await completeSummarization( model, @@ -753,6 +755,7 @@ export async function compact( signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, + env?: Record, ): Promise { const { firstKeptEntryId, @@ -783,6 +786,7 @@ export async function compact( previousSummary, thinkingLevel, streamFn, + env, ) : Promise.resolve("No prior history."), generateTurnPrefixSummary( @@ -791,6 +795,7 @@ export async function compact( settings.reserveTokens, apiKey, headers, + env, signal, thinkingLevel, streamFn, @@ -811,6 +816,7 @@ export async function compact( previousSummary, thinkingLevel, streamFn, + env, ); } @@ -839,6 +845,7 @@ async function generateTurnPrefixSummary( reserveTokens: number, apiKey: string | undefined, headers?: Record, + env?: Record, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, @@ -861,7 +868,7 @@ async function generateTurnPrefixSummary( const response = await completeSummarization( model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, - createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), + createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel), streamFn, ); diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index ff172966..60f9001f 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -240,6 +240,7 @@ export type ResolvedRequestAuth = ok: true; apiKey?: string; headers?: Record; + env?: Record; } | { ok: false; @@ -684,17 +685,27 @@ export class ModelRegistry { async getApiKeyAndHeaders(model: Model): Promise { try { const providerConfig = this.providerRequestConfigs.get(model.provider); + const providerEnv = this.authStorage.getProviderEnv(model.provider); const apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, { includeFallback: false }); const apiKey = apiKeyFromAuthStorage ?? (providerConfig?.apiKey - ? resolveConfigValueOrThrow(providerConfig.apiKey, `API key for provider "${model.provider}"`) + ? resolveConfigValueOrThrow( + providerConfig.apiKey, + `API key for provider "${model.provider}"`, + providerEnv, + ) : undefined); - const providerHeaders = resolveHeadersOrThrow(providerConfig?.headers, `provider "${model.provider}"`); + const providerHeaders = resolveHeadersOrThrow( + providerConfig?.headers, + `provider "${model.provider}"`, + providerEnv, + ); const modelHeaders = resolveHeadersOrThrow( this.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)), `model "${model.provider}/${model.id}"`, + providerEnv, ); let headers = @@ -713,6 +724,7 @@ export class ModelRegistry { ok: true, apiKey, headers: headers && Object.keys(headers).length > 0 ? headers : undefined, + env: providerEnv && Object.keys(providerEnv).length > 0 ? providerEnv : undefined, }; } catch (error) { return { @@ -777,7 +789,9 @@ export class ModelRegistry { } const providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey; - return providerApiKey ? resolveConfigValueUncached(providerApiKey) : undefined; + return providerApiKey + ? resolveConfigValueUncached(providerApiKey, this.authStorage.getProviderEnv(provider)) + : undefined; } /** diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 6c2263f6..9d47cff3 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -85,8 +85,8 @@ function parseConfigValueReference(config: string): ConfigValueReference { return { type: "template", parts: parseConfigValueTemplate(config) }; } -function resolveEnvConfigValue(name: string): string | undefined { - return process.env[name] || undefined; +function resolveEnvConfigValue(name: string, env?: Record): string | undefined { + return env?.[name] || process.env[name] || undefined; } function getTemplateEnvVarNames(parts: TemplatePart[]): string[] { @@ -98,14 +98,14 @@ function getTemplateEnvVarNames(parts: TemplatePart[]): string[] { return names; } -function resolveTemplate(parts: TemplatePart[]): string | undefined { +function resolveTemplate(parts: TemplatePart[], env?: Record): string | undefined { let resolved = ""; for (const part of parts) { if (part.type === "literal") { resolved += part.value; continue; } - const envValue = resolveEnvConfigValue(part.name); + const envValue = resolveEnvConfigValue(part.name, env); if (envValue === undefined) return undefined; resolved += envValue; } @@ -123,16 +123,16 @@ export function getConfigValueEnvVarNames(config: string): string[] { return reference.type === "template" ? getTemplateEnvVarNames(reference.parts) : []; } -export function getMissingConfigValueEnvVarNames(config: string): string[] { - return getConfigValueEnvVarNames(config).filter((name) => resolveEnvConfigValue(name) === undefined); +export function getMissingConfigValueEnvVarNames(config: string, env?: Record): string[] { + return getConfigValueEnvVarNames(config).filter((name) => resolveEnvConfigValue(name, env) === undefined); } export function isCommandConfigValue(config: string): boolean { return parseConfigValueReference(config).type === "command"; } -export function isConfigValueConfigured(config: string): boolean { - return getMissingConfigValueEnvVarNames(config).length === 0; +export function isConfigValueConfigured(config: string, env?: Record): boolean { + return getMissingConfigValueEnvVarNames(config, env).length === 0; } /** @@ -142,12 +142,12 @@ export function isConfigValueConfigured(config: string): boolean { * - In non-command values, "$$" escapes a literal "$" and "$!" escapes a literal "!" * - Otherwise treats the value as a literal */ -export function resolveConfigValue(config: string): string | undefined { +export function resolveConfigValue(config: string, env?: Record): string | undefined { const reference = parseConfigValueReference(config); if (reference.type === "command") { return executeCommand(reference.config); } - return resolveTemplate(reference.parts); + return resolveTemplate(reference.parts, env); } function executeWithConfiguredShell(command: string): { executed: boolean; value: string | undefined } { @@ -216,16 +216,16 @@ function executeCommand(commandConfig: string): string | undefined { /** * Resolve all header values using the same resolution logic as API keys. */ -export function resolveConfigValueUncached(config: string): string | undefined { +export function resolveConfigValueUncached(config: string, env?: Record): string | undefined { const reference = parseConfigValueReference(config); if (reference.type === "command") { return executeCommandUncached(reference.config); } - return resolveTemplate(reference.parts); + return resolveTemplate(reference.parts, env); } -export function resolveConfigValueOrThrow(config: string, description: string): string { - const resolvedValue = resolveConfigValueUncached(config); +export function resolveConfigValueOrThrow(config: string, description: string, env?: Record): string { + const resolvedValue = resolveConfigValueUncached(config, env); if (resolvedValue !== undefined) { return resolvedValue; } @@ -236,7 +236,7 @@ export function resolveConfigValueOrThrow(config: string, description: string): } if (reference.type === "template") { - const missingEnvVars = getMissingConfigValueEnvVarNames(config); + const missingEnvVars = getMissingConfigValueEnvVarNames(config, env); if (missingEnvVars.length === 1) { throw new Error(`Failed to resolve ${description} from environment variable: ${missingEnvVars[0]}`); } @@ -251,11 +251,14 @@ export function resolveConfigValueOrThrow(config: string, description: string): /** * Resolve all header values using the same resolution logic as API keys. */ -export function resolveHeaders(headers: Record | undefined): Record | undefined { +export function resolveHeaders( + headers: Record | undefined, + env?: Record, +): Record | undefined { if (!headers) return undefined; const resolved: Record = {}; for (const [key, value] of Object.entries(headers)) { - const resolvedValue = resolveConfigValue(value); + const resolvedValue = resolveConfigValue(value, env); if (resolvedValue) { resolved[key] = resolvedValue; } @@ -266,11 +269,12 @@ export function resolveHeaders(headers: Record | undefined): Rec export function resolveHeadersOrThrow( headers: Record | undefined, description: string, + env?: Record, ): Record | undefined { if (!headers) return undefined; const resolved: Record = {}; for (const [key, value] of Object.entries(headers)) { - resolved[key] = resolveConfigValueOrThrow(value, `${description} header "${key}"`); + resolved[key] = resolveConfigValueOrThrow(value, `${description} header "${key}"`, env); } return Object.keys(resolved).length > 0 ? resolved : undefined; } diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index e6db747b..180846cb 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -303,6 +303,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} if (!auth.ok) { throw new Error(auth.error); } + const env = auth.env || options?.env ? { ...(auth.env ?? {}), ...(options?.env ?? {}) } : undefined; const providerRetrySettings = settingsManager.getProviderRetrySettings(); const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs(); // SDKs treat timeout=0 as 0ms (immediate timeout), not "no timeout". @@ -314,6 +315,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} return streamSimple(model, context, { ...options, apiKey: auth.apiKey, + env, timeoutMs, websocketConnectTimeoutMs, maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries, diff --git a/packages/coding-agent/test/auth-storage.test.ts b/packages/coding-agent/test/auth-storage.test.ts index 24242b6b..bcc353c1 100644 --- a/packages/coding-agent/test/auth-storage.test.ts +++ b/packages/coding-agent/test/auth-storage.test.ts @@ -134,6 +134,34 @@ describe("AuthStorage", () => { } }); + test("apiKey env bag takes precedence over process.env", async () => { + const originalEnv = process.env.TEST_AUTH_SCOPED_API_KEY_12345; + process.env.TEST_AUTH_SCOPED_API_KEY_12345 = "process-env-value"; + + try { + writeAuthJson({ + anthropic: { + type: "api_key", + key: "$TEST_AUTH_SCOPED_API_KEY_12345", + env: { TEST_AUTH_SCOPED_API_KEY_12345: "credential-env-value" }, + }, + }); + + authStorage = AuthStorage.create(authJsonPath); + + expect(await authStorage.getApiKey("anthropic")).toBe("credential-env-value"); + expect(authStorage.getProviderEnv("anthropic")).toEqual({ + TEST_AUTH_SCOPED_API_KEY_12345: "credential-env-value", + }); + } finally { + if (originalEnv === undefined) { + delete process.env.TEST_AUTH_SCOPED_API_KEY_12345; + } else { + process.env.TEST_AUTH_SCOPED_API_KEY_12345 = originalEnv; + } + } + }); + test("apiKey with braced env syntax resolves to env value", async () => { const originalEnv = process.env.TEST_AUTH_BRACED_API_KEY_12345; process.env.TEST_AUTH_BRACED_API_KEY_12345 = "braced-env-api-key-value"; diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 3cba5264..fe8609f2 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -893,6 +893,38 @@ describe("ModelRegistry", () => { expect(registry.getProviderDisplayName("oauth-provider")).toBe("OAuth Provider"); }); + test("stored API key env propagates to request auth and resolves headers", async () => { + authStorage.set("cloudflare-ai-gateway", { + type: "api_key", + key: "$CLOUDFLARE_API_KEY", + env: { + CLOUDFLARE_API_KEY: "stored-cf-token", + CLOUDFLARE_ACCOUNT_ID: "stored-account", + }, + }); + writeRawModelsJson({ + "cloudflare-ai-gateway": { + headers: { "x-account": "$CLOUDFLARE_ACCOUNT_ID" }, + }, + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + const model = registry.getAll().find((m) => m.provider === "cloudflare-ai-gateway"); + expect(model).toBeDefined(); + + const auth = await registry.getApiKeyAndHeaders(model!); + + expect(auth).toEqual({ + ok: true, + apiKey: "stored-cf-token", + headers: { "x-account": "stored-account" }, + env: { + CLOUDFLARE_API_KEY: "stored-cf-token", + CLOUDFLARE_ACCOUNT_ID: "stored-account", + }, + }); + }); + test("registerProvider treats uppercase apiKey and headers as literals", async () => { const envKeys = ["CUSTOM_NAME", "BEARER", "MODEL_TOKEN"]; const savedEnv: Record = {}; From 8f0e9251b9265e73cb85e97b8a190db40c96e57c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 17:21:30 +0200 Subject: [PATCH 061/141] fix(coding-agent): do not open browser for device code login --- .../src/modes/interactive/components/login-dialog.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 958db6d6..3fc4b516 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -128,7 +128,6 @@ export class LoginDialogComponent extends Container implements Focusable { this.contentContainer.addChild(new Spacer(1)); this.contentContainer.addChild(new Text(theme.fg("warning", `Enter code: ${info.userCode}`), 1, 0)); - openBrowser(info.verificationUri); this.tui.requestRender(); } From 0680726ac446770f5cb7b629bf1d29f8a8b4aecc Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 17:41:59 +0200 Subject: [PATCH 062/141] fix: upgrade marked to 18.0.5 --- package-lock.json | 10 +-- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/npm-shrinkwrap.json | 10 +-- .../src/core/export-html/vendor/marked.min.js | 78 ++++++++++++++++++- packages/tui/CHANGELOG.md | 4 + packages/tui/package.json | 2 +- 6 files changed, 94 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4063babb..a522cacc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3981,15 +3981,15 @@ } }, "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/merge2": { @@ -6369,7 +6369,7 @@ "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "marked": "18.0.5" }, "devDependencies": { "@xterm/headless": "5.5.0", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 9cee0caf..672122da 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,10 @@ - Added `auth.json` API key `env` values so provider-specific environment overrides can be scoped to Pi and propagated to inherited provider configuration ([#5728](https://github.com/earendil-works/pi/issues/5728)). +### Changed + +- Updated the vendored Markdown parser used by HTML session exports to `marked` 18.0.5. + ### Fixed - Fixed successful `pi update` on Windows to exit naturally instead of calling `process.exit(0)`, avoiding a Node.js/libuv assertion after version-check network requests ([#5805](https://github.com/earendil-works/pi/issues/5805)). diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 438d1319..2a3434d6 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -516,7 +516,7 @@ "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "marked": "18.0.5" }, "engines": { "node": ">=22.19.0" @@ -1399,15 +1399,15 @@ } }, "node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/minimatch": { diff --git a/packages/coding-agent/src/core/export-html/vendor/marked.min.js b/packages/coding-agent/src/core/export-html/vendor/marked.min.js index 79394fd8..9d79575e 100644 --- a/packages/coding-agent/src/core/export-html/vendor/marked.min.js +++ b/packages/coding-agent/src/core/export-html/vendor/marked.min.js @@ -1,6 +1,78 @@ /** - * marked v15.0.4 - a markdown parser - * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * marked v18.0.5 - a markdown parser + * Copyright (c) 2018-2026, MarkedJS. (MIT License) + * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License) * https://github.com/markedjs/marked */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s={exec:()=>null};function r(e,t=""){let n="string"==typeof e?e:e.source;const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(i.caret,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}const i={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,o=/(?:[*+-]|\d{1,9}[.)])/,a=r(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),c=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,h=/(?!\s*\])(?:\\.|[^\[\]\\])+/,p=r(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",h).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),u=r(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,o).getRegex(),g="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",k=/|$))/,f=r("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",k).replace("tag",g).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),d=r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),x={blockquote:r(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",d).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:p,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:f,lheading:a,list:u,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:d,table:s,text:/^[^\n]+/},b=r("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),w={...x,table:b,paragraph:r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",b).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex()},m={...x,html:r("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",k).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:s,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:r(c).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",a).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},y=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,$=/^( {2,}|\\)\n(?!\s*$)/,R=/[\p{P}\p{S}]/u,S=/[\s\p{P}\p{S}]/u,T=/[^\s\p{P}\p{S}]/u,z=r(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,S).getRegex(),A=r(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,"u").replace(/punct/g,R).getRegex(),_=r("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),P=r("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),I=r(/\\(punct)/,"gu").replace(/punct/g,R).getRegex(),L=r(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),B=r(k).replace("(?:--\x3e|$)","--\x3e").getRegex(),C=r("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",B).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),E=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,q=r(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",E).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Z=r(/^!?\[(label)\]\[(ref)\]/).replace("label",E).replace("ref",h).getRegex(),v=r(/^!?\[(ref)\](?:\[\])?/).replace("ref",h).getRegex(),D={_backpedal:s,anyPunctuation:I,autolink:L,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:$,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:s,emStrongLDelim:A,emStrongRDelimAst:_,emStrongRDelimUnd:P,escape:y,link:q,nolink:v,punctuation:z,reflink:Z,reflinkSearch:r("reflink|nolink(?!\\()","g").replace("reflink",Z).replace("nolink",v).getRegex(),tag:C,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},H=e=>G[e];function X(e,t){if(t){if(i.escapeTest.test(e))return e.replace(i.escapeReplace,H)}else if(i.escapeTestNoEncode.test(e))return e.replace(i.escapeReplaceNoEncode,H);return e}function F(e){try{e=encodeURI(e).replace(i.percentDecode,"%")}catch{return null}return e}function U(e,t){const n=e.replace(i.findPipe,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(i.splitPipe);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:J(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t,n){const s=e.match(n.other.indentCodeCompensation);if(null===s)return t;const r=s[1];return t.split("\n").map((e=>{const t=e.match(n.other.beginningSpace);if(null===t)return e;const[s]=t;return s.length>=r.length?e.slice(r.length):e})).join("\n")}(e,t[3]||"",this.rules);return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){const t=J(e,"#");this.options.pedantic?e=t.trim():t&&!this.rules.other.endingSpaceChar.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:J(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=J(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=this.rules.other.listItemRegex(n);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(this.rules.other.listReplaceTabs,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(this.rules.other.nonSpaceChar),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&this.rules.other.blankLine.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=this.rules.other.nextBulletRegex(p),n=this.rules.other.hrRegex(p),r=this.rules.other.fencesBeginRegex(p),i=this.rules.other.headingBeginRegex(p),l=this.rules.other.htmlBeginRegex(p);for(;e;){const u=e.split("\n",1)[0];let g;if(c=u,this.options.pedantic?(c=c.replace(this.rules.other.listReplaceNesting," "),g=c):g=c.replace(this.rules.other.tabCharGlobal," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(g.search(this.rules.other.nonSpaceChar)>=p||!c.trim())o+="\n"+g.slice(p);else{if(h)break;if(a.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=g.slice(p)}}r.loose||(l?r.loose=!0:this.rules.other.doubleBlankLine.test(s)&&(l=!0));let u,g=null;this.options.gfm&&(g=this.rules.other.listIsTask.exec(o),g&&(u="[ ] "!==g[0],o=o.replace(this.rules.other.listReplaceTask,""))),r.items.push({type:"list_item",raw:s,task:!!g,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}const o=r.items.at(-1);if(!o)return;o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>this.rules.other.anyLine.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;const t=J(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),K(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return K(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(e),s=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&s&&(e=e.substring(1,e.length-1)),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=t[1],n="mailto:"+e):(e=t[1],n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=t[0],n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=t[0],n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const e=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:e}}}}class W{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new V,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={other:i,block:j.normal,inline:N.normal};this.options.pedantic?(n.block=j.pedantic,n.inline=N.pedantic):this.options.gfm&&(n.block=j.gfm,this.options.breaks?n.inline=N.breaks:n.inline=N.gfm),this.tokenizer.rules=n}static get rules(){return{block:j,inline:N}}static lex(e,t){return new W(t).lex(e)}static lexInline(e,t){return new W(t).inlineTokens(e)}lex(e){e=e.replace(i.carriageReturn,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);const n=t.at(-1);1===s.raw.length&&void 0!==n?n.raw+="\n":t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.at(-1).src=n.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let r=e;if(this.options.extensions?.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(r))){const i=t.at(-1);n&&"paragraph"===i?.type?(i.raw+="\n"+s.raw,i.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=i.text):t.push(s),n=r.length!==e.length,e=e.substring(s.raw.length)}else if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(s=this.tokenizer.rules.inline.reflinkSearch.exec(n));)e.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(s=this.tokenizer.rules.inline.blockSkip.exec(n));)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(s=this.tokenizer.rules.inline.anyPunctuation.exec(n));)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r=!1,i="";for(;e;){let s;if(r||(i=""),r=!1,this.options.extensions?.inline?.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.escape(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.tag(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.link(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===s.type&&"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s);continue}if(s=this.tokenizer.emStrong(e,n,i)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.codespan(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.br(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.del(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.autolink(e)){e=e.substring(s.raw.length),t.push(s);continue}if(!this.state.inLink&&(s=this.tokenizer.url(e))){e=e.substring(s.raw.length),t.push(s);continue}let l=e;if(this.options.extensions?.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(l=e.substring(0,t+1))}if(s=this.tokenizer.inlineText(l)){e=e.substring(s.raw.length),"_"!==s.raw.slice(-1)&&(i=s.raw.slice(-1)),r=!0;const n=t.at(-1);"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return t}}class Y{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(i.notSpaceStart)?.[0],r=e.replace(i.endingNewline,"")+"\n";return s?'
'+(n?r:X(r,!0))+"
\n":"
"+(n?r:X(r,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?"paragraph"===e.tokens[0]?.type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+X(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${X(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=F(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=F(e);if(null===s)return X(n);let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new Y(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new V(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ne;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];ne.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return W.lex(e,t??this.defaults)}parser(e,t){return te.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?W.lex:W.lexInline,o=r.hooks?r.hooks.provideParser():e?te.parse:te.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+X(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const re=new se;function ie(e,t){return re.parse(e,t)}ie.options=ie.setOptions=function(e){return re.setOptions(e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.getDefaults=t,ie.defaults=e.defaults,ie.use=function(...e){return re.use(...e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.walkTokens=function(e,t){return re.walkTokens(e,t)},ie.parseInline=re.parseInline,ie.Parser=te,ie.parser=te.parse,ie.Renderer=Y,ie.TextRenderer=ee,ie.Lexer=W,ie.lexer=W.lex,ie.Tokenizer=V,ie.Hooks=ne,ie.parse=ie;const le=ie.options,oe=ie.setOptions,ae=ie.use,ce=ie.walkTokens,he=ie.parseInline,pe=ie,ue=te.parse,ge=W.lex;e.Hooks=ne,e.Lexer=W,e.Marked=se,e.Parser=te,e.Renderer=Y,e.TextRenderer=ee,e.Tokenizer=V,e.getDefaults=t,e.lexer=ge,e.marked=ie,e.options=le,e.parse=pe,e.parseInline=he,e.parser=ue,e.setOptions=oe,e.use=ae,e.walkTokens=ce})); + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ +(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; +"use strict";var N=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var ye=Object.prototype.hasOwnProperty;var Pe=(l,e)=>{for(var t in e)N(l,t,{get:e[t],enumerable:!0})},Se=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of we(e))!ye.call(l,s)&&s!==t&&N(l,s,{get:()=>e[s],enumerable:!(n=Oe(e,s))||n.enumerable});return l};var $e=l=>Se(N({},"__esModule",{value:!0}),l);var Rt={};Pe(Rt,{Hooks:()=>P,Lexer:()=>x,Marked:()=>C,Parser:()=>b,Renderer:()=>y,TextRenderer:()=>S,Tokenizer:()=>w,defaults:()=>T,getDefaults:()=>_,lexer:()=>bt,marked:()=>g,options:()=>ht,parse:()=>mt,parseInline:()=>ft,parser:()=>xt,setOptions:()=>kt,use:()=>dt,walkTokens:()=>gt});module.exports=$e(Rt);function _(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var T=_();function Q(l){T=l}var z={exec:()=>null};function E(l){let e=[];return t=>{let n=Math.max(0,Math.min(3,t-1)),s=e[n];return s||(s=l(n),e[n]=s),s}}function d(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,r)=>{let i=typeof r=="string"?r:r.source;return i=i.replace(m.caret,"$1"),t=t.replace(s,i),n},getRegex:()=>new RegExp(t,e)};return n}var Le=((l="")=>{try{return!!new RegExp("(?<=1)(?/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^
    /i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:E(l=>new RegExp(`^ {0,${l}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`)),hrRegex:E(l=>new RegExp(`^ {0,${l}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`)),fencesBeginRegex:E(l=>new RegExp(`^ {0,${l}}(?:\`\`\`|~~~)`)),headingBeginRegex:E(l=>new RegExp(`^ {0,${l}}#`)),htmlBeginRegex:E(l=>new RegExp(`^ {0,${l}}<(?:[a-z].*>|!--)`,"i")),blockquoteBeginRegex:E(l=>new RegExp(`^ {0,${l}}>`))},_e=/^(?:[ \t]*(?:\n|$))+/,ze=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Me=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,D=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Ee=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/ {0,3}(?:[*+-]|\d{1,9}[.)])/,ae=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,le=d(ae).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Ie=d(ae).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),U=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Ae=/^[^\n]+/,K=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,Ce=d(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",K).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Be=d(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),H="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",W=/|$))/,De=d("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",W).replace("tag",H).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),ue=d(U).replace("hr",D).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",H).getRegex(),qe=d(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",ue).getRegex(),X={blockquote:qe,code:ze,def:Ce,fences:Me,heading:Ee,hr:D,html:De,lheading:le,list:Be,newline:_e,paragraph:ue,table:z,text:Ae},ie=d("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",D).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",H).getRegex(),ve={...X,lheading:Ie,table:ie,paragraph:d(U).replace("hr",D).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",ie).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",H).getRegex()},He={...X,html:d(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",W).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:z,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:d(U).replace("hr",D).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",le).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Ze=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,Ge=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,pe=/^( {2,}|\\)\n(?!\s*$)/,Ne=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Le?"(?`+)[^`]+\k(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),he=/^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/,Ke=d(he,"u").replace(/punct/g,I).getRegex(),We=d(he,"u").replace(/punct/g,ce).getRegex(),ke="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Xe=d(ke,"gu").replace(/notPunctSpace/g,J).replace(/punctSpace/g,Z).replace(/punct/g,I).getRegex(),Je=d(ke,"gu").replace(/notPunctSpace/g,Fe).replace(/punctSpace/g,je).replace(/punct/g,ce).getRegex(),Ve=d("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,J).replace(/punctSpace/g,Z).replace(/punct/g,I).getRegex(),Ye=d(/^~~?(?:((?!~)punct)|[^\s~])/,"u").replace(/punct/g,I).getRegex(),et="^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)",tt=d(et,"gu").replace(/notPunctSpace/g,J).replace(/punctSpace/g,Z).replace(/punct/g,I).getRegex(),nt=d(/\\(punct)/,"gu").replace(/punct/g,I).getRegex(),rt=d(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),st=d(W).replace("(?:-->|$)","-->").getRegex(),it=d("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",st).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),v=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/,ot=d(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace("label",v).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),de=d(/^!?\[(label)\]\[(ref)\]/).replace("label",v).replace("ref",K).getRegex(),ge=d(/^!?\[(ref)\](?:\[\])?/).replace("ref",K).getRegex(),at=d("reflink|nolink(?!\\()","g").replace("reflink",de).replace("nolink",ge).getRegex(),oe=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,V={_backpedal:z,anyPunctuation:nt,autolink:rt,blockSkip:Ue,br:pe,code:Ge,del:z,delLDelim:z,delRDelim:z,emStrongLDelim:Ke,emStrongRDelimAst:Xe,emStrongRDelimUnd:Ve,escape:Ze,link:ot,nolink:ge,punctuation:Qe,reflink:de,reflinkSearch:at,tag:it,text:Ne,url:z},lt={...V,link:d(/^!?\[(label)\]\((.*?)\)/).replace("label",v).getRegex(),reflink:d(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",v).getRegex()},j={...V,emStrongRDelimAst:Je,emStrongLDelim:We,delLDelim:Ye,delRDelim:tt,url:d(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",oe).replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:d(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},fe=l=>pt[l];function O(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function Y(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function ee(l,e){let t=l.replace(m.findPipe,(r,i,o)=>{let u=!1,a=i;for(;--a>=0&&o[a]==="\\";)u=!u;return u?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length=0&&m.blankLine.test(e[t]);)t--;return e.length-t<=2?l:e.slice(0,t+1).join(` +`)}function me(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n0?-2:-1}function xe(l,e=0){let t=e,n="";for(let s of l)if(s===" "){let r=4-t%4;n+=" ".repeat(r),t+=r}else n+=s,t++;return n}function be(l,e,t,n,s){let r=e.href,i=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let u={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:r,title:i,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,u}function ct(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(` +`).map(r=>{let i=r.match(t.other.beginningSpace);if(i===null)return r;let[o]=i;return o.length>=s.length?r.slice(s.length):r}).join(` +`)}var w=class{options;rules;lexer;constructor(e){this.options=e||T}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=this.options.pedantic?t[0]:te(t[0]),s=n.replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:n,codeBlockStyle:"indented",text:s}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=ct(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=L(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:L(t[0],` +`),depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:L(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=L(t[0],` +`).split(` +`),s="",r="",i=[];for(;n.length>0;){let o=!1,u=[],a;for(a=0;a1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let i=this.rules.other.listItemRegex(n),o=!1;for(;e;){let a=!1,c="",p="";if(!(t=i.exec(e))||this.rules.block.hr.test(e))break;c=t[0],e=e.substring(c.length);let k=xe(t[2].split(` +`,1)[0],t[1].length),h=e.split(` +`,1)[0],R=!k.trim(),f=0;if(this.options.pedantic?(f=2,p=k.trimStart()):R?f=t[1].length+1:(f=k.search(this.rules.other.nonSpaceChar),f=f>4?1:f,p=k.slice(f),f+=t[1].length),R&&this.rules.other.blankLine.test(h)&&(c+=h+` +`,e=e.substring(h.length+1),a=!0),!a){let $=this.rules.other.nextBulletRegex(f),ne=this.rules.other.hrRegex(f),re=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),Re=this.rules.other.htmlBeginRegex(f),Te=this.rules.other.blockquoteBeginRegex(f);for(;e;){let G=e.split(` +`,1)[0],B;if(h=G,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting," "),B=h):B=h.replace(this.rules.other.tabCharGlobal," "),re.test(h)||se.test(h)||Re.test(h)||Te.test(h)||$.test(h)||ne.test(h))break;if(B.search(this.rules.other.nonSpaceChar)>=f||!h.trim())p+=` +`+B.slice(f);else{if(R||k.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||re.test(k)||se.test(k)||ne.test(k))break;p+=` +`+h}R=!h.trim(),c+=G+` +`,e=e.substring(G.length+1),k=B.slice(f)}}r.loose||(o?r.loose=!0:this.rules.other.doubleBlankLine.test(c)&&(o=!0)),r.items.push({type:"list_item",raw:c,task:!!this.options.gfm&&this.rules.other.listIsTask.test(p),loose:!1,text:p,tokens:[]}),r.raw+=c}let u=r.items.at(-1);if(u)u.raw=u.raw.trimEnd(),u.text=u.text.trimEnd();else return;r.raw=r.raw.trimEnd();for(let a of r.items){this.lexer.state.top=!1,a.tokens=this.lexer.blockTokens(a.text,[]);let c=a.tokens[0];if(a.task&&(c?.type==="text"||c?.type==="paragraph")){a.text=a.text.replace(this.rules.other.listReplaceTask,""),c.raw=c.raw.replace(this.rules.other.listReplaceTask,""),c.text=c.text.replace(this.rules.other.listReplaceTask,"");for(let k=this.lexer.inlineQueue.length-1;k>=0;k--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[k].src)){this.lexer.inlineQueue[k].src=this.lexer.inlineQueue[k].src.replace(this.rules.other.listReplaceTask,"");break}let p=this.rules.other.listTaskCheckbox.exec(a.raw);if(p){let k={type:"checkbox",raw:p[0]+" ",checked:p[0]!=="[ ]"};a.checked=k.checked,r.loose?a.tokens[0]&&["paragraph","text"].includes(a.tokens[0].type)&&"tokens"in a.tokens[0]&&a.tokens[0].tokens?(a.tokens[0].raw=k.raw+a.tokens[0].raw,a.tokens[0].text=k.raw+a.tokens[0].text,a.tokens[0].tokens.unshift(k)):a.tokens.unshift({type:"paragraph",raw:k.raw,text:k.raw,tokens:[k]}):a.tokens.unshift(k)}}else a.task&&(a.task=!1);if(!r.loose){let p=a.tokens.filter(h=>h.type==="space"),k=p.length>0&&p.some(h=>this.rules.other.anyLine.test(h.raw));r.loose=k}}if(r.loose)for(let a of r.items){a.loose=!0;for(let c of a.tokens)c.type==="text"&&(c.type="paragraph")}return r}}html(e){let t=this.rules.block.html.exec(e);if(t){let n=te(t[0]);return{type:"html",block:!0,raw:n,pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:n}}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",r=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:L(t[0],` +`),href:s,title:r}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=ee(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),r=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(` +`):[],i={type:"table",raw:L(t[0],` +`),header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?i.align.push("right"):this.rules.other.tableAlignCenter.test(o)?i.align.push("center"):this.rules.other.tableAlignLeft.test(o)?i.align.push("left"):i.align.push(null);for(let o=0;o({text:u,tokens:this.lexer.inline(u),header:!1,align:i.align[a]})));return i}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t){let n=t[1].trim();return{type:"heading",raw:L(t[0],` +`),depth:t[2].charAt(0)==="="?1:2,text:n,tokens:this.lexer.inline(n)}}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let i=L(n.slice(0,-1),"\\");if((n.length-i.length)%2===0)return}else{let i=me(t[2],"()");if(i===-2)return;if(i>-1){let u=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,u).trim(),t[3]=""}}let s=t[2],r="";if(this.options.pedantic){let i=this.rules.other.pedanticHrefTitle.exec(s);i&&(s=i[1],r=i[3])}else r=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),be(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:r&&r.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),r=t[s.toLowerCase()];if(!r){let i=n[0].charAt(0);return{type:"text",raw:i,text:i}}return be(n,r,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||!s[1]&&!s[2]&&!s[3]&&!s[4]||s[4]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[3]||"")||!n||this.rules.inline.punctuation.exec(n)){let i=[...s[0]].length-1,o,u,a=i,c=0,p=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(p.lastIndex=0,t=t.slice(-1*e.length+i);(s=p.exec(t))!==null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(u=[...o].length,s[3]||s[4]){a+=u;continue}else if((s[5]||s[6])&&i%3&&!((i+u)%3)){c+=u;continue}if(a-=u,a>0)continue;u=Math.min(u,u+a+c);let k=[...s[0]][0].length,h=e.slice(0,i+s.index+k+u);if(Math.min(i,u)%2){let f=h.slice(1,-1);return{type:"em",raw:h,text:f,tokens:this.lexer.inlineTokens(f)}}let R=h.slice(2,-2);return{type:"strong",raw:h,text:R,tokens:this.lexer.inlineTokens(R)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),r=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&r&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e,t,n=""){let s=this.rules.inline.delLDelim.exec(e);if(!s)return;if(!(s[1]||"")||!n||this.rules.inline.punctuation.exec(n)){let i=[...s[0]].length-1,o,u,a=i,c=this.rules.inline.delRDelim;for(c.lastIndex=0,t=t.slice(-1*e.length+i);(s=c.exec(t))!==null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o||(u=[...o].length,u!==i))continue;if(s[3]||s[4]){a+=u;continue}if(a-=u,a>0)continue;u=Math.min(u,u+a);let p=[...s[0]][0].length,k=e.slice(0,i+s.index+p+u),h=k.slice(i,-i);return{type:"del",raw:k,text:h,tokens:this.lexer.inlineTokens(h)}}}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let r;do r=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(r!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||T,this.options.tokenizer=this.options.tokenizer||new w,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:q.normal,inline:A.normal};this.options.pedantic?(t.block=q.pedantic,t.inline=A.pedantic):this.options.gfm&&(t.block=q.gfm,this.options.breaks?t.inline=A.breaks:t.inline=A.gfm),this.tokenizer.rules=t}static get rules(){return{block:q,inline:A}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` +`),this.blockTokens(e,this.tokens);for(let t=0;t(r=o.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let o=t.at(-1);r.raw.length===1&&o!==void 0?o.raw+=` +`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.text,this.inlineQueue.at(-1).src=o.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.raw,this.inlineQueue.at(-1).src=o.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let o=1/0,u=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},u),typeof a=="number"&&a>=0&&(o=Math.min(o,a))}),o<1/0&&o>=0&&(i=e.substring(0,o+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let o=t.at(-1);n&&o?.type==="paragraph"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):t.push(r);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){this.tokenizer.lexer=this;let n=e,s=null;if(this.tokens.links){let a=Object.keys(this.tokens.links);if(a.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!==null;)a.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!==null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r;for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!==null;)r=s[2]?s[2].length:0,n=n.slice(0,s.index+r)+"["+"a".repeat(s[0].length-r-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let i=!1,o="",u=1/0;for(;e;){if(e.length(a=p.call({lexer:this},e,t))?(e=e.substring(a.raw.length),t.push(a),!0):!1))continue;if(a=this.tokenizer.escape(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.tag(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.link(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(a.raw.length);let p=t.at(-1);a.type==="text"&&p?.type==="text"?(p.raw+=a.raw,p.text+=a.text):t.push(a);continue}if(a=this.tokenizer.emStrong(e,n,o)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.codespan(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.br(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.del(e,n,o)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.autolink(e)){e=e.substring(a.raw.length),t.push(a);continue}if(!this.state.inLink&&(a=this.tokenizer.url(e))){e=e.substring(a.raw.length),t.push(a);continue}let c=e;if(this.options.extensions?.startInline){let p=1/0,k=e.slice(1),h;this.options.extensions.startInline.forEach(R=>{h=R.call({lexer:this},k),typeof h=="number"&&h>=0&&(p=Math.min(p,h))}),p<1/0&&p>=0&&(c=e.substring(0,p+1))}if(a=this.tokenizer.inlineText(c)){e=e.substring(a.raw.length),a.raw.slice(-1)!=="_"&&(o=a.raw.slice(-1)),i=!0;let p=t.at(-1);p?.type==="text"?(p.raw+=a.raw,p.text+=a.text):t.push(a);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return t}infiniteLoopError(e){let t="Infinite loop on byte: "+e;if(this.options.silent)console.error(t);else throw new Error(t)}};var y=class{options;parser;constructor(e){this.options=e||T}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],r=e.replace(m.endingNewline,"")+` +`;return s?'
    '+(n?r:O(r,!0))+`
    +`:"
    "+(n?r:O(r,!0))+`
    +`}blockquote({tokens:e}){return`
    +${this.parser.parse(e)}
    +`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} +`}hr(e){return`
    +`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o +`+s+" +`}listitem(e){return`
  • ${this.parser.parse(e.tokens)}
  • +`}checkbox({checked:e}){return" '}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    +`}table(e){let t="",n="";for(let r=0;r${s}`),` + +`+t+` +`+s+`
    +`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${O(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),r=Y(e);if(r===null)return s;e=r;let i='
    ",i}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let r=Y(e);if(r===null)return O(n);e=r;let i=`${O(n)}{let o=r[i].flat(1/0);n=n.concat(this.walkTokens(o,t))}):r.tokens&&(n=n.concat(this.walkTokens(r.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(r=>{if(!r.name)throw new Error("extension name required");if("renderer"in r){let i=t.renderers[r.name];i?t.renderers[r.name]=function(...o){let u=r.renderer.apply(this,o);return u===!1&&(u=i.apply(this,o)),u}:t.renderers[r.name]=r.renderer}if("tokenizer"in r){if(!r.level||r.level!=="block"&&r.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let i=t[r.level];i?i.unshift(r.tokenizer):t[r.level]=[r.tokenizer],r.start&&(r.level==="block"?t.startBlock?t.startBlock.push(r.start):t.startBlock=[r.start]:r.level==="inline"&&(t.startInline?t.startInline.push(r.start):t.startInline=[r.start]))}"childTokens"in r&&r.childTokens&&(t.childTokens[r.name]=r.childTokens)}),s.extensions=t),n.renderer){let r=this.defaults.renderer||new y(this.defaults);for(let i in n.renderer){if(!(i in r))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;let o=i,u=n.renderer[o],a=r[o];r[o]=(...c)=>{let p=u.apply(r,c);return p===!1&&(p=a.apply(r,c)),p||""}}s.renderer=r}if(n.tokenizer){let r=this.defaults.tokenizer||new w(this.defaults);for(let i in n.tokenizer){if(!(i in r))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;let o=i,u=n.tokenizer[o],a=r[o];r[o]=(...c)=>{let p=u.apply(r,c);return p===!1&&(p=a.apply(r,c)),p}}s.tokenizer=r}if(n.hooks){let r=this.defaults.hooks||new P;for(let i in n.hooks){if(!(i in r))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;let o=i,u=n.hooks[o],a=r[o];P.passThroughHooks.has(i)?r[o]=c=>{if(this.defaults.async&&P.passThroughHooksRespectAsync.has(i))return(async()=>{let k=await u.call(r,c);return a.call(r,k)})();let p=u.call(r,c);return a.call(r,p)}:r[o]=(...c)=>{if(this.defaults.async)return(async()=>{let k=await u.apply(r,c);return k===!1&&(k=await a.apply(r,c)),k})();let p=u.apply(r,c);return p===!1&&(p=a.apply(r,c)),p}}s.hooks=r}if(n.walkTokens){let r=this.defaults.walkTokens,i=n.walkTokens;s.walkTokens=function(o){let u=[];return u.push(i.call(this,o)),r&&(u=u.concat(r.call(this,o))),u}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let r={...s},i={...this.defaults,...r},o=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&r.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i,i.hooks.block=e),i.async)return(async()=>{let u=i.hooks?await i.hooks.preprocess(n):n,c=await(i.hooks?await i.hooks.provideLexer(e):e?x.lex:x.lexInline)(u,i),p=i.hooks?await i.hooks.processAllTokens(c):c;i.walkTokens&&await Promise.all(this.walkTokens(p,i.walkTokens));let h=await(i.hooks?await i.hooks.provideParser(e):e?b.parse:b.parseInline)(p,i);return i.hooks?await i.hooks.postprocess(h):h})().catch(o);try{i.hooks&&(n=i.hooks.preprocess(n));let a=(i.hooks?i.hooks.provideLexer(e):e?x.lex:x.lexInline)(n,i);i.hooks&&(a=i.hooks.processAllTokens(a)),i.walkTokens&&this.walkTokens(a,i.walkTokens);let p=(i.hooks?i.hooks.provideParser(e):e?b.parse:b.parseInline)(a,i);return i.hooks&&(p=i.hooks.postprocess(p)),p}catch(u){return o(u)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+O(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new C;function g(l,e){return M.parse(l,e)}g.options=g.setOptions=function(l){return M.setOptions(l),g.defaults=M.defaults,Q(g.defaults),g};g.getDefaults=_;g.defaults=T;g.use=function(...l){return M.use(...l),g.defaults=M.defaults,Q(g.defaults),g};g.walkTokens=function(l,e){return M.walkTokens(l,e)};g.parseInline=M.parseInline;g.Parser=b;g.parser=b.parse;g.Renderer=y;g.TextRenderer=S;g.Lexer=x;g.lexer=x.lex;g.Tokenizer=w;g.Hooks=P;g.parse=g;var ht=g.options,kt=g.setOptions,dt=g.use,gt=g.walkTokens,ft=g.parseInline,mt=g,xt=b.parse,bt=x.lex; + +if(__exports != exports)module.exports = exports;return module.exports})); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 5998b2b1..8896ee78 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Updated Markdown parsing to `marked` 18.0.5. + ## [0.79.4] - 2026-06-15 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index 749a3933..b634c8d3 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -38,7 +38,7 @@ "types": "./dist/index.d.ts", "dependencies": { "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "marked": "18.0.5" }, "devDependencies": { "@xterm/headless": "5.5.0", From 910508595a4a24a010a8ca18ec868d9307b13080 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 18:53:19 +0200 Subject: [PATCH 063/141] feat(coding-agent): add settings http proxy closes #5790 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/settings.md | 12 +++++ .../coding-agent/src/core/http-dispatcher.ts | 7 +++ .../coding-agent/src/core/settings-manager.ts | 1 + packages/coding-agent/src/main.ts | 13 +++-- .../coding-agent/test/http-dispatcher.test.ts | 53 +++++++++++++++++++ 6 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/test/http-dispatcher.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 672122da..042da954 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added a global `httpProxy` setting that applies as `HTTP_PROXY` and `HTTPS_PROXY` for Pi-managed HTTP clients ([#5790](https://github.com/earendil-works/pi/issues/5790)). - Added `auth.json` API key `env` values so provider-specific environment overrides can be scoped to Pi and propagated to inherited provider configuration ([#5728](https://github.com/earendil-works/pi/issues/5728)). ### Changed diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index fccfab06..18c9ad97 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -69,6 +69,18 @@ Use `/trust` in interactive mode to save a project trust decision for future ses Set `PI_SKIP_VERSION_CHECK=1` to disable the Pi version update check. Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry. +### Network + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `httpProxy` | string | - | HTTP proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY`. Global setting only. | + +```json +{ + "httpProxy": "http://127.0.0.1:7890" +} +``` + ### Warnings | Setting | Type | Default | Description | diff --git a/packages/coding-agent/src/core/http-dispatcher.ts b/packages/coding-agent/src/core/http-dispatcher.ts index 12ce9fb0..2ec8144e 100644 --- a/packages/coding-agent/src/core/http-dispatcher.ts +++ b/packages/coding-agent/src/core/http-dispatcher.ts @@ -36,6 +36,13 @@ export function formatHttpIdleTimeoutMs(timeoutMs: number): string { return `${timeoutMs / 1000} sec`; } +export function applyHttpProxySettings(httpProxy: string | undefined): void { + const proxy = httpProxy?.trim(); + if (!proxy) return; + process.env.HTTP_PROXY ??= proxy; + process.env.HTTPS_PROXY ??= proxy; +} + export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void { const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs); if (normalizedTimeoutMs === undefined) { diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 8acd5998..6b54a187 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -117,6 +117,7 @@ export interface Settings { markdown?: MarkdownSettings; warnings?: WarningSettings; sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag) + httpProxy?: string; // Proxy URL applied as HTTP_PROXY and HTTPS_PROXY for Pi-managed HTTP clients httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it websocketConnectTimeoutMs?: number; // WebSocket connect/open handshake timeout in milliseconds; 0 disables it } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 42ce1ce5..f66040bb 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -26,7 +26,7 @@ import { formatNoModelsAvailableMessage } from "./core/auth-guidance.ts"; import { AuthStorage } from "./core/auth-storage.ts"; import { exportFromFile } from "./core/export-html/index.ts"; import type { ExtensionFactory } from "./core/extensions/types.ts"; -import { configureHttpDispatcher } from "./core/http-dispatcher.ts"; +import { applyHttpProxySettings, configureHttpDispatcher } from "./core/http-dispatcher.ts"; import type { ModelRegistry } from "./core/model-registry.ts"; import { resolveCliModel, resolveModelScope, type ScopedModel } from "./core/model-resolver.ts"; import { restoreStdout, takeOverStdout } from "./core/output-guard.ts"; @@ -466,6 +466,12 @@ export async function main(args: string[], options?: MainOptions) { cleanupWindowsSelfUpdateQuarantine(getPackageDir()); } + const cwd = process.cwd(); + const agentDir = getAgentDir(); + const bootstrapSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false }); + applyHttpProxySettings(bootstrapSettingsManager.getGlobalSettings().httpProxy); + configureHttpDispatcher(); + if (await handlePackageCommand(args, { extensionFactories: options?.extensionFactories })) { const exitCode = process.exitCode ?? 0; if (process.platform === "win32" && exitCode === 0 && args[0] === "update") { @@ -529,11 +535,9 @@ export async function main(args: string[], options?: MainOptions) { validateSessionIdFlags(parsed); // Run migrations (pass cwd for project-local migrations) - const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd()); + const { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(cwd); time("runMigrations"); - const cwd = process.cwd(); - const agentDir = getAgentDir(); const startupSettingsManager = SettingsManager.create(cwd, agentDir); reportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, "startup session lookup")); @@ -726,6 +730,7 @@ export async function main(args: string[], options?: MainOptions) { time("createAgentSessionRuntime"); const { services, session, modelFallbackMessage } = runtime; const { settingsManager, modelRegistry, resourceLoader } = services; + applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy); configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs()); if (parsed.help) { diff --git a/packages/coding-agent/test/http-dispatcher.test.ts b/packages/coding-agent/test/http-dispatcher.test.ts new file mode 100644 index 00000000..f70022f1 --- /dev/null +++ b/packages/coding-agent/test/http-dispatcher.test.ts @@ -0,0 +1,53 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { applyHttpProxySettings } from "../src/core/http-dispatcher.ts"; + +const PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY"] as const; + +describe("http proxy settings", () => { + let savedEnv: Record<(typeof PROXY_ENV_KEYS)[number], string | undefined>; + + beforeEach(() => { + savedEnv = Object.fromEntries(PROXY_ENV_KEYS.map((key) => [key, process.env[key]])) as Record< + (typeof PROXY_ENV_KEYS)[number], + string | undefined + >; + for (const key of PROXY_ENV_KEYS) { + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of PROXY_ENV_KEYS) { + const value = savedEnv[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + }); + + it("applies httpProxy to HTTP_PROXY and HTTPS_PROXY", () => { + applyHttpProxySettings("http://127.0.0.1:7890"); + + expect(process.env.HTTP_PROXY).toBe("http://127.0.0.1:7890"); + expect(process.env.HTTPS_PROXY).toBe("http://127.0.0.1:7890"); + }); + + it("does not override existing proxy env vars", () => { + process.env.HTTP_PROXY = "http://env-http:8080"; + process.env.HTTPS_PROXY = "http://env-https:8080"; + + applyHttpProxySettings("http://settings:7890"); + + expect(process.env.HTTP_PROXY).toBe("http://env-http:8080"); + expect(process.env.HTTPS_PROXY).toBe("http://env-https:8080"); + }); + + it("ignores empty values", () => { + applyHttpProxySettings(" "); + + expect(process.env.HTTP_PROXY).toBeUndefined(); + expect(process.env.HTTPS_PROXY).toBeUndefined(); + }); +}); From 2d597f0212145c9e4b8d6f66d85349e5e2cb86a4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 19:00:12 +0200 Subject: [PATCH 064/141] fix(ai): tolerate null Responses message content closes #5819 --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/providers/openai-responses-shared.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e4d9ced0..bb108725 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed OpenAI Responses streaming to tolerate null message content from OpenAI-compatible servers before tool calls ([#5819](https://github.com/earendil-works/pi/issues/5819)). - Fixed Z.AI GLM-5.2 thinking requests to send `reasoning_effort` with the provider's `high`/`max` effort mapping ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). diff --git a/packages/ai/src/providers/openai-responses-shared.ts b/packages/ai/src/providers/openai-responses-shared.ts index c006c284..6fd59a44 100644 --- a/packages/ai/src/providers/openai-responses-shared.ts +++ b/packages/ai/src/providers/openai-responses-shared.ts @@ -456,7 +456,8 @@ export async function processResponsesStream( }); currentBlock = null; } else if (item.type === "message" && currentBlock?.type === "text") { - currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join(""); + currentBlock.text = + item.content?.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("") || ""; currentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined); stream.push({ type: "text_end", From 2431491c9278bd839c24f87e8baafb6a254414f9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 18:59:28 +0200 Subject: [PATCH 065/141] fix(ai): avoid duplicate OpenCode DeepSeek reasoning controls closes #5818 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 6 +++++- packages/ai/src/models.generated.ts | 27 +++++++++++++++++++++----- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index bb108725..6ee6ca4e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - Fixed OpenAI Responses streaming to tolerate null message content from OpenAI-compatible servers before tool calls ([#5819](https://github.com/earendil-works/pi/issues/5819)). +- Fixed OpenCode DeepSeek V4 thinking requests to avoid sending both `thinking` and `reasoning_effort` ([#5818](https://github.com/earendil-works/pi/issues/5818)). - Fixed Z.AI GLM-5.2 thinking requests to send `reasoning_effort` with the provider's `high`/`max` effort mapping ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed Moonshot AI China model metadata to include Kimi K2.7 Code, and omitted unsupported thinking-off payloads for Kimi K2.7 Code models ([#5760](https://github.com/earendil-works/pi/issues/5760)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 8cc890ee..7f4f9cf0 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1852,9 +1852,13 @@ async function generateModels() { for (const candidate of allModels) { if (candidate.api === "openai-completions" && candidate.id.includes("deepseek-v4")) { + const preservesNativeReasoningEffort = + candidate.provider === "openrouter" || + candidate.provider === "opencode" || + candidate.provider === "opencode-go"; candidate.compat = { ...candidate.compat, - ...(candidate.provider === "openrouter" + ...(preservesNativeReasoningEffort ? { requiresReasoningContentOnAssistantMessages: deepseekCompat.requiresReasoningContentOnAssistantMessages, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index d7e2f9f3..6f1f21de 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -7938,7 +7938,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -7957,7 +7957,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -7976,7 +7976,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode", baseUrl: "https://opencode.ai/zen/v1", - compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","supportsLongCacheRetention":false,"requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -8554,7 +8554,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -8573,7 +8573,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -16310,6 +16310,23 @@ export const MODELS = { contextWindow: 202800, maxTokens: 64000, } satisfies Model<"anthropic-messages">, + "zai/glm-5.2": { + id: "zai/glm-5.2", + name: "GLM 5.2", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "zai/glm-5v-turbo": { id: "zai/glm-5v-turbo", name: "GLM 5V Turbo", From b6b5bed9aebb4842f0d8daba3298b20b1e624b74 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 19:46:15 +0200 Subject: [PATCH 066/141] docs: update unreleased changelogs --- packages/coding-agent/CHANGELOG.md | 13 +++++++++++++ packages/tui/CHANGELOG.md | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 042da954..714b0140 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,8 +2,16 @@ ## [Unreleased] +### New Features + +- **Provider-scoped API key environments** - `auth.json` API key entries can now include `env` overrides for provider-specific Cloudflare, Azure OpenAI, Google Vertex, Amazon Bedrock, cache retention, and proxy settings without changing the project shell. See [Auth File](docs/providers.md#auth-file). +- **Global HTTP proxy setting** - Configure `httpProxy` once in global settings to apply `HTTP_PROXY` and `HTTPS_PROXY` to Pi-managed HTTP clients. See [Network](docs/settings.md#network). +- **Vercel AI Gateway attribution** - Vercel AI Gateway requests now include Pi attribution headers by default. See [API Keys](docs/providers.md#api-keys). + ### Added +- Added Vercel AI Gateway request attribution headers (`http-referer` and `x-title`) for Vercel AI Gateway models ([#5798](https://github.com/earendil-works/pi/pull/5798) by [@rwachtler](https://github.com/rwachtler)). +- Added an `xp` footer marker when experimental features are enabled. - Added a global `httpProxy` setting that applies as `HTTP_PROXY` and `HTTPS_PROXY` for Pi-managed HTTP clients ([#5790](https://github.com/earendil-works/pi/issues/5790)). - Added `auth.json` API key `env` values so provider-specific environment overrides can be scoped to Pi and propagated to inherited provider configuration ([#5728](https://github.com/earendil-works/pi/issues/5728)). @@ -13,6 +21,11 @@ ### Fixed +- Fixed inherited OpenAI Responses streaming to tolerate null message content from OpenAI-compatible servers before tool calls ([#5819](https://github.com/earendil-works/pi/issues/5819)). +- Fixed inherited OpenCode DeepSeek V4 thinking requests to avoid sending both `thinking` and `reasoning_effort` ([#5818](https://github.com/earendil-works/pi/issues/5818)). +- Fixed device-code login to stop opening the browser automatically. +- Fixed inherited editor Cursor Up handling so non-empty drafts jump to the start of the line before browsing input history ([#5789](https://github.com/earendil-works/pi/pull/5789) by [@4h9fbZ](https://github.com/4h9fbZ)). +- Fixed inherited Z.AI GLM-5.2 thinking requests to send `reasoning_effort` with the provider's `high`/`max` effort mapping ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed successful `pi update` on Windows to exit naturally instead of calling `process.exit(0)`, avoiding a Node.js/libuv assertion after version-check network requests ([#5805](https://github.com/earendil-works/pi/issues/5805)). - Fixed inherited Google and `google-vertex` Gemini model metadata to map `latest` aliases to the current models, add Gemini 3.5 Flash for Vertex, correct Gemini 2.5 Flash Vertex cache pricing, and remove shut-down Vertex preview models ([#5761](https://github.com/earendil-works/pi/issues/5761)). - Fixed the session selector to stay open and show the all-sessions empty state when both current-folder and all-scope session lists are empty ([#5747](https://github.com/earendil-works/pi/issues/5747)). diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8896ee78..8ecc6e87 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -6,6 +6,10 @@ - Updated Markdown parsing to `marked` 18.0.5. +### Fixed + +- Fixed editor Cursor Up handling so non-empty drafts jump to the start of the line before browsing input history ([#5789](https://github.com/earendil-works/pi/pull/5789) by [@4h9fbZ](https://github.com/4h9fbZ)). + ## [0.79.4] - 2026-06-15 ### Added From 6561cb293b1dfdbe1f3f98cd73592860ded4c30b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 19:54:23 +0200 Subject: [PATCH 067/141] Release v0.79.5 --- package-lock.json | 26 +++++++------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 35 +++++++++++++++++++ packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +-- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++------- packages/coding-agent/package.json | 8 ++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 85 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index a522cacc..3c0aa234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6176,10 +6176,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.4", + "version": "0.79.5", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.4", + "@earendil-works/pi-ai": "^0.79.5", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6213,7 +6213,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.4", + "version": "0.79.5", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6258,12 +6258,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.4", + "version": "0.79.5", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.4", - "@earendil-works/pi-ai": "^0.79.4", - "@earendil-works/pi-tui": "^0.79.4", + "@earendil-works/pi-agent-core": "^0.79.5", + "@earendil-works/pi-ai": "^0.79.5", + "@earendil-works/pi-tui": "^0.79.5", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6304,32 +6304,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.4", + "version": "0.79.5", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.4" + "version": "0.79.5" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.4", + "version": "0.79.5", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.4", + "version": "1.9.5", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.4", + "version": "0.79.5", "dependencies": { "ms": "2.1.3" }, @@ -6365,7 +6365,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.4", + "version": "0.79.5", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 64f79b1b..f99f0831 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.5] - 2026-06-16 ## [0.79.4] - 2026-06-15 diff --git a/packages/agent/package.json b/packages/agent/package.json index 36812b0f..4102aea3 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.4", + "version": "0.79.5", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.4", + "@earendil-works/pi-ai": "^0.79.5", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6ee6ca4e..b835ce48 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.5] - 2026-06-16 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 8278c9b9..f99e4052 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.4", + "version": "0.79.5", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 6f1f21de..564d92e3 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3813,6 +3813,24 @@ export const MODELS = { contextWindow: 131072, maxTokens: 131072, } satisfies Model<"openai-completions">, + "@cf/zai-org/glm-5.2": { + id: "@cf/zai-org/glm-5.2", + name: "Glm 5.2", + api: "openai-completions", + provider: "cloudflare-workers-ai", + baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"openai-completions">, }, "deepseek": { "deepseek-v4-flash": { @@ -12956,6 +12974,23 @@ export const MODELS = { contextWindow: 202752, maxTokens: 4096, } satisfies Model<"openai-completions">, + "z-ai/glm-5.2": { + id: "z-ai/glm-5.2", + name: "Z.ai: GLM 5.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", name: "Anthropic: Claude Fable Latest", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 714b0140..ea861202 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.5] - 2026-06-16 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 5d512449..fd9cacc8 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.4", + "version": "0.79.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.4", + "version": "0.79.5", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 404ac822..ccbe6692 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.4", + "version": "0.79.5", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index f5e5cdeb..02d15c6c 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.4", + "version": "0.79.5", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index c7cae39d..7f471fb6 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.4", + "version": "0.79.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.4", + "version": "0.79.5", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index ba925197..a14897e8 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.4", + "version": "0.79.5", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 84bc5c66..a27c9dc2 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.4", + "version": "1.9.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.4", + "version": "1.9.5", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index e2962554..1a0bf2e2 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.4", + "version": "1.9.5", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 555b18ba..91f7d746 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.4", + "version": "0.79.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.4", + "version": "0.79.5", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index db2ac17e..d1b9caf3 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.4", + "version": "0.79.5", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 2a3434d6..8171f15a 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.4", + "version": "0.79.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.4", + "version": "0.79.5", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.4", - "@earendil-works/pi-ai": "^0.79.4", - "@earendil-works/pi-tui": "^0.79.4", + "@earendil-works/pi-agent-core": "^0.79.5", + "@earendil-works/pi-ai": "^0.79.5", + "@earendil-works/pi-tui": "^0.79.5", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.4", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.4.tgz", + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.5.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.4", + "@earendil-works/pi-ai": "^0.79.5", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.4", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.4.tgz", + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.5.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -511,8 +511,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.4", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.4.tgz", + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.5.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index e67d13dd..96261f0d 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.4", + "version": "0.79.5", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.4", - "@earendil-works/pi-ai": "^0.79.4", - "@earendil-works/pi-tui": "^0.79.4", + "@earendil-works/pi-agent-core": "^0.79.5", + "@earendil-works/pi-ai": "^0.79.5", + "@earendil-works/pi-tui": "^0.79.5", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8ecc6e87..36aebf33 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.5] - 2026-06-16 ### Changed diff --git a/packages/tui/package.json b/packages/tui/package.json index b634c8d3..eddeb896 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.4", + "version": "0.79.5", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 0b0b9eae62157cdc48ac38a267bc7a2df705a069 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 19:54:26 +0200 Subject: [PATCH 068/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index f99f0831..ec27e984 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.5] - 2026-06-16 ## [0.79.4] - 2026-06-15 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b835ce48..6813c6ad 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.5] - 2026-06-16 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ea861202..d032a706 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.5] - 2026-06-16 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 36aebf33..e1ce9705 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.5] - 2026-06-16 ### Changed From a78cd7ccb18952c3139c086c54b488a87c6e32e4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 20:02:20 +0200 Subject: [PATCH 069/141] fix(coding-agent): stabilize self-update tests --- .../coding-agent/test/package-command-paths.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index 4cc1ce5e..df87cf34 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts"; import { ProjectTrustStore } from "../src/core/trust-manager.ts"; import { main } from "../src/main.ts"; +import { handlePackageCommand } from "../src/package-manager-cli.ts"; describe("package commands", () => { let tempDir: string; @@ -22,6 +23,10 @@ describe("package commands", () => { return `${major}.${minor}.${Number.parseInt(patch, 10) + 1}`; } + async function runPackageCommandDirectly(args: string[]): Promise { + expect(await handlePackageCommand(args)).toBe(true); + } + beforeEach(() => { tempDir = join(tmpdir(), `pi-package-commands-${Date.now()}-${Math.random().toString(36).slice(2)}`); agentDir = join(tempDir, "agent"); @@ -404,7 +409,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { - await expect(main(["update", "--self", "--force"])).resolves.toBeUndefined(); + await expect(runPackageCommandDirectly(["update", "--self", "--force"])).resolves.toBeUndefined(); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); @@ -448,7 +453,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { - await expect(main(["update", "--self"])).resolves.toBeUndefined(); + await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined(); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); @@ -497,7 +502,7 @@ else { const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { - await expect(main(["update", "--self"])).resolves.toBeUndefined(); + await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined(); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); @@ -550,7 +555,7 @@ if(args.includes("install")) process.exit(23); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); try { - await expect(main(["update", "--self"])).resolves.toBeUndefined(); + await expect(runPackageCommandDirectly(["update", "--self"])).resolves.toBeUndefined(); expect(process.exitCode).toBe(1); const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); From a93f0666076c6ea514a21995f35a633183a2215e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 20:03:36 +0200 Subject: [PATCH 070/141] fix(coding-agent): preserve fetch overrides --- packages/coding-agent/src/core/http-dispatcher.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/http-dispatcher.ts b/packages/coding-agent/src/core/http-dispatcher.ts index 2ec8144e..0910f4d6 100644 --- a/packages/coding-agent/src/core/http-dispatcher.ts +++ b/packages/coding-agent/src/core/http-dispatcher.ts @@ -10,6 +10,9 @@ export const HTTP_IDLE_TIMEOUT_CHOICES = [ { label: "disabled", timeoutMs: 0 }, ] as const; +const originalGlobalFetch = globalThis.fetch; +let installedGlobalFetch: typeof globalThis.fetch | undefined; + export function parseHttpIdleTimeoutMs(value: unknown): number | undefined { if (typeof value === "string") { const trimmed = value.trim(); @@ -58,5 +61,13 @@ export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TI // Keep fetch and the dispatcher on the same undici implementation. Node 26.0's // bundled fetch can otherwise consume compressed responses through npm undici's // dispatcher without decompressing them, causing response.json() failures. - undici.install?.(); + // If a caller replaced fetch after module load, preserve that deliberate override. + const shouldInstallGlobals = + installedGlobalFetch === undefined + ? globalThis.fetch === originalGlobalFetch + : globalThis.fetch === installedGlobalFetch; + if (shouldInstallGlobals) { + undici.install?.(); + installedGlobalFetch = globalThis.fetch; + } } From bd9f8773ad59aefc202ff470bd5240634ab9e81f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 23:14:35 +0200 Subject: [PATCH 071/141] fix(ai): restore OpenCode Go DeepSeek thinking controls --- packages/ai/CHANGELOG.md | 4 ++++ packages/ai/scripts/generate-models.ts | 5 +---- packages/ai/src/models.generated.ts | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6813c6ad..21e4b074 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenCode Go DeepSeek V4 thinking-off requests to send the provider's `thinking: { type: "disabled" }` compatibility parameter. + ## [0.79.5] - 2026-06-16 ### Added diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 7f4f9cf0..3d75a82e 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -1852,10 +1852,7 @@ async function generateModels() { for (const candidate of allModels) { if (candidate.api === "openai-completions" && candidate.id.includes("deepseek-v4")) { - const preservesNativeReasoningEffort = - candidate.provider === "openrouter" || - candidate.provider === "opencode" || - candidate.provider === "opencode-go"; + const preservesNativeReasoningEffort = candidate.provider === "openrouter" || candidate.provider === "opencode"; candidate.compat = { ...candidate.compat, ...(preservesNativeReasoningEffort diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 564d92e3..11851721 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -8572,7 +8572,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -8591,7 +8591,7 @@ export const MODELS = { api: "openai-completions", provider: "opencode-go", baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true}, + compat: {"maxTokensField":"max_tokens","requiresReasoningContentOnAssistantMessages":true,"thinkingFormat":"deepseek"}, reasoning: true, thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"max"}, input: ["text"], @@ -9499,13 +9499,13 @@ export const MODELS = { thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh"}, input: ["text"], cost: { - input: 0.098, - output: 0.196, + input: 0.09, + output: 0.18, cacheRead: 0.02, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 4096, + maxTokens: 65536, } satisfies Model<"openai-completions">, "deepseek/deepseek-v4-pro": { id: "deepseek/deepseek-v4-pro", @@ -10540,13 +10540,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.75, + input: 0.74, output: 3.5, - cacheRead: 0.16, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 16384, } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", @@ -12985,11 +12985,11 @@ export const MODELS = { cost: { input: 1.4, output: 4.4, - cacheRead: 0.26, + cacheRead: 0.25, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 16384, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", From 7da475db665a76228c2dd7bf86175c7ad0c3601e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 23:58:43 +0200 Subject: [PATCH 072/141] fix(ai): regenerate model catalog --- packages/ai/src/models.generated.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 11851721..52eac523 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -12985,11 +12985,11 @@ export const MODELS = { cost: { input: 1.4, output: 4.4, - cacheRead: 0.25, + cacheRead: 0.26, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 16384, + maxTokens: 262144, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", From 34b6aea1e6624320458169a945776963d160b3f4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 23:59:00 +0200 Subject: [PATCH 073/141] docs(coding-agent): add changelog entries for fetch override and DeepSeek V4 thinking-off fixes --- packages/coding-agent/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d032a706..95c585cc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Fixed + +- Fixed HTTP dispatcher configuration to preserve a caller's deliberate `fetch` override instead of reinstalling the undici global fetch over it. +- Fixed inherited OpenCode Go DeepSeek V4 thinking-off requests to send the provider's `thinking: { type: "disabled" }` compatibility parameter. + ## [0.79.5] - 2026-06-16 ### New Features From 31bfb2f16f7a1dd707876e970f0f80caa61f8435 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 23:59:14 +0200 Subject: [PATCH 074/141] Release v0.79.6 --- package-lock.json | 26 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +-- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +-- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +-- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +-- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +-- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++++--------- packages/coding-agent/package.json | 8 +++--- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 19 files changed, 50 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c0aa234..d4811db6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6176,10 +6176,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.5", + "version": "0.79.6", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.5", + "@earendil-works/pi-ai": "^0.79.6", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6213,7 +6213,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.5", + "version": "0.79.6", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6258,12 +6258,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.5", + "version": "0.79.6", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.5", - "@earendil-works/pi-ai": "^0.79.5", - "@earendil-works/pi-tui": "^0.79.5", + "@earendil-works/pi-agent-core": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-tui": "^0.79.6", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6304,32 +6304,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.5", + "version": "0.79.6", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.5" + "version": "0.79.6" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.5", + "version": "0.79.6", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.5", + "version": "1.9.6", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.5", + "version": "0.79.6", "dependencies": { "ms": "2.1.3" }, @@ -6365,7 +6365,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.5", + "version": "0.79.6", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index ec27e984..7f9cf03e 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.6] - 2026-06-16 ## [0.79.5] - 2026-06-16 diff --git a/packages/agent/package.json b/packages/agent/package.json index 4102aea3..66ee4847 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.5", + "version": "0.79.6", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.5", + "@earendil-works/pi-ai": "^0.79.6", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 21e4b074..776f5cef 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.6] - 2026-06-16 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index f99e4052..11e4261b 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.5", + "version": "0.79.6", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 95c585cc..87e933c5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.6] - 2026-06-16 ### Fixed diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index fd9cacc8..bd9ae9f3 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.5", + "version": "0.79.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.5", + "version": "0.79.6", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index ccbe6692..7f2d2bb4 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.5", + "version": "0.79.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 02d15c6c..0478f9c8 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.5", + "version": "0.79.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 7f471fb6..c573e8e0 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.5", + "version": "0.79.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.5", + "version": "0.79.6", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index a14897e8..71e89d6c 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.5", + "version": "0.79.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index a27c9dc2..24e20622 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.5", + "version": "1.9.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.5", + "version": "1.9.6", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 1a0bf2e2..7f1fa016 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.5", + "version": "1.9.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 91f7d746..fb5d53cd 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.5", + "version": "0.79.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.5", + "version": "0.79.6", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index d1b9caf3..b8d759de 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.5", + "version": "0.79.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 8171f15a..39313f9d 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.5", + "version": "0.79.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.5", + "version": "0.79.6", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.5", - "@earendil-works/pi-ai": "^0.79.5", - "@earendil-works/pi-tui": "^0.79.5", + "@earendil-works/pi-agent-core": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-tui": "^0.79.6", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.5.tgz", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.6.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.5", + "@earendil-works/pi-ai": "^0.79.6", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.5.tgz", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.6.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -511,8 +511,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.5.tgz", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.6.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 96261f0d..22b408ce 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.5", + "version": "0.79.6", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.5", - "@earendil-works/pi-ai": "^0.79.5", - "@earendil-works/pi-tui": "^0.79.5", + "@earendil-works/pi-agent-core": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-tui": "^0.79.6", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index e1ce9705..eb44f2e3 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.6] - 2026-06-16 ## [0.79.5] - 2026-06-16 diff --git a/packages/tui/package.json b/packages/tui/package.json index eddeb896..9c73829c 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.5", + "version": "0.79.6", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 12bb8dd2c965d2088f08d27a19a049c54de53ead Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 16 Jun 2026 23:59:17 +0200 Subject: [PATCH 075/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 7f9cf03e..41203247 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.6] - 2026-06-16 ## [0.79.5] - 2026-06-16 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 776f5cef..b69233a1 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.6] - 2026-06-16 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 87e933c5..e4bf022c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.6] - 2026-06-16 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index eb44f2e3..8fa900ce 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.6] - 2026-06-16 ## [0.79.5] - 2026-06-16 From 29c1504cc1e14822a9d058a51f59c19fb1ce076f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jun 2026 07:24:29 +0000 Subject: [PATCH 076/141] chore: approve contributor dodiego --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index f38c892d..a8db269c 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -239,3 +239,5 @@ vdxz pr dangooddd pr Mearman pr + +dodiego pr From 068ab5d19b9c7a17e14bde90bcbc4fc4c78da1b1 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Wed, 17 Jun 2026 11:04:01 +0200 Subject: [PATCH 077/141] fix(coding-agent): horizontally pan tree selector Fixes #5830 --- .../interactive/components/tree-selector.ts | 67 +++++++++++++++++-- packages/tui/src/index.ts | 2 +- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index 7c2cbf3e..8bcdc2fb 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -6,6 +6,7 @@ import { Input, type Keybinding, Spacer, + sliceByColumn, Text, truncateToWidth, visibleWidth, @@ -37,6 +38,59 @@ interface FlatNode { isVirtualRootChild: boolean; } +interface HorizontalViewportRow { + gutter: string; + body: string; + anchorCol: number; + bodyWidth: number; + isSelected: boolean; +} + +const TREE_GUTTER_WIDTH = 2; +const MIN_VISIBLE_ANCHOR_CONTENT_WIDTH = 4; +const MAX_VISIBLE_ANCHOR_CONTENT_WIDTH = 20; +const MIN_ANCHOR_CONTEXT_WIDTH = 2; +const MAX_ANCHOR_CONTEXT_WIDTH = 12; + +/** + * Render tree rows into a horizontally clipped viewport. + * + * The tree gutter is always kept visible. The row bodies are shifted left only + * when the selected row's anchor (the start of its entry text after tree + * indentation/markers) would otherwise be too far right to see useful content. + */ +function renderHorizontalViewport(rows: HorizontalViewportRow[], width: number): string[] { + const viewportWidth = Math.max(0, width - TREE_GUTTER_WIDTH); + const maxBodyWidth = rows.reduce((max, row) => Math.max(max, row.bodyWidth), 0); + const maxHorizontalScroll = Math.max(0, maxBodyWidth - viewportWidth); + const selectedRow = rows.find((row) => row.isSelected); + + // Only pan horizontally when needed to keep enough selected-row content visible after its anchor. + let horizontalScroll = 0; + if (selectedRow && maxHorizontalScroll > 0) { + const minVisibleAnchorContentWidth = Math.min( + MAX_VISIBLE_ANCHOR_CONTENT_WIDTH, + Math.max(MIN_VISIBLE_ANCHOR_CONTENT_WIDTH, Math.floor(viewportWidth / 3)), + ); + if (selectedRow.anchorCol > viewportWidth - minVisibleAnchorContentWidth) { + const anchorContextWidth = Math.min( + MAX_ANCHOR_CONTEXT_WIDTH, + Math.max(MIN_ANCHOR_CONTEXT_WIDTH, Math.floor(viewportWidth / 4)), + ); + horizontalScroll = Math.min(maxHorizontalScroll, selectedRow.anchorCol - anchorContextWidth); + } + } + + // Clip only the body; the fixed-width gutter remains visible as navigation context. + return rows.map((row) => { + const line = + horizontalScroll > 0 + ? `${row.gutter}${sliceByColumn(row.body, horizontalScroll, viewportWidth, true)}\x1b[0m` + : row.gutter + row.body; + return truncateToWidth(line, width, ""); + }); +} + /** Filter mode for tree display */ export type FilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "all"; @@ -619,6 +673,7 @@ class TreeList implements Component { ); const endIndex = Math.min(startIndex + this.maxVisibleLines, this.filteredNodes.length); + const renderedRows: HorizontalViewportRow[] = []; for (let i = startIndex; i < endIndex; i++) { const flatNode = this.filteredNodes[i]; const entry = flatNode.node.entry; @@ -682,14 +737,18 @@ class TreeList implements Component { ? theme.fg("muted", `${this.formatLabelTimestamp(flatNode.node.labelTimestamp)} `) : ""; const content = this.getEntryDisplayText(flatNode.node, isSelected); - - let line = cursor + theme.fg("dim", prefix) + foldMarker + pathMarker + label + labelTimestamp + content; + const prefixPart = theme.fg("dim", prefix) + foldMarker + pathMarker; + const anchorCol = visibleWidth(prefixPart); + let gutter = cursor; + let body = prefixPart + label + labelTimestamp + content; if (isSelected) { - line = theme.bg("selectedBg", line); + gutter = theme.bg("selectedBg", gutter); + body = theme.bg("selectedBg", body); } - lines.push(truncateToWidth(line, width)); + renderedRows.push({ gutter, body, anchorCol, bodyWidth: visibleWidth(body), isSelected }); } + lines.push(...renderHorizontalViewport(renderedRows, width)); lines.push( truncateToWidth( theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredNodes.length})${this.getStatusLabels()}`), diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 47bb5240..e22bf08a 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -106,4 +106,4 @@ export { TUI, } from "./tui.ts"; // Utilities -export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.ts"; +export { sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "./utils.ts"; From ae89286d076748a02034131af50d165879fac3a7 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Wed, 17 Jun 2026 14:48:35 +0200 Subject: [PATCH 078/141] docs: update changelogs for tree panning closes #5830 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/tui/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e4bf022c..73e538fc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the tree navigator to horizontally pan deep entries so the selected item remains readable ([#5830](https://github.com/earendil-works/pi/issues/5830)). + ## [0.79.6] - 2026-06-16 ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 8fa900ce..9b90a5b1 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Exported `sliceByColumn` for ANSI-aware horizontal column slicing. + ## [0.79.6] - 2026-06-16 ## [0.79.5] - 2026-06-16 From 6d5ede31c8b8584b422bd0fa2ce10a39b2a0cdce Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 17 Jun 2026 17:16:39 +0200 Subject: [PATCH 079/141] fix(coding-agent): match provider-first model searches --- packages/coding-agent/CHANGELOG.md | 1 + .../interactive/components/model-selector.ts | 7 ++- .../components/scoped-models-selector.ts | 7 ++- .../src/modes/interactive/interactive-mode.ts | 6 ++- .../src/modes/interactive/model-search.ts | 11 ++++ .../test/interactive-mode-status.test.ts | 53 +++++++++++++++++++ 6 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 packages/coding-agent/src/modes/interactive/model-search.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 73e538fc..3bbc9cbd 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed `/model` autocomplete and model selection searches to match provider/model queries regardless of whether the provider or model token is typed first. - Fixed the tree navigator to horizontally pan deep entries so the selected item remains readable ([#5830](https://github.com/earendil-works/pi/issues/5830)). ## [0.79.6] - 2026-06-16 diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index b9f5ec77..a3cdf7d5 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -11,6 +11,7 @@ import { } from "@earendil-works/pi-tui"; import type { ModelRegistry } from "../../../core/model-registry.ts"; import type { SettingsManager } from "../../../core/settings-manager.ts"; +import { getModelSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyHint } from "./keybinding-hints.ts"; @@ -217,10 +218,8 @@ export class ModelSelectorComponent extends Container implements Focusable { private filterModels(query: string): void { this.filteredModels = query - ? fuzzyFilter( - this.activeModels, - query, - ({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`, + ? fuzzyFilter(this.activeModels, query, ({ id, provider, model }) => + getModelSearchText({ id, provider, name: model.name }), ) : this.activeModels; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1)); diff --git a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts index 06ce9169..772e3af0 100644 --- a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -10,6 +10,7 @@ import { Spacer, Text, } from "@earendil-works/pi-tui"; +import { getModelSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyText } from "./keybinding-hints.ts"; @@ -182,7 +183,11 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl private refresh(): void { const query = this.searchInput.getValue(); const items = this.buildItems(); - this.filteredItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items; + this.filteredItems = query + ? fuzzyFilter(items, query, (i) => + getModelSearchText({ id: i.model.id, provider: i.model.provider, name: i.model.name }), + ) + : items; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1)); this.updateList(); this.footerText.setText(this.getFooterText()); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 56ef9098..c2dc7875 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -125,6 +125,7 @@ import { TreeSelectorComponent } from "./components/tree-selector.ts"; import { TrustSelectorComponent } from "./components/trust-selector.ts"; import { UserMessageComponent } from "./components/user-message.ts"; import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; +import { getModelSearchText } from "./model-search.ts"; import { detectTerminalBackgroundTheme, getAvailableThemes, @@ -518,11 +519,12 @@ export class InteractiveMode { const items = models.map((m) => ({ id: m.id, provider: m.provider, + name: m.name, label: `${m.provider}/${m.id}`, })); - // Fuzzy filter by model ID + provider (allows "opus anthropic" to match) - const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`); + // Fuzzy filter by model ID + provider in either order. + const filtered = fuzzyFilter(items, prefix, getModelSearchText); if (filtered.length === 0) return null; diff --git a/packages/coding-agent/src/modes/interactive/model-search.ts b/packages/coding-agent/src/modes/interactive/model-search.ts new file mode 100644 index 00000000..f1dbc73c --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/model-search.ts @@ -0,0 +1,11 @@ +export interface ModelSearchItem { + id: string; + provider: string; + name?: string; +} + +export function getModelSearchText(item: ModelSearchItem): string { + const { id, provider } = item; + const name = item.name ? ` ${item.name}` : ""; + return `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`; +} diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 7ec54c7b..8f1018b2 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -356,6 +356,59 @@ describe("InteractiveMode.setupAutocompleteProvider", () => { }); }); +describe("InteractiveMode.createBaseAutocompleteProvider", () => { + test("matches model command arguments across provider/model order", async () => { + type TestModel = { id: string; provider: string; name: string }; + type FakeInteractiveMode = { + session: { + scopedModels: Array<{ model: TestModel }>; + modelRegistry: { getAvailable: () => TestModel[] }; + promptTemplates: []; + extensionRunner: { getRegisteredCommands: () => [] }; + resourceLoader: { getSkills: () => { skills: [] } }; + }; + settingsManager: { getEnableSkillCommands: () => boolean }; + skillCommands: Map; + sessionManager: { getCwd: () => string }; + fdPath: null; + }; + + const createBaseAutocompleteProvider = ( + InteractiveMode as unknown as { + prototype: { createBaseAutocompleteProvider(this: FakeInteractiveMode): AutocompleteProvider }; + } + ).prototype.createBaseAutocompleteProvider; + const models = [ + { id: "gpt-5.2-codex", provider: "github-copilot", name: "GPT-5.2 Codex" }, + { id: "gpt-5.5", provider: "openai-codex", name: "GPT-5.5" }, + ]; + const fakeThis: FakeInteractiveMode = { + session: { + scopedModels: [], + modelRegistry: { getAvailable: () => models }, + promptTemplates: [], + extensionRunner: { getRegisteredCommands: () => [] }, + resourceLoader: { getSkills: () => ({ skills: [] }) }, + }, + settingsManager: { getEnableSkillCommands: () => false }, + skillCommands: new Map(), + sessionManager: { getCwd: () => "/tmp" }, + fdPath: null, + }; + + const provider = createBaseAutocompleteProvider.call(fakeThis); + const line = "/model codexgpt"; + const suggestions = await provider.getSuggestions([line], 0, line.length, { + signal: new AbortController().signal, + }); + + expect(suggestions?.items.map((item) => item.value)).toEqual([ + "openai-codex/gpt-5.5", + "github-copilot/gpt-5.2-codex", + ]); + }); +}); + describe("InteractiveMode.showLoadedResources", () => { beforeAll(() => { initTheme("dark"); From 58dd2f5996b23eda709582fb7883d63842a5ce98 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 18 Jun 2026 10:06:23 +0200 Subject: [PATCH 080/141] feat(ai): add GLM-5.2 to OpenCode Go model catalog closes #5860 --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/models.generated.ts | 171 +++++++++++++++++++--------- 2 files changed, 124 insertions(+), 51 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b69233a1..a49ddf52 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added GLM-5.2 model to the OpenCode Go subscription model catalog ([#5860](https://github.com/earendil-works/pi/issues/5860)). + ## [0.79.6] - 2026-06-16 ### Fixed diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 52eac523..424f8bcd 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3885,7 +3885,7 @@ export const MODELS = { cost: { input: 0.14, output: 0.28, - cacheRead: 0.03, + cacheRead: 0.028, cacheWrite: 0, }, contextWindow: 1000000, @@ -3927,6 +3927,24 @@ export const MODELS = { contextWindow: 202800, maxTokens: 131072, } satisfies Model<"anthropic-messages">, + "accounts/fireworks/models/glm-5p2": { + id: "accounts/fireworks/models/glm-5p2", + name: "GLM 5.2", + api: "anthropic-messages", + provider: "fireworks", + baseUrl: "https://api.fireworks.ai/inference", + compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1048576, + maxTokens: 131072, + } satisfies Model<"anthropic-messages">, "accounts/fireworks/models/gpt-oss-120b": { id: "accounts/fireworks/models/gpt-oss-120b", name: "GPT OSS 120B", @@ -3939,7 +3957,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.6, - cacheRead: 0.01, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 131072, @@ -4117,7 +4135,7 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 2, + input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 0, @@ -8604,24 +8622,6 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 384000, } satisfies Model<"openai-completions">, - "glm-5": { - id: "glm-5", - name: "GLM-5", - api: "openai-completions", - provider: "opencode-go", - baseUrl: "https://opencode.ai/zen/go/v1", - compat: {"maxTokensField":"max_tokens"}, - reasoning: true, - input: ["text"], - cost: { - input: 1, - output: 3.2, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 202752, - maxTokens: 32768, - } satisfies Model<"openai-completions">, "glm-5.1": { id: "glm-5.1", name: "GLM-5.1", @@ -8640,6 +8640,24 @@ export const MODELS = { contextWindow: 202752, maxTokens: 32768, } satisfies Model<"openai-completions">, + "glm-5.2": { + id: "glm-5.2", + name: "GLM-5.2", + api: "openai-completions", + provider: "opencode-go", + baseUrl: "https://opencode.ai/zen/go/v1", + compat: {"maxTokensField":"max_tokens"}, + reasoning: true, + input: ["text"], + cost: { + input: 1.4, + output: 4.4, + cacheRead: 0.26, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "kimi-k2.6": { id: "kimi-k2.6", name: "Kimi K2.6", @@ -9352,6 +9370,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4000, } satisfies Model<"openai-completions">, + "cohere/north-mini-code:free": { + id: "cohere/north-mini-code:free", + name: "Cohere: North Mini Code (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 64000, + } satisfies Model<"openai-completions">, "deepseek/deepseek-chat": { id: "deepseek/deepseek-chat", name: "DeepSeek: DeepSeek V3", @@ -9660,7 +9695,24 @@ export const MODELS = { cacheWrite: 0.083333, }, contextWindow: 1048576, - maxTokens: 65536, + maxTokens: 65535, + } satisfies Model<"openai-completions">, + "google/gemini-3-pro-image": { + id: "google/gemini-3-pro-image", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 12, + cacheRead: 0.2, + cacheWrite: 0.375, + }, + contextWindow: 65536, + maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { id: "google/gemini-3.1-flash-lite", @@ -9847,7 +9899,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 32768, + maxTokens: 8192, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -9952,6 +10004,23 @@ export const MODELS = { contextWindow: 256000, maxTokens: 80000, } satisfies Model<"openai-completions">, + "liquid/lfm-2.5-1.2b-thinking:free": { + id: "liquid/lfm-2.5-1.2b-thinking:free", + name: "LiquidAI: LFM2.5-1.2B-Thinking (free)", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 32768, + maxTokens: 4096, + } satisfies Model<"openai-completions">, "meta-llama/llama-3.1-70b-instruct": { id: "meta-llama/llama-3.1-70b-instruct", name: "Meta: Llama 3.1 70B Instruct", @@ -10677,8 +10746,8 @@ export const MODELS = { input: ["text"], cost: { input: 0.5, - output: 2.5, - cacheRead: 0.15, + output: 2.2, + cacheRead: 0.1, cacheWrite: 0, }, contextWindow: 1000000, @@ -11493,7 +11562,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 8192, + maxTokens: 32768, } satisfies Model<"openai-completions">, "openai/gpt-oss-safeguard-20b": { id: "openai/gpt-oss-safeguard-20b", @@ -11784,6 +11853,23 @@ export const MODELS = { contextWindow: 131072, maxTokens: 16384, } satisfies Model<"openai-completions">, + "qwen/qwen-2.5-7b-instruct": { + id: "qwen/qwen-2.5-7b-instruct", + name: "Qwen: Qwen2.5 7B Instruct", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.04, + output: 0.1, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "qwen/qwen-plus": { id: "qwen/qwen-plus", name: "Qwen: Qwen-Plus", @@ -12753,23 +12839,6 @@ export const MODELS = { contextWindow: 256000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "xiaomi/mimo-v2-flash": { - id: "xiaomi/mimo-v2-flash", - name: "Xiaomi: MiMo-V2-Flash", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: true, - input: ["text"], - cost: { - input: 0.1, - output: 0.3, - cacheRead: 0.01, - cacheWrite: 0, - }, - contextWindow: 262144, - maxTokens: 65536, - } satisfies Model<"openai-completions">, "xiaomi/mimo-v2.5": { id: "xiaomi/mimo-v2.5", name: "Xiaomi: MiMo-V2.5", @@ -14948,13 +15017,13 @@ export const MODELS = { reasoning: false, input: ["text"], cost: { - input: 0.02, - output: 0.04, + input: 0.15, + output: 0.15, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, - maxTokens: 131072, + contextWindow: 128000, + maxTokens: 128000, } satisfies Model<"anthropic-messages">, "mistral/mistral-small": { id: "mistral/mistral-small", @@ -15107,7 +15176,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 32768, } satisfies Model<"anthropic-messages">, "nvidia/nemotron-3-super-120b-a12b": { id: "nvidia/nemotron-3-super-120b-a12b", @@ -16354,9 +16423,9 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.26, + input: 1.5, + output: 4.5, + cacheRead: 0.3, cacheWrite: 0, }, contextWindow: 1000000, From 008c76f955ae95b6a15703064cc313fdd7b0fde0 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 18 Jun 2026 10:27:11 +0200 Subject: [PATCH 081/141] feat(coding-agent): export project config dir name --- packages/coding-agent/CHANGELOG.md | 8 ++++++++ packages/coding-agent/docs/extensions.md | 14 ++++++++++++++ packages/coding-agent/docs/sdk.md | 3 ++- .../coding-agent/examples/extensions/preset.ts | 14 ++++++++++---- .../examples/extensions/provider-payload.ts | 10 +++++----- .../examples/extensions/sandbox/index.ts | 4 ++-- .../examples/extensions/subagent/agents.ts | 4 ++-- .../examples/extensions/subagent/index.ts | 12 +++++++++--- packages/coding-agent/src/core/project-trust.ts | 3 ++- packages/coding-agent/src/index.ts | 10 +++++++++- .../interactive/components/config-selector.ts | 10 +++++----- .../src/modes/interactive/interactive-mode.ts | 3 ++- packages/coding-agent/src/package-manager-cli.ts | 5 +++-- 13 files changed, 73 insertions(+), 27 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3bbc9cbd..716b210f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Added + +- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi`. + +### Changed + +- Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. + ### Fixed - Fixed `/model` autocomplete and model selection searches to match provider/model queries regardless of whether the provider or model token is typed first. diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index ab3188ce..5a559813 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -898,6 +898,20 @@ Current run mode: `"tui"`, `"rpc"`, `"json"`, or `"print"`. Use `ctx.mode === "t Current working directory. +Use `CONFIG_DIR_NAME` instead of hardcoding `.pi` when constructing project-local config paths. Rebranded distributions can use a different config directory name. + +```typescript +import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { join } from "node:path"; + +export default function (pi: ExtensionAPI) { + pi.on("session_start", (_event, ctx) => { + const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json"); + // ... + }); +} +``` + ### ctx.isProjectTrusted() Returns whether project-local trust is active for the current session context. This includes temporary trust decisions and CLI trust overrides, not just saved decisions in the global trust store. diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index c6b2a75d..0c521e74 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -1110,7 +1110,8 @@ DefaultResourceLoader type ResourceLoader createEventBus -// Helpers +// Constants and helpers +CONFIG_DIR_NAME defineTool getAgentDir getPackageDir diff --git a/packages/coding-agent/examples/extensions/preset.ts b/packages/coding-agent/examples/extensions/preset.ts index 92224ec2..b78237ca 100644 --- a/packages/coding-agent/examples/extensions/preset.ts +++ b/packages/coding-agent/examples/extensions/preset.ts @@ -42,7 +42,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import type { Api, Model } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent"; +import { CONFIG_DIR_NAME, DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent"; import { Container, Key, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui"; // Preset configuration @@ -69,7 +69,7 @@ interface PresetsConfig { */ function loadPresets(cwd: string): PresetsConfig { const globalPath = join(getAgentDir(), "presets.json"); - const projectPath = join(cwd, ".pi", "presets.json"); + const projectPath = join(cwd, CONFIG_DIR_NAME, "presets.json"); let globalPresets: PresetsConfig = {}; let projectPresets: PresetsConfig = {}; @@ -200,7 +200,10 @@ export default function presetExtension(pi: ExtensionAPI) { const presetNames = Object.keys(presets); if (presetNames.length === 0) { - ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning"); + ctx.ui.notify( + `No presets defined. Add presets to ${join(getAgentDir(), "presets.json")} or ${join(ctx.cwd, CONFIG_DIR_NAME, "presets.json")}`, + "warning", + ); return; } @@ -308,7 +311,10 @@ export default function presetExtension(pi: ExtensionAPI) { async function cyclePreset(ctx: ExtensionContext): Promise { const presetNames = getPresetOrder(); if (presetNames.length === 0) { - ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning"); + ctx.ui.notify( + `No presets defined. Add presets to ${join(getAgentDir(), "presets.json")} or ${join(ctx.cwd, CONFIG_DIR_NAME, "presets.json")}`, + "warning", + ); return; } diff --git a/packages/coding-agent/examples/extensions/provider-payload.ts b/packages/coding-agent/examples/extensions/provider-payload.ts index 860ddc00..7f02a077 100644 --- a/packages/coding-agent/examples/extensions/provider-payload.ts +++ b/packages/coding-agent/examples/extensions/provider-payload.ts @@ -1,18 +1,18 @@ import { appendFileSync } from "node:fs"; import { join } from "node:path"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; export default function (pi: ExtensionAPI) { - const logFile = join(process.cwd(), ".pi", "provider-payload.log"); - - pi.on("before_provider_request", (event) => { + pi.on("before_provider_request", (event, ctx) => { + const logFile = join(ctx.cwd, CONFIG_DIR_NAME, "provider-payload.log"); appendFileSync(logFile, `${JSON.stringify(event.payload, null, 2)}\n\n`, "utf8"); // Optional: replace the payload instead of only logging it. // return { ...event.payload, temperature: 0 }; }); - pi.on("after_provider_response", (event) => { + pi.on("after_provider_response", (event, ctx) => { + const logFile = join(ctx.cwd, CONFIG_DIR_NAME, "provider-payload.log"); appendFileSync(logFile, `[${event.status}] ${JSON.stringify(event.headers)}\n\n`, "utf8"); }); } diff --git a/packages/coding-agent/examples/extensions/sandbox/index.ts b/packages/coding-agent/examples/extensions/sandbox/index.ts index 94f8f1cf..b54d75d1 100644 --- a/packages/coding-agent/examples/extensions/sandbox/index.ts +++ b/packages/coding-agent/examples/extensions/sandbox/index.ts @@ -46,7 +46,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { type BashOperations, createBashTool, getAgentDir } from "@earendil-works/pi-coding-agent"; +import { type BashOperations, CONFIG_DIR_NAME, createBashTool, getAgentDir } from "@earendil-works/pi-coding-agent"; interface SandboxConfig extends SandboxRuntimeConfig { enabled?: boolean; @@ -77,7 +77,7 @@ const DEFAULT_CONFIG: SandboxConfig = { }; function loadConfig(cwd: string): SandboxConfig { - const projectConfigPath = join(cwd, ".pi", "sandbox.json"); + const projectConfigPath = join(cwd, CONFIG_DIR_NAME, "sandbox.json"); const globalConfigPath = join(getAgentDir(), "extensions", "sandbox.json"); let globalConfig: Partial = {}; diff --git a/packages/coding-agent/examples/extensions/subagent/agents.ts b/packages/coding-agent/examples/extensions/subagent/agents.ts index eab6c630..c41ef579 100644 --- a/packages/coding-agent/examples/extensions/subagent/agents.ts +++ b/packages/coding-agent/examples/extensions/subagent/agents.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; +import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; export type AgentScope = "user" | "project" | "both"; @@ -85,7 +85,7 @@ function isDirectory(p: string): boolean { function findNearestProjectAgentsDir(cwd: string): string | null { let currentDir = cwd; while (true) { - const candidate = path.join(currentDir, ".pi", "agents"); + const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents"); if (isDirectory(candidate)) return candidate; const parentDir = path.dirname(currentDir); diff --git a/packages/coding-agent/examples/extensions/subagent/index.ts b/packages/coding-agent/examples/extensions/subagent/index.ts index 92b471a3..832dcc74 100644 --- a/packages/coding-agent/examples/extensions/subagent/index.ts +++ b/packages/coding-agent/examples/extensions/subagent/index.ts @@ -19,7 +19,13 @@ import * as path from "node:path"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { Message } from "@earendil-works/pi-ai"; import { StringEnum } from "@earendil-works/pi-ai"; -import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent"; +import { + CONFIG_DIR_NAME, + type ExtensionAPI, + getAgentDir, + getMarkdownTheme, + withFileMutationQueue, +} from "@earendil-works/pi-coding-agent"; import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts"; @@ -458,8 +464,8 @@ export default function (pi: ExtensionAPI) { description: [ "Delegate tasks to specialized subagents with isolated context.", "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).", - 'Default agent scope is "user" (from ~/.pi/agent/agents).', - 'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").', + `Default agent scope is "user" (from ${path.join(getAgentDir(), "agents")}).`, + `To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" (or "project").`, ].join(" "), parameters: SubagentParams, diff --git a/packages/coding-agent/src/core/project-trust.ts b/packages/coding-agent/src/core/project-trust.ts index c0892439..2521ad5d 100644 --- a/packages/coding-agent/src/core/project-trust.ts +++ b/packages/coding-agent/src/core/project-trust.ts @@ -1,3 +1,4 @@ +import { CONFIG_DIR_NAME } from "../config.ts"; import { emitProjectTrustEvent } from "./extensions/runner.ts"; import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/types.ts"; import type { DefaultProjectTrust } from "./settings-manager.ts"; @@ -21,7 +22,7 @@ export interface ResolveProjectTrustedOptions { } function formatProjectTrustPrompt(cwd: string): string { - return `Trust project folder?\n${cwd}\n\nThis allows pi to load .pi settings and resources, install missing project packages, and execute project extensions.`; + return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`; } async function selectProjectTrustOption( diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 3176a167..70416af1 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -3,7 +3,15 @@ export { type Args, parseArgs } from "./cli/args.ts"; // Config paths -export { getAgentDir, getDocsPath, getExamplesPath, getPackageDir, getReadmePath, VERSION } from "./config.ts"; +export { + CONFIG_DIR_NAME, + getAgentDir, + getDocsPath, + getExamplesPath, + getPackageDir, + getReadmePath, + VERSION, +} from "./config.ts"; export { AgentSession, type AgentSessionConfig, diff --git a/packages/coding-agent/src/modes/interactive/components/config-selector.ts b/packages/coding-agent/src/modes/interactive/components/config-selector.ts index 93ef4bea..7c46841d 100644 --- a/packages/coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/config-selector.ts @@ -73,7 +73,7 @@ function formatBaseDir(baseDir: string): string { return displayPath.endsWith("/") ? displayPath : `${displayPath}/`; } -function getGroupLabel(metadata: PathMetadata): string { +function getGroupLabel(metadata: PathMetadata, agentDir: string): string { if (metadata.origin === "package") { return `${metadata.source} (${metadata.scope})`; } @@ -84,12 +84,12 @@ function getGroupLabel(metadata: PathMetadata): string { ? `User (${formatBaseDir(metadata.baseDir)})` : `Project (${formatBaseDir(metadata.baseDir)})`; } - return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)"; + return metadata.scope === "user" ? `User (${formatBaseDir(agentDir)})` : `Project (${CONFIG_DIR_NAME}/)`; } return metadata.scope === "user" ? "User settings" : "Project settings"; } -function buildGroups(resolved: ResolvedPaths): ResourceGroup[] { +function buildGroups(resolved: ResolvedPaths, agentDir: string): ResourceGroup[] { const groupMap = new Map(); const addToGroup = (resources: ResolvedResource[], resourceType: ResourceType) => { @@ -100,7 +100,7 @@ function buildGroups(resolved: ResolvedPaths): ResourceGroup[] { if (!groupMap.has(groupKey)) { groupMap.set(groupKey, { key: groupKey, - label: getGroupLabel(metadata), + label: getGroupLabel(metadata, agentDir), scope: metadata.scope, origin: metadata.origin, source: metadata.source, @@ -601,7 +601,7 @@ export class ConfigSelectorComponent extends Container implements Focusable { ) { super(); - const groups = buildGroups(resolvedPaths); + const groups = buildGroups(resolvedPaths, agentDir); // Add header this.addChild(new Spacer(1)); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c2dc7875..ad9d3d3a 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -52,6 +52,7 @@ import { spawn, spawnSync } from "child_process"; import { APP_NAME, APP_TITLE, + CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, @@ -3307,7 +3308,7 @@ export class InteractiveMode { new Text( theme.fg( "warning", - "This project is not trusted. Project .pi resources and packages are ignored. Use /trust to save a trust decision, then restart pi.", + `This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart pi.`, ), 1, 0, diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index 0dbe750c..e00de1b4 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -4,6 +4,7 @@ import { selectConfig } from "./cli/config-selector.ts"; import { createProjectTrustContext } from "./cli/project-trust.ts"; import { APP_NAME, + CONFIG_DIR_NAME, detectInstallMethod, getAgentDir, getPackageDir, @@ -93,7 +94,7 @@ function printPackageCommandHelp(command: PackageCommand): void { Install a package and add it to settings. Options: - -l, --local Install project-locally (.pi/settings.json) + -l, --local Install project-locally (${CONFIG_DIR_NAME}/settings.json) -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command @@ -115,7 +116,7 @@ Remove a package and its source from settings. Alias: ${APP_NAME} uninstall [-l] Options: - -l, --local Remove from project settings (.pi/settings.json) + -l, --local Remove from project settings (${CONFIG_DIR_NAME}/settings.json) -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command From 51f752358712bc69f313d75ec505db244d8c7ff5 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 18 Jun 2026 10:44:59 +0200 Subject: [PATCH 082/141] fix(coding-agent): include RPC request id on unknown commands closes #5868 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/modes/rpc/rpc-mode.ts | 2 +- .../5868-rpc-unknown-command-id.test.ts | 113 ++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/suite/regressions/5868-rpc-unknown-command-id.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 716b210f..deb0a5e5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,7 @@ ### Fixed +- Fixed RPC unknown-command errors to include the request id so clients do not hang waiting for a response ([#5868](https://github.com/earendil-works/pi/issues/5868)). - Fixed `/model` autocomplete and model selection searches to match provider/model queries regardless of whether the provider or model token is typed first. - Fixed the tree navigator to horizontally pan deep entries so the selected item remains readable ([#5830](https://github.com/earendil-works/pi/issues/5830)). diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 9bb1f089..1150b826 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -667,7 +667,7 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise ({ + outputLines: [] as string[], + lineHandler: undefined as ((line: string) => void) | undefined, +})); + +vi.mock("../../../src/core/output-guard.js", () => ({ + flushRawStdout: vi.fn(async () => {}), + takeOverStdout: vi.fn(), + waitForRawStdoutBackpressure: vi.fn(async () => {}), + writeRawStdout: (line: string) => { + rpcIo.outputLines.push(line); + }, +})); + +vi.mock("../../../src/modes/interactive/theme/theme.js", () => ({ theme: {} })); + +vi.mock("../../../src/modes/rpc/jsonl.js", () => ({ + attachJsonlLineReader: vi.fn((_stream: NodeJS.ReadableStream, onLine: (line: string) => void) => { + rpcIo.lineHandler = onLine; + return () => { + rpcIo.lineHandler = undefined; + }; + }), + serializeJsonLine: (value: unknown) => `${JSON.stringify(value)}\n`, +})); + +type NodeListener = Parameters[1]; + +type ListenerSnapshot = { + stdinEnd: NodeListener[]; + signals: Map; +}; + +function takeListenerSnapshot(): ListenerSnapshot { + const signals: NodeJS.Signals[] = process.platform === "win32" ? ["SIGTERM"] : ["SIGTERM", "SIGHUP"]; + return { + stdinEnd: process.stdin.listeners("end") as NodeListener[], + signals: new Map(signals.map((signal) => [signal, process.listeners(signal) as NodeListener[]])), + }; +} + +function restoreListeners(snapshot: ListenerSnapshot): void { + for (const listener of process.stdin.listeners("end") as NodeListener[]) { + if (!snapshot.stdinEnd.includes(listener)) { + process.stdin.off("end", listener); + } + } + + for (const [signal, previousListeners] of snapshot.signals) { + for (const listener of process.listeners(signal) as NodeListener[]) { + if (!previousListeners.includes(listener)) { + process.off(signal, listener); + } + } + } +} + +function parseOutputLines(): Array> { + return rpcIo.outputLines + .flatMap((line) => line.split("\n")) + .filter((line) => line.trim().length > 0) + .map((line) => JSON.parse(line) as Record); +} + +function createRuntimeHost(harness: Harness): AgentSessionRuntime { + return { + session: harness.session, + newSession: vi.fn(async () => ({ cancelled: true })), + switchSession: vi.fn(async () => ({ cancelled: true })), + fork: vi.fn(async () => ({ cancelled: true, selectedText: "" })), + dispose: vi.fn(async () => {}), + setRebindSession: vi.fn(), + } as unknown as AgentSessionRuntime; +} + +describe("RPC unknown command responses (#5868)", () => { + afterEach(() => { + rpcIo.outputLines = []; + rpcIo.lineHandler = undefined; + }); + + test("preserves the request id on unknown command errors", async () => { + const listenerSnapshot = takeListenerSnapshot(); + const harness = await createHarness(); + + try { + void runRpcMode(createRuntimeHost(harness)); + await vi.waitFor(() => expect(rpcIo.lineHandler).toBeDefined()); + + rpcIo.lineHandler?.(JSON.stringify({ id: "test", type: "foobar" })); + + await vi.waitFor(() => { + expect(parseOutputLines()).toContainEqual({ + id: "test", + type: "response", + command: "foobar", + success: false, + error: "Unknown command: foobar", + }); + }); + } finally { + harness.cleanup(); + restoreListeners(listenerSnapshot); + } + }); +}); From 7a14325b24e91824a57916d5d2d44a8b45cdefd4 Mon Sep 17 00:00:00 2001 From: Diego de Oliveira Date: Thu, 18 Jun 2026 05:45:51 -0300 Subject: [PATCH 083/141] feat(tui): detect Warp terminal and enable Kitty image protocol (#5841) Warp supports the Kitty graphics protocol and OSC 8 hyperlinks, but pi's terminal-image capability detection did not recognize it. Add detection via TERM_PROGRAM=WarpTerminal, WARP_SESSION_ID, and WARP_TERMINAL_SESSION_UUID so images render inline without requiring the TERM_PROGRAM=kitty workaround. Fixes #5827 --- packages/coding-agent/docs/tui.md | 2 +- packages/tui/src/terminal-image.ts | 5 +++ packages/tui/test/terminal-image.test.ts | 44 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index c3b31204..c8ff3554 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -257,7 +257,7 @@ md.setText("Updated markdown"); ### Image -Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm). +Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm, Warp). ```typescript const image = new Image( diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index d7012c1d..8854a86a 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -92,6 +92,11 @@ export function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeT return { images: "kitty", trueColor: true, hyperlinks: true }; } + // Warp supports the Kitty graphics protocol and OSC 8 hyperlinks. + if (termProgram === "warpterminal" || process.env.WARP_SESSION_ID || process.env.WARP_TERMINAL_SESSION_UUID) { + return { images: "kitty", trueColor: true, hyperlinks: true }; + } + if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") { return { images: "iterm2", trueColor: true, hyperlinks: true }; } diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index 4e9a3fc9..cc7e01e5 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -30,6 +30,8 @@ const ENV_KEYS = [ "ITERM_SESSION_ID", "WT_SESSION", "CMUX_WORKSPACE_ID", + "WARP_SESSION_ID", + "WARP_TERMINAL_SESSION_UUID", ] as const; function withEnv(overrides: Record, fn: () => void): void { @@ -271,6 +273,48 @@ describe("detectCapabilities", () => { }); }); + it("enables images and hyperlinks for Warp via TERM_PROGRAM", () => { + withEnv({ TERM_PROGRAM: "WarpTerminal" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.images, "kitty"); + assert.strictEqual(caps.trueColor, true); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("enables images and hyperlinks for Warp via WARP_SESSION_ID", () => { + withEnv({ WARP_SESSION_ID: "some-session-id" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.images, "kitty"); + assert.strictEqual(caps.trueColor, true); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("enables images and hyperlinks for Warp via WARP_TERMINAL_SESSION_UUID", () => { + withEnv({ WARP_TERMINAL_SESSION_UUID: "d0e1a2e5-7ca7-44cd-9037-ac7222011161" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.images, "kitty"); + assert.strictEqual(caps.trueColor, true); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("disables images for Warp inside tmux", () => { + withEnv( + { + TERM_PROGRAM: "WarpTerminal", + TMUX: "/tmp/tmux-1000/default,1234,0", + TERM: "tmux-256color", + }, + () => { + const caps = detectCapabilities(() => true); + assert.strictEqual(caps.images, null); + assert.strictEqual(caps.hyperlinks, true); + }, + ); + }); + it("enables hyperlinks for iTerm2", () => { withEnv({ TERM_PROGRAM: "iterm.app" }, () => { const caps = detectCapabilities(); From 20da9bc1407c027406e770caf23f09c97966936d Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Thu, 18 Jun 2026 10:59:25 +0200 Subject: [PATCH 084/141] fix attribution for 008c76f9 --- packages/coding-agent/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index deb0a5e5..a2fd43e8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi`. +- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pulls/5869) by [@xl0](https://github.com/xl0)) ### Changed From bc93655e1c07627d40b32c978d3208eff688dafb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 16:26:01 +0200 Subject: [PATCH 085/141] meta: Added report template --- .github/ISSUE_TEMPLATE/package-report.yml | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/package-report.yml diff --git a/.github/ISSUE_TEMPLATE/package-report.yml b/.github/ISSUE_TEMPLATE/package-report.yml new file mode 100644 index 00000000..c2b4e76a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/package-report.yml @@ -0,0 +1,51 @@ +name: Package Report +description: Report a problematic Pi package listed on pi.dev +labels: ["package-report"] +body: + - type: markdown + attributes: + value: | + Use this form to report a package listed on pi.dev. For Pi core bugs, use the bug report template instead. + + New issues from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/earendil-works/pi-mono/blob/main/CONTRIBUTING.md) will not be reopened or receive a reply. + + Keep this short. If it doesn't fit on one screen, it's too long. Write in your own voice. + + - type: input + id: package-name + attributes: + label: Package name + description: The npm package name from pi.dev. + placeholder: "@scope/package" + validations: + required: true + + - type: input + id: package-version + attributes: + label: Version + description: The package version shown on pi.dev. + placeholder: "0.1.0" + validations: + required: false + + - type: dropdown + id: report-type + attributes: + label: What are you reporting? + options: + - Malicious or unsafe behavior + - Broken package or install instructions + - Misleading metadata or impersonation + - Spam or low-quality package + - Other + validations: + required: true + + - type: textarea + id: details + attributes: + label: Details + description: Describe the concern and include links, logs, or screenshots if helpful. + validations: + required: true From 908be616f27ac2f97f8049feb94f3d21de6cb7b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 17:12:03 +0200 Subject: [PATCH 086/141] ref: Remove some options from package reporting --- .github/ISSUE_TEMPLATE/package-report.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/package-report.yml b/.github/ISSUE_TEMPLATE/package-report.yml index c2b4e76a..846e25ee 100644 --- a/.github/ISSUE_TEMPLATE/package-report.yml +++ b/.github/ISSUE_TEMPLATE/package-report.yml @@ -35,10 +35,8 @@ body: label: What are you reporting? options: - Malicious or unsafe behavior - - Broken package or install instructions - - Misleading metadata or impersonation - - Spam or low-quality package - - Other + - Impersonation + - Trademark / TOS Violations validations: required: true From d0b46764b1dde621ce89932f92f6e59a4c286133 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 17:13:47 +0200 Subject: [PATCH 087/141] feat(coding-agent): add automatic theme mode (#5874) --- packages/coding-agent/docs/themes.md | 2 +- .../coding-agent/src/core/settings-manager.ts | 9 +- .../components/settings-selector.ts | 279 ++++++++++++++++-- .../src/modes/interactive/interactive-mode.ts | 71 ++--- .../interactive/theme/theme-controller.ts | 135 +++++++++ .../modes/interactive/theme/theme-schema.json | 3 +- .../src/modes/interactive/theme/theme.ts | 43 ++- .../test/interactive-mode-status.test.ts | 13 + .../test/settings-manager.test.ts | 18 ++ ...-signal-shutdown-extension-cleanup.test.ts | 2 + .../5724-sigterm-signal-exit.test.ts | 2 + .../coding-agent/test/theme-detection.test.ts | 12 + packages/tui/src/index.ts | 7 +- packages/tui/src/terminal-colors.ts | 11 + packages/tui/src/tui.ts | 75 ++++- packages/tui/test/terminal-colors.test.ts | 18 +- 16 files changed, 620 insertions(+), 80 deletions(-) create mode 100644 packages/coding-agent/src/modes/interactive/theme/theme-controller.ts diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md index c18e954b..11655128 100644 --- a/packages/coding-agent/docs/themes.md +++ b/packages/coding-agent/docs/themes.md @@ -137,7 +137,7 @@ vim ~/.pi/agent/themes/my-theme.json } ``` -- `name` is required and must be unique. +- `name` is required, must be unique, and must not contain `/`. - `vars` is optional. Define reusable colors here, then reference them in `colors`. - `colors` must define all 51 required tokens. diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 6b54a187..a90916a4 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -714,8 +714,15 @@ export class SettingsManager { this.save(); } + getThemeSetting(): string | undefined { + const value = this.settings.theme; + if (typeof value === "string") return value; + return undefined; + } + getTheme(): string | undefined { - return this.settings.theme; + const theme = this.getThemeSetting(); + return theme?.includes("/") ? undefined : theme; } setTheme(theme: string): void { diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 39d25f80..7cc92614 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -1,6 +1,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { Transport } from "@earendil-works/pi-ai"; import { + type Component, Container, getCapabilities, type SelectItem, @@ -13,7 +14,13 @@ import { } from "@earendil-works/pi-tui"; import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.ts"; import type { DefaultProjectTrust, WarningSettings } from "../../../core/settings-manager.ts"; -import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.ts"; +import { + getSelectListTheme, + getSettingsListTheme, + parseAutoThemeSetting, + type TerminalTheme, + theme, +} from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyDisplayText } from "./keybinding-hints.ts"; @@ -55,6 +62,7 @@ export interface SettingsConfig { thinkingLevel: ThinkingLevel; availableThinkingLevels: ThinkingLevel[]; currentTheme: string; + terminalTheme: TerminalTheme; availableThemes: string[]; hideThinkingBlock: boolean; collapseChangelog: boolean; @@ -210,6 +218,249 @@ class SelectSubmenu extends Container { } } +function themeItems(availableThemes: string[]): SelectItem[] { + return availableThemes.map((name) => ({ value: name, label: name })); +} + +const AUTOMATIC_THEME_VALUE = "/"; + +function singleModeThemeItems(availableThemes: string[]): SelectItem[] { + return [ + { + value: AUTOMATIC_THEME_VALUE, + label: "Automatic", + description: "Use separate themes for light and dark terminal appearance", + }, + ...themeItems(availableThemes), + ]; +} + +function preferredTheme(availableThemes: string[], preferred: string | undefined, fallback: string): string { + if (preferred && availableThemes.includes(preferred)) return preferred; + if (availableThemes.includes(fallback)) return fallback; + return availableThemes[0] ?? fallback; +} + +function defaultAutomaticThemes( + currentThemeSetting: string, + availableThemes: string[], +): { lightTheme: string; darkTheme: string } { + const autoTheme = parseAutoThemeSetting(currentThemeSetting); + if (autoTheme) return autoTheme; + + const currentFixedTheme = currentThemeSetting.includes("/") ? undefined : currentThemeSetting; + const themeName = preferredTheme(availableThemes, currentFixedTheme, "dark"); + return { lightTheme: themeName, darkTheme: themeName }; +} + +class ThemeSubmenu extends Container { + private inputComponent: Component | undefined; + private readonly callbacks: SettingsCallbacks; + private readonly availableThemes: string[]; + private readonly terminalTheme: TerminalTheme; + private readonly onDone: (selectedValue?: string) => void; + private readonly originalThemeSetting: string; + private mode: "single" | "automatic"; + private singleTheme: string; + private lightTheme: string; + private darkTheme: string; + + constructor( + currentThemeSetting: string, + terminalTheme: TerminalTheme, + availableThemes: string[], + callbacks: SettingsCallbacks, + onDone: (selectedValue?: string) => void, + ) { + super(); + this.callbacks = callbacks; + this.availableThemes = availableThemes; + this.terminalTheme = terminalTheme; + this.onDone = onDone; + this.originalThemeSetting = currentThemeSetting; + const autoTheme = parseAutoThemeSetting(currentThemeSetting); + const automaticThemes = defaultAutomaticThemes(currentThemeSetting, availableThemes); + const fixedTheme = autoTheme || currentThemeSetting.includes("/") ? undefined : currentThemeSetting; + this.mode = autoTheme ? "automatic" : "single"; + this.lightTheme = automaticThemes.lightTheme; + this.darkTheme = automaticThemes.darkTheme; + this.singleTheme = preferredTheme( + availableThemes, + fixedTheme ?? (autoTheme ? this.getActiveAutomaticTheme() : undefined), + "dark", + ); + + if (this.mode === "automatic") { + this.showAutomaticMenu(); + } else { + this.showSingleMenu(); + } + } + + handleInput(data: string): void { + this.inputComponent?.handleInput?.(data); + } + + private setContent(renderComponent: Component, inputComponent: Component = renderComponent): void { + this.clear(); + this.addChild(renderComponent); + this.inputComponent = inputComponent; + } + + private showSingleMenu(): void { + this.mode = "single"; + const menu = new SelectSubmenu( + "Theme", + "Select a theme, or choose Automatic to follow terminal appearance.", + singleModeThemeItems(this.availableThemes), + this.singleTheme, + (value) => { + if (value === AUTOMATIC_THEME_VALUE) { + this.mode = "automatic"; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + this.showAutomaticMenu(); + return; + } + + this.singleTheme = value; + this.apply(value); + }, + () => this.cancel(), + (value) => { + this.callbacks.onThemePreview?.(value === AUTOMATIC_THEME_VALUE ? this.getAutomaticThemeSetting() : value); + }, + ); + this.setContent(menu); + } + + private showAutomaticMenu(): void { + this.mode = "automatic"; + const content = new Container(); + content.addChild(new Text(theme.bold(theme.fg("accent", "Automatic Theme")), 0, 0)); + content.addChild(new Spacer(1)); + content.addChild(new Text(theme.fg("muted", "Choose themes for terminal light and dark appearance."), 0, 0)); + content.addChild(new Text(theme.fg("muted", "Light/dark detection requires terminal support."), 0, 0)); + content.addChild(new Spacer(1)); + + const items: SettingItem[] = [ + { + id: "light-theme", + label: "Light theme", + description: "Theme to use in automatic mode when the terminal is light", + currentValue: this.lightTheme, + submenu: (currentValue, done) => + this.createThemeSelect( + "Light Theme", + "Select the theme to use for light terminal appearance", + currentValue, + done, + (value) => { + this.lightTheme = value; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(value); + }, + ), + }, + { + id: "dark-theme", + label: "Dark theme", + description: "Theme to use in automatic mode when the terminal is dark", + currentValue: this.darkTheme, + submenu: (currentValue, done) => + this.createThemeSelect( + "Dark Theme", + "Select the theme to use for dark terminal appearance", + currentValue, + done, + (value) => { + this.darkTheme = value; + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(value); + }, + ), + }, + { + id: "apply", + label: "Apply", + description: "Save and go back", + currentValue: "save and go back", + values: ["save and go back"], + }, + { + id: "single-mode", + label: "Change mode", + description: "Switch to one theme for light and dark", + currentValue: "switch to single theme", + values: ["switch to single theme"], + }, + ]; + + const settingsList = new SettingsList( + items, + Math.min(items.length, 10), + getSettingsListTheme(), + (id) => { + switch (id) { + case "single-mode": + this.mode = "single"; + this.singleTheme = this.getActiveAutomaticTheme(); + this.callbacks.onThemePreview?.(this.singleTheme); + this.showSingleMenu(); + break; + case "apply": + this.apply(this.getAutomaticThemeSetting()); + break; + } + }, + () => this.cancel(), + ); + content.addChild(settingsList); + this.setContent(content, settingsList); + } + + private createThemeSelect( + title: string, + description: string, + currentValue: string, + done: (selectedValue?: string) => void, + onSelect: (value: string) => void, + ): SelectSubmenu { + return new SelectSubmenu( + title, + description, + themeItems(this.availableThemes), + currentValue, + onSelect, + () => { + this.callbacks.onThemePreview?.(this.getThemeSetting()); + done(); + }, + (value) => this.callbacks.onThemePreview?.(value), + ); + } + + private getThemeSetting(): string { + return this.mode === "automatic" ? this.getAutomaticThemeSetting() : this.singleTheme; + } + + private getActiveAutomaticTheme(): string { + return this.terminalTheme === "light" ? this.lightTheme : this.darkTheme; + } + + private getAutomaticThemeSetting(): string { + return `${this.lightTheme}/${this.darkTheme}`; + } + + private apply(themeSetting: string): void { + this.onDone(themeSetting); + } + + private cancel(): void { + this.callbacks.onThemePreview?.(this.originalThemeSetting); + this.onDone(); + } +} + /** * Main settings selector component. */ @@ -353,28 +604,7 @@ export class SettingsSelectorComponent extends Container { description: "Color theme for the interface", currentValue: config.currentTheme, submenu: (currentValue, done) => - new SelectSubmenu( - "Theme", - "Select color theme", - config.availableThemes.map((t) => ({ - value: t, - label: t, - })), - currentValue, - (value) => { - callbacks.onThemeChange(value); - done(value); - }, - () => { - // Restore original theme on cancel - callbacks.onThemePreview?.(currentValue); - done(); - }, - (value) => { - // Preview theme on selection change - callbacks.onThemePreview?.(value); - }, - ), + new ThemeSubmenu(currentValue, config.terminalTheme, config.availableThemes, callbacks, done), }, ]; @@ -561,6 +791,9 @@ export class SettingsSelectorComponent extends Container { case "terminal-progress": callbacks.onShowTerminalProgressChange(newValue === "true"); break; + case "theme": + callbacks.onThemeChange(newValue); + break; } }, callbacks.onCancel, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index ad9d3d3a..0cde5ee4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -128,22 +128,19 @@ import { UserMessageComponent } from "./components/user-message.ts"; import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; import { getModelSearchText } from "./model-search.ts"; import { - detectTerminalBackgroundTheme, getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, - initTheme, onThemeChange, setRegisteredThemes, - setTheme, - setThemeInstance, stopThemeWatcher, Theme, type ThemeColor, theme, } from "./theme/theme.ts"; +import { InteractiveThemeController } from "./theme/theme-controller.ts"; /** Interface for components that can be expanded/collapsed */ interface Expandable { @@ -374,6 +371,7 @@ export class InteractiveMode { private options: InteractiveModeOptions; private autoTrustOnReloadCwd: string | undefined; + private themeController: InteractiveThemeController; // Convenience accessors private get session(): AgentSession { @@ -428,26 +426,12 @@ export class InteractiveMode { // Register themes from resource loader and initialize setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - initTheme(this.settingsManager.getTheme(), true); - } - - private async detectThemeIfUnset(): Promise { - if (this.settingsManager.getTheme()) { - return; - } - - const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); - const result = setTheme(detection.theme, true); - if (!result.success) { - return; - } - - if (detection.confidence === "high") { - this.settingsManager.setTheme(detection.theme); - await this.settingsManager.flush(); - } - this.updateEditorBorderColor(); - this.ui.requestRender(); + this.themeController = new InteractiveThemeController( + this.ui, + this.settingsManager, + (message) => this.showError(message), + () => this.updateEditorBorderColor(), + ); } private getAutocompleteSourceTag(sourceInfo?: SourceInfo): string | undefined { @@ -672,7 +656,7 @@ export class InteractiveMode { this.ui.start(); this.isInitialized = true; - await this.detectThemeIfUnset(); + await this.themeController.applyFromSettings(); // Add header with keybindings from config (unless silenced) if (this.options.verbose || !this.settingsManager.getQuietStartup()) { @@ -2080,16 +2064,13 @@ export class InteractiveMode { getTheme: (name) => getThemeByName(name), setTheme: (themeOrName) => { if (themeOrName instanceof Theme) { - setThemeInstance(themeOrName); - this.ui.requestRender(); - return { success: true }; + return this.themeController.setThemeInstance(themeOrName); } - const result = setTheme(themeOrName, true); + const result = this.themeController.setThemeName(themeOrName); if (result.success) { if (this.settingsManager.getTheme() !== themeOrName) { this.settingsManager.setTheme(themeOrName); } - this.ui.requestRender(); } return result; }, @@ -3378,6 +3359,7 @@ export class InteractiveMode { // which the stdout/stderr error handler turns into emergencyTerminalExit; // the render loop is already idle, so this cannot hot-spin (see #4144). await this.runtimeHost.dispose(); + this.themeController.disableAutoSync(); await this.ui.terminal.drainInput(1000); this.stop(); process.exit(0); @@ -3388,6 +3370,7 @@ export class InteractiveMode { // the final frame while the process is exiting. // Drain any in-flight Kitty key release events before stopping. // This prevents escape sequences from leaking to the parent shell over slow SSH. + this.themeController.disableAutoSync(); await this.ui.terminal.drainInput(1000); this.stop(); @@ -3991,7 +3974,8 @@ export class InteractiveMode { httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(), thinkingLevel: this.session.thinkingLevel, availableThinkingLevels: this.session.getAvailableThinkingLevels(), - currentTheme: this.settingsManager.getTheme() || "dark", + currentTheme: this.settingsManager.getThemeSetting() || "dark", + terminalTheme: this.themeController.getTerminalTheme(), availableThemes: getAvailableThemes(), hideThinkingBlock: this.hideThinkingBlock, collapseChangelog: this.settingsManager.getCollapseChangelog(), @@ -4058,21 +4042,11 @@ export class InteractiveMode { this.footer.invalidate(); this.updateEditorBorderColor(); }, - onThemeChange: (themeName) => { - const result = setTheme(themeName, true); - this.settingsManager.setTheme(themeName); - this.ui.invalidate(); - if (!result.success) { - this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); - } - }, - onThemePreview: (themeName) => { - const result = setTheme(themeName, true); - if (result.success) { - this.ui.invalidate(); - this.ui.requestRender(); - } + onThemeChange: (themeSetting) => { + this.settingsManager.setTheme(themeSetting); + void this.themeController.applyFromSettings(); }, + onThemePreview: (themeName) => this.themeController.preview(themeName), onHideThinkingBlockChange: (hidden) => { this.hideThinkingBlock = hidden; this.settingsManager.setHideThinkingBlock(hidden); @@ -5109,11 +5083,7 @@ export class InteractiveMode { } setRegisteredThemes(this.session.resourceLoader.getThemes().themes); this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); - const themeName = this.settingsManager.getTheme(); - const themeResult = themeName ? setTheme(themeName, true) : { success: true }; - if (!themeResult.success) { - this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`); - } + await this.themeController.applyFromSettings(); const editorPaddingX = this.settingsManager.getEditorPaddingX(); const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible(); this.defaultEditor.setPaddingX(editorPaddingX); @@ -5754,6 +5724,7 @@ export class InteractiveMode { this.loadingAnimation.stop(); this.loadingAnimation = undefined; } + this.themeController.disableAutoSync(); this.clearExtensionTerminalInputListeners(); this.footer.dispose(); this.footerDataProvider.dispose(); diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts new file mode 100644 index 00000000..9fe9e8cc --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts @@ -0,0 +1,135 @@ +import type { TUI } from "@earendil-works/pi-tui"; +import type { SettingsManager } from "../../../core/settings-manager.ts"; +import { + detectTerminalBackgroundFromEnv, + detectTerminalBackgroundTheme, + initTheme, + parseAutoThemeSetting, + resolveThemeSetting, + setTheme, + setThemeInstance, + type TerminalTheme, + type Theme, +} from "./theme.ts"; + +type ThemeResult = { success: boolean; error?: string }; + +export class InteractiveThemeController { + private readonly ui: TUI; + private readonly settingsManager: SettingsManager; + private readonly showError: (message: string) => void; + private readonly onChanged: () => void; + private terminalTheme: TerminalTheme = detectTerminalBackgroundFromEnv().theme; + private activeThemeName: string | undefined; + private autoSyncEnabled = false; + + constructor(ui: TUI, settingsManager: SettingsManager, showError: (message: string) => void, onChanged: () => void) { + this.ui = ui; + this.settingsManager = settingsManager; + this.showError = showError; + this.onChanged = onChanged; + this.activeThemeName = resolveThemeSetting(this.settingsManager.getThemeSetting(), this.terminalTheme); + initTheme(this.activeThemeName, true); + this.ui.onTerminalColorSchemeChange((terminalTheme) => this.applyTerminalTheme(terminalTheme)); + } + + async applyFromSettings(): Promise { + const themeSetting = this.settingsManager.getThemeSetting(); + const autoTheme = parseAutoThemeSetting(themeSetting); + if (autoTheme) { + this.terminalTheme = await this.detectTerminalThemeForAuto(); + this.setAutoSync(true); + this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); + return; + } + + this.setAutoSync(false); + if (themeSetting !== undefined) { + this.applyThemeName(themeSetting, true); + return; + } + + const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 }); + this.terminalTheme = detection.theme; + if (!this.applyThemeName(detection.theme).success) return; + if (detection.confidence === "high") { + this.settingsManager.setTheme(detection.theme); + await this.settingsManager.flush(); + } + } + + setThemeName(themeName: string, showError = false): ThemeResult { + this.setAutoSync(false); + return this.applyThemeName(themeName, showError); + } + + setThemeInstance(themeInstance: Theme): ThemeResult { + this.setAutoSync(false); + setThemeInstance(themeInstance); + this.activeThemeName = ""; + this.notifyChanged(); + return { success: true }; + } + + preview(themeSettingOrName: string): void { + const themeName = resolveThemeSetting(themeSettingOrName, this.terminalTheme) ?? this.activeThemeName; + if (!themeName) return; + if (setTheme(themeName, true).success) { + this.ui.invalidate(); + this.ui.requestRender(); + } + } + + disableAutoSync(): void { + this.setAutoSync(false); + } + + getTerminalTheme(): TerminalTheme { + return this.terminalTheme; + } + + private applyThemeName(themeName: string, showError = false): ThemeResult { + const result = setTheme(themeName, true); + this.activeThemeName = result.success ? themeName : "dark"; + this.notifyChanged(); + if (!result.success && showError) { + this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`); + } + return result; + } + + private notifyChanged(): void { + this.ui.invalidate(); + this.onChanged(); + } + + private setAutoSync(enabled: boolean): void { + if (this.autoSyncEnabled === enabled) return; + this.autoSyncEnabled = enabled; + this.ui.setTerminalColorSchemeNotifications(enabled); + } + + private async detectTerminalThemeForAuto(): Promise { + try { + const colorScheme = await this.ui.queryTerminalColorScheme({ timeoutMs: 100 }); + if (colorScheme) return colorScheme; + } catch { + // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. + } + return (await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 })).theme; + } + + private applyTerminalTheme(terminalTheme: TerminalTheme): void { + if (!this.autoSyncEnabled) return; + this.terminalTheme = terminalTheme; + const autoTheme = parseAutoThemeSetting(this.settingsManager.getThemeSetting()); + if (!autoTheme) { + this.setAutoSync(false); + return; + } + const themeName = terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; + if (themeName !== this.activeThemeName) { + this.applyThemeName(themeName); + } + } +} diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index 7bc495da..9d94a12a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -11,7 +11,8 @@ }, "name": { "type": "string", - "description": "Theme name" + "pattern": "^[^/]+$", + "description": "Theme name. Must not contain '/' because it is reserved for automatic light/dark theme settings." }, "vars": { "type": "object", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 778f8028..58e5aac3 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -503,6 +503,14 @@ function getCustomThemeInfos(): ThemeInfo[] { return result; } +function assertThemeNameIsValid(name: string): void { + if (name.includes("/")) { + throw new Error( + `Invalid theme name "${name}": theme names cannot contain "/" because it is reserved for automatic light/dark theme settings.`, + ); + } +} + function parseThemeJson(label: string, json: unknown): ThemeJson { if (!validateThemeJson.Check(json)) { const errors = Array.from(validateThemeJson.Errors(json)); @@ -539,7 +547,9 @@ function parseThemeJson(label: string, json: unknown): ThemeJson { throw new Error(errorMessage); } - return json as ThemeJson; + const themeJson = json as ThemeJson; + assertThemeNameIsValid(themeJson.name); + return themeJson; } function parseThemeJsonContent(label: string, content: string): ThemeJson { @@ -625,6 +635,36 @@ export function getThemeByName(name: string): Theme | undefined { export type TerminalTheme = "dark" | "light"; +export function parseAutoThemeSetting( + themeSetting: string | undefined, +): { lightTheme: string; darkTheme: string } | undefined { + if (!themeSetting) return undefined; + const slashIndex = themeSetting.indexOf("/"); + if (slashIndex === -1 || themeSetting.indexOf("/", slashIndex + 1) !== -1) { + return undefined; + } + + const lightTheme = themeSetting.slice(0, slashIndex).trim(); + const darkTheme = themeSetting.slice(slashIndex + 1).trim(); + if (!lightTheme || !darkTheme) { + return undefined; + } + return { lightTheme, darkTheme }; +} + +export function resolveThemeSetting( + themeSetting: string | undefined, + terminalTheme: TerminalTheme, +): string | undefined { + const autoTheme = parseAutoThemeSetting(themeSetting); + if (autoTheme) { + return terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme; + } + if (themeSetting?.includes("/")) return undefined; + if (typeof themeSetting === "string") return themeSetting; + return undefined; +} + export interface TerminalThemeDetection { theme: TerminalTheme; source: "terminal background" | "COLORFGBG" | "fallback"; @@ -752,6 +792,7 @@ export function setRegisteredThemes(themes: Theme[]): void { registeredThemes.clear(); for (const theme of themes) { if (theme.name) { + assertThemeNameIsValid(theme.name); registeredThemes.set(theme.name, theme); } } diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 8f1018b2..6ba7db91 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -151,6 +151,13 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const fakeThis: any = { session: { settingsManager }, settingsManager, + themeController: { + setThemeInstance: vi.fn(() => ({ success: true })), + setThemeName: vi.fn(() => { + fakeThis.ui.requestRender(); + return { success: true }; + }), + }, ui: { requestRender: vi.fn() }, }; @@ -158,6 +165,7 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const result = uiContext.setTheme("light"); expect(result.success).toBe(true); + expect(fakeThis.themeController.setThemeName).toHaveBeenCalledWith("light"); expect(settingsManager.setTheme).toHaveBeenCalledWith("light"); expect(currentTheme).toBe("light"); expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); @@ -173,6 +181,10 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const fakeThis: any = { session: { settingsManager }, settingsManager, + themeController: { + setThemeInstance: vi.fn(() => ({ success: true })), + setThemeName: vi.fn(() => ({ success: false, error: "Theme not found" })), + }, ui: { requestRender: vi.fn() }, }; @@ -180,6 +192,7 @@ describe("InteractiveMode.createExtensionUIContext setTheme", () => { const result = uiContext.setTheme("__missing_theme__"); expect(result.success).toBe(false); + expect(fakeThis.themeController.setThemeName).toHaveBeenCalledWith("__missing_theme__"); expect(settingsManager.setTheme).not.toHaveBeenCalled(); expect(fakeThis.ui.requestRender).not.toHaveBeenCalled(); }); diff --git a/packages/coding-agent/test/settings-manager.test.ts b/packages/coding-agent/test/settings-manager.test.ts index 279bece1..d86c3f91 100644 --- a/packages/coding-agent/test/settings-manager.test.ts +++ b/packages/coding-agent/test/settings-manager.test.ts @@ -198,6 +198,24 @@ describe("SettingsManager", () => { }); }); + describe("theme setting", () => { + it("stores slash-separated automatic theme settings separately from fixed theme names", async () => { + const settingsPath = join(agentDir, "settings.json"); + writeFileSync(settingsPath, JSON.stringify({ theme: "light/dark" })); + + const manager = SettingsManager.create(projectDir, agentDir); + + expect(manager.getTheme()).toBeUndefined(); + expect(manager.getThemeSetting()).toBe("light/dark"); + + manager.setTheme("solarized-light/tokyo-night"); + await manager.flush(); + + const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8")); + expect(savedSettings.theme).toBe("solarized-light/tokyo-night"); + }); + }); + describe("error tracking", () => { it("should collect and clear load errors via drainErrors", () => { const globalSettingsPath = join(agentDir, "settings.json"); diff --git a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts index 33a960e2..9ccf77cd 100644 --- a/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts +++ b/packages/coding-agent/test/suite/regressions/5080-signal-shutdown-extension-cleanup.test.ts @@ -21,6 +21,7 @@ type ShutdownThis = { unregisterSignalHandlers: () => void; runtimeHost: { dispose: () => Promise }; ui: { terminal: { drainInput: (ms: number) => Promise } }; + themeController: { disableAutoSync: () => void }; stop: () => void; sessionManager: SessionManager; }; @@ -81,6 +82,7 @@ function createContext(order: string[], sessionManager = createSessionManager()) }), }, }, + themeController: { disableAutoSync: vi.fn() }, stop: vi.fn(() => { order.push("stop"); }), diff --git a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts index d170f1cf..75e82fb7 100644 --- a/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts +++ b/packages/coding-agent/test/suite/regressions/5724-sigterm-signal-exit.test.ts @@ -13,6 +13,7 @@ type ShutdownThis = { unregisterSignalHandlers: () => void; runtimeHost: { dispose: () => Promise }; ui: { terminal: { drainInput: (ms: number) => Promise } }; + themeController: { disableAutoSync: () => void }; stop: () => void; }; @@ -73,6 +74,7 @@ describe("InteractiveMode SIGTERM shutdown with signal-exit (#5724)", () => { }), }, }, + themeController: { disableAutoSync: vi.fn() }, stop: vi.fn(() => { order.push("stop"); }), diff --git a/packages/coding-agent/test/theme-detection.test.ts b/packages/coding-agent/test/theme-detection.test.ts index 4ad0cf56..6bdc597b 100644 --- a/packages/coding-agent/test/theme-detection.test.ts +++ b/packages/coding-agent/test/theme-detection.test.ts @@ -5,6 +5,8 @@ import { detectTerminalBackgroundTheme, getThemeByName, getThemeForRgbColor, + parseAutoThemeSetting, + resolveThemeSetting, } from "../src/modes/interactive/theme/theme.ts"; afterEach(() => { @@ -119,3 +121,13 @@ describe("theme detection from RGB", () => { expect(getThemeForRgbColor({ r: 250, g: 250, b: 250 })).toBe("light"); }); }); + +describe("theme setting helpers", () => { + it("parses and resolves automatic theme settings", () => { + expect(parseAutoThemeSetting("light/dark")).toEqual({ lightTheme: "light", darkTheme: "dark" }); + expect(resolveThemeSetting("dark", "light")).toBe("dark"); + expect(resolveThemeSetting("light/dark", "light")).toBe("light"); + expect(resolveThemeSetting("light/dark", "dark")).toBe("dark"); + expect(resolveThemeSetting("light/dark/extra", "dark")).toBeUndefined(); + }); +}); diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index e22bf08a..4e76b107 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -62,7 +62,12 @@ export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from " // Terminal interface and implementations export { ProcessTerminal, type Terminal } from "./terminal.ts"; // Terminal colors -export { parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; +export { + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type RgbColor, + type TerminalColorScheme, +} from "./terminal-colors.ts"; // Terminal image support export { allocateImageId, diff --git a/packages/tui/src/terminal-colors.ts b/packages/tui/src/terminal-colors.ts index 1c383a51..cff6dc8e 100644 --- a/packages/tui/src/terminal-colors.ts +++ b/packages/tui/src/terminal-colors.ts @@ -4,6 +4,8 @@ export interface RgbColor { b: number; } +export type TerminalColorScheme = "dark" | "light"; + function hexToRgb(hex: string): RgbColor { const normalized = hex.startsWith("#") ? hex.slice(1) : hex; const r = parseInt(normalized.slice(0, 2), 16); @@ -24,6 +26,7 @@ function parseOscHexChannel(channel: string): number | undefined { } const OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN = /^\x1b\]11;([^\x07\x1b]*)(?:\x07|\x1b\\)$/i; +const COLOR_SCHEME_REPORT_PATTERN = /^\x1b\[\?997;(1|2)n$/; export function isOsc11BackgroundColorResponse(data: string): boolean { return OSC11_BACKGROUND_COLOR_RESPONSE_PATTERN.test(data); @@ -60,3 +63,11 @@ export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { const b = parseOscHexChannel(blue); return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; } + +export function parseTerminalColorSchemeReport(data: string): TerminalColorScheme | undefined { + const match = data.match(COLOR_SCHEME_REPORT_PATTERN); + if (!match) { + return undefined; + } + return match[1] === "2" ? "light" : "dark"; +} diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 1fff6545..a7054888 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -8,7 +8,13 @@ import * as path from "node:path"; import { performance } from "node:perf_hooks"; import { isKeyRelease, matchesKey } from "./keys.ts"; import type { Terminal } from "./terminal.ts"; -import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts"; +import { + isOsc11BackgroundColorResponse, + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type RgbColor, + type TerminalColorScheme, +} from "./terminal-colors.ts"; import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts"; import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts"; @@ -311,6 +317,8 @@ export class TUI extends Container { private stopped = false; private pendingOsc11BackgroundReplies = 0; private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = []; + private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>(); + private terminalColorSchemeNotificationsEnabled = false; // Overlay stack for modal components rendered on top of base content private focusOrderCounter = 0; @@ -631,6 +639,9 @@ export class TUI extends Container { () => this.requestRender(), ); this.terminal.hideCursor(); + if (this.terminalColorSchemeNotificationsEnabled) { + this.terminal.write("\x1b[?2031h"); + } this.queryCellSize(); this.requestRender(); } @@ -646,6 +657,23 @@ export class TUI extends Container { this.inputListeners.delete(listener); } + onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void { + this.terminalColorSchemeListeners.add(listener); + return () => { + this.terminalColorSchemeListeners.delete(listener); + }; + } + + setTerminalColorSchemeNotifications(enabled: boolean): void { + if (this.terminalColorSchemeNotificationsEnabled === enabled) { + return; + } + this.terminalColorSchemeNotificationsEnabled = enabled; + if (!this.stopped) { + this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l"); + } + } + private queryCellSize(): void { // Only query if terminal supports images (cell size is only used for image rendering) if (!getCapabilities().images) { @@ -662,6 +690,9 @@ export class TUI extends Container { clearTimeout(this.renderTimer); this.renderTimer = undefined; } + if (this.terminalColorSchemeNotificationsEnabled) { + this.terminal.write("\x1b[?2031l"); + } // Move cursor to the end of the content to prevent overwriting/artifacts on exit if (this.previousLines.length > 0) { const targetRow = this.previousLines.length; // Line after the last content @@ -731,6 +762,9 @@ export class TUI extends Container { if (this.consumeOsc11BackgroundResponse(data)) { return; } + if (this.consumeTerminalColorSchemeReport(data)) { + return; + } if (this.inputListeners.size > 0) { let current = data; @@ -824,6 +858,18 @@ export class TUI extends Container { return true; } + private consumeTerminalColorSchemeReport(data: string): boolean { + const scheme = parseTerminalColorSchemeReport(data); + if (!scheme) { + return false; + } + + for (const listener of this.terminalColorSchemeListeners) { + listener(scheme); + } + return true; + } + private consumeCellSizeResponse(data: string): boolean { // Response format: ESC [ 6 ; height ; width t const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/); @@ -1638,4 +1684,31 @@ export class TUI extends Container { this.terminal.write("\x1b]11;?\x07"); }); } + + /** + * Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`). + * Terminals that support the color palette notification protocol reply with + * `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light. + */ + queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise { + return new Promise((resolve) => { + let settled = false; + let timer: NodeJS.Timeout | undefined; + let unsubscribe: () => void = () => {}; + const settle = (scheme: TerminalColorScheme | undefined) => { + if (settled) return; + settled = true; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + unsubscribe(); + resolve(scheme); + }; + + unsubscribe = this.onTerminalColorSchemeChange(settle); + timer = setTimeout(() => settle(undefined), timeoutMs); + this.terminal.write("\x1b[?996n"); + }); + } } diff --git a/packages/tui/test/terminal-colors.test.ts b/packages/tui/test/terminal-colors.test.ts index 9ac2a4ee..d777e061 100644 --- a/packages/tui/test/terminal-colors.test.ts +++ b/packages/tui/test/terminal-colors.test.ts @@ -1,6 +1,12 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { type Component, parseOsc11BackgroundColor, type Terminal, TUI } from "../src/index.ts"; +import { + type Component, + parseOsc11BackgroundColor, + parseTerminalColorSchemeReport, + type Terminal, + TUI, +} from "../src/index.ts"; class TestTerminal implements Terminal { private inputHandler?: (data: string) => void; @@ -104,6 +110,16 @@ describe("parseOsc11BackgroundColor", () => { }); }); +describe("parseTerminalColorSchemeReport", () => { + it("parses color scheme reports", () => { + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;1n"), "dark"); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;2n"), "light"); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?997;3n"), undefined); + assert.strictEqual(parseTerminalColorSchemeReport("\x1b[?996n"), undefined); + assert.strictEqual(parseTerminalColorSchemeReport("x\x1b[?997;1n"), undefined); + }); +}); + describe("TUI.queryTerminalBackgroundColor", () => { it("writes OSC 11 query and resolves with the parsed RGB reply", async () => { const terminal = new TestTerminal(); From 2b46f388635f61a3fab2eed241998a61d67e7753 Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Thu, 18 Jun 2026 10:14:39 -0500 Subject: [PATCH 088/141] feat(coding-agent): Expose edit-diff for extensions (#5756) --- packages/coding-agent/src/core/tools/edit-diff.ts | 3 +-- packages/coding-agent/src/index.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index f280bf19..cc94c202 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -1,6 +1,5 @@ /** - * Shared diff computation utilities for the edit tool. - * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering). + * Shared diff computation utilities for the edit and similar tools. */ import * as Diff from "diff"; diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 70416af1..5830ecda 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -246,6 +246,7 @@ export { type SkillFrontmatter, } from "./core/skills.ts"; export { createSyntheticSourceInfo } from "./core/source-info.ts"; +export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts"; // Tools export { type BashOperations, From aae62dfa87c7935f124c4349fb34216eee84502a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:09:03 +0200 Subject: [PATCH 089/141] feat(coding-agent): make bare update self-only --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 8 +-- packages/coding-agent/docs/packages.md | 9 ++-- packages/coding-agent/docs/usage.md | 5 +- packages/coding-agent/src/cli/args.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 2 +- .../coding-agent/src/package-manager-cli.ts | 51 +++++++++++++++---- 7 files changed, 58 insertions(+), 20 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a2fd43e8..e29e19d2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed +- Changed bare `pi update` to update only pi, added `pi update --all` for updating pi and extensions together, and clarified extension update prompts. - Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. ### Fixed diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 41a7bbc2..ce7dba2a 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -419,7 +419,8 @@ pi install ssh://git@github.com/user/repo@v1 # tag or commit pi remove npm:@foo/pi-tools pi uninstall npm:@foo/pi-tools # alias for remove pi list -pi update # update pi and packages (skips pinned packages) +pi update # update pi only +pi update --all # update pi and packages pi update --extensions # update packages only pi update --self # update pi only pi update --self --force # reinstall pi even if current @@ -427,7 +428,7 @@ pi update npm:@foo/pi-tools # update one package pi config # enable/disable extensions, skills, prompts, themes ``` -Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. +Packages install to `~/.pi/agent/git/` (git) or `~/.pi/agent/npm/` (npm). Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). Git `@ref` values are pinned tags or commits; pinned packages are skipped by `pi update --extensions` and `pi update --all`, so use `pi install git:host/user/repo@new-ref` to move an existing package to a new ref. Git packages install dependencies with `npm install --omit=dev` by default, so runtime deps must be listed under `dependencies`; when `npmCommand` is configured, git packages use plain `install` for compatibility with wrappers. If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. Create a package by adding a `pi` key to `package.json`: @@ -518,7 +519,8 @@ pi [options] [@files...] [messages...] pi install [-l] # Install package, -l for project-local pi remove [-l] # Remove package pi uninstall [-l] # Alias for remove -pi update [source|self|pi] # Update pi and packages (skips pinned packages) +pi update [source|self|pi] # Update pi only, or one package source +pi update --all # Update pi and packages pi update --extensions # Update packages only pi update --self # Update pi only pi update --self --force # Reinstall pi even if current diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index 7009b773..73f2dccb 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -28,7 +28,8 @@ pi install ./relative/path/to/package pi remove npm:@foo/bar pi list # show installed packages from settings -pi update # update pi, update packages, and reconcile pinned git refs +pi update # update pi only +pi update --all # update pi, update packages, and reconcile pinned git refs pi update --extensions # update packages and reconcile pinned git refs only pi update --self # update pi only pi update --self --force # reinstall pi even if current @@ -36,7 +37,7 @@ pi update npm:@foo/bar # update one package pi update --extension npm:@foo/bar ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). +These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). By default, `install` and `remove` write to user settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup after the project is trusted. @@ -58,7 +59,7 @@ npm:@scope/pkg@1.2.3 npm:pkg ``` -- Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`). +- Versioned specs are pinned and skipped by package updates (`pi update --extensions`, `pi update --all`). - User installs go under `~/.pi/agent/npm/`. - Project installs go under `.pi/npm/`. - Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`. @@ -85,7 +86,7 @@ ssh://git@github.com/user/repo@v1 - HTTPS and SSH URLs are both supported. - SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`). - For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast. -- Refs are pinned tags or commits. `pi update` and `pi update --extensions` do not move them to newer refs, but they do reconcile an existing clone to the configured ref. +- Refs are pinned tags or commits. `pi update --extensions` and `pi update --all` do not move them to newer refs, but they do reconcile an existing clone to the configured ref. - Use `pi install git:host/user/repo@new-ref` to update settings and move an existing package to a new pinned ref. - Cloned to `~/.pi/agent/git//` (global) or `.pi/git//` (project). - When reconciliation changes the checkout, pi resets and cleans the clone, then runs `npm install` if `package.json` exists. diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 4f8f5954..5ff0cf73 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -145,7 +145,8 @@ pi [options] [@files...] [messages...] pi install [-l] # Install package, -l for project-local pi remove [-l] # Remove package pi uninstall [-l] # Alias for remove -pi update [source|self|pi] # Update pi and packages; reconcile pinned git refs +pi update [source|self|pi] # Update pi only, or one package source +pi update --all # Update pi and packages; reconcile pinned git refs pi update --extensions # Update packages only; reconcile pinned git refs pi update --self # Update pi only pi update --extension # Update one package @@ -153,7 +154,7 @@ pi list # List installed packages pi config # Enable/disable package resources ``` -These commands manage pi packages, not the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. +These commands manage pi packages and `pi update` can update the pi CLI installation. To uninstall pi itself, see [Quickstart](quickstart.md#uninstall). `pi config` and project package commands accept `--approve`/`--no-approve` to trust or ignore project-local settings for one command. `pi update` never prompts for project trust. See [Pi Packages](packages.md) for package sources and security notes. diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 839c60e8..5d3c4af8 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -229,7 +229,7 @@ ${chalk.bold("Commands:")} ${APP_NAME} install [-l] Install extension source and add to settings ${APP_NAME} remove [-l] Remove extension source from settings ${APP_NAME} uninstall [-l] Alias for remove - ${APP_NAME} update [source|self|pi] Update pi and installed extensions + ${APP_NAME} update [source|self|pi] Update pi (use --all for pi and extensions) ${APP_NAME} list List installed extensions from settings ${APP_NAME} config Open TUI to enable/disable package resources ${APP_NAME} --help Show help for install/remove/uninstall/update/list diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 0cde5ee4..00054eb2 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3740,7 +3740,7 @@ export class InteractiveMode { } showPackageUpdateNotification(packages: string[]): void { - const action = theme.fg("accent", `${APP_NAME} update`); + const action = theme.fg("accent", `${APP_NAME} update --extensions`); const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action; const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n"); diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index e00de1b4..d47df930 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -52,6 +52,7 @@ interface PackageCommandOptions { command: PackageCommand; source?: string; updateTarget?: UpdateTarget; + showExtensionsSkippedNote: boolean; local: boolean; force: boolean; projectTrustOverride?: boolean; @@ -79,7 +80,7 @@ function getPackageCommandUsage(command: PackageCommand): string { case "remove": return `${APP_NAME} remove [-l] [--approve|--no-approve]`; case "update": - return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension ] [--approve|--no-approve] [--force]`; + return `${APP_NAME} update [source|self|pi] [--self|--extensions|--all] [--extension ] [--approve|--no-approve] [--force]`; case "list": return `${APP_NAME} list [--approve|--no-approve]`; } @@ -133,15 +134,17 @@ Examples: Update pi and installed packages. Options: - --self Update pi only + --self Update pi only (default when no target is given) --extensions Update installed packages only + --all Update pi and installed packages --extension Update one package only -a, --approve Trust project-local files for this command -na, --no-approve Ignore project-local files for this command --force Reinstall pi even if the current version is latest Short forms: - ${APP_NAME} update Update pi and all extensions + ${APP_NAME} update Update pi only + ${APP_NAME} update --all Update pi and all extensions ${APP_NAME} update Update one package ${APP_NAME} update pi Update pi only (self works as alias to pi) `); @@ -184,6 +187,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined let source: string | undefined; let selfFlag = false; let extensionsFlag = false; + let allFlag = false; let extensionFlagSource: string | undefined; for (let index = 0; index < rest.length; index++) { @@ -220,6 +224,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined continue; } + if (arg === "--all") { + if (command === "update") { + allFlag = true; + } else { + invalidOption = invalidOption ?? arg; + } + continue; + } + if (arg === "--approve" || arg === "-a") { projectTrustOverride = true; continue; @@ -271,10 +284,20 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined } let updateTarget: UpdateTarget | undefined; + let showExtensionsSkippedNote = false; if (command === "update") { + if (allFlag && (selfFlag || extensionsFlag || extensionFlagSource)) { + conflictingOptions = + conflictingOptions ?? "--all cannot be combined with --self, --extensions, or --extension"; + } + if (allFlag && source) { + conflictingOptions = conflictingOptions ?? "--all cannot be combined with a positional source"; + } + if (extensionFlagSource) { - if (selfFlag || extensionsFlag) { - conflictingOptions = conflictingOptions ?? "--extension cannot be combined with --self or --extensions"; + if (selfFlag || extensionsFlag || allFlag) { + conflictingOptions = + conflictingOptions ?? "--extension cannot be combined with --self, --extensions, or --all"; } if (source) { conflictingOptions = conflictingOptions ?? "--extension cannot be combined with a positional source"; @@ -285,12 +308,15 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined if (sourceIsSelf) { updateTarget = extensionsFlag ? { type: "all" } : { type: "self" }; } else { - if (extensionsFlag || selfFlag) { + if (extensionsFlag || selfFlag || allFlag) { conflictingOptions = - conflictingOptions ?? "positional update targets cannot be combined with --self or --extensions"; + conflictingOptions ?? + "positional update targets cannot be combined with --self, --extensions, or --all"; } updateTarget = { type: "extensions", source }; } + } else if (allFlag) { + updateTarget = { type: "all" }; } else if (selfFlag && extensionsFlag) { updateTarget = { type: "all" }; } else if (selfFlag) { @@ -298,7 +324,8 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined } else if (extensionsFlag) { updateTarget = { type: "extensions" }; } else { - updateTarget = { type: "all" }; + updateTarget = { type: "self" }; + showExtensionsSkippedNote = true; } } @@ -306,6 +333,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined command, source, updateTarget, + showExtensionsSkippedNote, local, force, projectTrustOverride, @@ -660,7 +688,12 @@ export async function handlePackageCommand( } case "update": { - const target = options.updateTarget ?? { type: "all" }; + const target = options.updateTarget ?? { type: "self" }; + if (options.showExtensionsSkippedNote) { + console.log( + chalk.dim(`Extensions are skipped. Run ${APP_NAME} update --extensions to update extensions.`), + ); + } if (updateTargetIncludesExtensions(target)) { const updateSource = target.type === "extensions" ? target.source : undefined; await packageManager.update(updateSource); From 717494223d41d663224f1b177c8427c05ad62448 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:16:48 +0200 Subject: [PATCH 090/141] docs: audit unreleased changelogs --- packages/coding-agent/CHANGELOG.md | 13 ++++++++++++- packages/tui/CHANGELOG.md | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e29e19d2..3f953510 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,13 +2,24 @@ ## [Unreleased] +### New Features + +- **Automatic theme mode** - `/settings` can choose separate light and dark themes and follow terminal color-scheme changes. See [Selecting a Theme](docs/themes.md#selecting-a-theme). +- **Self-only updates by default** - `pi update` now updates pi only, with `pi update --all` for updating pi and packages together. See [Install and Manage](docs/packages.md#install-and-manage). +- **Extension API helpers** - extensions can use `CONFIG_DIR_NAME` for project config paths and import edit diff helpers for edit-style diffs. See [`ctx.cwd`](docs/extensions.md#ctxcwd) and [SDK Exports](docs/sdk.md#exports). +- **Warp inline images** - Warp terminals now get inline image rendering through Kitty graphics detection. See [Image](docs/tui.md#image). + ### Added -- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pulls/5869) by [@xl0](https://github.com/xl0)) +- Added automatic theme mode so `/settings` can use separate light and dark themes and follow terminal color-scheme changes ([#5874](https://github.com/earendil-works/pi/pull/5874)). +- Added inherited Warp terminal image capability detection so inline images render through Warp's Kitty graphics support ([#5841](https://github.com/earendil-works/pi/pull/5841) by [@dodiego](https://github.com/dodiego)). +- Exported `CONFIG_DIR_NAME` from the coding-agent public API so extensions can resolve project config paths without hardcoding `.pi` ([#5869](https://github.com/earendil-works/pi/pull/5869) by [@xl0](https://github.com/xl0)). +- Exported edit diff helpers (`generateDiffString`, `generateUnifiedPatch`, and `EditDiffResult`) from the public API for extensions that need edit-style diffs ([#5756](https://github.com/earendil-works/pi/pull/5756) by [@xl0](https://github.com/xl0)). ### Changed - Changed bare `pi update` to update only pi, added `pi update --all` for updating pi and extensions together, and clarified extension update prompts. +- Reserved `/` in theme names for automatic light/dark theme settings. - Updated extension docs, examples, runtime help, trust prompts, and config labels to use the configured project config directory instead of hardcoded `.pi` paths. ### Fixed diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9b90a5b1..ca28b0a3 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added terminal color-scheme query and notification support for light/dark appearance detection (`TUI.queryTerminalColorScheme()`, `TUI.onTerminalColorSchemeChange()`, and `TUI.setTerminalColorSchemeNotifications()`) ([#5874](https://github.com/earendil-works/pi/pull/5874)). +- Added Warp terminal detection for Kitty graphics inline image support ([#5841](https://github.com/earendil-works/pi/pull/5841) by [@dodiego](https://github.com/dodiego)). - Exported `sliceByColumn` for ANSI-aware horizontal column slicing. ## [0.79.6] - 2026-06-16 From c4ab61dcbf719e75018f0ae90eb900b78a881603 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:22:07 +0200 Subject: [PATCH 091/141] Release v0.79.7 --- package-lock.json | 26 ++++---- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/image-models.generated.ts | 30 +++++++++ packages/ai/src/models.generated.ts | 65 ++++++++++++------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 +++---- packages/coding-agent/package.json | 8 +-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 21 files changed, 120 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4811db6..821c9d30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6176,10 +6176,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.7", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -6213,7 +6213,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -6258,12 +6258,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@earendil-works/pi-agent-core": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-tui": "^0.79.7", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -6304,32 +6304,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.6" + "version": "0.79.7" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.9.7", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "ms": "2.1.3" }, @@ -6365,7 +6365,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 41203247..76368ff6 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ## [0.79.6] - 2026-06-16 diff --git a/packages/agent/package.json b/packages/agent/package.json index 66ee4847..81d3e356 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.6", + "version": "0.79.7", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -29,7 +29,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.7", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a49ddf52..f6070588 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 11e4261b..9c71d156 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.6", + "version": "0.79.7", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/image-models.generated.ts b/packages/ai/src/image-models.generated.ts index 09c74180..7545a9f1 100644 --- a/packages/ai/src/image-models.generated.ts +++ b/packages/ai/src/image-models.generated.ts @@ -95,6 +95,21 @@ export const IMAGE_MODELS = { cacheWrite: 0.08333333333333334, }, } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3-pro-image": { + id: "google/gemini-3-pro-image", + name: "Google: Nano Banana Pro (Gemini 3 Pro Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 2, + output: 12, + cacheRead: 0.19999999999999998, + cacheWrite: 0.375, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-3-pro-image-preview": { id: "google/gemini-3-pro-image-preview", name: "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)", @@ -110,6 +125,21 @@ export const IMAGE_MODELS = { cacheWrite: 0.375, }, } satisfies ImagesModel<"openrouter-images">, + "google/gemini-3.1-flash-image": { + id: "google/gemini-3.1-flash-image", + name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image)", + api: "openrouter-images", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + input: ["image", "text"], + output: ["image", "text"], + cost: { + input: 0.5, + output: 3, + cacheRead: 0, + cacheWrite: 0, + }, + } satisfies ImagesModel<"openrouter-images">, "google/gemini-3.1-flash-image-preview": { id: "google/gemini-3.1-flash-image-preview", name: "Google: Nano Banana 2 (Gemini 3.1 Flash Image Preview)", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 424f8bcd..8e1b776f 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4566,25 +4566,6 @@ export const MODELS = { contextWindow: 400000, maxTokens: 128000, } satisfies Model<"openai-responses">, - "raptor-mini": { - id: "raptor-mini", - name: "Raptor mini", - api: "openai-completions", - provider: "github-copilot", - baseUrl: "https://api.individual.githubcopilot.com", - headers: {"User-Agent":"GitHubCopilotChat/0.35.0","Editor-Version":"vscode/1.107.0","Editor-Plugin-Version":"copilot-chat/0.35.0","Copilot-Integration-Id":"vscode-chat"}, - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, }, "google": { "gemini-2.0-flash": { @@ -11785,6 +11766,23 @@ export const MODELS = { contextWindow: 1048756, maxTokens: 262144, } satisfies Model<"openai-completions">, + "poolside/laguna-m.1": { + id: "poolside/laguna-m.1", + name: "Poolside: Laguna M.1", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.2, + output: 0.4, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "poolside/laguna-m.1:free": { id: "poolside/laguna-m.1:free", name: "Poolside: Laguna M.1 (free)", @@ -11802,6 +11800,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, + "poolside/laguna-xs.2": { + id: "poolside/laguna-xs.2", + name: "Poolside: Laguna XS.2", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0.1, + output: 0.2, + cacheRead: 0.05, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 32768, + } satisfies Model<"openai-completions">, "poolside/laguna-xs.2:free": { id: "poolside/laguna-xs.2:free", name: "Poolside: Laguna XS.2 (free)", @@ -12425,11 +12440,11 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12542,9 +12557,9 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.15, + input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -13054,11 +13069,11 @@ export const MODELS = { cost: { input: 1.4, output: 4.4, - cacheRead: 0.26, + cacheRead: 0.7, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 262144, + maxTokens: 524288, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3f953510..89e7f892 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index bd9ae9f3..c3f35b25 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 7f2d2bb4..c817a94e 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 0478f9c8..12363537 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index c573e8e0..ea80b459 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 71e89d6c..89705d97 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 24e20622..41b01437 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.9.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.6", + "version": "1.9.7", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 7f1fa016..a80abe4a 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.6", + "version": "1.9.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index fb5d53cd..b45b2214 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.6", + "version": "0.79.7", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index b8d759de..02abd397 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.6", + "version": "0.79.7", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 39313f9d..c366f1a5 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@earendil-works/pi-agent-core": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-tui": "^0.79.7", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.6.tgz", + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.7.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.6", + "@earendil-works/pi-ai": "^0.79.7", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.6.tgz", + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.7.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -511,8 +511,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.6", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.6.tgz", + "version": "0.79.7", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.7.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 22b408ce..c873bb8d 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.6", + "version": "0.79.7", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.6", - "@earendil-works/pi-ai": "^0.79.6", - "@earendil-works/pi-tui": "^0.79.6", + "@earendil-works/pi-agent-core": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-tui": "^0.79.7", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index ca28b0a3..28a38ba8 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index 9c73829c..24590a18 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.6", + "version": "0.79.7", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 788a044483da3d3c1fe1b4603fde7ffac0fbae3f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 18:22:10 +0200 Subject: [PATCH 092/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 76368ff6..3050117f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ## [0.79.6] - 2026-06-16 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f6070588..72851f5f 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 89e7f892..ed277120 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 28a38ba8..4db80b91 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.7] - 2026-06-18 ### Added From 6b9f3f492f1de0683246e6fb2f86d35f70335f56 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 19:30:14 +0200 Subject: [PATCH 093/141] fix(coding-agent): avoid retrying successful overflow compaction closes #5720 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../coding-agent/src/core/agent-session.ts | 13 ++++++-- .../suite/agent-session-compaction.test.ts | 31 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ed277120..ab9812c5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). + ## [0.79.7] - 2026-06-18 ### New Features diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 54de3103..23fb5983 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1824,8 +1824,17 @@ export class AgentSession { return false; } - // Case 1: Overflow - LLM returned context overflow error + // Case 1: Overflow - LLM returned context overflow error, or reported usage exceeded + // the configured window. A successful response over the configured window should compact + // but must not retry: the assistant answer already completed and agent.continue() cannot + // continue from an assistant message. if (sameModel && isContextOverflow(assistantMessage, contextWindow)) { + const willRetry = assistantMessage.stopReason !== "stop"; + + if (!willRetry) { + return await this._runAutoCompaction("overflow", false); + } + if (this._overflowRecoveryAttempted) { this._emit({ type: "compaction_end", @@ -1846,7 +1855,7 @@ export class AgentSession { if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { this.agent.state.messages = messages.slice(0, -1); } - return await this._runAutoCompaction("overflow", true); + return await this._runAutoCompaction("overflow", willRetry); } // Case 2: Threshold - context is getting large diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index e04732d0..e790d896 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -248,6 +248,37 @@ describe("AgentSession compaction characterization", () => { ); }); + it("compacts successful overflow responses without retrying", async () => { + const harness = await createHarness({ + settings: { compaction: { enabled: true, keepRecentTokens: 1, reserveTokens: 0 } }, + models: [{ id: "faux-1", contextWindow: 1, maxTokens: 100 }], + extensionFactories: [ + (pi) => { + pi.on("session_before_compact", async (event) => ({ + compaction: { + summary: "successful overflow compacted", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + })); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([fauxAssistantMessage("completed answer")]); + + await expect(harness.session.prompt("hello")).resolves.toBeUndefined(); + + const compactionEnd = harness.eventsOfType("compaction_end").at(-1); + expect(compactionEnd).toMatchObject({ + reason: "overflow", + aborted: false, + willRetry: false, + }); + expect(harness.faux.state.callCount).toBe(1); + }); + it("ignores stale pre-compaction assistant usage on pre-prompt checks", async () => { const harness = await createHarness(); harnesses.push(harness); From 7d08c81a0962b4731ca41e89cddc9ed9eb103455 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 22:17:57 +0200 Subject: [PATCH 094/141] fix(coding-agent): avoid empty compaction summaries closes #4811 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 53 +++++++------------ .../src/core/compaction/compaction.ts | 4 ++ .../src/modes/interactive/interactive-mode.ts | 9 ---- .../test/compaction-extensions.test.ts | 1 + packages/coding-agent/test/compaction.test.ts | 25 +-------- .../suite/agent-session-compaction.test.ts | 19 ++++--- 7 files changed, 37 insertions(+), 75 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ab9812c5..0fcc303a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). - Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). ## [0.79.7] - 2026-06-18 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 23fb5983..cae330f3 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1892,19 +1892,10 @@ export class AgentSession { */ private async _runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise { const settings = this.settingsManager.getCompactionSettings(); - - this._emit({ type: "compaction_start", reason }); - this._autoCompactionAbortController = new AbortController(); + let started = false; try { if (!this.model) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } @@ -1914,13 +1905,6 @@ export class AgentSession { if (this.agent.streamFn === streamSimple) { const authResult = await this._modelRegistry.getApiKeyAndHeaders(this.model); if (!authResult.ok || !authResult.apiKey) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } apiKey = authResult.apiKey; @@ -1934,16 +1918,13 @@ export class AgentSession { const preparation = prepareCompaction(pathEntries, settings); if (!preparation) { - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - }); return false; } + this._emit({ type: "compaction_start", reason }); + this._autoCompactionAbortController = new AbortController(); + started = true; + let extensionCompaction: CompactionResult | undefined; let fromExtension = false; @@ -2054,17 +2035,19 @@ export class AgentSession { return this.agent.hasQueuedMessages(); } catch (error) { const errorMessage = error instanceof Error ? error.message : "compaction failed"; - this._emit({ - type: "compaction_end", - reason, - result: undefined, - aborted: false, - willRetry: false, - errorMessage: - reason === "overflow" - ? `Context overflow recovery failed: ${errorMessage}` - : `Auto-compaction failed: ${errorMessage}`, - }); + if (started) { + this._emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + errorMessage: + reason === "overflow" + ? `Context overflow recovery failed: ${errorMessage}` + : `Auto-compaction failed: ${errorMessage}`, + }); + } return false; } finally { this._autoCompactionAbortController = undefined; diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index 07f2f9fe..f3f3d59f 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -698,6 +698,10 @@ export function prepareCompaction( } } + if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) { + return undefined; + } + // Extract file operations from messages and previous compaction const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 00054eb2..962c414f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3700,7 +3700,6 @@ export class InteractiveMode { showError(errorMessage: string): void { this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0)); - this.chatContainer.addChild(new Spacer(1)); this.ui.requestRender(); } @@ -5695,14 +5694,6 @@ export class InteractiveMode { } private async handleCompactCommand(customInstructions?: string): Promise { - const entries = this.sessionManager.getEntries(); - const messageCount = entries.filter((e) => e.type === "message").length; - - if (messageCount < 2) { - this.showWarning("Nothing to compact (no messages yet)"); - return; - } - if (this.loadingAnimation) { this.loadingAnimation.stop(); this.loadingAnimation = undefined; diff --git a/packages/coding-agent/test/compaction-extensions.test.ts b/packages/coding-agent/test/compaction-extensions.test.ts index 2c41210e..c95125f2 100644 --- a/packages/coding-agent/test/compaction-extensions.test.ts +++ b/packages/coding-agent/test/compaction-extensions.test.ts @@ -98,6 +98,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => { const sessionManager = SessionManager.create(tempDir); const settingsManager = SettingsManager.create(tempDir, tempDir); + settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); const authStorage = AuthStorage.create(join(tempDir, "auth.json")); const modelRegistry = ModelRegistry.create(authStorage); diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index 929d06ec..be3d5c63 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -9,7 +9,6 @@ import { calculateContextTokens, compact, DEFAULT_COMPACTION_SETTINGS, - estimateContextTokens, findCutPoint, getLastAssistantUsage, prepareCompaction, @@ -396,7 +395,7 @@ describe("buildSessionContext", () => { }); describe("prepareCompaction with previous compaction", () => { - it("should preserve kept messages across repeated compactions when they still fit", () => { + it("should skip repeated compactions when kept messages still fit", () => { const u1 = createMessageEntry(createUserMessage("user msg 1 (summarized by compaction1)")); const a1 = createMessageEntry(createAssistantMessage("assistant msg 1")); const u2 = createMessageEntry(createUserMessage("user msg 2 - kept by compaction1")); @@ -408,29 +407,9 @@ describe("prepareCompaction with previous compaction", () => { const a4 = createMessageEntry(createAssistantMessage("assistant msg 4", createMockUsage(8000, 2000))); const pathEntries = [u1, a1, u2, a2, u3, a3, compaction1, u4, a4]; - const contextBefore = buildSessionContext(pathEntries); const preparation = prepareCompaction(pathEntries, DEFAULT_COMPACTION_SETTINGS); - expect(preparation).toBeDefined(); - expect(preparation!.firstKeptEntryId).toBe(u2.id); - expect(preparation!.previousSummary).toBe("First summary"); - expect(extractText(preparation!.messagesToSummarize)).not.toContain("First summary"); - expect(preparation!.tokensBefore).toBe(estimateContextTokens(contextBefore.messages).tokens); - - const compaction2: CompactionEntry = { - type: "compaction", - id: "compaction2-id", - parentId: a4.id, - timestamp: new Date().toISOString(), - summary: "Second summary", - firstKeptEntryId: preparation!.firstKeptEntryId, - tokensBefore: preparation!.tokensBefore, - }; - const contextAfter = buildSessionContext([...pathEntries, compaction2]); - const contextAfterText = extractText(contextAfter.messages); - - expect(contextAfterText).toContain("user msg 2 - kept by compaction1"); - expect(contextAfterText).toContain("user msg 3 - kept by compaction1"); + expect(preparation).toBeUndefined(); }); it("should re-summarize previously kept messages when the recent window moves past them", () => { diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index e790d896..a3302adf 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -67,19 +67,20 @@ function useSummaryStreamFn(harness: Harness, summary: string): () => number { } function seedCompactableSession(harness: Harness): void { + harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); const now = Date.now(); harness.sessionManager.appendMessage({ role: "user", content: [{ type: "text", text: "message to compact" }], timestamp: now - 1000, }); - harness.sessionManager.appendMessage( - createAssistant(harness, { - stopReason: "stop", - totalTokens: 100, - timestamp: now - 500, - }), - ); + const assistant = createAssistant(harness, { + stopReason: "stop", + totalTokens: 100, + timestamp: now - 500, + }); + assistant.content = [{ type: "text", text: "assistant response to compact" }]; + harness.sessionManager.appendMessage(assistant); harness.session.agent.state.messages = harness.sessionManager.buildSessionContext().messages; } @@ -96,6 +97,7 @@ describe("AgentSession compaction characterization", () => { it("manually compacts using an extension-provided summary", async () => { const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, extensionFactories: [ (pi) => { pi.on("session_before_compact", async (event) => ({ @@ -145,7 +147,7 @@ describe("AgentSession compaction characterization", () => { const result = await harness.session.compact(); - expect(result.summary).toBe("summary from custom stream"); + expect(result.summary).toContain("summary from custom stream"); expect(getStreamCallCount()).toBe(1); }); @@ -165,6 +167,7 @@ describe("AgentSession compaction characterization", () => { it("cancels in-progress manual compaction when abortCompaction is called", async () => { const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, extensionFactories: [ (pi) => { pi.on("session_before_compact", async (event) => { From b09fbde00eaeb33413b9879137612e002bbb4086 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 18 Jun 2026 23:39:02 +0300 Subject: [PATCH 095/141] feat(ai): add OpenRouter Fusion alias (#5866) --- packages/ai/scripts/generate-models.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 3d75a82e..0c1bcc42 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -2030,6 +2030,32 @@ async function generateModels() { }); } + // Add "fusion" alias for openrouter/fusion. OpenRouter exposes Fusion as a + // router alias/plugin entry point; its model metadata does not advertise + // tools, but the alias resolves to a concrete model that can invoke caller + // tools and has the openrouter:fusion server tool auto-injected. + if (!allModels.some(m => m.provider === "openrouter" && m.id === "openrouter/fusion")) { + allModels.push({ + id: "openrouter/fusion", + name: "OpenRouter: Fusion", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + // we dont know about the costs because Fusion routes to multiple models + // and then charges you for the underlying used models + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + }); + } + // Azure Foundry deploys these with larger context windows than OpenAI's own API, // which caps gpt-5.4/gpt-5.5 at 272k. See models-sold-directly-by-azure docs. const AZURE_CONTEXT_WINDOW_OVERRIDES: Record = { From c60f6a8ab3b36155d572fe9c10df61f7613281ee Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:01:10 +0200 Subject: [PATCH 096/141] feat(coding-agent): expose post-compaction token estimates closes #5877 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/docs/rpc.md | 4 ++++ packages/coding-agent/src/core/agent-session.ts | 15 ++++++++++++++- .../src/core/compaction/compaction.ts | 1 + .../test/suite/agent-session-compaction.test.ts | 5 +++++ 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0fcc303a..e58cd877 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). + ### Fixed - Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). diff --git a/packages/coding-agent/docs/rpc.md b/packages/coding-agent/docs/rpc.md index 9aa16ffc..a9942409 100644 --- a/packages/coding-agent/docs/rpc.md +++ b/packages/coding-agent/docs/rpc.md @@ -374,11 +374,14 @@ Response: "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, + "estimatedTokensAfter": 32000, "details": {} } } ``` +`estimatedTokensAfter` is a heuristic estimate over the rebuilt message context immediately after compaction, not a provider-exact token count. + #### set_auto_compaction Enable or disable automatic compaction when context is nearly full. @@ -924,6 +927,7 @@ The `reason` field is `"manual"`, `"threshold"`, or `"overflow"`. "summary": "Summary of conversation...", "firstKeptEntryId": "abc123", "tokensBefore": 150000, + "estimatedTokensAfter": 32000, "details": {} }, "aborted": false, diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index cae330f3..f8af4320 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -45,6 +45,7 @@ import { collectEntriesForBranchSummary, compact, estimateContextTokens, + estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, @@ -242,6 +243,14 @@ interface ToolDefinitionEntry { sourceInfo: SourceInfo; } +function estimateMessagesTokens(messages: AgentMessage[]): number { + let tokens = 0; + for (const message of messages) { + tokens += estimateTokens(message); + } + return tokens; +} + // ============================================================================ // Constants // ============================================================================ @@ -1726,6 +1735,7 @@ export class AgentSession { const newEntries = this.sessionManager.getEntries(); const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; + const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages); // Get the saved compaction entry for the extension event const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as @@ -1740,10 +1750,11 @@ export class AgentSession { }); } - const compactionResult = { + const compactionResult: CompactionResult = { summary, firstKeptEntryId, tokensBefore, + estimatedTokensAfter, details, }; this._emit({ @@ -1999,6 +2010,7 @@ export class AgentSession { const newEntries = this.sessionManager.getEntries(); const sessionContext = this.sessionManager.buildSessionContext(); this.agent.state.messages = sessionContext.messages; + const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages); // Get the saved compaction entry for the extension event const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary) as @@ -2017,6 +2029,7 @@ export class AgentSession { summary, firstKeptEntryId, tokensBefore, + estimatedTokensAfter, details, }; this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry }); diff --git a/packages/coding-agent/src/core/compaction/compaction.ts b/packages/coding-agent/src/core/compaction/compaction.ts index f3f3d59f..83315a84 100644 --- a/packages/coding-agent/src/core/compaction/compaction.ts +++ b/packages/coding-agent/src/core/compaction/compaction.ts @@ -104,6 +104,7 @@ export interface CompactionResult { summary: string; firstKeptEntryId: string; tokensBefore: number; + estimatedTokensAfter?: number; /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ details?: T; } diff --git a/packages/coding-agent/test/suite/agent-session-compaction.test.ts b/packages/coding-agent/test/suite/agent-session-compaction.test.ts index a3302adf..ba5ae5c7 100644 --- a/packages/coding-agent/test/suite/agent-session-compaction.test.ts +++ b/packages/coding-agent/test/suite/agent-session-compaction.test.ts @@ -5,6 +5,7 @@ import { type Model, } from "@earendil-works/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { estimateTokens } from "../../src/core/compaction/index.ts"; import { createHarness, type Harness } from "./harness.ts"; type SessionWithCompactionInternals = { @@ -118,8 +119,10 @@ describe("AgentSession compaction characterization", () => { const result = await harness.session.compact(); const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + const estimatedTokensAfter = harness.session.messages.reduce((sum, message) => sum + estimateTokens(message), 0); expect(result.summary).toBe("summary from extension"); + expect(result.estimatedTokensAfter).toBe(estimatedTokensAfter); expect(compactionEntries).toHaveLength(1); expect(harness.session.messages[0]?.role).toBe("compactionSummary"); }); @@ -161,7 +164,9 @@ describe("AgentSession compaction characterization", () => { await sessionInternals._runAutoCompaction("threshold", false); const compactionEntries = harness.sessionManager.getEntries().filter((entry) => entry.type === "compaction"); + const compactionEnd = harness.eventsOfType("compaction_end").at(-1); expect(compactionEntries).toHaveLength(1); + expect(compactionEnd?.result?.estimatedTokensAfter).toBeGreaterThan(0); expect(getStreamCallCount()).toBe(1); }); From cab89d143587e19b4f77b0d390028bd90a279b19 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:09:41 +0200 Subject: [PATCH 097/141] docs: audit unreleased changelogs --- packages/ai/CHANGELOG.md | 4 ++++ packages/coding-agent/CHANGELOG.md | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 72851f5f..2b5aeca1 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). + ## [0.79.7] - 2026-06-18 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e58cd877..5f0c6186 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,9 +2,15 @@ ## [Unreleased] +### New Features + +- **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). +- **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). + ### Added - Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). +- Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). ### Fixed From fd1ba2c7feaedb78e5d87107f6e6a274044121b8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:15:18 +0200 Subject: [PATCH 098/141] test(coding-agent): seed auto-compaction queue fixture --- ...gent-session-auto-compaction-queue.test.ts | 103 ++++++++++-------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 1abe3e28..7fd56b46 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -2,7 +2,12 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent } from "@earendil-works/pi-agent-core"; -import { type AssistantMessage, getModel } from "@earendil-works/pi-ai"; +import { + type AssistantMessage, + createAssistantMessageEventStream, + fauxAssistantMessage, + getModel, +} from "@earendil-works/pi-ai"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AgentSession } from "../src/core/agent-session.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; @@ -11,51 +16,10 @@ import { SessionManager } from "../src/core/session-manager.ts"; import { SettingsManager } from "../src/core/settings-manager.ts"; import { createTestResourceLoader } from "./utilities.ts"; -vi.mock("../src/core/compaction/index.js", () => ({ - calculateContextTokens: (usage: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens?: number; - }) => usage.totalTokens ?? usage.input + usage.output + usage.cacheRead + usage.cacheWrite, - collectEntriesForBranchSummary: () => ({ entries: [], commonAncestorId: null }), - compact: async () => ({ - summary: "compacted", - firstKeptEntryId: "entry-1", - tokensBefore: 100, - details: {}, - }), - estimateContextTokens: ( - messages: Array<{ - role: string; - usage?: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens?: number }; - stopReason?: string; - }>, - ) => { - // Walk backwards to find last non-error, non-aborted assistant with usage - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg.role === "assistant" && msg.stopReason !== "error" && msg.stopReason !== "aborted" && msg.usage) { - const tokens = - msg.usage.totalTokens ?? msg.usage.input + msg.usage.output + msg.usage.cacheRead + msg.usage.cacheWrite; - return { tokens, usageTokens: tokens, trailingTokens: 0, lastUsageIndex: i }; - } - } - return { tokens: 0, usageTokens: 0, trailingTokens: 0, lastUsageIndex: null }; - }, - generateBranchSummary: async () => ({ summary: "", aborted: false, readFiles: [], modifiedFiles: [] }), - prepareCompaction: () => ({ dummy: true }), - shouldCompact: ( - contextTokens: number, - contextWindow: number, - settings: { enabled: boolean; reserveTokens: number }, - ) => settings.enabled && contextTokens > contextWindow - settings.reserveTokens, -})); - describe("AgentSession auto-compaction queue resume", () => { let session: AgentSession; let sessionManager: SessionManager; + let settingsManager: SettingsManager; let tempDir: string; beforeEach(() => { @@ -73,7 +37,7 @@ describe("AgentSession auto-compaction queue resume", () => { }); sessionManager = SessionManager.inMemory(); - const settingsManager = SettingsManager.create(tempDir, tempDir); + settingsManager = SettingsManager.create(tempDir, tempDir); const authStorage = AuthStorage.create(join(tempDir, "auth.json")); authStorage.setRuntimeApiKey("anthropic", "test-key"); const modelRegistry = ModelRegistry.create(authStorage, tempDir); @@ -98,6 +62,57 @@ describe("AgentSession auto-compaction queue resume", () => { }); it("should resume after threshold compaction when only agent-level queued messages exist", async () => { + settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); + const model = session.model!; + const now = Date.now(); + sessionManager.appendMessage({ + role: "user", + content: [{ type: "text", text: "message to compact" }], + timestamp: now - 1000, + }); + sessionManager.appendMessage({ + role: "assistant", + content: [{ type: "text", text: "assistant response to compact" }], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 100, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 100, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: now - 500, + }); + session.agent.state.messages = sessionManager.buildSessionContext().messages; + session.agent.streamFn = (summaryModel) => { + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => { + stream.push({ + type: "done", + reason: "stop", + message: { + ...fauxAssistantMessage("compacted"), + api: summaryModel.api, + provider: summaryModel.provider, + model: summaryModel.id, + usage: { + input: 10, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 10, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + }, + }); + }); + return stream; + }; + session.agent.followUp({ role: "custom", customType: "test", From 8025fdd01f718f3dd2624b18089ac3623ee3c628 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:36:08 +0200 Subject: [PATCH 099/141] meta: Update readmes slightly --- README.md | 52 ++++++++++++++++----------------- packages/coding-agent/README.md | 13 ++++----- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index dc31b930..130a412f 100644 --- a/README.md +++ b/README.md @@ -5,46 +5,24 @@

    Discord -

    -

    - pi.dev domain graciously donated by -

    - Exy mascot
    exe.dev
    + npm

    > New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](CONTRIBUTING.md). ---- +# Pi Agent Harness -# Pi Agent Harness Mono Repo - -This is the home of the pi agent harness project including our self extensible coding agent. +This is the home of the Pi agent harness project including our self extensible coding agent. * **[@earendil-works/pi-coding-agent](packages/coding-agent)**: Interactive coding agent CLI * **[@earendil-works/pi-agent-core](packages/agent)**: Agent runtime with tool calling and state management * **[@earendil-works/pi-ai](packages/ai)**: Unified multi-provider LLM API (OpenAI, Anthropic, Google, …) -To learn more about pi: +To learn more about Pi: * [Visit pi.dev](https://pi.dev), the project website with demos * [Read the documentation](https://pi.dev/docs/latest), but you can also ask the agent to explain itself -## Share your OSS coding agent sessions - -If you use pi or other coding agents for open source work, please share your sessions. - -Public OSS session data helps improve coding agents with real-world tasks, tool use, failures, and fixes instead of toy benchmarks. - -For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911). - -To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`. - -You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions. - -I regularly publish my own `pi-mono` work sessions here: - -- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono) - ## All Packages | Package | Description | @@ -94,6 +72,28 @@ We treat npm dependency changes as reviewed code changes. - CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`. - Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed. +## Share your OSS coding agent sessions + +If you use Pi or other coding agents for open source work, please share your sessions. + +Public OSS session data helps improve coding agents with real-world tasks, tool use, failures, and fixes instead of toy benchmarks. + +For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911). + +To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`. + +You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions. + +I regularly publish my own `pi-mono` work sessions here: + +- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono) + ## License MIT + +

    + pi.dev domain graciously donated by +

    + Exy mascot
    exe.dev
    +

    diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index ce7dba2a..68e17a62 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -7,11 +7,6 @@ Discord npm

    -

    - pi.dev domain graciously donated by -

    - Exy mascot
    exe.dev
    -

    > New issues and PRs from new contributors are auto-closed by default. Maintainers review auto-closed issues daily. See [CONTRIBUTING.md](../../CONTRIBUTING.md). @@ -675,8 +670,6 @@ pi --thinking high "Solve this complex problem" See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines and [docs/development.md](docs/development.md) for setup, forking, and debugging. ---- - ## License MIT @@ -686,3 +679,9 @@ MIT - [@earendil-works/pi-ai](https://www.npmjs.com/package/@earendil-works/pi-ai): Core LLM toolkit - [@earendil-works/pi-agent-core](https://www.npmjs.com/package/@earendil-works/pi-agent-core): Agent framework - [@earendil-works/pi-tui](https://www.npmjs.com/package/@earendil-works/pi-tui): Terminal UI components + +

    + pi.dev domain graciously donated by +

    + Exy mascot
    exe.dev
    +

    From 651d10d90b4bd80c85c3a09e18a719e4d5dc4c13 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:47:25 +0200 Subject: [PATCH 100/141] feat(ai): enable Mistral prompt caching --- package-lock.json | 36 ++++++++++-- packages/ai/CHANGELOG.md | 1 + packages/ai/package.json | 5 +- packages/ai/scripts/generate-models.ts | 2 +- packages/ai/src/models.generated.ts | 58 +++++++++---------- packages/ai/src/providers/mistral.ts | 39 +++++++++++-- .../ai/test/mistral-reasoning-mode.test.ts | 18 ++++++ packages/coding-agent/npm-shrinkwrap.json | 38 ++++++++++-- 8 files changed, 152 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 821c9d30..d1abbf97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1624,14 +1624,23 @@ } }, "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@nodable/entities": { @@ -1684,6 +1693,24 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6219,7 +6246,8 @@ "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2b5aeca1..c4c63d8b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added prompt caching for Mistral requests using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). ## [0.79.7] - 2026-06-18 diff --git a/packages/ai/package.json b/packages/ai/package.json index 9c71d156..2c0bd6cf 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -69,9 +69,10 @@ "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@smithy/node-http-handler": "4.7.3", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 0c1bcc42..c1fddc92 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -948,7 +948,7 @@ async function loadModelsDevData(): Promise[]> { cost: { input: m.cost?.input || 0, output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, + cacheRead: m.cost?.cache_read ?? (m.cost?.input ? roundCost(m.cost.input * 0.1) : 0), cacheWrite: m.cost?.cache_write || 0, }, contextWindow: m.limit?.context || 4096, diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 8e1b776f..82d0e137 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -5761,7 +5761,7 @@ export const MODELS = { cost: { input: 0.3, output: 0.9, - cacheRead: 0, + cacheRead: 0.03, cacheWrite: 0, }, contextWindow: 256000, @@ -5778,7 +5778,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -5795,7 +5795,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -5812,7 +5812,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 128000, @@ -5829,7 +5829,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -5846,7 +5846,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.3, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -5863,7 +5863,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.3, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -5897,7 +5897,7 @@ export const MODELS = { cost: { input: 2, output: 5, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 128000, @@ -5914,7 +5914,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 128000, @@ -5931,7 +5931,7 @@ export const MODELS = { cost: { input: 0.04, output: 0.04, - cacheRead: 0, + cacheRead: 0.004, cacheWrite: 0, }, contextWindow: 128000, @@ -5948,7 +5948,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.1, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -5965,7 +5965,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 131072, @@ -5982,7 +5982,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -5999,7 +5999,7 @@ export const MODELS = { cost: { input: 0.5, output: 1.5, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, @@ -6016,7 +6016,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 131072, @@ -6033,7 +6033,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -6050,7 +6050,7 @@ export const MODELS = { cost: { input: 1.5, output: 7.5, - cacheRead: 0, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 262144, @@ -6067,7 +6067,7 @@ export const MODELS = { cost: { input: 1.5, output: 7.5, - cacheRead: 0, + cacheRead: 0.15, cacheWrite: 0, }, contextWindow: 262144, @@ -6084,7 +6084,7 @@ export const MODELS = { cost: { input: 0.4, output: 2, - cacheRead: 0, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, @@ -6101,7 +6101,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.15, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 128000, @@ -6118,7 +6118,7 @@ export const MODELS = { cost: { input: 0.1, output: 0.3, - cacheRead: 0, + cacheRead: 0.01, cacheWrite: 0, }, contextWindow: 128000, @@ -6135,7 +6135,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.6, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 256000, @@ -6152,7 +6152,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.6, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 256000, @@ -6169,7 +6169,7 @@ export const MODELS = { cost: { input: 0.25, output: 0.25, - cacheRead: 0, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 8000, @@ -6186,7 +6186,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.15, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 128000, @@ -6203,7 +6203,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 64000, @@ -6220,7 +6220,7 @@ export const MODELS = { cost: { input: 0.7, output: 0.7, - cacheRead: 0, + cacheRead: 0.07, cacheWrite: 0, }, contextWindow: 32000, @@ -6237,7 +6237,7 @@ export const MODELS = { cost: { input: 0.15, output: 0.15, - cacheRead: 0, + cacheRead: 0.015, cacheWrite: 0, }, contextWindow: 128000, @@ -6254,7 +6254,7 @@ export const MODELS = { cost: { input: 2, output: 6, - cacheRead: 0, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 128000, diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index 1bc7d4ce..6a132236 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -226,7 +226,7 @@ function buildRequestOptions(model: Model<"mistral-conversations">, options?: Mi // Mistral infrastructure uses `x-affinity` for KV-cache reuse (prefix caching). // Respect explicit caller-provided header values. - if (options?.sessionId && !headers["x-affinity"]) { + if (shouldUsePromptCaching(options) && !headers["x-affinity"]) { headers["x-affinity"] = options.sessionId; } @@ -255,6 +255,7 @@ function buildChatPayload( if (options?.toolChoice) payload.toolChoice = mapToolChoice(options.toolChoice); if (options?.promptMode) payload.promptMode = options.promptMode; if (options?.reasoningEffort) payload.reasoningEffort = options.reasoningEffort; + if (shouldUsePromptCaching(options)) payload.promptCacheKey = options.sessionId; if (context.systemPrompt) { payload.messages.unshift({ @@ -266,6 +267,31 @@ function buildChatPayload( return payload; } +function shouldUsePromptCaching(options?: MistralOptions): options is MistralOptions & { sessionId: string } { + return options?.cacheRetention !== "none" && !!options?.sessionId; +} + +function getMistralCachedPromptTokens(usage: unknown, promptTokens: number): number { + const rawUsage = usage as { + promptTokensDetails?: { cachedTokens?: unknown } | null; + prompt_tokens_details?: { cached_tokens?: unknown } | null; + promptTokenDetails?: { cachedTokens?: unknown } | null; + prompt_token_details?: { cached_tokens?: unknown } | null; + numCachedTokens?: unknown; + num_cached_tokens?: unknown; + }; + const rawCachedTokens = + rawUsage.promptTokensDetails?.cachedTokens ?? + rawUsage.prompt_tokens_details?.cached_tokens ?? + rawUsage.promptTokenDetails?.cachedTokens ?? + rawUsage.prompt_token_details?.cached_tokens ?? + rawUsage.numCachedTokens ?? + rawUsage.num_cached_tokens ?? + 0; + const cachedTokens = typeof rawCachedTokens === "number" && Number.isFinite(rawCachedTokens) ? rawCachedTokens : 0; + return Math.min(promptTokens, Math.max(0, cachedTokens)); +} + async function consumeChatStream( model: Model<"mistral-conversations">, output: AssistantMessage, @@ -305,11 +331,16 @@ async function consumeChatStream( output.responseId ||= chunk.id; if (chunk.usage) { - output.usage.input = chunk.usage.promptTokens || 0; + const promptTokens = chunk.usage.promptTokens || 0; + const cachedPromptTokens = getMistralCachedPromptTokens(chunk.usage, promptTokens); + + output.usage.input = Math.max(0, promptTokens - cachedPromptTokens); output.usage.output = chunk.usage.completionTokens || 0; - output.usage.cacheRead = 0; + output.usage.cacheRead = cachedPromptTokens; output.usage.cacheWrite = 0; - output.usage.totalTokens = chunk.usage.totalTokens || output.usage.input + output.usage.output; + output.usage.totalTokens = + chunk.usage.totalTokens || + output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; calculateCost(model, output.usage); } diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 35a5b96b..d197a56a 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -6,6 +6,7 @@ import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { promptMode?: "reasoning"; reasoningEffort?: "none" | "high"; + promptCacheKey?: string; } function makeContext(): Context { @@ -77,4 +78,21 @@ describe("Mistral reasoning mode selection", () => { expect(payload.reasoningEffort).toBeUndefined(); expect(payload.promptMode).toBeUndefined(); }); + + it("uses the session id as prompt cache key", async () => { + const payload = await capturePayload(getModel("mistral", "mistral-large-latest"), { + sessionId: "session-123", + }); + + expect(payload.promptCacheKey).toBe("session-123"); + }); + + it("omits prompt cache key when cache retention is disabled", async () => { + const payload = await capturePayload(getModel("mistral", "mistral-large-latest"), { + sessionId: "session-123", + cacheRetention: "none", + }); + + expect(payload.promptCacheKey).toBeUndefined(); + }); }); diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index c366f1a5..ad672bf2 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -494,9 +494,10 @@ "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@smithy/node-http-handler": "4.7.3", "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.1", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", @@ -741,14 +742,23 @@ "optional": true }, "node_modules/@mistralai/mistralai": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", - "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/@nodable/entities": { @@ -763,6 +773,24 @@ } ] }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", From 9179734cc205fb40a4deaa70e4cd586529cdf927 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 18 Jun 2026 23:58:34 +0200 Subject: [PATCH 101/141] docs(coding-agent): audit unreleased changelog --- packages/coding-agent/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5f0c6186..06ab4f9b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,11 +4,13 @@ ### New Features +- **Mistral prompt caching** - Mistral sessions now use provider-side prompt caching with session affinity and cached-token usage/cost accounting. See [API Keys](docs/providers.md#api-keys) and [Environment Variables](docs/usage.md#environment-variables). - **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). - **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). ### Added +- Added inherited Mistral prompt caching using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). - Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). From 1a418ad223a13a58aab10549f0dcf15a84670c8f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 00:26:30 +0200 Subject: [PATCH 102/141] chore: remove inprogress label on close --- .../workflows/remove-inprogress-on-close.yml | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/remove-inprogress-on-close.yml diff --git a/.github/workflows/remove-inprogress-on-close.yml b/.github/workflows/remove-inprogress-on-close.yml new file mode 100644 index 00000000..e94b10e2 --- /dev/null +++ b/.github/workflows/remove-inprogress-on-close.yml @@ -0,0 +1,31 @@ +name: Remove In Progress Label On Close + +on: + issues: + types: [closed] + +jobs: + remove-label: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Remove inprogress label + uses: actions/github-script@v7 + with: + script: | + const labelName = 'inprogress'; + const labels = context.payload.issue.labels ?? []; + const hasLabel = labels.some((label) => label.name === labelName); + + if (!hasLabel) { + console.log(`Issue does not have ${labelName} label`); + return; + } + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: labelName, + }); From 0d89a333739d35254cc62e5579a8cf526c096550 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" <622227+FredKSchott@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:41:49 -0700 Subject: [PATCH 103/141] feat(packages): Add selective pi-ai base entrypoints (#5348) --- packages/agent/README.md | 18 + packages/agent/package.json | 4 + packages/agent/src/agent-loop.ts | 2 +- packages/agent/src/agent.ts | 2 +- packages/agent/src/base.ts | 39 ++ packages/agent/src/harness/agent-harness.ts | 2 +- .../compaction/branch-summarization.ts | 3 +- .../src/harness/compaction/compaction.ts | 10 +- .../agent/src/harness/compaction/utils.ts | 2 +- packages/agent/src/harness/messages.ts | 2 +- packages/agent/src/harness/session/session.ts | 2 +- packages/agent/src/harness/types.ts | 4 +- packages/agent/src/index.ts | 49 +- packages/agent/src/proxy.ts | 2 +- packages/agent/src/types.ts | 2 +- packages/agent/vitest.config.ts | 6 + packages/agent/vitest.harness.config.ts | 6 + packages/ai/README.md | 32 +- packages/ai/package.json | 12 + packages/ai/src/base.ts | 45 ++ packages/ai/src/bedrock-provider.ts | 4 +- packages/ai/src/images-api-registry.ts | 4 + packages/ai/src/images.ts | 2 - packages/ai/src/index.ts | 49 +- packages/ai/src/providers/amazon-bedrock.ts | 9 + packages/ai/src/providers/anthropic.ts | 9 + .../src/providers/azure-openai-responses.ts | 9 + packages/ai/src/providers/google-vertex.ts | 9 + packages/ai/src/providers/google.ts | 9 + .../ai/src/providers/images/openrouter.ts | 8 + .../src/providers/images/register-builtins.ts | 50 -- packages/ai/src/providers/mistral.ts | 9 + .../src/providers/openai-codex-responses.ts | 9 + .../ai/src/providers/openai-completions.ts | 9 + packages/ai/src/providers/openai-responses.ts | 9 + .../ai/src/providers/register-builtins.ts | 464 +++++++----------- packages/ai/src/stream.ts | 4 - packages/ai/test/abort.test.ts | 2 +- .../anthropic-eager-tool-input-e2e.test.ts | 2 +- ...ic-empty-thinking-signature-compat.test.ts | 2 +- .../anthropic-force-adaptive-thinking.test.ts | 2 +- ...anthropic-long-cache-retention-e2e.test.ts | 2 +- .../ai/test/anthropic-opus-4-8-smoke.test.ts | 2 +- .../test/anthropic-temperature-compat.test.ts | 2 +- .../test/anthropic-thinking-disable.test.ts | 2 +- .../anthropic-tool-name-normalization.test.ts | 2 +- packages/ai/test/base-entrypoint.test.ts | 68 +++ packages/ai/test/bedrock-models.test.ts | 2 +- packages/ai/test/cache-retention.test.ts | 2 +- packages/ai/test/context-overflow.test.ts | 2 +- .../ai/test/cross-provider-handoff.test.ts | 2 +- packages/ai/test/empty.test.ts | 2 +- .../ai/test/google-thinking-disable.test.ts | 2 +- packages/ai/test/interleaved-thinking.test.ts | 2 +- packages/ai/test/lazy-module-load.test.ts | 52 +- .../ai/test/mistral-reasoning-mode.test.ts | 2 +- packages/ai/test/mistral-tool-schema.test.ts | 2 +- .../openai-codex-cache-affinity-e2e.test.ts | 2 +- .../openai-completions-empty-tools.test.ts | 2 +- .../openai-completions-response-model.test.ts | 2 +- .../openai-completions-tool-choice.test.ts | 2 +- ...penai-responses-cache-affinity-e2e.test.ts | 2 +- ...nai-responses-reasoning-replay-e2e.test.ts | 2 +- .../test/openrouter-cache-write-repro.test.ts | 2 +- packages/ai/test/openrouter-images.test.ts | 12 + packages/ai/test/responseid.test.ts | 2 +- packages/ai/test/stream.test.ts | 2 +- packages/ai/test/tokens.test.ts | 2 +- .../test/tool-call-id-normalization.test.ts | 2 +- .../ai/test/tool-call-without-result.test.ts | 2 +- packages/ai/test/total-tokens.test.ts | 2 +- packages/ai/test/unicode-surrogate.test.ts | 2 +- packages/ai/test/xhigh.test.ts | 2 +- ...ms-anthropic-empty-signature-smoke.test.ts | 2 +- packages/ai/test/zen.test.ts | 2 +- packages/coding-agent/vitest.config.ts | 9 + scripts/check-browser-smoke.mjs | 44 ++ tsconfig.json | 1 + 78 files changed, 671 insertions(+), 495 deletions(-) create mode 100644 packages/agent/src/base.ts create mode 100644 packages/ai/src/base.ts delete mode 100644 packages/ai/src/providers/images/register-builtins.ts create mode 100644 packages/ai/test/base-entrypoint.test.ts diff --git a/packages/agent/README.md b/packages/agent/README.md index f0fedb81..ca4c1080 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -31,6 +31,24 @@ agent.subscribe((event) => { await agent.prompt("Hello!"); ``` +## Base Entry Point + +Use `@earendil-works/pi-agent-core/base` with `@earendil-works/pi-ai/base` when bundling applications that should include only selected provider transports: + +```typescript +import { Agent } from "@earendil-works/pi-agent-core/base"; +import { getModel } from "@earendil-works/pi-ai/base"; +import { register } from "@earendil-works/pi-ai/anthropic"; + +register(); + +const agent = new Agent({ + initialState: { model: getModel("anthropic", "claude-sonnet-4-6") }, +}); +``` + +The default `@earendil-works/pi-agent-core` entry point remains batteries-included and registers pi-ai's lazy built-in transports for backward compatibility. + ## Core Concepts ### AgentMessage vs LLM Message diff --git a/packages/agent/package.json b/packages/agent/package.json index 81d3e356..48934f8f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -10,6 +10,10 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./base": { + "types": "./dist/base.d.ts", + "import": "./dist/base.js" + }, "./node": { "types": "./dist/node.d.ts", "import": "./dist/node.js" diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 77bd9568..8a8f20f1 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -10,7 +10,7 @@ import { streamSimple, type ToolResultMessage, validateToolArguments, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import type { AgentContext, AgentEvent, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index db6684a8..ef26e213 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -7,7 +7,7 @@ import { type TextContent, type ThinkingBudgets, type Transport, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts"; import type { AfterToolCallContext, diff --git a/packages/agent/src/base.ts b/packages/agent/src/base.ts new file mode 100644 index 00000000..ad1abafa --- /dev/null +++ b/packages/agent/src/base.ts @@ -0,0 +1,39 @@ +export * from "./agent.ts"; +export * from "./agent-loop.ts"; +export * from "./harness/agent-harness.ts"; +export { + type BranchPreparation, + type BranchSummaryDetails, + type CollectEntriesResult, + collectEntriesForBranchSummary, + generateBranchSummary, + prepareBranchEntries, +} from "./harness/compaction/branch-summarization.ts"; +export { + calculateContextTokens, + compact, + DEFAULT_COMPACTION_SETTINGS, + estimateContextTokens, + estimateTokens, + findCutPoint, + findTurnStartIndex, + generateSummary, + getLastAssistantUsage, + prepareCompaction, + serializeConversation, + shouldCompact, +} from "./harness/compaction/compaction.ts"; +export * from "./harness/messages.ts"; +export * from "./harness/prompt-templates.ts"; +export * from "./harness/session/jsonl-repo.ts"; +export * from "./harness/session/memory-repo.ts"; +export * from "./harness/session/repo-utils.ts"; +export * from "./harness/session/session.ts"; +export { uuidv7 } from "./harness/session/uuid.ts"; +export * from "./harness/skills.ts"; +export * from "./harness/system-prompt.ts"; +export * from "./harness/types.ts"; +export * from "./harness/utils/shell-output.ts"; +export * from "./harness/utils/truncate.ts"; +export * from "./proxy.ts"; +export * from "./types.ts"; diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 96563465..f097ee75 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -4,7 +4,7 @@ import { type Model, streamSimple, type UserMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index c1824ebf..60ffe771 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,5 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import { completeSimple, type Model } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index dba753d7..d619b34a 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,5 +1,11 @@ -import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; -import { completeSimple } from "@earendil-works/pi-ai"; +import { + type AssistantMessage, + completeSimple, + type ImageContent, + type Model, + type TextContent, + type Usage, +} from "@earendil-works/pi-ai/base"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/utils.ts b/packages/agent/src/harness/compaction/utils.ts index 07535b79..388dd84a 100644 --- a/packages/agent/src/harness/compaction/utils.ts +++ b/packages/agent/src/harness/compaction/utils.ts @@ -1,4 +1,4 @@ -import type { Message } from "@earendil-works/pi-ai"; +import type { Message } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../../types.ts"; /** File paths touched by a session branch or compaction range. */ diff --git a/packages/agent/src/harness/messages.ts b/packages/agent/src/harness/messages.ts index 36ce96a1..8368dd34 100644 --- a/packages/agent/src/harness/messages.ts +++ b/packages/agent/src/harness/messages.ts @@ -1,4 +1,4 @@ -import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; +import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../types.ts"; export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 6f136208..3fd76c6b 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -1,4 +1,4 @@ -import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; +import type { ImageContent, TextContent } from "@earendil-works/pi-ai/base"; import type { AgentMessage } from "../../types.ts"; import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.ts"; import type { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 4756ca84..8f281570 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,5 +1,5 @@ -import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; -import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts"; +import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai/base"; +import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../types.ts"; import type { Session } from "./session/session.ts"; /** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index fd3c2b9f..08b7b9cb 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -1,44 +1,5 @@ -// Core Agent -export * from "./agent.ts"; -// Loop functions -export * from "./agent-loop.ts"; -export * from "./harness/agent-harness.ts"; -export { - type BranchPreparation, - type BranchSummaryDetails, - type CollectEntriesResult, - collectEntriesForBranchSummary, - generateBranchSummary, - prepareBranchEntries, -} from "./harness/compaction/branch-summarization.ts"; -export { - calculateContextTokens, - compact, - DEFAULT_COMPACTION_SETTINGS, - estimateContextTokens, - estimateTokens, - findCutPoint, - findTurnStartIndex, - generateSummary, - getLastAssistantUsage, - prepareCompaction, - serializeConversation, - shouldCompact, -} from "./harness/compaction/compaction.ts"; -export * from "./harness/messages.ts"; -export * from "./harness/prompt-templates.ts"; -export * from "./harness/session/jsonl-repo.ts"; -export * from "./harness/session/memory-repo.ts"; -export * from "./harness/session/repo-utils.ts"; -export * from "./harness/session/session.ts"; -export { uuidv7 } from "./harness/session/uuid.ts"; -export * from "./harness/skills.ts"; -export * from "./harness/system-prompt.ts"; -// Harness -export * from "./harness/types.ts"; -export * from "./harness/utils/shell-output.ts"; -export * from "./harness/utils/truncate.ts"; -// Proxy utilities -export * from "./proxy.ts"; -// Types -export * from "./types.ts"; +// Import the default pi-ai entrypoint so that all built-in providers register +// automatically. Unlike "@earendil-works/pi-ai/base" which does not. +import "@earendil-works/pi-ai"; + +export * from "./base.ts"; diff --git a/packages/agent/src/proxy.ts b/packages/agent/src/proxy.ts index 5f0925c9..8e5cb2c2 100644 --- a/packages/agent/src/proxy.ts +++ b/packages/agent/src/proxy.ts @@ -14,7 +14,7 @@ import { type SimpleStreamOptions, type StopReason, type ToolCall, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; // Create stream class matching ProxyMessageEventStream class ProxyMessageEventStream extends EventStream { diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index cb99a79a..541d8d63 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -9,7 +9,7 @@ import type { TextContent, Tool, ToolResultMessage, -} from "@earendil-works/pi-ai"; +} from "@earendil-works/pi-ai/base"; import type { Static, TSchema } from "typebox"; /** diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts index bcc497fa..297fbd50 100644 --- a/packages/agent/vitest.config.ts +++ b/packages/agent/vitest.config.ts @@ -1,6 +1,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + alias: { + "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, + "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, + }, + }, test: { globals: true, environment: "node", diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts index 9421e5a9..37acc34d 100644 --- a/packages/agent/vitest.harness.config.ts +++ b/packages/agent/vitest.harness.config.ts @@ -1,6 +1,12 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + alias: { + "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, + "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, + }, + }, test: { globals: true, environment: "node", diff --git a/packages/ai/README.md b/packages/ai/README.md index a7b8028e..8137ccd4 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -8,6 +8,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Supported Providers](#supported-providers) - [Installation](#installation) +- [Base Entry Point](#base-entry-point) - [Quick Start](#quick-start) - [Tools](#tools) - [Defining Tools](#defining-tools) @@ -88,6 +89,30 @@ npm install @earendil-works/pi-ai TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, and `TSchema`. +## Base Entry Point + +Use `@earendil-works/pi-ai/base` when a bundler should include only explicitly selected transport implementations. The base entry point exposes model discovery, registries, generic dispatch, environment-key helpers, and provider-neutral utilities without registering built-in transports during module initialization. Generic dispatch still resolves configured environment keys when called. + +Import each selected transport directly and call its `register()` function: + +```typescript +import { getModel, streamSimple } from '@earendil-works/pi-ai/base'; +import { register as registerAnthropic } from '@earendil-works/pi-ai/anthropic'; + +registerAnthropic(); + +const model = getModel('anthropic', 'claude-sonnet-4-6'); +const response = await streamSimple(model, { + messages: [{ role: 'user', content: 'Hello!' }] +}, { + apiKey: process.env.ANTHROPIC_API_KEY +}).result(); +``` + +Direct transport imports bundle their implementation and SDK code. Register only the transports used by the application. For example, OpenRouter, Groq, xAI, and DeepSeek chat models share the `@earendil-works/pi-ai/openai-completions` transport. + +Use `@earendil-works/pi-ai` for the batteries-included behavior. The root entry point remains backward-compatible: it registers lazy wrappers for all built-in transports and resolves environment API keys automatically during dispatch. + ## Quick Start ```typescript @@ -438,7 +463,7 @@ Do not use `stream()` or `complete()` for image generation. Image generation is ### Basic Image Generation ```typescript -import { getImageModel, generateImages } from '@mariozechner/pi-ai'; +import { getImageModel, generateImages } from '@earendil-works/pi-ai'; const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); @@ -1097,6 +1122,7 @@ const response = await complete(model, { - OAuth login flows are not supported in browser environments. Use the `@earendil-works/pi-ai/oauth` entry point in Node.js. - In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime. - Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app. +- Use `@earendil-works/pi-ai/base` plus explicit direct transport registration when a browser bundle should exclude unused provider SDK implementations. ### Environment Variables (Node.js only) @@ -1333,6 +1359,7 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - `stream()` function returning `AssistantMessageEventStream` - `streamSimple()` for `SimpleStreamOptions` mapping +- `register()` for explicit direct transport registration from `@earendil-works/pi-ai/base` - Provider-specific options interface - Message conversion functions to transform `Context` to provider format - Tool conversion if the provider supports tools @@ -1343,7 +1370,8 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - Register the API with `registerApiProvider()` - Add a package subpath export in `package.json` for the provider module (`./dist/providers/.js`) - Add lazy loader wrappers in `src/providers/register-builtins.ts`, do not statically import provider implementation modules there -- Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai` +- Add any root-level `export type` re-exports in `src/index.ts` and `src/base.ts` that should remain available from `@earendil-works/pi-ai` and `@earendil-works/pi-ai/base` +- Keep `src/base.ts` free of built-in registration imports - Add credential detection in `env-api-keys.ts` for the new provider - Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth diff --git a/packages/ai/package.json b/packages/ai/package.json index 2c0bd6cf..1b34843a 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -10,6 +10,14 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./base": { + "types": "./dist/base.d.ts", + "import": "./dist/base.js" + }, + "./amazon-bedrock": { + "types": "./dist/providers/amazon-bedrock.d.ts", + "import": "./dist/providers/amazon-bedrock.js" + }, "./anthropic": { "types": "./dist/providers/anthropic.d.ts", "import": "./dist/providers/anthropic.js" @@ -42,6 +50,10 @@ "types": "./dist/providers/openai-responses.d.ts", "import": "./dist/providers/openai-responses.js" }, + "./openrouter-images": { + "types": "./dist/providers/images/openrouter.d.ts", + "import": "./dist/providers/images/openrouter.js" + }, "./oauth": { "types": "./dist/oauth.d.ts", "import": "./dist/oauth.js" diff --git a/packages/ai/src/base.ts b/packages/ai/src/base.ts new file mode 100644 index 00000000..f1a8a110 --- /dev/null +++ b/packages/ai/src/base.ts @@ -0,0 +1,45 @@ +export type { Static, TSchema } from "typebox"; +export { Type } from "typebox"; + +export * from "./api-registry.ts"; +export * from "./env-api-keys.ts"; +export * from "./image-models.ts"; +export * from "./images.ts"; +export * from "./images-api-registry.ts"; +export * from "./models.ts"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; +export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; +export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; +export * from "./providers/faux.ts"; +export type { GoogleOptions } from "./providers/google.ts"; +export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; +export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; +export type { MistralOptions } from "./providers/mistral.ts"; +export type { + OpenAICodexResponsesOptions, + OpenAICodexWebSocketDebugStats, +} from "./providers/openai-codex-responses.ts"; +export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; +export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; +export * from "./session-resources.ts"; +export * from "./stream.ts"; +export * from "./types.ts"; +export * from "./utils/diagnostics.ts"; +export * from "./utils/event-stream.ts"; +export * from "./utils/json-parse.ts"; +export type { + OAuthAuthInfo, + OAuthCredentials, + OAuthDeviceCodeInfo, + OAuthLoginCallbacks, + OAuthPrompt, + OAuthProvider, + OAuthProviderId, + OAuthProviderInfo, + OAuthProviderInterface, + OAuthSelectOption, + OAuthSelectPrompt, +} from "./utils/oauth/types.ts"; +export * from "./utils/overflow.ts"; +export * from "./utils/typebox-helpers.ts"; +export * from "./utils/validation.ts"; diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index cf08b33e..f5aa3998 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,4 +1,6 @@ -import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; +import { register, streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; + +export { register }; export const bedrockProviderModule = { streamBedrock, diff --git a/packages/ai/src/images-api-registry.ts b/packages/ai/src/images-api-registry.ts index 4758439b..5335a926 100644 --- a/packages/ai/src/images-api-registry.ts +++ b/packages/ai/src/images-api-registry.ts @@ -51,3 +51,7 @@ export function registerImagesApiProvider, project: string, diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index a270792a..62ad9cfd 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -4,6 +4,7 @@ import { GoogleGenAI, type ThinkingConfig, } from "@google/genai"; +import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, @@ -315,6 +316,14 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt } satisfies GoogleOptions); }; +export function register(): void { + registerApiProvider({ + api: "google-generative-ai", + stream: streamGoogle, + streamSimple: streamSimpleGoogle, + }); +} + function createClient( model: Model<"google-generative-ai">, apiKey?: string, diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/providers/images/openrouter.ts index 54caeaf0..d1b2c7e8 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/providers/images/openrouter.ts @@ -6,6 +6,7 @@ import type { ChatCompletionContentPartText, ChatCompletionCreateParamsNonStreaming, } from "openai/resources/chat/completions.js"; +import { registerImagesApiProvider } from "../../images-api-registry.ts"; import type { AssistantImages, ImageContent, @@ -103,6 +104,13 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image } }; +export function register(): void { + registerImagesApiProvider({ + api: "openrouter-images", + generateImages: generateImagesOpenRouter, + }); +} + function createClient( model: ImagesModel<"openrouter-images">, apiKey: string, diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts deleted file mode 100644 index e3decbb9..00000000 --- a/packages/ai/src/providers/images/register-builtins.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { registerImagesApiProvider } from "../../images-api-registry.ts"; -import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; -import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; - -interface OpenRouterImagesProviderModule { - generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; -} - -let openRouterImagesProviderModulePromise: Promise | undefined; - -function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { - return { - api: model.api, - provider: model.provider, - model: model.id, - output: [], - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now(), - }; -} - -function loadOpenRouterImagesProviderModule(): Promise { - openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( - (module) => module as OpenRouterImagesProviderModule, - ); - return openRouterImagesProviderModulePromise; -} - -export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async ( - model: ImagesModel<"openrouter-images">, - context: ImagesContext, - options?: ImagesOptions, -) => { - try { - const module = await loadOpenRouterImagesProviderModule(); - return await module.generateImagesOpenRouter(model, context, options); - } catch (error) { - return createLazyLoadErrorImages(model, error); - } -}; - -export function registerBuiltInImagesApiProviders(): void { - registerImagesApiProvider({ - api: "openrouter-images", - generateImages: generateImagesOpenRouter, - }); -} - -registerBuiltInImagesApiProviders(); diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index 6a132236..ba331af4 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -6,6 +6,7 @@ import type { ContentChunk, FunctionTool, } from "@mistralai/mistralai/models/components"; +import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -130,6 +131,14 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple } satisfies MistralOptions); }; +export function register(): void { + registerApiProvider({ + api: "mistral-conversations", + stream: streamMistral, + streamSimple: streamSimpleMistral, + }); +} + function createOutput(model: Model<"mistral-conversations">): AssistantMessage { return { role: "assistant", diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 19a2f5d7..1bc2879a 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -20,6 +20,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } +import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts"; import type { @@ -428,6 +429,14 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp } satisfies OpenAICodexResponsesOptions); }; +export function register(): void { + registerApiProvider({ + api: "openai-codex-responses", + stream: streamOpenAICodexResponses, + streamSimple: streamSimpleOpenAICodexResponses, + }); +} + // ============================================================================ // Request Building // ============================================================================ diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 61b03631..2f5edb62 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,6 +10,7 @@ import type { ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; +import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -449,6 +450,14 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", } satisfies OpenAICompletionsOptions); }; +export function register(): void { + registerApiProvider({ + api: "openai-completions", + stream: streamOpenAICompletions, + streamSimple: streamSimpleOpenAICompletions, + }); +} + function createClient( model: Model<"openai-completions">, context: Context, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 014233d5..b1692978 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,5 +1,6 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; +import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import type { Api, @@ -181,6 +182,14 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim } satisfies OpenAIResponsesOptions); }; +export function register(): void { + registerApiProvider({ + api: "openai-responses", + stream: streamOpenAIResponses, + streamSimple: streamSimpleOpenAIResponses, + }); +} + function createClient( model: Model<"openai-responses">, context: Context, diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 8fdcaaf0..6192f8a2 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -1,9 +1,14 @@ -import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; +import { type ApiProvider, clearApiProviders, getApiProvider, registerApiProvider } from "../api-registry.ts"; +import { getImagesApiProvider, type ImagesApiProvider, registerImagesApiProvider } from "../images-api-registry.ts"; import type { Api, + AssistantImages, AssistantMessage, AssistantMessageEvent, - Context, + ImagesApi, + ImagesContext, + ImagesModel, + ImagesOptions, Model, SimpleStreamOptions, StreamFunction, @@ -20,70 +25,47 @@ import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; import type { OpenAICompletionsOptions } from "./openai-completions.ts"; import type { OpenAIResponsesOptions } from "./openai-responses.ts"; -interface LazyProviderModule< - TApi extends Api, - TOptions extends StreamOptions, - TSimpleOptions extends SimpleStreamOptions, -> { - stream: (model: Model, context: Context, options?: TOptions) => AsyncIterable; - streamSimple: ( - model: Model, - context: Context, - options?: TSimpleOptions, - ) => AsyncIterable; +interface RegisteringProviderModule { + register(): void; } -interface AnthropicProviderModule { - streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; - streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; +function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [], + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; } -interface AzureOpenAIResponsesProviderModule { - streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions>; - streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions>; -} - -interface GoogleProviderModule { - streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>; - streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; -} - -interface GoogleVertexProviderModule { - streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; - streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; -} - -interface MistralProviderModule { - streamMistral: StreamFunction<"mistral-conversations", MistralOptions>; - streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions>; -} - -interface OpenAICodexResponsesProviderModule { - streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>; - streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>; -} - -interface OpenAICompletionsProviderModule { - streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions>; - streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions>; -} - -interface OpenAIResponsesProviderModule { - streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions>; - streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions>; +function createLazyImagesApiProvider( + api: TApi, + loadModule: () => Promise, +): ImagesApiProvider { + return { + api, + generateImages: async (model: ImagesModel, context: ImagesContext, options?: TOptions) => { + try { + const module = await loadModule(); + module.register(); + const provider = getImagesApiProvider(api); + if (!provider) { + throw new Error(`No API provider registered for api: ${api}`); + } + return await provider.generateImages(model, context, options); + } catch (error) { + return createLazyLoadErrorImages(model as ImagesModel<"openrouter-images">, error); + } + }, + }; } interface BedrockProviderModule { - streamBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: BedrockOptions, - ) => AsyncIterable; - streamSimpleBedrock: ( - model: Model<"bedrock-converse-stream">, - context: Context, - options?: SimpleStreamOptions, - ) => AsyncIterable; + streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions>; + streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions>; } const importNodeOnlyProvider = (specifier: string): Promise => { @@ -91,42 +73,10 @@ const importNodeOnlyProvider = (specifier: string): Promise => { return import(runtimeSpecifier); }; -let anthropicProviderModulePromise: - | Promise> - | undefined; -let azureOpenAIResponsesProviderModulePromise: - | Promise> - | undefined; -let googleProviderModulePromise: - | Promise> - | undefined; -let googleVertexProviderModulePromise: - | Promise> - | undefined; -let mistralProviderModulePromise: - | Promise> - | undefined; -let openAICodexResponsesProviderModulePromise: - | Promise> - | undefined; -let openAICompletionsProviderModulePromise: - | Promise> - | undefined; -let openAIResponsesProviderModulePromise: - | Promise> - | undefined; -let bedrockProviderModuleOverride: - | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> - | undefined; -let bedrockProviderModulePromise: - | Promise> - | undefined; +let bedrockProviderModuleOverride: BedrockProviderModule | undefined; export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = { - stream: module.streamBedrock, - streamSimple: module.streamSimpleBedrock, - }; + bedrockProviderModuleOverride = module; } function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { @@ -159,15 +109,29 @@ function createLazyLoadErrorMessage(model: Model, error: }; } -function createLazyStream( - loadModule: () => Promise>, +async function loadAndRegisterProvider( + api: TApi, + loadModule: () => Promise, +) { + const module = await loadModule(); + module.register(); + const provider = getApiProvider(api); + if (!provider) { + throw new Error(`No API provider registered for api: ${api}`); + } + return provider; +} + +function createLazyStream( + api: TApi, + loadModule: () => Promise, ): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadModule() - .then((module) => { - const inner = module.stream(model, context, options); + loadAndRegisterProvider(api, loadModule) + .then((provider) => { + const inner = provider.stream(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -180,17 +144,16 @@ function createLazyStream(loadModule: () => Promise>): StreamFunction { +function createLazySimpleStream( + api: TApi, + loadModule: () => Promise, +): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadModule() - .then((module) => { - const inner = module.streamSimple(model, context, options); + loadAndRegisterProvider(api, loadModule) + .then((provider) => { + const inner = provider.streamSimple(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -203,201 +166,114 @@ function createLazySimpleStream< }; } -function loadAnthropicProviderModule(): Promise< - LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> -> { - anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { - const provider = module as AnthropicProviderModule; - return { - stream: provider.streamAnthropic, - streamSimple: provider.streamSimpleAnthropic, - }; - }); - return anthropicProviderModulePromise; +function createLazyApiProvider( + api: TApi, + loadModule: () => Promise, +): ApiProvider { + return { + api, + stream: createLazyStream(api, loadModule), + streamSimple: createLazySimpleStream(api, loadModule), + }; } -function loadAzureOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> -> { - azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { - const provider = module as AzureOpenAIResponsesProviderModule; - return { - stream: provider.streamAzureOpenAIResponses, - streamSimple: provider.streamSimpleAzureOpenAIResponses, - }; - }); - return azureOpenAIResponsesProviderModulePromise; -} - -function loadGoogleProviderModule(): Promise< - LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> -> { - googleProviderModulePromise ||= import("./google.ts").then((module) => { - const provider = module as GoogleProviderModule; - return { - stream: provider.streamGoogle, - streamSimple: provider.streamSimpleGoogle, - }; - }); - return googleProviderModulePromise; -} - -function loadGoogleVertexProviderModule(): Promise< - LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> -> { - googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { - const provider = module as GoogleVertexProviderModule; - return { - stream: provider.streamGoogleVertex, - streamSimple: provider.streamSimpleGoogleVertex, - }; - }); - return googleVertexProviderModulePromise; -} - -function loadMistralProviderModule(): Promise< - LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> -> { - mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { - const provider = module as MistralProviderModule; - return { - stream: provider.streamMistral, - streamSimple: provider.streamSimpleMistral, - }; - }); - return mistralProviderModulePromise; -} - -function loadOpenAICodexResponsesProviderModule(): Promise< - LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> -> { - openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { - const provider = module as OpenAICodexResponsesProviderModule; - return { - stream: provider.streamOpenAICodexResponses, - streamSimple: provider.streamSimpleOpenAICodexResponses, - }; - }); - return openAICodexResponsesProviderModulePromise; -} - -function loadOpenAICompletionsProviderModule(): Promise< - LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> -> { - openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { - const provider = module as OpenAICompletionsProviderModule; - return { - stream: provider.streamOpenAICompletions, - streamSimple: provider.streamSimpleOpenAICompletions, - }; - }); - return openAICompletionsProviderModulePromise; -} - -function loadOpenAIResponsesProviderModule(): Promise< - LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> -> { - openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { - const provider = module as OpenAIResponsesProviderModule; - return { - stream: provider.streamOpenAIResponses, - streamSimple: provider.streamSimpleOpenAIResponses, - }; - }); - return openAIResponsesProviderModulePromise; -} - -function loadBedrockProviderModule(): Promise< - LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> -> { - if (bedrockProviderModuleOverride) { - return Promise.resolve(bedrockProviderModuleOverride); - } - bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { - const provider = module as BedrockProviderModule; - return { - stream: provider.streamBedrock, - streamSimple: provider.streamSimpleBedrock, - }; - }); - return bedrockProviderModulePromise; -} - -export const streamAnthropic = createLazyStream(loadAnthropicProviderModule); -export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule); -export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule); -export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); -export const streamGoogle = createLazyStream(loadGoogleProviderModule); -export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); -export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); -export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); -export const streamMistral = createLazyStream(loadMistralProviderModule); -export const streamSimpleMistral = createLazySimpleStream(loadMistralProviderModule); -export const streamOpenAICodexResponses = createLazyStream(loadOpenAICodexResponsesProviderModule); -export const streamSimpleOpenAICodexResponses = createLazySimpleStream(loadOpenAICodexResponsesProviderModule); -export const streamOpenAICompletions = createLazyStream(loadOpenAICompletionsProviderModule); -export const streamSimpleOpenAICompletions = createLazySimpleStream(loadOpenAICompletionsProviderModule); -export const streamOpenAIResponses = createLazyStream(loadOpenAIResponsesProviderModule); -export const streamSimpleOpenAIResponses = createLazySimpleStream(loadOpenAIResponsesProviderModule); -const streamBedrockLazy = createLazyStream(loadBedrockProviderModule); -const streamSimpleBedrockLazy = createLazySimpleStream(loadBedrockProviderModule); - -export function registerBuiltInApiProviders(): void { - registerApiProvider({ - api: "anthropic-messages", - stream: streamAnthropic, - streamSimple: streamSimpleAnthropic, - }); - - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); - - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); - - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); - - registerApiProvider({ - api: "azure-openai-responses", - stream: streamAzureOpenAIResponses, - streamSimple: streamSimpleAzureOpenAIResponses, - }); - - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); - - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); - - registerApiProvider({ - api: "google-vertex", - stream: streamGoogleVertex, - streamSimple: streamSimpleGoogleVertex, - }); - +function registerBedrockProviderModule(module: BedrockProviderModule): void { registerApiProvider({ api: "bedrock-converse-stream", - stream: streamBedrockLazy, - streamSimple: streamSimpleBedrockLazy, + stream: module.streamBedrock, + streamSimple: module.streamSimpleBedrock, }); } +function loadBedrockProviderModule(): Promise { + const module = bedrockProviderModuleOverride; + if (module) { + return Promise.resolve({ register: () => registerBedrockProviderModule(module) }); + } + return importNodeOnlyProvider("./amazon-bedrock.ts").then((provider) => provider as RegisteringProviderModule); +} + +const anthropicProvider = createLazyApiProvider<"anthropic-messages", AnthropicOptions>( + "anthropic-messages", + () => import("./anthropic.ts"), +); +const azureOpenAIResponsesProvider = createLazyApiProvider<"azure-openai-responses", AzureOpenAIResponsesOptions>( + "azure-openai-responses", + () => import("./azure-openai-responses.ts"), +); +const googleProvider = createLazyApiProvider<"google-generative-ai", GoogleOptions>( + "google-generative-ai", + () => import("./google.ts"), +); +const googleVertexProvider = createLazyApiProvider<"google-vertex", GoogleVertexOptions>( + "google-vertex", + () => import("./google-vertex.ts"), +); +const mistralProvider = createLazyApiProvider<"mistral-conversations", MistralOptions>( + "mistral-conversations", + () => import("./mistral.ts"), +); +const openAICodexResponsesProvider = createLazyApiProvider<"openai-codex-responses", OpenAICodexResponsesOptions>( + "openai-codex-responses", + () => import("./openai-codex-responses.ts"), +); +const openAICompletionsProvider = createLazyApiProvider<"openai-completions", OpenAICompletionsOptions>( + "openai-completions", + () => import("./openai-completions.ts"), +); +const openAIResponsesProvider = createLazyApiProvider<"openai-responses", OpenAIResponsesOptions>( + "openai-responses", + () => import("./openai-responses.ts"), +); +const bedrockProvider = createLazyApiProvider<"bedrock-converse-stream", BedrockOptions>( + "bedrock-converse-stream", + loadBedrockProviderModule, +); +const openRouterImagesProvider = createLazyImagesApiProvider( + "openrouter-images", + () => import("./images/openrouter.ts"), +); + +export const generateImagesOpenRouter = openRouterImagesProvider.generateImages; +export const streamAnthropic = anthropicProvider.stream; +export const streamSimpleAnthropic = anthropicProvider.streamSimple; +export const streamAzureOpenAIResponses = azureOpenAIResponsesProvider.stream; +export const streamSimpleAzureOpenAIResponses = azureOpenAIResponsesProvider.streamSimple; +export const streamGoogle = googleProvider.stream; +export const streamSimpleGoogle = googleProvider.streamSimple; +export const streamGoogleVertex = googleVertexProvider.stream; +export const streamSimpleGoogleVertex = googleVertexProvider.streamSimple; +export const streamMistral = mistralProvider.stream; +export const streamSimpleMistral = mistralProvider.streamSimple; +export const streamOpenAICodexResponses = openAICodexResponsesProvider.stream; +export const streamSimpleOpenAICodexResponses = openAICodexResponsesProvider.streamSimple; +export const streamOpenAICompletions = openAICompletionsProvider.stream; +export const streamSimpleOpenAICompletions = openAICompletionsProvider.streamSimple; +export const streamOpenAIResponses = openAIResponsesProvider.stream; +export const streamSimpleOpenAIResponses = openAIResponsesProvider.streamSimple; + +const registerBuiltInApiProviderFunctions = [ + () => registerApiProvider(anthropicProvider), + () => registerApiProvider(openAICompletionsProvider), + () => registerApiProvider(mistralProvider), + () => registerApiProvider(openAIResponsesProvider), + () => registerApiProvider(azureOpenAIResponsesProvider), + () => registerApiProvider(openAICodexResponsesProvider), + () => registerApiProvider(googleProvider), + () => registerApiProvider(googleVertexProvider), + () => registerApiProvider(bedrockProvider), +]; + +export function registerBuiltInImagesApiProviders(): void { + registerImagesApiProvider(openRouterImagesProvider); +} + +export function registerBuiltInApiProviders(): void { + for (const register of registerBuiltInApiProviderFunctions) { + register(); + } +} + export function resetApiProviders(): void { clearApiProviders(); registerBuiltInApiProviders(); diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 3f333d9e..7b6b28c6 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,5 +1,3 @@ -import "./providers/register-builtins.ts"; - import { getApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; import type { @@ -13,8 +11,6 @@ import type { StreamOptions, } from "./types.ts"; -export { getEnvApiKey } from "./env-api-keys.ts"; - function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { return typeof apiKey === "string" && apiKey.trim().length > 0; } diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 27c274aa..303044ad 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index 2483e1a4..a9862485 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 69e58e27..5663e2a8 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/stream.ts"; +import { streamSimple } from "../src/index.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index e797c3a9..cef2ca48 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index 2b7667d7..0c9d590f 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts index bb4b739b..0c1288b3 100644 --- a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts index 00161ab8..06a05698 100644 --- a/packages/ai/test/anthropic-temperature-compat.test.ts +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicTemperaturePayload { diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 13d333e5..581d3a34 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index bb454081..d52ff50e 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/base-entrypoint.test.ts b/packages/ai/test/base-entrypoint.test.ts new file mode 100644 index 00000000..d54fa492 --- /dev/null +++ b/packages/ai/test/base-entrypoint.test.ts @@ -0,0 +1,68 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { clearApiProviders, complete, getApiProvider, getApiProviders } from "../src/base.ts"; +import { register as registerAmazonBedrock } from "../src/providers/amazon-bedrock.ts"; +import { register as registerAnthropic } from "../src/providers/anthropic.ts"; +import { register as registerAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; +import { fauxAssistantMessage, registerFauxProvider } from "../src/providers/faux.ts"; +import { register as registerGoogle } from "../src/providers/google.ts"; +import { register as registerGoogleVertex } from "../src/providers/google-vertex.ts"; +import { register as registerMistral } from "../src/providers/mistral.ts"; +import { register as registerOpenAICodexResponses } from "../src/providers/openai-codex-responses.ts"; +import { register as registerOpenAICompletions } from "../src/providers/openai-completions.ts"; +import { register as registerOpenAIResponses } from "../src/providers/openai-responses.ts"; + +const registrations = [ + ["bedrock-converse-stream", registerAmazonBedrock], + ["anthropic-messages", registerAnthropic], + ["azure-openai-responses", registerAzureOpenAIResponses], + ["google-generative-ai", registerGoogle], + ["google-vertex", registerGoogleVertex], + ["mistral-conversations", registerMistral], + ["openai-codex-responses", registerOpenAICodexResponses], + ["openai-completions", registerOpenAICompletions], + ["openai-responses", registerOpenAIResponses], +] as const; + +afterEach(() => { + clearApiProviders(); +}); + +describe("base entrypoint", () => { + it("starts without built-in provider registrations", () => { + expect(getApiProviders()).toEqual([]); + }); + + it.each(registrations)("registers the %s transport explicitly", (api, register) => { + register(); + expect(getApiProvider(api)?.api).toBe(api); + }); + + it("dispatches custom providers", async () => { + const registration = registerFauxProvider(); + registration.setResponses([fauxAssistantMessage("hello")]); + const response = await complete(registration.getModel(), { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }); + expect(response.content).toEqual([{ type: "text", text: "hello" }]); + }); + + it("fails clearly when no transport is registered", async () => { + await expect( + complete( + { + id: "missing-model", + name: "Missing Model", + api: "missing-api", + provider: "missing-provider", + baseUrl: "https://example.com", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1, + maxTokens: 1, + }, + { messages: [] }, + ), + ).rejects.toThrow("No API provider registered for api: missing-api"); + }); +}); diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 2cfd8fb9..5bd5a1f3 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,8 +17,8 @@ */ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index 6e2c1a5b..b5c55198 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; class PayloadCaptured extends Error { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 9f021a41..54087a29 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,8 +14,8 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; import { isContextOverflow } from "../src/utils/overflow.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 23593394..73fac229 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,8 +25,8 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; +import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index a8453dcc..fadd67a9 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 30df3638..3920f4c8 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index 4cf387ce..a2d3f5a6 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; +import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; import { StringEnum } from "../src/utils/typebox-helpers.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index e21f0d12..14bccff0 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; +const baseEntryUrl = new URL("../src/base.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -16,9 +17,10 @@ const SDK_SPECIFIERS = [ type ProbeResult = { loadedSpecifiers: string[]; + value?: unknown; }; -function runProbe(action: string): ProbeResult { +function runProbe(action: string, entryUrl = aiEntryUrl): ProbeResult { const script = ` import { registerHooks } from "node:module"; @@ -34,9 +36,11 @@ function runProbe(action: string): ProbeResult { }, }); - const mod = await import(${JSON.stringify(aiEntryUrl)}); - ${action} - console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] })); + const mod = await import(${JSON.stringify(entryUrl)}); + const value = await (async () => { + ${action} + })(); + console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)], value })); `; const result = spawnSync(process.execPath, ["--input-type=module", "--eval", script], { @@ -66,6 +70,33 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); + it("registers built-in transports when importing the root barrel", () => { + const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api).sort();`); + expect(result.value).toEqual([ + "anthropic-messages", + "azure-openai-responses", + "bedrock-converse-stream", + "google-generative-ai", + "google-vertex", + "mistral-conversations", + "openai-codex-responses", + "openai-completions", + "openai-responses", + ]); + }); + + it("registers built-in image transports when importing the root barrel", () => { + const result = runProbe(`return mod.getImagesApiProvider("openrouter-images")?.api;`); + expect(result.loadedSpecifiers).toEqual([]); + expect(result.value).toBe("openrouter-images"); + }); + + it("does not load provider SDKs or register transports when importing the base barrel", () => { + const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api);`, baseEntryUrl); + expect(result.loadedSpecifiers).toEqual([]); + expect(result.value).toEqual([]); + }); + it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { const result = runProbe(` const model = { @@ -96,4 +127,17 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); }); + + it("dispatches through a lazy wrapper again after resetting providers", () => { + const result = runProbe(` + const model = mod.getModel("anthropic", "claude-sonnet-4-6"); + const context = { messages: [{ role: "user", content: "hi" }] }; + await mod.streamSimple(model, context).result(); + mod.resetApiProviders(); + return (await mod.streamSimple(model, context).result()).stopReason; + `); + + expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); + expect(result.value).toBe("error"); + }); }); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index d197a56a..0e2f45c5 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index c6898fc6..3c490c6d 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 0abb46f6..62245dcf 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index be233670..0ecf8e13 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { streamSimple } from "../src/stream.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index d8e4d2f9..ec2abea1 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/stream.ts"; +import { complete } from "../src/index.ts"; import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 8ca86025..373b1b78 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { stream, streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; import { convertMessages } from "../src/providers/openai-completions.ts"; -import { stream, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index d173694f..0d247b0b 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index aa6f2e23..87945bc2 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index 4bdeb286..2cf2a791 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple } from "../src/stream.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/openrouter-images.test.ts b/packages/ai/test/openrouter-images.test.ts index 31882bec..5811e6f4 100644 --- a/packages/ai/test/openrouter-images.test.ts +++ b/packages/ai/test/openrouter-images.test.ts @@ -1,5 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { generateImages } from "../src/images.ts"; +import { clearImagesApiProviders, getImagesApiProvider } from "../src/images-api-registry.ts"; +import { register as registerOpenRouterImages } from "../src/providers/images/openrouter.ts"; +import { registerBuiltInImagesApiProviders } from "../src/providers/register-builtins.ts"; import type { ImagesContext, ImagesModel } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ @@ -62,6 +65,15 @@ describe("openrouter images", () => { beforeEach(() => { mockState.lastParams = undefined; mockState.lastRequestOptions = undefined; + clearImagesApiProviders(); + registerBuiltInImagesApiProviders(); + }); + + it("registers the direct OpenRouter images transport explicitly", () => { + clearImagesApiProviders(); + expect(getImagesApiProvider("openrouter-images")).toBeUndefined(); + registerOpenRouterImages(); + expect(getImagesApiProvider("openrouter-images")?.api).toBe("openrouter-images"); }); it("returns text plus images in final output", async () => { diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index d250f5f4..bc5d6c54 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 9a70b6c2..3a381966 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,8 +4,8 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete, stream } from "../src/stream.ts"; import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 676f5a29..fdf45b94 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 0672181b..622e5127 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,8 +12,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 11b832f3..b21c1ce8 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 972913a7..4df4b2e1 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,8 +13,8 @@ */ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 9cdddefa..d8b02392 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index 3c279863..1bda5c14 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; -import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { diff --git a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts index ab2fec30..9a371b60 100644 --- a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts +++ b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey, streamSimple } from "../src/stream.ts"; +import { completeSimple, getEnvApiKey, streamSimple } from "../src/index.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; const provider = "xiaomi-token-plan-ams"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 8ca80014..82a2079f 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; +import { complete } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; -import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index 67ce0fca..5759e3c4 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -2,8 +2,11 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); +const aiSrcBase = fileURLToPath(new URL("../ai/src/base.ts", import.meta.url)); const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); +const aiOpenRouterImages = fileURLToPath(new URL("../ai/src/providers/images/openrouter.ts", import.meta.url)); const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); +const agentSrcBase = fileURLToPath(new URL("../agent/src/base.ts", import.meta.url)); const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); export default defineConfig({ @@ -20,12 +23,18 @@ export default defineConfig({ resolve: { alias: [ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@earendil-works\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, + { find: /^@earendil-works\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, + { find: /^@earendil-works\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, + { find: /^@mariozechner\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, + { find: /^@mariozechner\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex }, + { find: /^@mariozechner\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@mariozechner\/pi-tui$/, replacement: tuiSrcIndex }, ], }, diff --git a/scripts/check-browser-smoke.mjs b/scripts/check-browser-smoke.mjs index d590988d..906eccd3 100644 --- a/scripts/check-browser-smoke.mjs +++ b/scripts/check-browser-smoke.mjs @@ -4,7 +4,21 @@ import { join } from "node:path"; import { build } from "esbuild"; const outputPath = join(tmpdir(), "pi-browser-smoke.js"); +const baseOutputPath = join(tmpdir(), "pi-browser-base-smoke.js"); +const selectiveOutputPath = join(tmpdir(), "pi-browser-selective-smoke.js"); const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log"); +const providerImplementationInputs = [ + "packages/ai/src/providers/amazon-bedrock.ts", + "packages/ai/src/providers/anthropic.ts", + "packages/ai/src/providers/azure-openai-responses.ts", + "packages/ai/src/providers/google.ts", + "packages/ai/src/providers/google-vertex.ts", + "packages/ai/src/providers/images/openrouter.ts", + "packages/ai/src/providers/mistral.ts", + "packages/ai/src/providers/openai-codex-responses.ts", + "packages/ai/src/providers/openai-completions.ts", + "packages/ai/src/providers/openai-responses.ts", +]; try { await build({ @@ -15,6 +29,36 @@ try { logLevel: "silent", outfile: outputPath, }); + const baseBuild = await build({ + stdin: { + contents: `import { complete } from "@earendil-works/pi-ai/base";\nimport { Agent } from "@earendil-works/pi-agent-core/base";\nconsole.log(typeof complete, typeof Agent);\n`, + resolveDir: process.cwd(), + sourcefile: "pi-browser-base-smoke-entry.ts", + }, + bundle: true, + platform: "browser", + format: "esm", + logLevel: "silent", + metafile: true, + outfile: baseOutputPath, + }); + const bundledInputs = new Set(Object.keys(baseBuild.metafile.inputs)); + const reachableProviderImplementations = providerImplementationInputs.filter((input) => bundledInputs.has(input)); + if (reachableProviderImplementations.length > 0) { + throw new Error(`Base browser bundle reached provider implementations:\n${reachableProviderImplementations.join("\n")}`); + } + await build({ + stdin: { + contents: `import { register as registerAnthropic } from "@earendil-works/pi-ai/anthropic";\nimport { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";\nimport { register as registerOpenRouterImages } from "@earendil-works/pi-ai/openrouter-images";\nconsole.log(typeof registerAnthropic, typeof registerOpenAICompletions, typeof registerOpenRouterImages);\n`, + resolveDir: process.cwd(), + sourcefile: "pi-browser-selective-smoke-entry.ts", + }, + bundle: true, + platform: "browser", + format: "esm", + logLevel: "silent", + outfile: selectiveOutputPath, + }); process.exit(0); } catch (error) { let detailedErrors = ""; diff --git a/tsconfig.json b/tsconfig.json index 903f80f3..bcb12eee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "*": ["./*"], "@earendil-works/pi-ai": ["./packages/ai/src/index.ts"], "@earendil-works/pi-ai/oauth": ["./packages/ai/src/oauth.ts"], + "@earendil-works/pi-ai/openrouter-images": ["./packages/ai/src/providers/images/openrouter.ts"], "@earendil-works/pi-ai/*": ["./packages/ai/src/*.ts", "./packages/ai/src/providers/*.ts"], "@earendil-works/pi-ai/dist/*": ["./packages/ai/src/*"], "@earendil-works/pi-agent-core": ["./packages/agent/src/index.ts"], From ea65a51a15c6c5965d1ffa87fee357758fba5e7f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 00:42:53 +0200 Subject: [PATCH 104/141] fix: update vulnerable dependencies --- package-lock.json | 3307 ++++++++---------- package.json | 2 +- packages/agent/package.json | 4 +- packages/ai/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 37 +- packages/coding-agent/package.json | 4 +- scripts/generate-coding-agent-shrinkwrap.mjs | 2 +- 7 files changed, 1547 insertions(+), 1811 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1abbf97..ce9dd552 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@biomejs/biome": "2.3.5", "@types/node": "22.19.19", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "husky": "9.1.7", "jiti": "2.7.0", "shx": "0.4.0", @@ -31,20 +31,6 @@ "node": ">=22.19.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@anthropic-ai/sandbox-runtime": { "version": "0.0.26", "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.26.tgz", @@ -811,10 +797,44 @@ "resolved": "packages/tui", "link": true }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -829,9 +849,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -846,9 +866,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -863,9 +883,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -880,9 +900,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -897,9 +917,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -914,9 +934,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -931,9 +951,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -948,9 +968,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -965,9 +985,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -982,9 +1002,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -999,9 +1019,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -1016,9 +1036,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -1033,9 +1053,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -1050,9 +1070,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1067,9 +1087,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1084,9 +1104,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1101,9 +1121,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1118,9 +1138,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -1135,9 +1155,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -1152,9 +1172,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -1169,9 +1189,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -1186,9 +1206,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -1203,9 +1223,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -1220,9 +1240,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -1237,9 +1257,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1277,130 +1297,6 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1643,6 +1539,25 @@ } } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodable/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", @@ -1711,15 +1626,14 @@ "node": ">=14" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@pondwader/socks5-server": { @@ -1747,9 +1661,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { @@ -1767,12 +1681,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1791,24 +1699,10 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1817,12 +1711,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1831,12 +1728,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1845,26 +1745,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1873,12 +1762,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1887,194 +1779,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" + "libc": [ + "musl" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", - "cpu": [ - "ppc64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -2083,12 +1916,34 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -2097,26 +1952,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -2125,21 +1969,17 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", @@ -2267,6 +2107,24 @@ "node": ">=14.0.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -2488,155 +2346,6 @@ "win32" ] }, - "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@xterm/headless": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.5.0.tgz", @@ -2653,32 +2362,6 @@ "node": ">= 14" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2698,18 +2381,6 @@ "node": ">=12" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -2840,16 +2511,6 @@ "node": ">=10.0.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/canvas": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", @@ -2877,23 +2538,6 @@ "node": ">=20" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -2906,16 +2550,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -2923,26 +2557,6 @@ "dev": true, "license": "ISC" }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -2952,6 +2566,13 @@ "node": ">=18" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -3022,16 +2643,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3061,13 +2672,6 @@ "node": ">=0.3.1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3077,13 +2681,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3104,17 +2701,10 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3125,32 +2715,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/estree-walker": { @@ -3381,36 +2971,6 @@ "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3750,16 +3310,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3837,21 +3387,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -3866,22 +3401,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -3941,6 +3460,279 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -3953,13 +3745,6 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", @@ -3979,18 +3764,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -4233,6 +4006,20 @@ "node": ">=4" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4293,13 +4080,6 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -4360,16 +4140,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pi-extension-custom-provider-anthropic": { "resolved": "packages/coding-agent/examples/extensions/custom-provider-anthropic", "link": true @@ -4411,9 +4181,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4431,7 +4201,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4488,24 +4258,23 @@ } }, "node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -4628,58 +4397,40 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4905,13 +4656,6 @@ "dev": true, "license": "MIT" }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4922,64 +4666,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -5000,26 +4686,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/strnum": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", @@ -5075,100 +4741,6 @@ "node": ">=6" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5176,17 +4748,10 @@ "dev": true, "license": "MIT" }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -5231,36 +4796,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5345,9 +4880,9 @@ } }, "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -5367,18 +4902,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -5394,9 +4928,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -5409,15 +4944,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -5441,531 +4979,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -5979,92 +4992,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -6106,25 +5033,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6133,9 +5041,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6213,9 +5121,9 @@ }, "devDependencies": { "@types/node": "24.12.4", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.1.9", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "engines": { "node": ">=22.19.0" @@ -6231,6 +5139,204 @@ "undici-types": "~7.16.0" } }, + "packages/agent/node_modules/@vitest/coverage-v8": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.9", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "packages/agent/node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/agent/node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "packages/agent/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/agent/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "packages/agent/node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "packages/agent/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/agent/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "packages/agent/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/agent/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "packages/agent/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6238,6 +5344,123 @@ "dev": true, "license": "MIT" }, + "packages/agent/node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "packages/agent/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "packages/ai": { "name": "@earendil-works/pi-ai", "version": "0.79.7", @@ -6261,7 +5484,7 @@ "devDependencies": { "@types/node": "24.12.4", "canvas": "3.2.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "engines": { "node": ">=22.19.0" @@ -6277,6 +5500,176 @@ "undici-types": "~7.16.0" } }, + "packages/ai/node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "packages/ai/node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/ai/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/ai/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "packages/ai/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/ai/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "packages/ai/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/ai/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "packages/ai/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6284,6 +5677,96 @@ "dev": true, "license": "MIT" }, + "packages/ai/node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", "version": "0.79.7", @@ -6305,7 +5788,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "bin": { @@ -6321,7 +5804,7 @@ "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "engines": { "node": ">=22.19.0" @@ -6384,6 +5867,149 @@ "undici-types": "~7.16.0" } }, + "packages/coding-agent/node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "packages/coding-agent/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/coding-agent/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "packages/coding-agent/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/coding-agent/node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "packages/coding-agent/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/coding-agent/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "packages/coding-agent/node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6391,6 +6017,123 @@ "dev": true, "license": "MIT" }, + "packages/coding-agent/node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "packages/coding-agent/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "packages/tui": { "name": "@earendil-works/pi-tui", "version": "0.79.7", diff --git a/package.json b/package.json index c88d7123..0deabeca 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@biomejs/biome": "2.3.5", "@types/node": "22.19.19", "@typescript/native-preview": "7.0.0-dev.20260120.1", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "husky": "9.1.7", "jiti": "2.7.0", "shx": "0.4.0", diff --git a/packages/agent/package.json b/packages/agent/package.json index 48934f8f..ee24151f 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -57,8 +57,8 @@ }, "devDependencies": { "@types/node": "24.12.4", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.1.9", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" } } diff --git a/packages/ai/package.json b/packages/ai/package.json index 1b34843a..68165eba 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -114,6 +114,6 @@ "devDependencies": { "@types/node": "24.12.4", "canvas": "3.2.3", - "vitest": "3.2.4" + "vitest": "4.1.9" } } diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index ad672bf2..934827aa 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -25,7 +25,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "optionalDependencies": { @@ -810,9 +810,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { @@ -830,12 +830,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1613,23 +1607,22 @@ } }, "node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -1735,9 +1728,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", "engines": { "node": ">=22.19.0" @@ -1774,9 +1767,9 @@ } }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "peerDependencies": { "bufferutil": "^4.0.1", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index c873bb8d..3d890c7a 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -52,7 +52,7 @@ "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.1.38", - "undici": "8.3.0", + "undici": "8.5.0", "yaml": "2.9.0" }, "overrides": { @@ -74,7 +74,7 @@ "@types/semver": "7.7.1", "shx": "0.4.0", "typescript": "5.9.3", - "vitest": "3.2.4" + "vitest": "4.1.9" }, "keywords": [ "coding-agent", diff --git a/scripts/generate-coding-agent-shrinkwrap.mjs b/scripts/generate-coding-agent-shrinkwrap.mjs index 7bcf5681..1971869c 100644 --- a/scripts/generate-coding-agent-shrinkwrap.mjs +++ b/scripts/generate-coding-agent-shrinkwrap.mjs @@ -12,7 +12,7 @@ const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json"); const internalPackagePrefix = "@earendil-works/pi-"; const allowedInstallScriptPackages = new Map([ ["@google/genai@1.52.0", "preinstall is a no-op in the published package"], - ["protobufjs@7.5.9", "postinstall only warns about protobufjs version scheme mismatches"], + ["protobufjs@7.6.4", "postinstall only warns about protobufjs version scheme mismatches"], ]); const args = new Set(process.argv.slice(2)); From a2f70e5f2817fd37433109c4aa7128019fd9bf97 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:22:13 +0200 Subject: [PATCH 105/141] fix(coding-agent): reset tool test mocks --- packages/coding-agent/test/tools.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index c5e8fa6f..f187da62 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -44,6 +44,7 @@ describe("Coding Agent Tools", () => { }); afterEach(() => { + vi.restoreAllMocks(); // Clean up test directory rmSync(testDir, { recursive: true, force: true }); }); From 74677bbfbf7fd089b47b4197b8ef21468d5f2878 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:45:03 +0200 Subject: [PATCH 106/141] docs: audit unreleased changelogs --- packages/agent/CHANGELOG.md | 4 ++++ packages/ai/CHANGELOG.md | 1 + packages/coding-agent/CHANGELOG.md | 3 +++ 3 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 3050117f..d9a204c6 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `@earendil-works/pi-agent-core/base` for bundlers that want to pair the agent core with selective `@earendil-works/pi-ai/base` provider registration ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). + ## [0.79.7] - 2026-06-18 ## [0.79.6] - 2026-06-16 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index c4c63d8b..2aab174a 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Added `@earendil-works/pi-ai/base` and direct provider registration exports for bundlers that want selective provider transports without root built-in registration ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). - Added prompt caching for Mistral requests using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added the OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 06ab4f9b..2f0ecb2a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,18 +4,21 @@ ### New Features +- **Selective provider base entry points** - SDK users can pair `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` with explicit provider registration to keep bundled applications from including unused provider transports. See [`pi-ai` Base Entry Point](../ai/README.md#base-entry-point) and [`pi-agent-core` Base Entry Point](../agent/README.md#base-entry-point). - **Mistral prompt caching** - Mistral sessions now use provider-side prompt caching with session affinity and cached-token usage/cost accounting. See [API Keys](docs/providers.md#api-keys) and [Environment Variables](docs/usage.md#environment-variables). - **Post-compaction token estimates** - Compact results and compaction events now include estimated post-compaction token counts so clients can show the approximate context reduction. See [RPC compact](docs/rpc.md#compact) and [compaction events](docs/rpc.md#compaction_start--compaction_end). - **OpenRouter Fusion alias** - `openrouter/fusion` is available as a built-in OpenRouter model alias. See [API Keys](docs/providers.md#api-keys). ### Added +- Added inherited `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` entry points for selective provider registration in bundled applications ([#5348](https://github.com/earendil-works/pi/pull/5348) by [@FredKSchott](https://github.com/FredKSchott)). - Added inherited Mistral prompt caching using the pi session ID as `prompt_cache_key`, including cached-token usage and cost accounting ([#5854](https://github.com/earendil-works/pi/issues/5854)). - Added estimated post-compaction token counts to compact results and compaction events ([#5877](https://github.com/earendil-works/pi/issues/5877)). - Added the inherited OpenRouter Fusion alias as `openrouter/fusion` ([#5866](https://github.com/earendil-works/pi/pull/5866) by [@dannote](https://github.com/dannote)). ### Fixed +- Updated vulnerable runtime dependencies, including `undici` and the packaged `protobufjs` transitive dependency. - Fixed compaction to refuse sessions with no eligible messages instead of producing empty summaries ([#4811](https://github.com/earendil-works/pi/issues/4811)). - Fixed successful overflow-triggered auto-compaction to avoid retrying completed assistant responses ([#5720](https://github.com/earendil-works/pi/issues/5720)). From 8eb9704b3e75391dcc4f584b7fc1a03c9e04e383 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:53:43 +0200 Subject: [PATCH 107/141] Release v0.79.8 --- package-lock.json | 26 +++++------ packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 45 +++++++++++++------ packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 +++++----- packages/coding-agent/package.json | 8 ++-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 81 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce9dd552..1f9d4e66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.8", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.7", - "@earendil-works/pi-ai": "^0.79.7", - "@earendil-works/pi-tui": "^0.79.7", + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.7" + "version": "0.79.8" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.7", + "version": "1.9.8", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index d9a204c6..8adfae20 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/agent/package.json b/packages/agent/package.json index ee24151f..b3aa9bae 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.7", + "version": "0.79.8", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.8", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2aab174a..f3d696fd 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 68165eba..830df19e 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.7", + "version": "0.79.8", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 82d0e137..8639bf13 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -6067,7 +6067,7 @@ export const MODELS = { cost: { input: 1.5, output: 7.5, - cacheRead: 0.15, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -10573,13 +10573,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.34, + input: 0.67, + output: 3.5, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2.7-code": { id: "moonshotai/kimi-k2.7-code", @@ -11556,7 +11556,7 @@ export const MODELS = { cost: { input: 0.075, output: 0.3, - cacheRead: 0.037, + cacheRead: 0.0375, cacheWrite: 0, }, contextWindow: 131072, @@ -11749,6 +11749,23 @@ export const MODELS = { contextWindow: 200000, maxTokens: 4096, } satisfies Model<"openai-completions">, + "openrouter/fusion": { + id: "openrouter/fusion", + name: "OpenRouter: Fusion", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 30000, + } satisfies Model<"openai-completions">, "openrouter/owl-alpha": { id: "openrouter/owl-alpha", name: "Owl Alpha", @@ -13067,13 +13084,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 1.4, - output: 4.4, - cacheRead: 0.7, + input: 1.2, + output: 4.1, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 524288, + maxTokens: 131072, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", @@ -13186,13 +13203,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.68, - output: 3.41, - cacheRead: 0.34, + input: 0.67, + output: 3.5, + cacheRead: 0.2, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2f0ecb2a..e52073bc 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index c3f35b25..436f151f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index c817a94e..aa509051 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 12363537..51de298b 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index ea80b459..3f126430 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 89705d97..1d42d841 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 41b01437..2a587f88 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.7", + "version": "1.9.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.7", + "version": "1.9.8", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index a80abe4a..c9d4aa3f 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.7", + "version": "1.9.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index b45b2214..2dc3506b 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.7", + "version": "0.79.8", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 02abd397..1c409ca4 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.7", + "version": "0.79.8", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 934827aa..13ff9161 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.7", - "@earendil-works/pi-ai": "^0.79.7", - "@earendil-works/pi-tui": "^0.79.7", + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.7.tgz", + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.8.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.7", + "@earendil-works/pi-ai": "^0.79.8", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.7.tgz", + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.7", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.7.tgz", + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 3d890c7a..4aa30dd2 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.7", + "version": "0.79.8", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.7", - "@earendil-works/pi-ai": "^0.79.7", - "@earendil-works/pi-tui": "^0.79.7", + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 4db80b91..9f54a3e3 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.8] - 2026-06-19 ## [0.79.7] - 2026-06-18 diff --git a/packages/tui/package.json b/packages/tui/package.json index 24590a18..36f002a7 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.7", + "version": "0.79.8", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 56b22768db0bf06ba09719ea9a62f0b29065aaad Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 09:53:45 +0200 Subject: [PATCH 108/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 8adfae20..0ac61b2f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f3d696fd..63e7bba7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e52073bc..53f60f55 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 9f54a3e3..2e9f6d31 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.8] - 2026-06-19 ## [0.79.7] - 2026-06-18 From 783571a615b08b286247b86f383197d43990c2a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 11:03:11 +0200 Subject: [PATCH 109/141] feat: track auto-closed issue triage --- .github/workflows/issue-gate.yml | 7 ++ .github/workflows/issue-triage-labels.yml | 120 ++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 .github/workflows/issue-triage-labels.yml diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index 62663940..a8103fd1 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -111,6 +111,13 @@ jobs: body: message, }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['untriaged'], + }); + await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml new file mode 100644 index 00000000..0be1e669 --- /dev/null +++ b/.github/workflows/issue-triage-labels.yml @@ -0,0 +1,120 @@ +name: Issue Triage Labels + +on: + issues: + types: [reopened, labeled] + +jobs: + update-labels: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Update triage labels + uses: actions/github-script@v7 + with: + script: | + const UNTRIAGED_LABEL = 'untriaged'; + const NO_ACTION_LABEL = 'no-action'; + const LAST_READ_LABEL = 'last-read'; + + function issueHasLabel(issue, labelName) { + return (issue.labels ?? []).some((label) => label.name === labelName); + } + + async function removeLabelIfPresent(issueNumber, issue, labelName) { + if (!issueHasLabel(issue, labelName)) { + console.log(`Issue #${issueNumber} does not have ${labelName}`); + return; + } + + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + name: labelName, + }); + console.log(`Removed ${labelName} from #${issueNumber}`); + } catch (error) { + if (error.status === 404) { + console.log(`Label ${labelName} was already absent from #${issueNumber}`); + return; + } + throw error; + } + } + + if (context.payload.action === 'reopened') { + await removeLabelIfPresent(context.issue.number, context.payload.issue, UNTRIAGED_LABEL); + await removeLabelIfPresent(context.issue.number, context.payload.issue, NO_ACTION_LABEL); + return; + } + + if (context.payload.action !== 'labeled' || context.payload.label?.name !== LAST_READ_LABEL) { + console.log('Not a last-read label event'); + return; + } + + const currentIssueNumber = context.issue.number; + const lastReadIssues = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + labels: LAST_READ_LABEL, + per_page: 100, + }); + + const previousIssueNumbers = lastReadIssues + .filter((issue) => !issue.pull_request) + .map((issue) => issue.number) + .filter((issueNumber) => issueNumber !== currentIssueNumber); + + if (previousIssueNumbers.length === 0) { + console.log('No previous last-read issue found'); + return; + } + + const previousIssueNumber = Math.max(...previousIssueNumbers); + if (currentIssueNumber <= previousIssueNumber) { + console.log( + `Last-read was added to old issue #${currentIssueNumber}; latest last-read is #${previousIssueNumber}`, + ); + return; + } + + const untriagedIssues = await github.paginate(github.rest.issues.listForRepo, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + labels: UNTRIAGED_LABEL, + per_page: 100, + }); + + const issuesToMark = untriagedIssues + .filter((issue) => !issue.pull_request) + .filter((issue) => issue.number >= previousIssueNumber && issue.number <= currentIssueNumber) + .sort((a, b) => a.number - b.number); + + if (issuesToMark.length === 0) { + console.log(`No untriaged issues found from #${previousIssueNumber} to #${currentIssueNumber}`); + return; + } + + for (const issue of issuesToMark) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [NO_ACTION_LABEL], + }); + console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: UNTRIAGED_LABEL, + }); + console.log(`Removed ${UNTRIAGED_LABEL} from #${issue.number}`); + } From 47d1d90a8ed156d7d0ca90265f4ac60c75f56621 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 11:08:16 +0200 Subject: [PATCH 110/141] fix: close no-action issues as not planned --- .github/workflows/issue-triage-labels.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index 0be1e669..4a9ea9d7 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -110,6 +110,15 @@ jobs: }); console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'not_planned', + }); + console.log(`Closed #${issue.number} as not planned`); + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, From 373cd6ae52b6a8954ee530b887bd39a2dc820931 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 19 Jun 2026 12:25:02 +0200 Subject: [PATCH 111/141] fix(coding-agent): prioritize provider matches in model selector closes #5892 --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../src/modes/interactive/components/model-selector.ts | 4 ++-- .../coding-agent/src/modes/interactive/model-search.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 53f60f55..07529966 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). + ## [0.79.8] - 2026-06-19 ### New Features diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index a3cdf7d5..32711929 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -11,7 +11,7 @@ import { } from "@earendil-works/pi-tui"; import type { ModelRegistry } from "../../../core/model-registry.ts"; import type { SettingsManager } from "../../../core/settings-manager.ts"; -import { getModelSearchText } from "../model-search.ts"; +import { getModelSelectorSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyHint } from "./keybinding-hints.ts"; @@ -219,7 +219,7 @@ export class ModelSelectorComponent extends Container implements Focusable { private filterModels(query: string): void { this.filteredModels = query ? fuzzyFilter(this.activeModels, query, ({ id, provider, model }) => - getModelSearchText({ id, provider, name: model.name }), + getModelSelectorSearchText({ id, provider, name: model.name }), ) : this.activeModels; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1)); diff --git a/packages/coding-agent/src/modes/interactive/model-search.ts b/packages/coding-agent/src/modes/interactive/model-search.ts index f1dbc73c..bab9c5a5 100644 --- a/packages/coding-agent/src/modes/interactive/model-search.ts +++ b/packages/coding-agent/src/modes/interactive/model-search.ts @@ -9,3 +9,13 @@ export function getModelSearchText(item: ModelSearchItem): string { const name = item.name ? ` ${item.name}` : ""; return `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`; } + +/** + * The /model selector search should rank exact provider-prefixed queries before proxy-provider IDs + * like openrouter/openai/gpt-5, so keep the bare model ID out of the leading position. + */ +export function getModelSelectorSearchText(item: ModelSearchItem): string { + const { id, provider } = item; + const name = item.name ? ` ${item.name}` : ""; + return `${provider} ${provider}/${id} ${provider} ${id}${name}`; +} From 226a316824dc1835ffc34acad274488f5fe298f6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 12:46:31 +0200 Subject: [PATCH 112/141] fix: mark auto-closed issues not planned --- .github/workflows/issue-gate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index a8103fd1..d2bf830e 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -123,4 +123,5 @@ jobs: repo: context.repo.repo, issue_number: context.issue.number, state: 'closed', + state_reason: 'not_planned', }); From 6e6ce70caf3328683517b0e308fdbbc6d1c1abc9 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 19 Jun 2026 13:16:51 +0200 Subject: [PATCH 113/141] fix(ai): filter Copilot models by account availability closes #5897 --- packages/ai/CHANGELOG.md | 4 + packages/ai/src/utils/oauth/github-copilot.ts | 83 +++++++++++++++++-- packages/ai/test/github-copilot-oauth.test.ts | 70 +++++++++++++++- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/test/model-registry.test.ts | 19 +++++ 5 files changed, 169 insertions(+), 8 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 63e7bba7..4f316c3b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index 6a27b9d1..bb78a93a 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -9,6 +9,7 @@ import type { OAuthCredentials, OAuthDeviceCodeInfo, OAuthLoginCallbacks, OAuthP type CopilotCredentials = OAuthCredentials & { enterpriseUrl?: string; + availableModelIds: string[]; }; const decode = (s: string) => atob(s); @@ -20,6 +21,7 @@ const COPILOT_HEADERS = { "Editor-Plugin-Version": "copilot-chat/0.35.0", "Copilot-Integration-Id": "vscode-chat", } as const; +const COPILOT_API_VERSION = "2026-06-01"; type DeviceCodeResponse = { device_code: string; @@ -88,6 +90,48 @@ export function getGitHubCopilotBaseUrl(token?: string, enterpriseDomain?: strin return "https://api.individual.githubcopilot.com"; } +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" ? (value as Record) : undefined; +} + +function isSelectableCopilotModel(item: Record): boolean { + const policy = asRecord(item.policy); + const capabilities = asRecord(item.capabilities); + const supports = asRecord(capabilities?.supports); + return item.model_picker_enabled === true && policy?.state !== "disabled" && supports?.tool_calls !== false; +} + +function parseAvailableCopilotModelIds(raw: unknown): string[] { + const data = asRecord(raw)?.data; + if (!Array.isArray(data)) { + throw new Error("Invalid Copilot models response"); + } + + const ids: string[] = []; + for (const rawItem of data) { + const item = asRecord(rawItem); + const id = item?.id; + if (typeof id === "string" && item && isSelectableCopilotModel(item)) { + ids.push(id); + } + } + return ids; +} + +async function fetchAvailableGitHubCopilotModelIds(copilotToken: string, enterpriseDomain?: string): Promise { + const baseUrl = getGitHubCopilotBaseUrl(copilotToken, enterpriseDomain); + const raw = await fetchJson(`${baseUrl}/models`, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${copilotToken}`, + ...COPILOT_HEADERS, + "X-GitHub-Api-Version": COPILOT_API_VERSION, + }, + signal: AbortSignal.timeout(5000), + }); + return parseAvailableCopilotModelIds(raw); +} + async function fetchJson(url: string, init: RequestInit): Promise { const response = await fetch(url, init); if (!response.ok) { @@ -201,10 +245,7 @@ async function pollForGitHubAccessToken( }); } -/** - * Refresh GitHub Copilot token - */ -export async function refreshGitHubCopilotToken( +async function refreshGitHubCopilotAccessToken( refreshToken: string, enterpriseDomain?: string, ): Promise { @@ -238,6 +279,20 @@ export async function refreshGitHubCopilotToken( }; } +/** + * Refresh GitHub Copilot token + */ +export async function refreshGitHubCopilotToken( + refreshToken: string, + enterpriseDomain?: string, +): Promise { + const credentials = await refreshGitHubCopilotAccessToken(refreshToken, enterpriseDomain); + return { + ...credentials, + availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain), + }; +} + /** * Enable a model for the user's GitHub Copilot account. * This is required for some models (like Claude, Grok) before they can be used. @@ -322,12 +377,18 @@ export async function loginGitHubCopilot(options: { }); const githubAccessToken = await pollForGitHubAccessToken(domain, device, options.signal); - const credentials = await refreshGitHubCopilotToken(githubAccessToken, enterpriseDomain ?? undefined); + const credentials = await refreshGitHubCopilotAccessToken(githubAccessToken, enterpriseDomain ?? undefined); // Enable all models after successful login options.onProgress?.("Enabling models..."); await enableAllGitHubCopilotModels(credentials.access, enterpriseDomain ?? undefined); - return credentials; + + // Fetch availability after policy enable so newly enabled models are included, + // while unavailable models are still filtered out. + return { + ...credentials, + availableModelIds: await fetchAvailableGitHubCopilotModelIds(credentials.access, enterpriseDomain ?? undefined), + }; } export const githubCopilotOAuthProvider: OAuthProviderInterface = { @@ -356,6 +417,14 @@ export const githubCopilotOAuthProvider: OAuthProviderInterface = { const creds = credentials as CopilotCredentials; const domain = creds.enterpriseUrl ? (normalizeDomain(creds.enterpriseUrl) ?? undefined) : undefined; const baseUrl = getGitHubCopilotBaseUrl(creds.access, domain); - return models.map((m) => (m.provider === "github-copilot" ? { ...m, baseUrl } : m)); + // Older stored Pi auth entries do not have account-specific model IDs yet; + // keep their existing generated-catalog behavior until the next refresh/login. + const availableModelIds = "availableModelIds" in creds ? new Set(creds.availableModelIds) : undefined; + + return models.flatMap((m) => { + if (m.provider !== "github-copilot") return [m]; + if (availableModelIds && !availableModelIds.has(m.id)) return []; + return [{ ...m, baseUrl }]; + }); }, }; diff --git a/packages/ai/test/github-copilot-oauth.test.ts b/packages/ai/test/github-copilot-oauth.test.ts index c94370da..2aba3084 100644 --- a/packages/ai/test/github-copilot-oauth.test.ts +++ b/packages/ai/test/github-copilot-oauth.test.ts @@ -1,5 +1,10 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { loginGitHubCopilot } from "../src/utils/oauth/github-copilot.ts"; +import { getModels } from "../src/models.ts"; +import { + githubCopilotOAuthProvider, + loginGitHubCopilot, + refreshGitHubCopilotToken, +} from "../src/utils/oauth/github-copilot.ts"; function jsonResponse(body: unknown, status: number = 200): Response { return new Response(JSON.stringify(body), { @@ -29,6 +34,57 @@ describe("GitHub Copilot OAuth device flow", () => { vi.useRealTimers(); }); + it("filters models to the authenticated account picker catalog", async () => { + const fetchMock = vi.fn(async (input: unknown, init?: RequestInit): Promise => { + const url = getUrl(input); + + if (url.includes("/copilot_internal/v2/token")) { + return jsonResponse({ + token: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + expires_at: 9999999999, + }); + } + + if (url === "https://api.individual.githubcopilot.com/models") { + expect(init?.headers).toMatchObject({ + Authorization: "Bearer tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + }); + return jsonResponse({ + data: [ + { + id: "gpt-4.1", + model_picker_enabled: true, + capabilities: { supports: { tool_calls: true } }, + }, + { + id: "claude-opus-4.7", + model_picker_enabled: true, + policy: { state: "disabled" }, + capabilities: { supports: { tool_calls: true } }, + }, + { + id: "gpt-5.4-nano", + model_picker_enabled: false, + capabilities: { supports: { tool_calls: true } }, + }, + ], + }); + } + + throw new Error(`Unexpected fetch URL: ${url}`); + }); + + vi.stubGlobal("fetch", fetchMock); + + const credentials = await refreshGitHubCopilotToken("ghu_refresh_token"); + expect(credentials.availableModelIds).toEqual(["gpt-4.1"]); + + const modifiedModels = githubCopilotOAuthProvider.modifyModels?.(getModels("github-copilot"), credentials) ?? []; + expect(modifiedModels.filter((model) => model.provider === "github-copilot").map((model) => model.id)).toEqual([ + "gpt-4.1", + ]); + }); + it("reports device-code details through onDeviceCode", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-09T00:00:00Z")); @@ -57,6 +113,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } @@ -146,6 +206,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } @@ -231,6 +295,10 @@ describe("GitHub Copilot OAuth device flow", () => { }); } + if (url.endsWith("/models")) { + return jsonResponse({ data: [] }); + } + if (url.includes("/models/") && url.endsWith("/policy")) { return new Response("", { status: 200 }); } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 07529966..d1245f20 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). - Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). ## [0.79.8] - 2026-06-19 diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index fe8609f2..1d720628 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -1669,6 +1669,25 @@ describe("ModelRegistry", () => { expect(count).toBe(0); }); + test("getAvailable filters GitHub Copilot OAuth models to account picker availability", () => { + authStorage.set("github-copilot", { + type: "oauth", + refresh: "github-access-token", + access: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;", + expires: Date.now() + 60_000, + availableModelIds: ["gpt-4.1"], + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + + expect( + registry + .getAvailable() + .filter((m) => m.provider === "github-copilot") + .map((m) => m.id), + ).toEqual(["gpt-4.1"]); + }); + test("getApiKeyAndHeaders resolves authHeader on every request", async () => { const tokenFile = join(tempDir, "token"); writeFileSync(tokenFile, "token-1"); From 1287b69fe026a9c3f9cec8a220ad9405851f7dc3 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Fri, 19 Jun 2026 15:05:16 +0200 Subject: [PATCH 114/141] fix(coding-agent): run legacy WSL bash commands via stdin closes #5893 --- packages/agent/CHANGELOG.md | 4 ++ packages/agent/src/harness/env/nodejs.ts | 52 +++++++++++++----- .../agent/test/harness/nodejs-env.test.ts | 35 +++++++++++- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/resolve-config-value.ts | 8 ++- packages/coding-agent/src/core/tools/bash.ts | 11 +++- packages/coding-agent/src/utils/shell.ts | 20 +++++-- .../coding-agent/test/auth-storage.test.ts | 27 ++++++++- packages/coding-agent/test/tools.test.ts | 50 +++++++++++++++++ scripts/repro-5893-wsl-bash.mjs | 55 +++++++++++++++++++ 10 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 scripts/repro-5893-wsl-bash.mjs diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 0ac61b2f..18d0e561 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Node execution environment commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). + ## [0.79.8] - 2026-06-19 ### Added diff --git a/packages/agent/src/harness/env/nodejs.ts b/packages/agent/src/harness/env/nodejs.ts index e56e7aeb..3d929c82 100644 --- a/packages/agent/src/harness/env/nodejs.ts +++ b/packages/agent/src/harness/env/nodejs.ts @@ -144,12 +144,25 @@ async function findBashOnPath(): Promise { return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null; } -async function getShellConfig( - customShellPath?: string, -): Promise> { +interface ShellConfig { + shell: string; + args: string[]; + commandTransport?: "argv" | "stdin"; +} + +function isLegacyWslBashPath(path: string): boolean { + const normalized = path.replace(/\//g, "\\").toLowerCase(); + return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized); +} + +function getBashShellConfig(shell: string): ShellConfig { + return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] }; +} + +async function getShellConfig(customShellPath?: string): Promise> { if (customShellPath) { if (await pathExists(customShellPath)) { - return ok({ shell: customShellPath, args: ["-c"] }); + return ok(getBashShellConfig(customShellPath)); } return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`)); } @@ -161,22 +174,22 @@ async function getShellConfig( if (programFilesX86) candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`); for (const candidate of candidates) { if (await pathExists(candidate)) { - return ok({ shell: candidate, args: ["-c"] }); + return ok(getBashShellConfig(candidate)); } } const bashOnPath = await findBashOnPath(); if (bashOnPath) { - return ok({ shell: bashOnPath, args: ["-c"] }); + return ok(getBashShellConfig(bashOnPath)); } return err(new ExecutionError("shell_unavailable", "No bash shell found")); } if (await pathExists("/bin/bash")) { - return ok({ shell: "/bin/bash", args: ["-c"] }); + return ok(getBashShellConfig("/bin/bash")); } const bashOnPath = await findBashOnPath(); if (bashOnPath) { - return ok({ shell: bashOnPath, args: ["-c"] }); + return ok(getBashShellConfig(bashOnPath)); } return ok({ shell: "sh", args: ["-c"] }); } @@ -274,13 +287,22 @@ export class NodeExecutionEnv implements ExecutionEnv { }; try { - child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], { - cwd, - detached: process.platform !== "win32", - env: getShellEnv(this.shellEnv, options?.env), - stdio: ["ignore", "pipe", "pipe"], - windowsHide: true, - }); + const commandFromStdin = shellConfig.value.commandTransport === "stdin"; + child = spawn( + shellConfig.value.shell, + commandFromStdin ? shellConfig.value.args : [...shellConfig.value.args, command], + { + cwd, + detached: process.platform !== "win32", + env: getShellEnv(this.shellEnv, options?.env), + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"], + windowsHide: true, + }, + ); + if (commandFromStdin) { + child.stdin?.on("error", () => {}); + child.stdin?.end(command); + } } catch (error) { const cause = toError(error); settle(err(new ExecutionError("spawn_error", cause.message, cause))); diff --git a/packages/agent/test/harness/nodejs-env.test.ts b/packages/agent/test/harness/nodejs-env.test.ts index 758d5f59..d2d33a6f 100644 --- a/packages/agent/test/harness/nodejs-env.test.ts +++ b/packages/agent/test/harness/nodejs-env.test.ts @@ -1,5 +1,5 @@ import { access, chmod, realpath, symlink } from "node:fs/promises"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; import { FileError, getOrThrow } from "../../src/harness/types.ts"; @@ -201,6 +201,39 @@ describe("NodeExecutionEnv", () => { expect(result).toEqual({ stdout: `${await realpath(root)}:ok`, stderr: "", exitCode: 0 }); }); + it("uses stdin command transport for legacy WSL bash paths", async () => { + if (process.platform === "win32") return; + const root = createTempDir(); + const shellPath = "C:\\Windows\\System32\\bash.exe"; + const env = new NodeExecutionEnv({ cwd: root }); + getOrThrow(await env.writeFile(shellPath, '#!/bin/sh\nprintf \'args:%s\\n\' "$*" >&2\nexec /bin/bash "$@"\n')); + await chmod(join(root, shellPath), 0o755); + + const originalCwd = process.cwd(); + const originalPath = process.env.PATH; + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + try { + process.chdir(root); + process.env.PATH = `${root}${delimiter}${originalPath ?? ""}`; + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + + const wslEnv = new NodeExecutionEnv({ cwd: root, shellPath }); + const nameExpansion = "$" + "{name}"; + const result = getOrThrow(await wslEnv.exec(`name='World'; echo "Hello, ${nameExpansion}!"`)); + + expect(result).toEqual({ stdout: "Hello, World!\n", stderr: "args:-s\n", exitCode: 0 }); + } finally { + process.chdir(originalCwd); + process.env.PATH = originalPath; + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + it("streams stdout and stderr chunks", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d1245f20..c5aae0f9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). - Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). diff --git a/packages/coding-agent/src/core/resolve-config-value.ts b/packages/coding-agent/src/core/resolve-config-value.ts index 9d47cff3..6d75b001 100644 --- a/packages/coding-agent/src/core/resolve-config-value.ts +++ b/packages/coding-agent/src/core/resolve-config-value.ts @@ -152,11 +152,13 @@ export function resolveConfigValue(config: string, env?: Record) function executeWithConfiguredShell(command: string): { executed: boolean; value: string | undefined } { try { - const { shell, args } = getShellConfig(); - const result = spawnSync(shell, [...args, command], { + const { shell, args, commandTransport } = getShellConfig(); + const commandFromStdin = commandTransport === "stdin"; + const result = spawnSync(shell, commandFromStdin ? args : [...args, command], { encoding: "utf-8", + input: commandFromStdin ? command : undefined, timeout: 10000, - stdio: ["ignore", "pipe", "ignore"], + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "ignore"], shell: false, windowsHide: true, }); diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index e56291bb..da6934e7 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -66,7 +66,7 @@ export interface BashOperations { export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations { return { exec: async (command, cwd, { onData, signal, timeout, env }) => { - const { shell, args } = getShellConfig(options?.shellPath); + const shellConfig = getShellConfig(options?.shellPath); try { await fsAccess(cwd, constants.F_OK); } catch { @@ -76,13 +76,18 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas throw new Error("aborted"); } - const child = spawn(shell, [...args, command], { + const commandFromStdin = shellConfig.commandTransport === "stdin"; + const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], { cwd, detached: process.platform !== "win32", env: env ?? getShellEnv(), - stdio: ["ignore", "pipe", "pipe"], + stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"], windowsHide: true, }); + if (commandFromStdin) { + child.stdin?.on("error", () => {}); + child.stdin?.end(command); + } if (child.pid) trackDetachedChildPid(child.pid); let timedOut = false; let timeoutHandle: NodeJS.Timeout | undefined; diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 817ba4f9..2cafa595 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -6,11 +6,21 @@ import { getBinDir } from "../config.ts"; export interface ShellConfig { shell: string; args: string[]; + commandTransport?: "argv" | "stdin"; } /** * Find bash executable on PATH (cross-platform) */ +function isLegacyWslBashPath(path: string): boolean { + const normalized = path.replace(/\//g, "\\").toLowerCase(); + return /^[a-z]:\\windows\\(?:system32|sysnative)\\bash\.exe$/.test(normalized); +} + +function getBashShellConfig(shell: string): ShellConfig { + return isLegacyWslBashPath(shell) ? { shell, args: ["-s"], commandTransport: "stdin" } : { shell, args: ["-c"] }; +} + function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) @@ -58,7 +68,7 @@ export function getShellConfig(customShellPath?: string): ShellConfig { // 1. Check user-specified shell path if (customShellPath) { if (existsSync(customShellPath)) { - return { shell: customShellPath, args: ["-c"] }; + return getBashShellConfig(customShellPath); } throw new Error(`Custom shell path not found: ${customShellPath}`); } @@ -77,14 +87,14 @@ export function getShellConfig(customShellPath?: string): ShellConfig { for (const path of paths) { if (existsSync(path)) { - return { shell: path, args: ["-c"] }; + return getBashShellConfig(path); } } // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.) const bashOnPath = findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return getBashShellConfig(bashOnPath); } throw new Error( @@ -98,12 +108,12 @@ export function getShellConfig(customShellPath?: string): ShellConfig { // Unix: try /bin/bash, then bash on PATH, then fallback to sh if (existsSync("/bin/bash")) { - return { shell: "/bin/bash", args: ["-c"] }; + return getBashShellConfig("/bin/bash"); } const bashOnPath = findBashOnPath(); if (bashOnPath) { - return { shell: bashOnPath, args: ["-c"] }; + return getBashShellConfig(bashOnPath); } return { shell: "sh", args: ["-c"] }; diff --git a/packages/coding-agent/test/auth-storage.test.ts b/packages/coding-agent/test/auth-storage.test.ts index bcc353c1..651f9a17 100644 --- a/packages/coding-agent/test/auth-storage.test.ts +++ b/packages/coding-agent/test/auth-storage.test.ts @@ -5,7 +5,8 @@ import { registerOAuthProvider } from "@earendil-works/pi-ai/oauth"; import lockfile from "proper-lockfile"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage } from "../src/core/auth-storage.ts"; -import { clearConfigValueCache } from "../src/core/resolve-config-value.ts"; +import { clearConfigValueCache, resolveConfigValueUncached } from "../src/core/resolve-config-value.ts"; +import * as shellModule from "../src/utils/shell.ts"; describe("AuthStorage", () => { let tempDir: string; @@ -321,6 +322,30 @@ describe("AuthStorage", () => { expect(apiKey).toBe("hello-world"); }); + test("command config uses stdin when configured shell requires it", () => { + if (process.platform === "win32") return; + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + vi.spyOn(shellModule, "getShellConfig").mockReturnValue({ + shell: "/bin/bash", + args: ["-s"], + commandTransport: "stdin", + }); + + try { + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + const nameExpansion = "$" + "{name}"; + + expect(resolveConfigValueUncached(`!name='World'; echo "Hello, ${nameExpansion}!"`)).toBe("Hello, World!"); + } finally { + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + describe("caching", () => { test("command is only executed once per process", async () => { // Use a command that writes to a file to count invocations diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index f187da62..b06590f7 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -536,6 +536,56 @@ describe("Coding Agent Tools", () => { expect(getShellConfigSpy).toHaveBeenCalledWith("/custom/bash"); }); + it("should send commands over stdin when shell resolution requires it", async () => { + vi.spyOn(shellModule, "getShellConfig").mockReturnValue({ + shell: process.execPath, + args: [ + "-e", + 'let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { input += chunk; }); process.stdin.on("end", () => { process.stdout.write(input); });', + ], + commandTransport: "stdin", + }); + const chunks: Buffer[] = []; + const ops = createLocalBashOperations({ shellPath: "C:\\Windows\\System32\\bash.exe" }); + const nameExpansion = "$" + "{name}"; + const countExpansion = "$" + "{count}"; + const iExpansion = "$" + "{i}"; + const command = `name='World'; echo "Hello, ${nameExpansion}!"; count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`; + + const result = await ops.exec(command, testDir, { + onData: (data) => chunks.push(data), + }); + + expect(result.exitCode).toBe(0); + expect(Buffer.concat(chunks).toString("utf-8")).toBe(command); + }); + + it("should resolve legacy WSL bash.exe to stdin command transport", () => { + if (process.platform === "win32") return; + const originalCwd = process.cwd(); + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform"); + const shellPath = "C:\\Windows\\System32\\bash.exe"; + writeFileSync(join(testDir, shellPath), ""); + try { + process.chdir(testDir); + Object.defineProperty(process, "platform", { + configurable: true, + value: "win32", + }); + + expect(shellModule.getShellConfig(shellPath)).toEqual({ + shell: shellPath, + args: ["-s"], + commandTransport: "stdin", + }); + } finally { + process.chdir(originalCwd); + if (platformDescriptor) { + Object.defineProperty(process, "platform", platformDescriptor); + } + } + }); + it("should prepend command prefix when configured", async () => { const bashWithPrefix = createBashTool(testDir, { commandPrefix: "export TEST_VAR=hello", diff --git a/scripts/repro-5893-wsl-bash.mjs b/scripts/repro-5893-wsl-bash.mjs new file mode 100644 index 00000000..b5048de7 --- /dev/null +++ b/scripts/repro-5893-wsl-bash.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +import { existsSync } from "node:fs"; +import { createBashTool } from "../packages/coding-agent/src/core/tools/bash.ts"; + +const shellPath = "C:\\Windows\\System32\\bash.exe"; +const nameExpansion = "$" + "{name}"; +const countExpansion = "$" + "{count}"; +const iExpansion = "$" + "{i}"; + +function getTextOutput(result) { + return result.content + .filter((content) => content.type === "text") + .map((content) => content.text ?? "") + .join("\n"); +} + +async function runCase(label, command, expectedOutput) { + const tool = createBashTool(process.cwd(), { shellPath }); + const result = await tool.execute(label, { command }); + const output = getTextOutput(result).trimEnd(); + if (output !== expectedOutput) { + throw new Error( + [ + `${label} failed`, + "Expected:", + expectedOutput, + "Actual:", + output, + ].join("\n"), + ); + } + console.log(output); +} + +if (process.platform !== "win32") { + throw new Error("This repro must run from Windows PowerShell/CMD, not macOS/Linux or inside WSL."); +} + +if (!existsSync(shellPath)) { + throw new Error(`WSL bash launcher not found at ${shellPath}. Install/enable WSL first.`); +} + +await runCase( + "issue-5893-simple-variable", + `name='World'; echo "Hello, ${nameExpansion}!"`, + "Hello, World!", +); + +await runCase( + "issue-5893-loop-variable", + `count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`, + "Iteration 1 of 3\nIteration 2 of 3\nIteration 3 of 3", +); + +console.log("issue #5893 WSL bash repro passed"); From 128330e36f277452f6d07ae749643ee77f4ca4e8 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 20:16:05 +0200 Subject: [PATCH 115/141] fix(coding-agent): preserve untouched lines in fuzzy edit closes #5899 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/tools/edit-diff.ts | 157 +++++++++++++++--- packages/coding-agent/test/tools.test.ts | 51 ++++++ 3 files changed, 184 insertions(+), 25 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c5aae0f9..fdc99e61 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). - Fixed `/model` selector search to rank exact provider-prefixed matches before proxy-provider model ID matches ([#5892](https://github.com/earendil-works/pi/issues/5892)). diff --git a/packages/coding-agent/src/core/tools/edit-diff.ts b/packages/coding-agent/src/core/tools/edit-diff.ts index cc94c202..5a4d966b 100644 --- a/packages/coding-agent/src/core/tools/edit-diff.ts +++ b/packages/coding-agent/src/core/tools/edit-diff.ts @@ -53,6 +53,124 @@ export function normalizeForFuzzyMatch(text: string): string { ); } +function splitLinesWithEndings(content: string): string[] { + return content.match(/[^\n]*\n|[^\n]+/g) ?? []; +} + +interface LineSpan { + start: number; + end: number; +} + +interface MatchedEdit { + editIndex: number; + matchIndex: number; + matchLength: number; + newText: string; +} + +type TextReplacement = Pick; + +function getLineSpans(content: string): LineSpan[] { + let offset = 0; + return splitLinesWithEndings(content).map((line) => { + const span = { start: offset, end: offset + line.length }; + offset = span.end; + return span; + }); +} + +function getReplacementLineRange(lines: LineSpan[], replacement: TextReplacement) { + const replacementStart = replacement.matchIndex; + const replacementEnd = replacement.matchIndex + replacement.matchLength; + + let startLine = -1; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (replacementStart >= line.start && replacementStart < line.end) { + startLine = i; + break; + } + } + if (startLine === -1) { + throw new Error("Replacement range is outside the base content."); + } + + let endLine = startLine; + while (endLine < lines.length && lines[endLine].end < replacementEnd) { + endLine++; + } + if (endLine >= lines.length) { + throw new Error("Replacement range is outside the base content."); + } + + return { startLine, endLine: endLine + 1 }; +} + +function applyReplacements(content: string, replacements: TextReplacement[], offset = 0): string { + let result = content; + for (let i = replacements.length - 1; i >= 0; i--) { + const replacement = replacements[i]; + const matchIndex = replacement.matchIndex - offset; + result = + result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength); + } + return result; +} + +/** + * Apply replacements matched against `baseContent` to `originalContent` while + * preserving unchanged line blocks from the original. + * + * This is useful when `baseContent` is a normalized view of the original. Each + * replacement is widened to the lines it actually touches, those touched lines + * are rewritten from the normalized base, and all other lines are copied back + * from `originalContent`. The actual replacement ranges drive preservation so + * duplicate normalized lines cannot be aligned to the wrong occurrence. + */ +export function applyReplacementsPreservingUnchangedLines( + originalContent: string, + baseContent: string, + replacements: TextReplacement[], +): string { + const originalLines = splitLinesWithEndings(originalContent); + const baseLines = getLineSpans(baseContent); + if (originalLines.length !== baseLines.length) { + throw new Error("Cannot preserve unchanged lines because the base content has a different line count."); + } + + const groups: Array<{ startLine: number; endLine: number; replacements: TextReplacement[] }> = []; + const sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex); + for (const replacement of sortedReplacements) { + const range = getReplacementLineRange(baseLines, replacement); + const current = groups[groups.length - 1]; + if (current && range.startLine < current.endLine) { + current.endLine = Math.max(current.endLine, range.endLine); + current.replacements.push(replacement); + continue; + } + groups.push({ ...range, replacements: [replacement] }); + } + + let originalLineIndex = 0; + let result = ""; + for (const group of groups) { + result += originalLines.slice(originalLineIndex, group.startLine).join(""); + + const groupStartOffset = baseLines[group.startLine].start; + const groupEndOffset = baseLines[group.endLine - 1].end; + result += applyReplacements( + baseContent.slice(groupStartOffset, groupEndOffset), + group.replacements, + groupStartOffset, + ); + originalLineIndex = group.endLine; + } + result += originalLines.slice(originalLineIndex).join(""); + + return result; +} + export interface FuzzyMatchResult { /** Whether a match was found */ found: boolean; @@ -74,13 +192,6 @@ export interface Edit { newText: string; } -interface MatchedEdit { - editIndex: number; - matchIndex: number; - matchLength: number; - newText: string; -} - export interface AppliedEditsResult { baseContent: string; newContent: string; @@ -120,9 +231,9 @@ export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResul }; } - // When fuzzy matching, we work in the normalized space for replacement. - // This means the output will have normalized whitespace/quotes/dashes, - // which is acceptable since we're fixing minor formatting differences anyway. + // When fuzzy matching, return offsets in normalized space. Callers can use + // the normalized content to compute replacements, then decide how much of + // that normalized output should be written back. return { found: true, index: fuzzyIndex, @@ -186,8 +297,9 @@ function getNoChangeError(path: string, totalEdits: number): Error { * * All edits are matched against the same original content. Replacements are * then applied in reverse order so offsets remain stable. If any edit needs - * fuzzy matching, the operation runs in fuzzy-normalized content space to - * preserve current single-edit behavior. + * fuzzy matching, the operation runs in fuzzy-normalized content space and then + * overlays those line-level changes onto the original content so unchanged line + * blocks keep their original bytes. */ export function applyEditsToNormalizedContent( normalizedContent: string, @@ -206,19 +318,18 @@ export function applyEditsToNormalizedContent( } const initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText)); - const baseContent = initialMatches.some((match) => match.usedFuzzyMatch) - ? normalizeForFuzzyMatch(normalizedContent) - : normalizedContent; + const usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch); + const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent; const matchedEdits: MatchedEdit[] = []; for (let i = 0; i < normalizedEdits.length; i++) { const edit = normalizedEdits[i]; - const matchResult = fuzzyFindText(baseContent, edit.oldText); + const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText); if (!matchResult.found) { throw getNotFoundError(path, i, normalizedEdits.length); } - const occurrences = countOccurrences(baseContent, edit.oldText); + const occurrences = countOccurrences(replacementBaseContent, edit.oldText); if (occurrences > 1) { throw getDuplicateError(path, i, normalizedEdits.length, occurrences); } @@ -242,14 +353,10 @@ export function applyEditsToNormalizedContent( } } - let newContent = baseContent; - for (let i = matchedEdits.length - 1; i >= 0; i--) { - const edit = matchedEdits[i]; - newContent = - newContent.substring(0, edit.matchIndex) + - edit.newText + - newContent.substring(edit.matchIndex + edit.matchLength); - } + const baseContent = normalizedContent; + const newContent = usedFuzzyMatch + ? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits) + : applyReplacements(replacementBaseContent, matchedEdits); if (baseContent === newContent) { throw getNoChangeError(path, normalizedEdits.length); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index b06590f7..f8b09c90 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -1031,6 +1031,57 @@ describe("edit tool fuzzy matching", () => { expect(readFileSync(testFile, "utf-8")).toBe("console.log('world');\nhello universe\n"); }); + + it("should preserve the correct occurrence when fuzzy replacement equals a nearby line", async () => { + const testFile = join(testDir, "fuzzy-preserve-duplicate-line.txt"); + const originalContent = ["replace me\u0020\u0020\u0020", "after\u0020\u0020\u0020", ""].join("\n"); + writeFileSync(testFile, originalContent); + + const result = await editTool.execute("test-fuzzy-preserve-duplicate-line", { + path: testFile, + edits: [{ oldText: "replace me\n", newText: "after\n" }], + }); + + const expectedContent = ["after", "after\u0020\u0020\u0020", ""].join("\n"); + expect(readFileSync(testFile, "utf-8")).toBe(expectedContent); + expect(applyPatch(originalContent, result.details?.patch ?? "")).toBe(expectedContent); + }); + + it("should preserve untouched lines and produce an applicable patch for fuzzy multi-edits", async () => { + const testFile = join(testDir, "fuzzy-preserve-multi.txt"); + const originalContent = [ + "keep before\u0020\u0020", + "first target\u0020\u0020", + "first after", + "keep middle\u0020\u0020\u0020", + "second target\u0020\u0020", + "second after", + "keep after\u0020\u0020", + "", + ].join("\n"); + writeFileSync(testFile, originalContent); + + const result = await editTool.execute("test-fuzzy-preserve-multi", { + path: testFile, + edits: [ + { oldText: "first target\nfirst after", newText: "FIRST\nFIRST2" }, + { oldText: "second target\nsecond after", newText: "SECOND\nSECOND2" }, + ], + }); + + const expectedContent = [ + "keep before\u0020\u0020", + "FIRST", + "FIRST2", + "keep middle\u0020\u0020\u0020", + "SECOND", + "SECOND2", + "keep after\u0020\u0020", + "", + ].join("\n"); + expect(readFileSync(testFile, "utf-8")).toBe(expectedContent); + expect(applyPatch(originalContent, result.details?.patch ?? "")).toBe(expectedContent); + }); }); describe("edit tool CRLF handling", () => { From 8b97e75c6b149fdd4dec95fe3321d1e94fd5c1d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 19 Jun 2026 23:34:17 +0200 Subject: [PATCH 116/141] feat(ai): add chat-template thinking compat closes #5673 --- packages/ai/CHANGELOG.md | 4 + packages/ai/README.md | 3 +- .../ai/src/providers/openai-completions.ts | 48 ++++++++ packages/ai/src/types.ts | 14 ++- ...penai-completions-thinking-as-text.test.ts | 1 + .../openai-completions-tool-choice.test.ts | 114 +++++++++++++++++- ...nai-completions-tool-result-images.test.ts | 1 + packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/docs/custom-provider.md | 7 +- packages/coding-agent/docs/models.md | 5 +- .../coding-agent/src/core/model-registry.ts | 18 +++ .../coding-agent/test/model-registry.test.ts | 37 ++++++ 12 files changed, 248 insertions(+), 8 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 4f316c3b..dc1a931b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)). + ### Fixed - Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/ai/README.md b/packages/ai/README.md index 8137ccd4..bec437aa 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -967,7 +967,8 @@ interface OpenAICompletionsCompat { requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false) requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false) requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek) - thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses enable_thinking, 'qwen' uses enable_thinking, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai) + thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai) + chatTemplateKwargs?: Record; // chat_template_kwargs values; use $var for pi-controlled thinking values cacheControlFormat?: 'anthropic'; // Anthropic-style cache_control on system prompt, last tool, and last user/assistant text content openRouterRouting?: OpenRouterRouting; // OpenRouter routing preferences (default: {}) vercelGatewayRouting?: VercelGatewayRouting; // Vercel AI Gateway routing preferences (default: {}) diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 2f5edb62..10c8e7e6 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -15,6 +15,7 @@ import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, CacheRetention, + ChatTemplateKwargValue, Context, ImageContent, Message, @@ -91,6 +92,8 @@ type ResolvedOpenAICompletionsCompat = Omit, " cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"]; }; +type ResolvedChatTemplateKwargValue = string | number | boolean | null; + type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam; type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & { @@ -585,6 +588,11 @@ function buildParams( enable_thinking: !!options?.reasoningEffort, preserve_thinking: true, }; + } else if (compat.thinkingFormat === "chat-template" && model.reasoning) { + const chatTemplateKwargs = buildChatTemplateKwargs(model, options, compat); + if (chatTemplateKwargs) { + (params as any).chat_template_kwargs = chatTemplateKwargs; + } } else if (compat.thinkingFormat === "deepseek" && model.reasoning) { if (options?.reasoningEffort) { (params as any).thinking = { type: "enabled" }; @@ -655,6 +663,44 @@ function buildParams( return params; } +function buildChatTemplateKwargs( + model: Model<"openai-completions">, + options: OpenAICompletionsOptions | undefined, + compat: ResolvedOpenAICompletionsCompat, +): Record | undefined { + const kwargs: Record = {}; + + for (const [key, value] of Object.entries(compat.chatTemplateKwargs)) { + const resolved = resolveChatTemplateKwargValue(model, options, value); + if (resolved !== undefined) { + kwargs[key] = resolved; + } + } + + return Object.keys(kwargs).length > 0 ? kwargs : undefined; +} + +function resolveChatTemplateKwargValue( + model: Model<"openai-completions">, + options: OpenAICompletionsOptions | undefined, + value: ChatTemplateKwargValue, +): ResolvedChatTemplateKwargValue | undefined { + if (typeof value !== "object" || value === null) { + return value; + } + + const reasoningEffort = options?.reasoningEffort; + if (!reasoningEffort && value.omitWhenOff) { + return undefined; + } + if (value.$var === "thinking.enabled") { + return !!reasoningEffort; + } + + const mappedValue = reasoningEffort ? model.thinkingLevelMap?.[reasoningEffort] : model.thinkingLevelMap?.off; + return mappedValue === undefined ? reasoningEffort : typeof mappedValue === "string" ? mappedValue : undefined; +} + function getCompatCacheControl( compat: ResolvedOpenAICompletionsCompat, cacheRetention: CacheRetention, @@ -1167,6 +1213,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet : "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: !isMoonshot && !isTogether && !isCloudflareAiGateway && !isNvidia, cacheControlFormat, @@ -1205,6 +1252,7 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, openRouterRouting: model.compat.openRouterRouting ?? {}, vercelGatewayRouting: model.compat.vercelGatewayRouting ?? detected.vercelGatewayRouting, + chatTemplateKwargs: model.compat.chatTemplateKwargs ?? detected.chatTemplateKwargs, zaiToolStream: model.compat.zaiToolStream ?? detected.zaiToolStream, supportsStrictMode: model.compat.supportsStrictMode ?? detected.supportsStrictMode, cacheControlFormat: model.compat.cacheControlFormat ?? detected.cacheControlFormat, diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index cedac6e8..54f44685 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -65,6 +65,15 @@ export type ImagesProvider = KnownImagesProvider | string; export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh"; export type ModelThinkingLevel = "off" | ThinkingLevel; export type ThinkingLevelMap = Partial>; +export type ChatTemplateKwargValue = + | string + | number + | boolean + | null + | { + $var: "thinking.enabled" | "thinking.effort"; + omitWhenOff?: boolean; + }; /** Token budgets for each thinking level (token-based providers only) */ export interface ThinkingBudgets { @@ -403,7 +412,7 @@ export interface OpenAICompletionsCompat { requiresThinkingAsText?: boolean; /** Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. */ requiresReasoningContentOnAssistantMessages?: boolean; - /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ + /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "openrouter" uses reasoning: { effort }, "deepseek" uses thinking: { type } plus reasoning_effort when supported, "together" uses reasoning: { enabled } plus reasoning_effort when supported, "zai" uses thinking: { type }, "qwen" uses top-level enable_thinking: boolean, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking: string, and "ant-ling" uses reasoning: { effort } only when the mapped effort is non-null. Default: "openai". */ thinkingFormat?: | "openai" | "openrouter" @@ -411,9 +420,12 @@ export interface OpenAICompletionsCompat { | "together" | "zai" | "qwen" + | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + /** Kwargs to send as `chat_template_kwargs` when `thinkingFormat` is `chat-template`. Use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values. */ + chatTemplateKwargs?: Record; /** OpenRouter-compatible routing preferences sent as the `provider` request field. */ openRouterRouting?: OpenRouterRouting; /** Vercel AI Gateway routing preferences. Only used when baseUrl points to Vercel AI Gateway. */ diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 1c49aad3..138eb3e3 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -34,6 +34,7 @@ const compat = { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, cacheControlFormat: undefined, diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 373b1b78..41601281 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { stream, streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; import { convertMessages } from "../src/providers/openai-completions.ts"; -import type { AssistantMessage, Model, Tool, ToolResultMessage } from "../src/types.ts"; +import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, @@ -64,6 +64,46 @@ vi.mock("openai", () => { return { default: FakeOpenAI }; }); +const localOpenAICompletionsModel = { + api: "openai-completions", + provider: "local-vllm", + baseUrl: "http://localhost:8000/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, +} satisfies Omit, "id" | "name" | "compat">; + +type CapturedParams = { + chat_template_kwargs?: Record; + thinking?: unknown; + reasoning_effort?: string; +}; + +async function captureSimpleParams( + model: Model<"openai-completions">, + reasoning?: SimpleStreamOptions["reasoning"], +): Promise { + let payload: unknown; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "Hi", timestamp: Date.now() }], + }, + { + apiKey: "test", + reasoning, + onPayload: (params: unknown) => { + payload = params; + }, + }, + ).result(); + + return (payload ?? mockState.lastParams) as CapturedParams; +} + describe("openai-completions tool_choice", () => { beforeEach(() => { mockState.lastParams = undefined; @@ -1142,6 +1182,7 @@ describe("openai-completions tool_choice", () => { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, sendSessionAffinityHeaders: false, @@ -1449,6 +1490,77 @@ describe("openai-completions tool_choice", () => { expect(params.reasoning_effort).toBeUndefined(); }); + it("uses configurable chat template boolean thinking kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "deepseek-ai/DeepSeek-V3.1", + name: "DeepSeek V3.1 via vLLM", + compat: { + thinkingFormat: "chat-template", + supportsReasoningEffort: false, + chatTemplateKwargs: { thinking: { $var: "thinking.enabled" } }, + }, + } satisfies Model<"openai-completions">; + + for (const testCase of [ + { reasoning: "high" as const, expected: true }, + { reasoning: undefined, expected: false }, + ]) { + const params = await captureSimpleParams(model, testCase.reasoning); + + expect(params.chat_template_kwargs).toEqual({ thinking: testCase.expected }); + expect(params.thinking).toBeUndefined(); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("uses qwen chat template thinking kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "Qwen/Qwen3-Coder", + name: "Qwen3 Coder via vLLM", + compat: { + thinkingFormat: "qwen-chat-template", + supportsReasoningEffort: false, + }, + } satisfies Model<"openai-completions">; + + for (const testCase of [ + { reasoning: "high" as const, expected: true }, + { reasoning: undefined, expected: false }, + ]) { + const params = await captureSimpleParams(model, testCase.reasoning); + + expect(params.chat_template_kwargs).toEqual({ + enable_thinking: testCase.expected, + preserve_thinking: true, + }); + expect(params.reasoning_effort).toBeUndefined(); + } + }); + + it("uses configurable chat template effort kwargs with static kwargs", async () => { + const model = { + ...localOpenAICompletionsModel, + id: "unsloth/gpt-oss-120b-GGUF", + name: "GPT OSS via vLLM", + thinkingLevelMap: { xhigh: "max" }, + compat: { + thinkingFormat: "chat-template", + supportsReasoningEffort: false, + chatTemplateKwargs: { + preserve_thinking: true, + reasoning_effort: { $var: "thinking.effort", omitWhenOff: true }, + }, + }, + } satisfies Model<"openai-completions">; + + const params = await captureSimpleParams(model, "xhigh"); + + expect(params.chat_template_kwargs).toEqual({ preserve_thinking: true, reasoning_effort: "max" }); + expect(params.reasoning_effort).toBeUndefined(); + }); + it("uses Ant Ling compatibility metadata", async () => { const model = getModel("ant-ling", "Ring-2.6-1T")!; let payload: unknown; diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 3510f396..4458e51f 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -32,6 +32,7 @@ const compat: Required = { thinkingFormat: "openai", openRouterRouting: {}, vercelGatewayRouting: {}, + chatTemplateKwargs: {}, zaiToolStream: false, supportsStrictMode: true, cacheControlFormat: "anthropic", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fdc99e61..8813170b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added inherited configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)). + ### Fixed - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index c2bf4455..612d1e60 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -229,7 +229,7 @@ models: [{ }] ``` -Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. +Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user/assistant text content. For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: ""` on replay. @@ -718,7 +718,8 @@ interface ProviderModelConfig { requiresAssistantAfterToolResult?: boolean; requiresThinkingAsText?: boolean; requiresReasoningContentOnAssistantMessages?: boolean; - thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template"; + thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling"; + chatTemplateKwargs?: Record; cacheControlFormat?: "anthropic"; // anthropic-messages @@ -732,5 +733,5 @@ interface ProviderModelConfig { } ``` -`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`. +`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`. `cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user/assistant text content. diff --git a/packages/coding-agent/docs/models.md b/packages/coding-agent/docs/models.md index 830eaacd..5679981a 100644 --- a/packages/coding-agent/docs/models.md +++ b/packages/coding-agent/docs/models.md @@ -399,14 +399,15 @@ For providers with partial OpenAI compatibility, use the `compat` field. | `requiresAssistantAfterToolResult` | Insert an assistant message before a user message after tool results | | `requiresThinkingAsText` | Convert thinking blocks to plain text | | `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled | -| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, or `qwen-chat-template` thinking parameters | +| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters | +| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values | | `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user/assistant text content. Currently only `anthropic` is supported. | | `supportsStrictMode` | Include the `strict` field in tool definitions | | `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. | | `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). | | `vercelGatewayRouting` | Vercel AI Gateway routing config for provider selection (`only`, `order`) | -`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking`. +`openrouter` uses `reasoning: { effort }`. `together` uses `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` uses top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that require `chat_template_kwargs.enable_thinking` and `preserve_thinking`. Use `chat-template` for vLLM/Hugging Face chat templates that need configurable `chat_template_kwargs`, such as `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }` for DeepSeek V3.x templates. `cacheControlFormat: "anthropic"` is for OpenAI-compatible providers that expose Anthropic-style prompt caching through `cache_control` markers on text content and tool definitions. diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts index 60f9001f..305dca92 100644 --- a/packages/coding-agent/src/core/model-registry.ts +++ b/packages/coding-agent/src/core/model-registry.ts @@ -96,6 +96,13 @@ const ThinkingLevelMapSchema = Type.Object({ xhigh: Type.Optional(ThinkingLevelMapValueSchema), }); +const ChatTemplateKwargScalarSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]); +const ChatTemplateKwargVariableSchema = Type.Object({ + $var: Type.Union([Type.Literal("thinking.enabled"), Type.Literal("thinking.effort")]), + omitWhenOff: Type.Optional(Type.Boolean()), +}); +const ChatTemplateKwargSchema = Type.Union([ChatTemplateKwargScalarSchema, ChatTemplateKwargVariableSchema]); + const OpenAICompletionsCompatSchema = Type.Object({ supportsStore: Type.Optional(Type.Boolean()), supportsDeveloperRole: Type.Optional(Type.Boolean()), @@ -114,9 +121,13 @@ const OpenAICompletionsCompatSchema = Type.Object({ Type.Literal("deepseek"), Type.Literal("zai"), Type.Literal("qwen"), + Type.Literal("chat-template"), Type.Literal("qwen-chat-template"), + Type.Literal("string-thinking"), + Type.Literal("ant-ling"), ]), ), + chatTemplateKwargs: Type.Optional(Type.Record(Type.String(), ChatTemplateKwargSchema)), cacheControlFormat: Type.Optional(Type.Literal("anthropic")), openRouterRouting: Type.Optional(OpenRouterRoutingSchema), vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema), @@ -289,6 +300,13 @@ function mergeCompat( }; } + if (baseCompletions?.chatTemplateKwargs || overrideCompletions.chatTemplateKwargs) { + mergedCompletions.chatTemplateKwargs = { + ...baseCompletions?.chatTemplateKwargs, + ...overrideCompletions.chatTemplateKwargs, + }; + } + return merged as Model["compat"]; } diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 1d720628..1479808a 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -434,6 +434,43 @@ describe("ModelRegistry", () => { expect(compat?.cacheControlFormat).toBe("anthropic"); }); + test("compat schema accepts chat template thinking configuration", () => { + writeRawModelsJson({ + demo: { + baseUrl: "https://example.com/v1", + apiKey: "DEMO_KEY", + api: "openai-completions", + models: [ + { + id: "demo-model", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1000, + maxTokens: 100, + compat: { + thinkingFormat: "chat-template", + chatTemplateKwargs: { + preserve_thinking: true, + thinking: { $var: "thinking.enabled" }, + }, + }, + }, + ], + }, + }); + + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + const compat = registry.find("demo", "demo-model")?.compat as OpenAICompletionsCompat | undefined; + + expect(registry.getError()).toBeUndefined(); + expect(compat?.thinkingFormat).toBe("chat-template"); + expect(compat?.chatTemplateKwargs).toEqual({ + preserve_thinking: true, + thinking: { $var: "thinking.enabled" }, + }); + }); + test("compat schema accepts Anthropic eager tool input streaming flag", () => { writeRawModelsJson({ demo: { From 3095977d13961e3fb64770c53ca4f9f9f8baf7df Mon Sep 17 00:00:00 2001 From: Alexey Zaytsev Date: Sat, 20 Jun 2026 07:58:30 -0500 Subject: [PATCH 117/141] fix(tui): stabilize streaming code fence rendering (#5846) --- packages/tui/src/components/markdown.ts | 26 +++++++++++++++ packages/tui/test/markdown.test.ts | 43 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 83f15aa5..1034cb47 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -22,6 +22,31 @@ class StrictStrikethroughTokenizer extends Tokenizer { } } +function trimPartialClosingFences(tokens: readonly Token[]): void { + const token = tokens[tokens.length - 1]; + if (token?.type === "list") { + trimPartialClosingFences(token.items[token.items.length - 1]?.tokens ?? []); + return; + } + if (token?.type === "blockquote") { + trimPartialClosingFences(token.tokens ?? []); + return; + } + if (token?.type !== "code") { + return; + } + + // Trim streamed partial closing fences so code blocks do not shrink/flicker + // when the final fence character arrives. See https://github.com/earendil-works/pi/issues/5825. + const marker = /^(`{3,}|~{3,})/.exec(token.raw)?.[1]; + const lastLine = token.raw.split("\n").pop(); + if (!marker || !lastLine || lastLine.length >= marker.length || lastLine !== marker[0]?.repeat(lastLine.length)) { + return; + } + + token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, ""); +} + const markdownParser = new Marked(); markdownParser.setOptions({ tokenizer: new StrictStrikethroughTokenizer(), @@ -145,6 +170,7 @@ export class Markdown implements Component { // Parse markdown to HTML-like tokens const tokens = markdownParser.lexer(normalizedText); + trimPartialClosingFences(tokens); // Convert tokens to styled terminal output const renderedLines: string[] = []; diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index dc595b11..47bc0a81 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -1376,4 +1376,47 @@ bar`, ); }); }); + + describe("Streaming code fences", () => { + it("stabilizes partial closing fence rendering", () => { + const cases = [ + { + input: "```ts\nconst x = 1;\n``", + expected: ["```ts", " const x = 1;", "```"], + }, + { + input: "```md\nnot a closing fence:\n``\n```", + expected: ["```md", " not a closing fence:", " ``", "```"], + }, + { + input: "```ts\n``", + expected: ["```ts", "", "```"], + }, + { + input: "````\n```", + expected: ["```", "", "```"], + }, + { + input: "~~~~~\n~~~~", + expected: ["```", "", "```"], + }, + { + input: "```md\nnot a closing fence:\n``\n```\n\nafter", + expected: ["```md", " not a closing fence:", " ``", "```", "", "after"], + }, + ]; + + for (const { input, expected } of cases) { + const markdown = new Markdown(input, 0, 0, defaultMarkdownTheme); + const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd()); + + assert.deepStrictEqual(lines, expected); + } + + const partial = new Markdown("```ts\nconst x = 1;\n``", 0, 0, defaultMarkdownTheme); + const complete = new Markdown("```ts\nconst x = 1;\n```", 0, 0, defaultMarkdownTheme); + + assert.strictEqual(partial.render(80).length, complete.render(80).length); + }); + }); }); From 416c673dfe7f2c1fea051a10a74affa1a84f7ad1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 16:13:13 +0200 Subject: [PATCH 118/141] fix: skip no-action for to-discuss issues --- .github/workflows/issue-triage-labels.yml | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index 4a9ea9d7..cf17c39e 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -17,6 +17,7 @@ jobs: const UNTRIAGED_LABEL = 'untriaged'; const NO_ACTION_LABEL = 'no-action'; const LAST_READ_LABEL = 'last-read'; + const TO_DISCUSS_LABEL = 'to-discuss'; function issueHasLabel(issue, labelName) { return (issue.labels ?? []).some((label) => label.name === labelName); @@ -102,13 +103,17 @@ jobs: } for (const issue of issuesToMark) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: [NO_ACTION_LABEL], - }); - console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + if (issueHasLabel(issue, TO_DISCUSS_LABEL)) { + console.log(`Skipped ${NO_ACTION_LABEL} for #${issue.number} because it has ${TO_DISCUSS_LABEL}`); + } else { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: [NO_ACTION_LABEL], + }); + console.log(`Added ${NO_ACTION_LABEL} to #${issue.number}`); + } await github.rest.issues.update({ owner: context.repo.owner, From 8597ebafd9f8930fc8e7251946999e21274694d4 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 18:47:42 +0200 Subject: [PATCH 119/141] fix(ai): expose OpenRouter GLM-5.2 xhigh effort closes #5770 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 3 + packages/ai/src/models.generated.ts | 79 +++++++------------------- 3 files changed, 26 insertions(+), 57 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index dc1a931b..567a2273 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). ## [0.79.8] - 2026-06-19 diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index c1fddc92..ad9ca95b 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -374,6 +374,9 @@ function applyThinkingLevelMetadata(model: Model): void { // Pi's low/medium/high pass through verbatim; OpenRouter normalizes to Mercury's vocabulary. mergeThinkingLevelMap(model, { off: null }); } + if (model.provider === "openrouter" && model.id === "z-ai/glm-5.2") { + mergeThinkingLevelMap(model, { xhigh: "xhigh" }); + } if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { // OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers. mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null }); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 8639bf13..497f766a 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4851,42 +4851,6 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"google-generative-ai">, - "gemma-4-E2B-it": { - id: "gemma-4-E2B-it", - name: "Gemma 4 E2B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, - "gemma-4-E4B-it": { - id: "gemma-4-E4B-it", - name: "Gemma 4 E4B IT", - api: "google-generative-ai", - provider: "google", - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - thinkingLevelMap: {"off":null,"minimal":"MINIMAL","low":null,"medium":null,"high":"HIGH"}, - input: ["text", "image"], - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"google-generative-ai">, }, "google-vertex": { "gemini-2.5-flash": { @@ -9190,13 +9154,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.22, - output: 0.85, + input: 0.25, + output: 0.8, cacheRead: 0.06, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 80000, } satisfies Model<"openai-completions">, "arcee-ai/trinity-mini": { id: "arcee-ai/trinity-mini", @@ -10573,13 +10537,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.67, + input: 0.66, output: 3.5, - cacheRead: 0.2, + cacheRead: 0.33, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 262142, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2.7-code": { id: "moonshotai/kimi-k2.7-code", @@ -10590,13 +10554,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.74, - output: 3.5, - cacheRead: 0.15, + input: 0.612, + output: 3.069, + cacheRead: 0.1296, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 16384, + maxTokens: 262144, } satisfies Model<"openai-completions">, "nex-agi/nex-n2-pro:free": { id: "nex-agi/nex-n2-pro:free", @@ -12761,13 +12725,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.066, - output: 0.26, - cacheRead: 0.029, + input: 0.063, + output: 0.21, + cacheRead: 0.021, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 4096, } satisfies Model<"openai-completions">, "thedrummer/rocinante-12b": { id: "thedrummer/rocinante-12b", @@ -13069,11 +13033,11 @@ export const MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.182, + cacheRead: 0.49, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 4096, + maxTokens: 65535, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", @@ -13082,6 +13046,7 @@ export const MODELS = { provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text"], cost: { input: 1.2, @@ -13203,13 +13168,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.67, + input: 0.66, output: 3.5, - cacheRead: 0.2, + cacheRead: 0.33, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 262142, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -15981,8 +15946,8 @@ export const MODELS = { cost: { input: 0.09, output: 0.3, - cacheRead: 0, - cacheWrite: 0.02, + cacheRead: 0.02, + cacheWrite: 0, }, contextWindow: 262114, maxTokens: 262114, From a1da88aed4a1584f4ad4e58bba2e3391760785a7 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 20:22:53 +0200 Subject: [PATCH 120/141] fix(coding-agent): make session path traversal linear Avoid quadratic path construction when walking deep session branches. On a 600k-entry pathological session, path construction improved from about 20.3s with Array.unshift() to about 35ms with push() plus reverse(). Refs #5804. --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/session-manager.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8813170b..1e2a54d6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index 62942480..b07968b3 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -357,9 +357,10 @@ export function buildSessionContext( const path: SessionEntry[] = []; let current: SessionEntry | undefined = leaf; while (current) { - path.unshift(current); + path.push(current); current = current.parentId ? byId.get(current.parentId) : undefined; } + path.reverse(); // Extract settings and find compaction let thinkingLevel = "off"; @@ -1152,9 +1153,10 @@ export class SessionManager { const startId = fromId ?? this.leafId; let current = startId ? this.byId.get(startId) : undefined; while (current) { - path.unshift(current); + path.push(current); current = current.parentId ? this.byId.get(current.parentId) : undefined; } + path.reverse(); return path; } From 5505316ea28720574f493bb4cac1b910ad705f44 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 21:05:55 +0200 Subject: [PATCH 121/141] fix(coding-agent): cache extension imports for session switches Closes #5905. --- packages/coding-agent/CHANGELOG.md | 1 + .../src/core/extensions/loader.ts | 84 ++++++++++- .../coding-agent/src/core/resource-loader.ts | 20 ++- .../extension-factory-cache.test.ts | 131 ++++++++++++++++++ 4 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1e2a54d6..31f4b5f0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed same-directory session switches to reuse imported extension modules while preserving fresh extension instances and lifecycle events ([#5905](https://github.com/earendil-works/pi/issues/5905)). - Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 081d2d11..dcaf389f 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -117,6 +117,30 @@ function getAliases(): Record { type HandlerFn = (...args: unknown[]) => Promise; +let extensionCacheCwd: string | undefined; +let extensionCacheGeneration = 0; +const extensionCache = new Map(); + +interface ExtensionCacheToken { + cwd: string; + generation: number; +} + +export function clearExtensionCache(): void { + extensionCache.clear(); + extensionCacheCwd = undefined; + extensionCacheGeneration++; +} + +function useExtensionCacheCwd(cwd: string): ExtensionCacheToken { + const resolvedCwd = resolvePath(cwd); + if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) { + clearExtensionCache(); + } + extensionCacheCwd = resolvedCwd; + return { cwd: resolvedCwd, generation: extensionCacheGeneration }; +} + /** * Create a runtime with throwing stubs for action methods. * Runner.bindCore() replaces these with real implementations. @@ -328,7 +352,22 @@ function createExtensionAPI( return api; } -async function loadExtensionModule(extensionPath: string) { +function isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken { + return ( + cacheToken !== undefined && + extensionCacheCwd === cacheToken.cwd && + extensionCacheGeneration === cacheToken.generation + ); +} + +async function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) { + if (isCurrentCacheToken(cacheToken)) { + const cachedFactory = extensionCache.get(extensionPath); + if (cachedFactory) { + return cachedFactory; + } + } + const jiti = createJiti(import.meta.url, { moduleCache: false, // In Bun binary: use virtualModules for bundled packages (no filesystem resolution) @@ -339,7 +378,13 @@ async function loadExtensionModule(extensionPath: string) { const module = await jiti.import(extensionPath, { default: true }); const factory = module as ExtensionFactory; - return typeof factory !== "function" ? undefined : factory; + if (typeof factory !== "function") { + return undefined; + } + if (isCurrentCacheToken(cacheToken)) { + extensionCache.set(extensionPath, factory); + } + return factory; } /** @@ -370,11 +415,12 @@ async function loadExtension( cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, + cacheToken?: ExtensionCacheToken, ): Promise<{ extension: Extension | null; error: string | null }> { const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true }); try { - const factory = await loadExtensionModule(resolvedPath); + const factory = await loadExtensionModule(resolvedPath, cacheToken); if (!factory) { return { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` }; } @@ -410,20 +456,28 @@ export async function loadExtensionFromFactory( /** * Load extensions from paths. */ -export async function loadExtensions( +async function loadExtensionsInternal( paths: string[], cwd: string, eventBus?: EventBus, runtime?: ExtensionRuntime, + useCache = false, ): Promise { const extensions: Extension[] = []; const errors: Array<{ path: string; error: string }> = []; - const resolvedCwd = resolvePath(cwd); + const cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined; + const resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd); const resolvedEventBus = eventBus ?? createEventBus(); const resolvedRuntime = runtime ?? createExtensionRuntime(); for (const extPath of paths) { - const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, resolvedRuntime); + const { extension, error } = await loadExtension( + extPath, + resolvedCwd, + resolvedEventBus, + resolvedRuntime, + cacheToken, + ); if (error) { errors.push({ path: extPath, error }); @@ -442,6 +496,24 @@ export async function loadExtensions( }; } +export async function loadExtensions( + paths: string[], + cwd: string, + eventBus?: EventBus, + runtime?: ExtensionRuntime, +): Promise { + return loadExtensionsInternal(paths, cwd, eventBus, runtime); +} + +export async function loadExtensionsCached( + paths: string[], + cwd: string, + eventBus?: EventBus, + runtime?: ExtensionRuntime, +): Promise { + return loadExtensionsInternal(paths, cwd, eventBus, runtime, true); +} + interface PiManifest { extensions?: string[]; themes?: string[]; diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index b35787af..18486ead 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -9,7 +9,12 @@ export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts"; import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts"; import { createEventBus, type EventBus } from "./event-bus.ts"; -import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts"; +import { + clearExtensionCache, + createExtensionRuntime, + loadExtensionFromFactory, + loadExtensionsCached, +} from "./extensions/loader.ts"; import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts"; import { DefaultPackageManager, type PathMetadata, type ResolvedResource } from "./package-manager.ts"; import type { PromptTemplate } from "./prompt-templates.ts"; @@ -206,6 +211,7 @@ export class DefaultResourceLoader implements ResourceLoader { private extensionThemeSourceInfos: Map; private lastPromptPaths: string[]; private lastThemePaths: string[]; + private loaded: boolean; constructor(options: DefaultResourceLoaderOptions) { this.cwd = resolvePath(options.cwd); @@ -252,6 +258,7 @@ export class DefaultResourceLoader implements ResourceLoader { this.extensionThemeSourceInfos = new Map(); this.lastPromptPaths = []; this.lastThemePaths = []; + this.loaded = false; } getExtensions(): LoadExtensionsResult { @@ -331,6 +338,10 @@ export class DefaultResourceLoader implements ResourceLoader { } async reload(options?: ResourceLoaderReloadOptions): Promise { + if (this.loaded) { + clearExtensionCache(); + } + let preTrustExtensions: LoadExtensionsResult | undefined; if (options?.resolveProjectTrust) { preTrustExtensions = await this.loadProjectTrustExtensions(); @@ -475,6 +486,7 @@ export class DefaultResourceLoader implements ResourceLoader { this.appendSystemPrompt = this.appendSystemPromptOverride ? this.appendSystemPromptOverride(baseAppend) : baseAppend; + this.loaded = true; } private async loadCurrentExtensionSet(options: { includeInlineFactories: boolean }): Promise { @@ -487,7 +499,7 @@ export class DefaultResourceLoader implements ResourceLoader { const extensionPaths = this.noExtensions ? cliEnabledExtensions : this.mergePaths(cliEnabledExtensions, enabledExtensions); - const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); if (!options.includeInlineFactories) { return extensionsResult; } @@ -507,7 +519,7 @@ export class DefaultResourceLoader implements ResourceLoader { preTrustExtensions: LoadExtensionsResult | undefined, ): Promise { if (!preTrustExtensions) { - const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const extensionsResult = await loadExtensionsCached(extensionPaths, this.cwd, this.eventBus); const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime); extensionsResult.extensions.push(...inlineExtensions.extensions); extensionsResult.errors.push(...inlineExtensions.errors); @@ -527,7 +539,7 @@ export class DefaultResourceLoader implements ResourceLoader { const resolvedPath = this.resolveExtensionLoadPath(path); return !preloadedByPath.has(resolvedPath) && !failedPreloadPaths.has(resolvedPath); }); - const remainingExtensions = await loadExtensions( + const remainingExtensions = await loadExtensionsCached( remainingPaths, this.cwd, this.eventBus, diff --git a/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts b/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts new file mode 100644 index 00000000..ced01224 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/extension-factory-cache.test.ts @@ -0,0 +1,131 @@ +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { clearExtensionCache, loadExtensions, loadExtensionsCached } from "../../../src/core/extensions/loader.ts"; +import { DefaultResourceLoader } from "../../../src/core/resource-loader.ts"; + +interface TestState { + moduleLoads?: number; + factoryRuns?: number; +} + +function state(): TestState { + const global = globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }; + if (!global.__extensionFactoryCacheTest) { + global.__extensionFactoryCacheTest = {}; + } + return global.__extensionFactoryCacheTest; +} + +function resetState(): void { + delete (globalThis as typeof globalThis & { __extensionFactoryCacheTest?: TestState }).__extensionFactoryCacheTest; +} + +function writeCountingExtension(filePath: string): void { + writeFileSync( + filePath, + ` +const state = (globalThis.__extensionFactoryCacheTest ??= {}); +state.moduleLoads = (state.moduleLoads ?? 0) + 1; + +export default function () { + state.factoryRuns = (state.factoryRuns ?? 0) + 1; +} +`, + "utf-8", + ); +} + +describe("extension factory cache", () => { + const roots: string[] = []; + + function fixture(name: string) { + const root = join(tmpdir(), `pi-extension-cache-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`); + const cwd = join(root, "project"); + const agentDir = join(root, "agent"); + mkdirSync(cwd, { recursive: true }); + mkdirSync(agentDir, { recursive: true }); + roots.push(root); + return { root, cwd, agentDir }; + } + + beforeEach(() => { + resetState(); + clearExtensionCache(); + }); + + afterEach(() => { + while (roots.length > 0) { + const root = roots.pop(); + if (root && existsSync(root)) { + rmSync(root, { recursive: true, force: true }); + } + } + resetState(); + clearExtensionCache(); + }); + + it("caches extension modules for cached same-cwd loads but reruns factories", async () => { + const { root, cwd } = fixture("same-cwd"); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + const first = await loadExtensionsCached([extensionPath], cwd); + const second = await loadExtensionsCached([extensionPath], cwd); + + expect(state().moduleLoads).toBe(1); + expect(state().factoryRuns).toBe(2); + expect(first.extensions[0]).not.toBe(second.extensions[0]); + expect(first.runtime).not.toBe(second.runtime); + }); + + it("does not cache direct loadExtensions calls", async () => { + const { root, cwd } = fixture("direct"); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + await loadExtensions([extensionPath], cwd); + await loadExtensions([extensionPath], cwd); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(2); + }); + + it("clears the cache on resource loader reload", async () => { + const { cwd, agentDir } = fixture("reload"); + const extensionDir = join(agentDir, "extensions"); + mkdirSync(extensionDir, { recursive: true }); + writeCountingExtension(join(extensionDir, "counting.ts")); + const loader = new DefaultResourceLoader({ + cwd, + agentDir, + noSkills: true, + noPromptTemplates: true, + noThemes: true, + }); + + await loader.reload(); + await loader.reload(); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(2); + }); + + it("keeps the cache scoped to one cwd", async () => { + const { root } = fixture("cross-cwd"); + const firstCwd = join(root, "first"); + const secondCwd = join(root, "second"); + mkdirSync(firstCwd, { recursive: true }); + mkdirSync(secondCwd, { recursive: true }); + const extensionPath = join(root, "counting.ts"); + writeCountingExtension(extensionPath); + + await loadExtensionsCached([extensionPath], firstCwd); + await loadExtensionsCached([extensionPath], secondCwd); + await loadExtensionsCached([extensionPath], secondCwd); + + expect(state().moduleLoads).toBe(2); + expect(state().factoryRuns).toBe(3); + }); +}); From 500b568b04cc54109480920ab391e76e525339c0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 21:31:31 +0200 Subject: [PATCH 122/141] fix(ai): use OpenAI endpoint for Fireworks GLM-5.2 closes #5923 --- packages/ai/CHANGELOG.md | 1 + packages/ai/scripts/generate-models.ts | 9 ++++++++- packages/ai/src/models.generated.ts | 9 +++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 567a2273..90a1b3bb 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed Fireworks GLM-5.2 metadata to use the OpenAI-compatible Chat Completions endpoint with `reasoning_effort` support ([#5923](https://github.com/earendil-works/pi/issues/5923)). - Fixed OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)). - Fixed GitHub Copilot OAuth model availability to use the authenticated account's model picker catalog ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index ad9ca95b..3e85db82 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -377,6 +377,9 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "openrouter" && model.id === "z-ai/glm-5.2") { mergeThinkingLevelMap(model, { xhigh: "xhigh" }); } + if (model.provider === "fireworks" && model.id === "accounts/fireworks/models/glm-5p2") { + mergeThinkingLevelMap(model, { off: "none", minimal: null, low: "high", medium: "high", xhigh: "max" }); + } if (model.provider === "opencode-go" && model.id === "kimi-k2.6") { // OpenCode Go exposes Kimi K2.6 thinking as on/off, not distinct effort tiers. mergeThinkingLevelMap(model, { minimal: null, low: null, medium: null }); @@ -1514,7 +1517,11 @@ async function generateModels() { candidate.cost.output = 1.9; candidate.cost.cacheRead = 0.119; } - + if (candidate.provider === "fireworks" && candidate.id === "accounts/fireworks/models/glm-5p2") { + candidate.api = "openai-completions"; + candidate.baseUrl = "https://api.fireworks.ai/inference/v1"; + candidate.compat = { supportsStore: false, supportsDeveloperRole: false }; + } } diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 497f766a..32894f60 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3930,11 +3930,12 @@ export const MODELS = { "accounts/fireworks/models/glm-5p2": { id: "accounts/fireworks/models/glm-5p2", name: "GLM 5.2", - api: "anthropic-messages", + api: "openai-completions", provider: "fireworks", - baseUrl: "https://api.fireworks.ai/inference", - compat: {"sendSessionAffinityHeaders":true,"supportsEagerToolInputStreaming":false,"supportsCacheControlOnTools":false,"supportsLongCacheRetention":false}, + baseUrl: "https://api.fireworks.ai/inference/v1", + compat: {"supportsStore":false,"supportsDeveloperRole":false}, reasoning: true, + thinkingLevelMap: {"off":"none","minimal":null,"low":"high","medium":"high","xhigh":"max"}, input: ["text"], cost: { input: 1.4, @@ -3944,7 +3945,7 @@ export const MODELS = { }, contextWindow: 1048576, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "accounts/fireworks/models/gpt-oss-120b": { id: "accounts/fireworks/models/gpt-oss-120b", name: "GPT OSS 120B", From 350ac3f3440367368510151c60f4cbb18ff6a7fe Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 21:48:37 +0200 Subject: [PATCH 123/141] fix: remove inprogress from auto-closed issues --- .github/workflows/issue-triage-labels.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index cf17c39e..ebe5500c 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -18,6 +18,7 @@ jobs: const NO_ACTION_LABEL = 'no-action'; const LAST_READ_LABEL = 'last-read'; const TO_DISCUSS_LABEL = 'to-discuss'; + const INPROGRESS_LABEL = 'inprogress'; function issueHasLabel(issue, labelName) { return (issue.labels ?? []).some((label) => label.name === labelName); @@ -124,6 +125,8 @@ jobs: }); console.log(`Closed #${issue.number} as not planned`); + await removeLabelIfPresent(issue.number, issue, INPROGRESS_LABEL); + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, From 1aa79b9b29faacea6158ab958725032b16ef346f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:00:00 +0200 Subject: [PATCH 124/141] docs: update unreleased changelog audit --- packages/coding-agent/CHANGELOG.md | 8 ++++++++ packages/tui/CHANGELOG.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 31f4b5f0..5e0b229b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,14 +2,22 @@ ## [Unreleased] +### New Features + +- **Chat-template thinking compatibility** - OpenAI-compatible custom providers can map Pi thinking levels into `chat_template_kwargs`, enabling vLLM/Hugging Face chat-template models such as DeepSeek to use provider-native thinking controls. See [Custom Provider API Types](docs/custom-provider.md#api-types) and [OpenAI Compatibility](docs/models.md#openai-compatibility). +- **GLM-5.2 provider improvements** - GLM-5.2 now has corrected Fireworks OpenAI-compatible routing and OpenRouter `xhigh` thinking support, improving `/model` behavior and high-effort reasoning for GLM-5.2 users. See [Model Options](docs/usage.md#model-options). + ### Added - Added inherited configurable `chat-template` thinking support for OpenAI-compatible providers that use `chat_template_kwargs`, such as DeepSeek models behind vLLM ([#5673](https://github.com/earendil-works/pi/issues/5673)). ### Fixed +- Fixed inherited Fireworks GLM-5.2 metadata to use the OpenAI-compatible Chat Completions endpoint with `reasoning_effort` support ([#5923](https://github.com/earendil-works/pi/issues/5923)). - Fixed same-directory session switches to reuse imported extension modules while preserving fresh extension instances and lifecycle events ([#5905](https://github.com/earendil-works/pi/issues/5905)). - Fixed deep session branches taking quadratic time to build context or branch paths ([#5909](https://github.com/earendil-works/pi/issues/5909)). +- Fixed inherited OpenRouter GLM-5.2 metadata to expose `xhigh` reasoning and send OpenRouter's native `xhigh` effort ([#5770](https://github.com/earendil-works/pi/issues/5770)). +- Fixed inherited Markdown streaming code fence rendering so partial closing fences no longer make code blocks shrink or flicker while content streams ([#5846](https://github.com/earendil-works/pi/pull/5846) by [@xl0](https://github.com/xl0)). - Fixed fuzzy `edit` matches to preserve untouched line blocks instead of rewriting the whole file through normalized content ([#5899](https://github.com/earendil-works/pi/issues/5899)). - Fixed bash commands through legacy WSL `bash.exe` to pass scripts over stdin so shell variables expand in the target bash ([#5893](https://github.com/earendil-works/pi/issues/5893)). - Fixed `/model` to hide GitHub Copilot models that are unavailable to the authenticated account ([#5897](https://github.com/earendil-works/pi/issues/5897)). diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 2e9f6d31..065f1321 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Markdown streaming code fence rendering so partial closing fences no longer make code blocks shrink or flicker while content streams ([#5846](https://github.com/earendil-works/pi/pull/5846) by [@xl0](https://github.com/xl0)). + ## [0.79.8] - 2026-06-19 ## [0.79.7] - 2026-06-18 From 615bf2f87444c0f48756d487323519349304906c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:15:37 +0200 Subject: [PATCH 125/141] Release v0.79.9 --- package-lock.json | 26 ++++++------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 38 +++++++++---------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++++++------ packages/coding-agent/package.json | 8 ++-- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 69 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f9d4e66..9bec652f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.9", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.8" + "version": "0.79.9" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.8", + "version": "1.9.9", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 18d0e561..6c98303b 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### Fixed diff --git a/packages/agent/package.json b/packages/agent/package.json index b3aa9bae..75a73cb8 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.8", + "version": "0.79.9", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.9", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 90a1b3bb..2c7ff556 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 830df19e..77272079 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.8", + "version": "0.79.9", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 32894f60..afdea285 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -9657,7 +9657,7 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0.375, }, - contextWindow: 65536, + contextWindow: 131072, maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { @@ -9845,7 +9845,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 8192, + maxTokens: 32768, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -9947,8 +9947,8 @@ export const MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 80000, + contextWindow: 262144, + maxTokens: 144000, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-1.2b-thinking:free": { id: "liquid/lfm-2.5-1.2b-thinking:free", @@ -10169,7 +10169,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 512000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10505,7 +10505,7 @@ export const MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0, + cacheRead: 0.6, cacheWrite: 0, }, contextWindow: 262144, @@ -10884,7 +10884,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1047576, - maxTokens: 32768, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-4.1-nano": { id: "openai/gpt-4.1-nano", @@ -10914,7 +10914,7 @@ export const MODELS = { cost: { input: 2.5, output: 10, - cacheRead: 0, + cacheRead: 1.25, cacheWrite: 0, }, contextWindow: 128000, @@ -11050,11 +11050,11 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.025, + cacheRead: 0.03, cacheWrite: 0, }, contextWindow: 400000, - maxTokens: 128000, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-5-nano": { id: "openai/gpt-5-nano", @@ -11118,11 +11118,11 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.13, + cacheRead: 0.125, cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 32000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "openai/gpt-5.1-codex": { id: "openai/gpt-5.1-codex", @@ -11209,7 +11209,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 16384, + maxTokens: 32000, } satisfies Model<"openai-completions">, "openai/gpt-5.2-codex": { id: "openai/gpt-5.2-codex", @@ -12422,11 +12422,11 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0, + cacheRead: 0.05, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 81920, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12459,8 +12459,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 262144, + contextWindow: 256000, + maxTokens: 32768, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -13034,11 +13034,11 @@ export const MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.49, + cacheRead: 0.182, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 65535, + maxTokens: 4096, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 5e0b229b..48f64d23 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 436f151f..9a131279 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index aa509051..8b4182e7 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 51de298b..779d519c 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 3f126430..01ee2f61 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 1d42d841..67a9ac1e 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 2a587f88..01aad382 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.8", + "version": "1.9.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.8", + "version": "1.9.9", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index c9d4aa3f..b8dc74ab 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.8", + "version": "1.9.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 2dc3506b..89961b12 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.8", + "version": "0.79.9", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 1c409ca4..3ed465d2 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.8", + "version": "0.79.9", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 13ff9161..c311b556 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.8.tgz", + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.9.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.9", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.9.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 4aa30dd2..490a7751 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.8", + "version": "0.79.9", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 065f1321..e7cf9b0f 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.9] - 2026-06-20 ### Fixed diff --git a/packages/tui/package.json b/packages/tui/package.json index 36f002a7..d9d53e0e 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.8", + "version": "0.79.9", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From b4f31408b67bde605e9c2fe0eb365ef2b73690f9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:15:40 +0200 Subject: [PATCH 126/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 6c98303b..ff48ad2c 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### Fixed diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2c7ff556..586260cf 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### Added diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 48f64d23..ba9b1ae7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index e7cf9b0f..96ff6d5c 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.9] - 2026-06-20 ### Fixed From d93b92baca8fe2f9eac048185669a403d4e5ba8b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 20 Jun 2026 22:23:25 +0200 Subject: [PATCH 127/141] fix(coding-agent): show changelog URL in update notice --- packages/coding-agent/src/modes/interactive/interactive-mode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 962c414f..613d588c 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3714,7 +3714,7 @@ export class InteractiveMode { const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action; const changelogUrl = "https://pi.dev/changelog"; const changelogLink = getCapabilities().hyperlinks - ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl) + ? hyperlink(theme.fg("accent", changelogUrl), changelogUrl) : theme.fg("accent", changelogUrl); const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink; const note = release.note?.trim(); From bc0db643502ba0bf1b227a97d9d5885cefc2b909 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 21 Jun 2026 14:46:07 +0200 Subject: [PATCH 128/141] fix(coding-agent): install checked pi update version --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/config.ts | 42 +++++++++----- .../coding-agent/src/package-manager-cli.ts | 55 ++++++++++++------- packages/coding-agent/test/config.test.ts | 23 ++++++++ .../test/package-command-paths.test.ts | 23 +++++--- 5 files changed, 105 insertions(+), 42 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ba9b1ae7..92bdd6ba 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. + ## [0.79.9] - 2026-06-20 ### New Features diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 75b2efaf..38049f05 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -38,6 +38,18 @@ export interface SelfUpdateCommand extends SelfUpdateCommandStep { steps?: SelfUpdateCommandStep[]; } +export type SelfUpdatePackageTarget = string | { packageName: string; installSpec?: string }; + +function normalizeSelfUpdatePackageTarget(target: SelfUpdatePackageTarget): { + packageName: string; + installSpec: string; +} { + if (typeof target === "string") { + return { packageName: target, installSpec: target }; + } + return { packageName: target.packageName, installSpec: target.installSpec ?? target.packageName }; +} + function makeSelfUpdateCommand( installStep: SelfUpdateCommandStep, uninstallStep?: SelfUpdateCommandStep, @@ -103,9 +115,10 @@ function getInferredNpmInstall(): { root: string; prefix: string } | undefined { function getSelfUpdateCommandForMethod( method: InstallMethod, installedPackageName: string, - updatePackageName = installedPackageName, + updatePackageTarget: SelfUpdatePackageTarget = installedPackageName, npmCommand?: string[], ): SelfUpdateCommand | undefined { + const target = normalizeSelfUpdatePackageTarget(updatePackageTarget); switch (method) { case "bun-binary": return undefined; @@ -123,17 +136,17 @@ function getSelfUpdateCommandForMethod( "--ignore-scripts", "--config.minimumReleaseAge=0", ...binDirArgs, - updatePackageName, + target.installSpec, ]), - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]), ); } case "yarn": return makeSelfUpdateCommand( - makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), - updatePackageName === installedPackageName + makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", target.installSpec]), + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), ); @@ -144,9 +157,9 @@ function getSelfUpdateCommandForMethod( "-g", "--ignore-scripts", "--minimum-release-age=0", - updatePackageName, + target.installSpec, ]), - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), ); @@ -160,10 +173,10 @@ function getSelfUpdateCommandForMethod( "-g", "--ignore-scripts", "--min-release-age=0", - updatePackageName, + target.installSpec, ]); const uninstallStep = - updatePackageName === installedPackageName + target.packageName === installedPackageName ? undefined : makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]); return makeSelfUpdateCommand(installStep, uninstallStep); @@ -302,10 +315,10 @@ function isManagedByGlobalPackageManager(method: InstallMethod, packageName: str export function getSelfUpdateCommand( packageName: string, npmCommand?: string[], - updatePackageName = packageName, + updatePackageTarget: SelfUpdatePackageTarget = packageName, ): SelfUpdateCommand | undefined { const method = detectInstallMethod(); - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageTarget, npmCommand); if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) { return undefined; } @@ -315,20 +328,21 @@ export function getSelfUpdateCommand( export function getSelfUpdateUnavailableInstruction( packageName: string, npmCommand?: string[], - updatePackageName = packageName, + updatePackageTarget: SelfUpdatePackageTarget = packageName, ): string { const method = detectInstallMethod(); + const target = normalizeSelfUpdatePackageTarget(updatePackageTarget); if (method === "bun-binary") { return `Download from: https://github.com/earendil-works/pi-mono/releases/latest`; } - const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand); if (command) { if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) { return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; } return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; } - return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`; + return `Update ${target.installSpec} using the package manager, wrapper, or source checkout that provides this installation.`; } export function getUpdateInstruction(packageName: string): string { diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index d47df930..32980851 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -12,6 +12,7 @@ import { getSelfUpdateUnavailableInstruction, PACKAGE_NAME, type SelfUpdateCommand, + type SelfUpdatePackageTarget, VERSION, } from "./config.ts"; import type { ExtensionFactory } from "./core/extensions/types.ts"; @@ -353,9 +354,12 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean { return target.type === "all" || target.type === "extensions"; } -function printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void { +function printSelfUpdateUnavailable( + npmCommand?: string[], + updatePackageTarget: SelfUpdatePackageTarget = PACKAGE_NAME, +): void { console.error(`error: ${APP_NAME} cannot self-update this installation.`); - console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName)); + console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageTarget)); const entrypoint = process.argv[1]; if (entrypoint) { @@ -390,27 +394,38 @@ function printSelfUpdateNote(note: string): void { interface SelfUpdatePlan { packageName: string; + installSpec: string; + version: string; shouldRun: boolean; note?: string; } async function getSelfUpdatePlan(force: boolean): Promise { - if (force) { - return { packageName: PACKAGE_NAME, shouldRun: true }; + let latestRelease: Awaited>; + try { + latestRelease = await getLatestPiRelease(VERSION); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Could not determine latest ${APP_NAME} version: ${message}`); + } + if (!latestRelease) { + throw new Error(`Could not determine latest ${APP_NAME} version.`); } - try { - const latestRelease = await getLatestPiRelease(VERSION); - const packageName = latestRelease?.packageName ?? PACKAGE_NAME; - if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { - return { packageName, shouldRun: true, ...(latestRelease?.note ? { note: latestRelease.note } : {}) }; - } - } catch { - return { packageName: PACKAGE_NAME, shouldRun: true }; + const packageName = latestRelease.packageName ?? PACKAGE_NAME; + const installSpec = `${packageName}@${latestRelease.version}`; + if (force || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) { + return { + packageName, + installSpec, + version: latestRelease.version, + ...(latestRelease.note ? { note: latestRelease.note } : {}), + shouldRun: true, + }; } console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`)); - return { packageName: PACKAGE_NAME, shouldRun: false }; + return { packageName, installSpec, version: latestRelease.version, shouldRun: false }; } async function runSelfUpdate(command: SelfUpdateCommand): Promise { @@ -717,13 +732,13 @@ export async function handlePackageCommand( process.exitCode = 1; return true; } - const selfUpdateCommand = getSelfUpdateCommand( - PACKAGE_NAME, - selfUpdateNpmCommand, - selfUpdatePlan.packageName, - ); + const selfUpdateTarget = { + packageName: selfUpdatePlan.packageName, + installSpec: selfUpdatePlan.installSpec, + }; + const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, selfUpdateNpmCommand, selfUpdateTarget); if (!selfUpdateCommand) { - printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName); + printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdateTarget); process.exitCode = 1; return true; } @@ -742,7 +757,7 @@ export async function handlePackageCommand( process.exitCode = 1; return true; } - console.log(chalk.green(`Updated ${APP_NAME}`)); + console.log(chalk.green(`Updated ${APP_NAME} from ${VERSION} to ${selfUpdatePlan.version}`)); } return true; } diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index cbfa044e..49448cf1 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -188,6 +188,29 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates exact npm versions without uninstalling the current package", () => { + const { prefix } = createNpmPrefixInstall(); + + const command = getSelfUpdateCommand("@earendil-works/pi-coding-agent", undefined, { + packageName: "@earendil-works/pi-coding-agent", + installSpec: "@earendil-works/pi-coding-agent@1.2.3", + }); + + expect(command).toEqual({ + command: "npm", + args: [ + "--prefix", + prefix, + "install", + "-g", + "--ignore-scripts", + "--min-release-age=0", + "@earendil-works/pi-coding-agent@1.2.3", + ], + display: `npm --prefix ${prefix} install -g --ignore-scripts --min-release-age=0 @earendil-works/pi-coding-agent@1.2.3`, + }); + }); + test("self-updates renamed packages from the current install prefix", () => { const { prefix } = createNpmPrefixInstall(); diff --git a/packages/coding-agent/test/package-command-paths.test.ts b/packages/coding-agent/test/package-command-paths.test.ts index df87cf34..6df65ded 100644 --- a/packages/coding-agent/test/package-command-paths.test.ts +++ b/packages/coding-agent/test/package-command-paths.test.ts @@ -374,7 +374,7 @@ describe("package commands", () => { } }); - it("uses global npmCommand and current package name for forced self updates without checking the api", async () => { + it("uses the update check version for forced self updates even when current", async () => { const globalPrefix = join(tempDir, "global-prefix"); const projectPrefix = join(tempDir, "project-prefix"); const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@earendil-works", "pi-coding-agent"); @@ -402,7 +402,7 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); - const fetchMock = vi.fn(); + const fetchMock = vi.fn(async () => Response.json({ version: VERSION })); vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -413,11 +413,14 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); - expect(fetchMock).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledOnce(); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; expect(recordedArgs).toContain(globalPrefix); - expect(recordedArgs).toContain(PACKAGE_NAME); + expect(recordedArgs).toContain(`${PACKAGE_NAME}@${VERSION}`); + expect(recordedArgs).not.toContain(PACKAGE_NAME); expect(recordedArgs).not.toContain(projectPrefix); + expect(stdout).toContain(`Updated pi from ${VERSION} to ${VERSION}`); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -446,7 +449,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); value: join(selfPackageDir, "dist", "cli.js"), configurable: true, }); - const fetchMock = vi.fn(async () => Response.json({ version: getNewerPatchVersion() })); + const targetVersion = getNewerPatchVersion(); + const fetchMock = vi.fn(async () => Response.json({ version: targetVersion })); vi.stubGlobal("fetch", fetchMock); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); @@ -458,8 +462,11 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args)); expect(process.exitCode).toBeUndefined(); expect(errorSpy).not.toHaveBeenCalled(); expect(fetchMock).toHaveBeenCalledOnce(); + const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n"); const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[]; - expect(recordedArgs).toContain(PACKAGE_NAME); + expect(recordedArgs).toContain(`${PACKAGE_NAME}@${targetVersion}`); + expect(recordedArgs).not.toContain(PACKAGE_NAME); + expect(stdout).toContain(`Updated pi from ${VERSION} to ${targetVersion}`); } finally { logSpy.mockRestore(); errorSpy.mockRestore(); @@ -509,7 +516,7 @@ else { const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), - expect.arrayContaining(["install", "-g", activePackageName]), + expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]), ]); } finally { logSpy.mockRestore(); @@ -565,7 +572,7 @@ if(args.includes("install")) process.exit(23); const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][]; expect(recordedCalls).toEqual([ expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]), - expect.arrayContaining(["install", "-g", activePackageName]), + expect.arrayContaining(["install", "-g", `${activePackageName}@0.73.0`]), ]); } finally { logSpy.mockRestore(); From 542683b29ab2865976dddb006b4d70cffe315e25 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 21 Jun 2026 22:10:17 +0200 Subject: [PATCH 129/141] fix(coding-agent): fix plan-mode example closes #5940 --- packages/coding-agent/CHANGELOG.md | 1 + .../examples/extensions/plan-mode/README.md | 5 +- .../examples/extensions/plan-mode/index.ts | 128 ++++++++++---- .../test/plan-mode-extension.test.ts | 167 ++++++++++++++++++ 4 files changed, 260 insertions(+), 41 deletions(-) create mode 100644 packages/coding-agent/test/plan-mode-extension.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 92bdd6ba..20d13deb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. ## [0.79.9] - 2026-06-20 diff --git a/packages/coding-agent/examples/extensions/plan-mode/README.md b/packages/coding-agent/examples/extensions/plan-mode/README.md index 549e3473..2568a684 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/README.md +++ b/packages/coding-agent/examples/extensions/plan-mode/README.md @@ -4,7 +4,7 @@ Read-only exploration mode for safe code analysis. ## Features -- **Read-only tools**: Restricts available tools to read, bash, grep, find, ls, question +- **Built-in write tools disabled**: Disables edit/write while preserving other active tools - **Bash allowlist**: Only read-only bash commands are allowed - **Plan extraction**: Extracts numbered steps from `Plan:` sections - **Progress tracking**: Widget shows completion status during execution @@ -37,7 +37,8 @@ Plan: ## How It Works ### Plan Mode (Read-Only) -- Only read-only tools available +- Built-in edit/write tools disabled +- Other active tools remain available - Bash commands filtered through allowlist - Agent creates a plan without making changes diff --git a/packages/coding-agent/examples/extensions/plan-mode/index.ts b/packages/coding-agent/examples/extensions/plan-mode/index.ts index 40db408c..737ce56a 100644 --- a/packages/coding-agent/examples/extensions/plan-mode/index.ts +++ b/packages/coding-agent/examples/extensions/plan-mode/index.ts @@ -2,7 +2,7 @@ * Plan Mode Extension * * Read-only exploration mode for safe code analysis. - * When enabled, only read-only tools are available. + * When enabled, built-in write tools are disabled. * * Features: * - /plan command or Ctrl+Alt+P to toggle @@ -21,6 +21,15 @@ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } fr // Tools const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "questionnaire"]; const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"]; +const PLAN_MODE_DISABLED_TOOLS = new Set(["edit", "write"]); +const PLAN_MANAGED_TOOLS = new Set([...PLAN_MODE_TOOLS, ...NORMAL_MODE_TOOLS]); + +interface PlanModeState { + enabled: boolean; + todos?: TodoItem[]; + executing?: boolean; + toolsBeforePlanMode?: string[]; +} // Type guard for assistant messages function isAssistantMessage(m: AgentMessage): m is AssistantMessage { @@ -39,6 +48,7 @@ export default function planModeExtension(pi: ExtensionAPI): void { let planModeEnabled = false; let executionMode = false; let todoItems: TodoItem[] = []; + let toolsBeforePlanMode: string[] | undefined; pi.registerFlag("plan", { description: "Start in plan mode (read-only exploration)", @@ -73,19 +83,34 @@ export default function planModeExtension(pi: ExtensionAPI): void { } } - function togglePlanMode(ctx: ExtensionContext): void { - planModeEnabled = !planModeEnabled; - executionMode = false; - todoItems = []; + function uniqueToolNames(toolNames: string[]): string[] { + return [...new Set(toolNames)]; + } - if (planModeEnabled) { - pi.setActiveTools(PLAN_MODE_TOOLS); - ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`); - } else { - pi.setActiveTools(NORMAL_MODE_TOOLS); - ctx.ui.notify("Plan mode disabled. Full access restored."); + function getPlanModeTools(activeToolNames: string[]): string[] { + return uniqueToolNames([ + ...activeToolNames.filter((name) => !PLAN_MODE_DISABLED_TOOLS.has(name)), + ...PLAN_MODE_TOOLS, + ]); + } + + function getNormalModeTools(activeToolNames: string[]): string[] { + return uniqueToolNames([ + ...NORMAL_MODE_TOOLS, + ...activeToolNames.filter((name) => !PLAN_MANAGED_TOOLS.has(name)), + ]); + } + + function enablePlanModeTools(): void { + if (toolsBeforePlanMode === undefined) { + toolsBeforePlanMode = pi.getActiveTools(); } - updateStatus(ctx); + pi.setActiveTools(getPlanModeTools(toolsBeforePlanMode)); + } + + function restoreNormalModeTools(): void { + pi.setActiveTools(toolsBeforePlanMode ?? getNormalModeTools(pi.getActiveTools())); + toolsBeforePlanMode = undefined; } function persistState(): void { @@ -93,9 +118,26 @@ export default function planModeExtension(pi: ExtensionAPI): void { enabled: planModeEnabled, todos: todoItems, executing: executionMode, + toolsBeforePlanMode, }); } + function togglePlanMode(ctx: ExtensionContext): void { + planModeEnabled = !planModeEnabled; + executionMode = false; + todoItems = []; + + if (planModeEnabled) { + enablePlanModeTools(); + ctx.ui.notify("Plan mode enabled. Built-in write tools disabled."); + } else { + restoreNormalModeTools(); + ctx.ui.notify("Plan mode disabled. Full access restored."); + } + updateStatus(ctx); + persistState(); + } + pi.registerCommand("plan", { description: "Toggle plan mode (read-only exploration)", handler: async (_args, ctx) => togglePlanMode(ctx), @@ -165,8 +207,8 @@ export default function planModeExtension(pi: ExtensionAPI): void { You are in plan mode - a read-only exploration mode for safe code analysis. Restrictions: -- You can only use: read, bash, grep, find, ls, questionnaire -- You CANNOT use: edit, write (file modifications are disabled) +- Built-in edit and write tools are disabled +- Other currently active tools remain available - Bash is restricted to an allowlist of read-only commands Ask clarifying questions using the questionnaire tool. @@ -228,7 +270,6 @@ After completing a step, include a [DONE:n] tag in your response.`, ); executionMode = false; todoItems = []; - pi.setActiveTools(NORMAL_MODE_TOOLS); updateStatus(ctx); persistState(); // Save cleared state so resume doesn't restore old execution mode } @@ -246,43 +287,51 @@ After completing a step, include a [DONE:n] tag in your response.`, } } + if (todoItems.length === 0) return; + persistState(); + // Show plan steps and prompt for next action - if (todoItems.length > 0) { - const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n"); - pi.sendMessage( - { - customType: "plan-todo-list", - content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, - display: true, - }, - { triggerTurn: false }, - ); - } + const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join("\n"); + const planTodoListMessage = { + customType: "plan-todo-list", + content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`, + display: true, + }; const choice = await ctx.ui.select("Plan mode - what next?", [ - todoItems.length > 0 ? "Execute the plan (track progress)" : "Execute the plan", + "Execute the plan (track progress)", "Stay in plan mode", "Refine the plan", ]); if (choice?.startsWith("Execute")) { - planModeEnabled = false; - executionMode = todoItems.length > 0; - pi.setActiveTools(NORMAL_MODE_TOOLS); - updateStatus(ctx); + const firstTodoItem = todoItems[0]; + if (!firstTodoItem) return; - const execMessage = - todoItems.length > 0 - ? `Execute the plan. Start with: ${todoItems[0].text}` - : "Execute the plan you just created."; + planModeEnabled = false; + executionMode = true; + restoreNormalModeTools(); + updateStatus(ctx); + persistState(); + + const remainingList = todoItems.map((t) => `${t.step}. ${t.text}`).join("\n"); + const execMessage = `Execute the plan. + +Remaining steps: +${remainingList} + +Start with: ${firstTodoItem.text} +After completing a step, include a [DONE:n] tag in your response.`; + pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" }); pi.sendMessage( { customType: "plan-mode-execute", content: execMessage, display: true }, - { triggerTurn: true }, + { triggerTurn: true, deliverAs: "followUp" }, ); } else if (choice === "Refine the plan") { const refinement = await ctx.ui.editor("Refine the plan:", ""); if (refinement?.trim()) { - pi.sendUserMessage(refinement.trim()); + pi.sendMessage(planTodoListMessage, { deliverAs: "followUp" }); + pi.sendUserMessage(refinement.trim(), { deliverAs: "followUp" }); } } }); @@ -298,12 +347,13 @@ After completing a step, include a [DONE:n] tag in your response.`, // Restore persisted state const planModeEntry = entries .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode") - .pop() as { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } } | undefined; + .pop() as { data?: PlanModeState } | undefined; if (planModeEntry?.data) { planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled; todoItems = planModeEntry.data.todos ?? todoItems; executionMode = planModeEntry.data.executing ?? executionMode; + toolsBeforePlanMode = planModeEntry.data.toolsBeforePlanMode ?? toolsBeforePlanMode; } // On resume: re-scan messages to rebuild completion state @@ -333,7 +383,7 @@ After completing a step, include a [DONE:n] tag in your response.`, } if (planModeEnabled) { - pi.setActiveTools(PLAN_MODE_TOOLS); + enablePlanModeTools(); } updateStatus(ctx); }); diff --git a/packages/coding-agent/test/plan-mode-extension.test.ts b/packages/coding-agent/test/plan-mode-extension.test.ts new file mode 100644 index 00000000..419a707e --- /dev/null +++ b/packages/coding-agent/test/plan-mode-extension.test.ts @@ -0,0 +1,167 @@ +import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it, vi } from "vitest"; +import planModeExtension from "../examples/extensions/plan-mode/index.ts"; +import type { ExtensionAPI, ExtensionContext } from "../src/core/extensions/index.ts"; + +type CommandHandler = (args: string, ctx: ExtensionContext) => Promise | void; +type AgentEndHandler = ( + event: { type: "agent_end"; messages: AgentMessage[] }, + ctx: ExtensionContext, +) => Promise | void; + +function createAssistantMessage(text: string): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text }], + api: "anthropic-messages", + provider: "anthropic", + model: "mock", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +function setup(options: { activeTools?: string[]; selectChoice?: string; editorText?: string } = {}) { + let activeTools = options.activeTools ?? ["read", "bash", "edit", "write"]; + const commands = new Map(); + let agentEndHandler: AgentEndHandler | undefined; + + const sendMessage = vi.fn(); + const sendUserMessage = vi.fn(); + const setActiveTools = vi.fn((toolNames) => { + activeTools = [...toolNames]; + }); + const appendEntry = vi.fn(); + + const api = { + registerFlag: vi.fn(), + registerCommand(name: string, command: { handler: CommandHandler }) { + commands.set(name, command.handler); + }, + registerShortcut: vi.fn(), + on(event: string, handler: unknown) { + if (event === "agent_end") agentEndHandler = handler as AgentEndHandler; + }, + getFlag: vi.fn(() => false), + getActiveTools: vi.fn(() => [...activeTools]), + setActiveTools, + sendMessage, + sendUserMessage, + appendEntry, + } as unknown as ExtensionAPI; + + planModeExtension(api); + + const ctx = { + hasUI: true, + ui: { + notify: vi.fn(), + select: vi.fn(async () => options.selectChoice), + editor: vi.fn(async () => options.editorText), + setStatus: vi.fn(), + setWidget: vi.fn(), + theme: { + fg: (_name: string, text: string) => text, + strikethrough: (text: string) => text, + }, + }, + sessionManager: { getEntries: () => [] }, + isIdle: () => false, + hasPendingMessages: () => false, + } as unknown as ExtensionContext; + + async function runCommand(name: string): Promise { + const command = commands.get(name); + if (!command) throw new Error(`Missing command: ${name}`); + await command("", ctx); + } + + async function triggerAgentEnd(text: string): Promise { + if (!agentEndHandler) throw new Error("Missing agent_end handler"); + await agentEndHandler({ type: "agent_end", messages: [createAssistantMessage(text)] }, ctx); + } + + return { + activeTools: () => activeTools, + appendEntry, + ctx, + runCommand, + sendMessage, + sendUserMessage, + setActiveTools, + triggerAgentEnd, + }; +} + +describe("plan-mode example extension", () => { + it("preserves custom active tools while toggling plan mode", async () => { + const { activeTools, runCommand, setActiveTools } = setup({ + activeTools: ["read", "bash", "edit", "write", "echo_tool"], + }); + + await runCommand("plan"); + + expect(activeTools()).toEqual(["read", "bash", "echo_tool", "grep", "find", "ls", "questionnaire"]); + expect(setActiveTools).toHaveBeenLastCalledWith([ + "read", + "bash", + "echo_tool", + "grep", + "find", + "ls", + "questionnaire", + ]); + + await runCommand("plan"); + + expect(activeTools()).toEqual(["read", "bash", "edit", "write", "echo_tool"]); + expect(setActiveTools).toHaveBeenLastCalledWith(["read", "bash", "edit", "write", "echo_tool"]); + }); + + it("does not prompt when the assistant response contains no plan", async () => { + const { ctx, runCommand, sendMessage, triggerAgentEnd } = setup(); + + await runCommand("plan"); + await triggerAgentEnd("This file defines the command-line argument parser."); + + expect(ctx.ui.select).not.toHaveBeenCalled(); + expect(sendMessage).not.toHaveBeenCalled(); + }); + + it("queues plan refinement as a follow-up user message", async () => { + const { runCommand, sendUserMessage, triggerAgentEnd } = setup({ + selectChoice: "Refine the plan", + editorText: "Add a regression test.", + }); + + await runCommand("plan"); + await triggerAgentEnd("Plan:\n1. Inspect the current implementation\n2. Add a regression test"); + + expect(sendUserMessage).toHaveBeenCalledWith("Add a regression test.", { deliverAs: "followUp" }); + }); + + it("queues plan execution as a follow-up custom message", async () => { + const { activeTools, runCommand, sendMessage, triggerAgentEnd } = setup({ + activeTools: ["read", "bash", "edit", "write", "echo_tool"], + selectChoice: "Execute the plan (track progress)", + }); + + await runCommand("plan"); + await triggerAgentEnd("Plan:\n1. Inspect the current implementation\n2. Add a regression test"); + + expect(activeTools()).toEqual(["read", "bash", "edit", "write", "echo_tool"]); + expect(sendMessage).toHaveBeenCalledWith(expect.objectContaining({ customType: "plan-mode-execute" }), { + triggerTurn: true, + deliverAs: "followUp", + }); + }); +}); From 2417adb46a6ddc7f15bd396a78b0ca9b83ac2d0d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 21 Jun 2026 23:17:13 +0200 Subject: [PATCH 130/141] fix(coding-agent): preserve startup extension UI closes #5943 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 3 +- .../src/modes/interactive/interactive-mode.ts | 52 ++- .../interactive-mode-clone-command.test.ts | 2 +- .../5943-session-start-notify.test.ts | 430 ++++++++++++++++++ 5 files changed, 463 insertions(+), 25 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 20d13deb..341921f2 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index f8af4320..a1033128 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -2443,7 +2443,7 @@ export class AgentSession { }); } - async reload(): Promise { + async reload(options?: { beforeSessionStart?: () => void | Promise }): Promise { const previousFlagValues = this._extensionRunner.getFlagValues(); await emitSessionShutdownEvent(this._extensionRunner, { type: "session_shutdown", reason: "reload" }); await this.settingsManager.reload(); @@ -2462,6 +2462,7 @@ export class AgentSession { this._extensionShutdownHandler || this._extensionErrorListener; if (hasBindings) { + await options?.beforeSessionStart?.(); await this._extensionRunner.emit({ type: "session_start", reason: "reload" }); await this.extendResourcesFromExtensions("reload"); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 613d588c..100feb6b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -395,7 +395,7 @@ export class InteractiveMode { this.resetExtensionUI(); }); this.runtimeHost.setRebindSession(async () => { - await this.rebindCurrentSession(); + await this.rebindCurrentSession({ renderBeforeBind: true }); }); this.version = VERSION; this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor()); @@ -1543,12 +1543,7 @@ export class InteractiveMode { } this.statusContainer.clear(); try { - const result = await this.runtimeHost.newSession(options); - if (!result.cancelled) { - this.renderCurrentSessionState(); - this.ui.requestRender(); - } - return result; + return await this.runtimeHost.newSession(options); } catch (error: unknown) { return this.handleFatalRuntimeError("Failed to create session", error); } @@ -1557,7 +1552,6 @@ export class InteractiveMode { try { const result = await this.runtimeHost.fork(entryId, options); if (!result.cancelled) { - this.renderCurrentSessionState(); this.editor.setText(result.selectedText ?? ""); this.showStatus("Forked to new session"); } @@ -1631,12 +1625,18 @@ export class InteractiveMode { } } - private async rebindCurrentSession(): Promise { + private async rebindCurrentSession(options: { renderBeforeBind?: boolean } = {}): Promise { this.unsubscribe?.(); this.unsubscribe = undefined; this.applyRuntimeSettings(); - await this.bindCurrentSessionExtensions(); - this.subscribeToAgent(); + if (options.renderBeforeBind) { + this.renderCurrentSessionState(); + this.subscribeToAgent(); + await this.bindCurrentSessionExtensions(); + } else { + await this.bindCurrentSessionExtensions(); + this.subscribeToAgent(); + } await this.updateAvailableProviderCount(); this.updateEditorBorderColor(); this.updateTerminalTitle(); @@ -4376,7 +4376,6 @@ export class InteractiveMode { return; } - this.renderCurrentSessionState(); this.editor.setText(result.selectedText ?? ""); done(); this.showStatus("Forked to new session"); @@ -4409,7 +4408,6 @@ export class InteractiveMode { return; } - this.renderCurrentSessionState(); this.editor.setText(""); this.showStatus("Cloned to new session"); } catch (error: unknown) { @@ -4601,7 +4599,6 @@ export class InteractiveMode { if (result.cancelled) { return result; } - this.renderCurrentSessionState(); this.showStatus("Resumed session"); return result; } catch (error: unknown) { @@ -4619,7 +4616,6 @@ export class InteractiveMode { if (result.cancelled) { return result; } - this.renderCurrentSessionState(); this.showStatus("Resumed session in current cwd"); return result; } @@ -5072,8 +5068,20 @@ export class InteractiveMode { this.ui.requestRender(); }; + let chatRestoredBeforeSessionStart = false; + let reloadBoxDismissed = false; + const restoreChatBeforeSessionStart = () => { + if (chatRestoredBeforeSessionStart) { + return; + } + this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); + this.rebuildChatFromMessages(); + chatRestoredBeforeSessionStart = true; + }; + try { - await this.session.reload(); + await this.session.reload({ beforeSessionStart: restoreChatBeforeSessionStart }); + restoreChatBeforeSessionStart(); configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs()); this.keybindings.reload(); const activeHeader = this.customHeader ?? this.builtInHeader; @@ -5081,7 +5089,6 @@ export class InteractiveMode { activeHeader.setExpanded(this.toolOutputExpanded); } setRegisteredThemes(this.session.resourceLoader.getThemes().themes); - this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); await this.themeController.applyFromSettings(); const editorPaddingX = this.settingsManager.getEditorPaddingX(); const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible(); @@ -5096,8 +5103,6 @@ export class InteractiveMode { this.setupAutocompleteProvider(); const runner = this.session.extensionRunner; this.setupExtensionShortcuts(runner); - this.rebuildChatFromMessages(); - dismissReloadBox(this.editor as Component); this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true, @@ -5112,8 +5117,12 @@ export class InteractiveMode { ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust" : "Reloaded keybindings, extensions, skills, prompts, themes", ); + dismissReloadBox(this.editor as Component); + reloadBoxDismissed = true; } catch (error) { - dismissReloadBox(previousEditor as Component); + if (!reloadBoxDismissed) { + dismissReloadBox(previousEditor as Component); + } this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`); } } @@ -5187,7 +5196,6 @@ export class InteractiveMode { this.showStatus("Import cancelled"); return; } - this.renderCurrentSessionState(); this.showStatus(`Session imported from: ${inputPath}`); } catch (error: unknown) { if (error instanceof MissingSessionCwdError) { @@ -5201,7 +5209,6 @@ export class InteractiveMode { this.showStatus("Import cancelled"); return; } - this.renderCurrentSessionState(); this.showStatus(`Session imported from: ${inputPath}`); return; } @@ -5540,7 +5547,6 @@ export class InteractiveMode { if (result.cancelled) { return; } - this.renderCurrentSessionState(); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1)); this.ui.requestRender(); diff --git a/packages/coding-agent/test/interactive-mode-clone-command.test.ts b/packages/coding-agent/test/interactive-mode-clone-command.test.ts index fea69c5d..69864146 100644 --- a/packages/coding-agent/test/interactive-mode-clone-command.test.ts +++ b/packages/coding-agent/test/interactive-mode-clone-command.test.ts @@ -41,7 +41,7 @@ describe("InteractiveMode /clone", () => { await interactiveModePrototype.handleCloneCommand.call(context); expect(fork).toHaveBeenCalledWith("leaf-123", { position: "at" }); - expect(renderCurrentSessionState).toHaveBeenCalled(); + expect(renderCurrentSessionState).not.toHaveBeenCalled(); expect(setText).toHaveBeenCalledWith(""); expect(showStatus).toHaveBeenCalledWith("Cloned to new session"); expect(showError).not.toHaveBeenCalled(); diff --git a/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts new file mode 100644 index 00000000..db7ce26d --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5943-session-start-notify.test.ts @@ -0,0 +1,430 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { describe, expect, it, vi } from "vitest"; +import type { AgentSessionEvent } from "../../../src/core/agent-session.ts"; +import type { ExtensionUIContext } from "../../../src/core/extensions/index.ts"; +import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts"; +import { initTheme, type Theme, theme } from "../../../src/modes/interactive/theme/theme.ts"; +import { createHarness } from "../harness.ts"; + +function createUiContext( + onNotify: (message: string, type: "info" | "warning" | "error" | undefined) => void, +): ExtensionUIContext { + return { + select: async () => undefined, + confirm: async () => false, + input: async () => undefined, + notify: onNotify, + onTerminalInput: () => () => {}, + setStatus: () => {}, + setWorkingMessage: () => {}, + setWorkingVisible: () => {}, + setWorkingIndicator: () => {}, + setHiddenThinkingLabel: () => {}, + setWidget: () => {}, + setFooter: () => {}, + setHeader: () => {}, + setTitle: () => {}, + custom: async () => undefined as T, + pasteToEditor: () => {}, + setEditorText: () => {}, + getEditorText: () => "", + editor: async () => undefined, + addAutocompleteProvider: () => {}, + setEditorComponent: () => {}, + getEditorComponent: () => undefined, + get theme() { + return theme; + }, + getAllThemes: () => [], + getTheme: () => undefined, + setTheme: (_theme: string | Theme) => ({ success: false, error: "Theme switching not available in tests" }), + getToolsExpanded: () => false, + setToolsExpanded: () => {}, + }; +} + +type RebindContext = { + unsubscribe?: () => void; + applyRuntimeSettings: () => void; + renderCurrentSessionState: () => void; + bindCurrentSessionExtensions: () => Promise; + subscribeToAgent: () => void; + updateAvailableProviderCount: () => Promise; + updateEditorBorderColor: () => void; + updateTerminalTitle: () => void; +}; + +type ReloadCommandContext = { + hideThinkingBlock: boolean; + session: { + isStreaming: boolean; + isCompacting: boolean; + reload: (options?: { beforeSessionStart?: () => void | Promise }) => Promise; + resourceLoader: { getThemes: () => { themes: [] } }; + extensionRunner: unknown; + modelRegistry: { getError: () => string | undefined }; + }; + settingsManager: { + getHttpIdleTimeoutMs: () => number; + getHideThinkingBlock: () => boolean; + getEditorPaddingX: () => number; + getAutocompleteMaxVisible: () => number; + getShowHardwareCursor: () => boolean; + getClearOnShrink: () => boolean; + }; + keybindings: { reload: () => void }; + customHeader?: unknown; + builtInHeader?: unknown; + editorContainer: { clear: () => void; addChild: (component: unknown) => void }; + ui: { + setFocus: (component: unknown) => void; + requestRender: (force?: boolean) => void; + setShowHardwareCursor: (enabled: boolean) => void; + setClearOnShrink: (enabled: boolean) => void; + }; + editor: unknown; + defaultEditor: { setPaddingX: (padding: number) => void; setAutocompleteMaxVisible: (maxVisible: number) => void }; + themeController: { applyFromSettings: () => Promise }; + resetExtensionUI: () => void; + rebuildChatFromMessages: () => void; + setupAutocompleteProvider: () => void; + setupExtensionShortcuts: (runner: unknown) => void; + showLoadedResources: (options: unknown) => void; + maybeSaveImplicitProjectTrustAfterReload: () => boolean; + showStatus: (message: string) => void; + showWarning: (message: string) => void; + showError: (message: string) => void; +}; + +type InteractiveModePrototype = { + rebindCurrentSession(this: RebindContext, options?: { renderBeforeBind?: boolean }): Promise; + handleReloadCommand(this: ReloadCommandContext): Promise; +}; + +const interactiveModePrototype = InteractiveMode.prototype as unknown as InteractiveModePrototype; + +type ReloadCommandContextOverrides = Omit< + Partial, + "session" | "settingsManager" | "keybindings" | "editorContainer" | "ui" | "defaultEditor" | "themeController" +> & { + session?: Partial; + settingsManager?: Partial; + keybindings?: Partial; + editorContainer?: Partial; + ui?: Partial; + defaultEditor?: Partial; + themeController?: Partial; +}; + +function createReloadCommandContext(overrides: ReloadCommandContextOverrides = {}): ReloadCommandContext { + const editor = overrides.editor ?? {}; + return { + hideThinkingBlock: overrides.hideThinkingBlock ?? false, + session: { + isStreaming: false, + isCompacting: false, + reload: async (options) => { + await options?.beforeSessionStart?.(); + }, + resourceLoader: { getThemes: () => ({ themes: [] }) }, + extensionRunner: {}, + modelRegistry: { getError: () => undefined }, + ...overrides.session, + }, + settingsManager: { + getHttpIdleTimeoutMs: () => 0, + getHideThinkingBlock: () => false, + getEditorPaddingX: () => 1, + getAutocompleteMaxVisible: () => 10, + getShowHardwareCursor: () => false, + getClearOnShrink: () => false, + ...overrides.settingsManager, + }, + keybindings: { reload: () => {}, ...overrides.keybindings }, + editorContainer: { clear: () => {}, addChild: () => {}, ...overrides.editorContainer }, + ui: { + setFocus: () => {}, + requestRender: () => {}, + setShowHardwareCursor: () => {}, + setClearOnShrink: () => {}, + ...overrides.ui, + }, + editor, + defaultEditor: { setPaddingX: () => {}, setAutocompleteMaxVisible: () => {}, ...overrides.defaultEditor }, + themeController: { applyFromSettings: async () => {}, ...overrides.themeController }, + customHeader: overrides.customHeader, + builtInHeader: overrides.builtInHeader, + resetExtensionUI: overrides.resetExtensionUI ?? (() => {}), + rebuildChatFromMessages: overrides.rebuildChatFromMessages ?? (() => {}), + setupAutocompleteProvider: overrides.setupAutocompleteProvider ?? (() => {}), + setupExtensionShortcuts: overrides.setupExtensionShortcuts ?? (() => {}), + showLoadedResources: overrides.showLoadedResources ?? (() => {}), + maybeSaveImplicitProjectTrustAfterReload: overrides.maybeSaveImplicitProjectTrustAfterReload ?? (() => false), + showStatus: overrides.showStatus ?? (() => {}), + showWarning: overrides.showWarning ?? (() => {}), + showError: overrides.showError ?? (() => {}), + }; +} + +type MessageEvent = Extract; + +function getMessageText(event: MessageEvent): string { + const message = event.message; + if (!("content" in message)) { + return ""; + } + const content = message.content; + if (typeof content === "string") { + return content; + } + return content + .filter((part): part is { type: "text"; text: string } => part.type === "text") + .map((part) => part.text) + .join(""); +} + +describe("regression #5943: session_start transient UI", () => { + it("renders replacement session state before session_start handlers can notify", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", (_event, ctx) => { + ctx.ui.notify("Hello Error", "error"); + }); + }, + ], + }); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => events.push("apply"), + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext((message) => events.push(`notify:${message}`)), + mode: "tui", + }); + }, + subscribeToAgent: () => events.push("subscribe"), + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + + expect(events).toEqual(["apply", "render", "subscribe", "bind", "notify:Hello Error"]); + } finally { + harness.cleanup(); + } + }); + + it("subscribes before replacement session_start handlers send messages", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", () => { + pi.sendMessage({ + customType: "session-start", + content: "custom from start", + display: true, + }); + }); + }, + ], + }); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => {}, + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext(() => {}), + mode: "tui", + }); + }, + subscribeToAgent: () => { + events.push("subscribe"); + harness.session.subscribe((event) => { + if (event.type !== "message_start" && event.type !== "message_end") { + return; + } + events.push(`${event.type}:${event.message.role}:${getMessageText(event)}`); + }); + }, + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + + expect(events).toEqual([ + "render", + "subscribe", + "bind", + "message_start:custom:custom from start", + "message_end:custom:custom from start", + ]); + } finally { + harness.cleanup(); + } + }); + + it("subscribes before replacement session_start handlers send user messages", async () => { + const events: string[] = []; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", () => { + pi.sendUserMessage("user from start"); + }); + }, + ], + }); + harness.setResponses([fauxAssistantMessage("assistant from start")]); + + try { + const context: RebindContext = { + applyRuntimeSettings: () => {}, + renderCurrentSessionState: () => events.push("render"), + bindCurrentSessionExtensions: async () => { + events.push("bind"); + await harness.session.bindExtensions({ + uiContext: createUiContext(() => {}), + mode: "tui", + }); + }, + subscribeToAgent: () => { + events.push("subscribe"); + harness.session.subscribe((event) => { + if (event.type !== "message_start" && event.type !== "message_end") { + return; + } + events.push(`${event.type}:${event.message.role}:${getMessageText(event)}`); + }); + }, + updateAvailableProviderCount: async () => {}, + updateEditorBorderColor: () => {}, + updateTerminalTitle: () => {}, + }; + + await interactiveModePrototype.rebindCurrentSession.call(context, { renderBeforeBind: true }); + await harness.session.agent.waitForIdle(); + + expect(events.slice(0, 3)).toEqual(["render", "subscribe", "bind"]); + expect(events).toContain("message_start:user:user from start"); + expect(events).toContain("message_end:user:user from start"); + expect(events).toContain("message_end:assistant:assistant from start"); + } finally { + harness.cleanup(); + } + }); + + it("runs the reload render hook before reload session_start handlers can notify", async () => { + const events: string[] = []; + const beforeSessionStart = vi.fn(() => { + events.push("render"); + }); + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("session_start", (event, ctx) => { + events.push(`start:${event.reason}`); + ctx.ui.notify(`notify:${event.reason}`, "error"); + }); + }, + ], + }); + + try { + await harness.session.bindExtensions({ + uiContext: createUiContext((message) => events.push(message)), + mode: "tui", + }); + expect(events).toEqual(["start:startup", "notify:startup"]); + + events.length = 0; + await harness.session.reload({ beforeSessionStart }); + + expect(beforeSessionStart).toHaveBeenCalledTimes(1); + expect(events).toEqual(["render", "start:reload", "notify:reload"]); + } finally { + harness.cleanup(); + } + }); + + it("refreshes hideThinkingBlock before rebuilding chat during reload", async () => { + initTheme("dark", false); + const events: string[] = []; + let context: ReloadCommandContext; + context = createReloadCommandContext({ + settingsManager: { getHideThinkingBlock: () => true }, + session: { + reload: async (options) => { + events.push("reload"); + await options?.beforeSessionStart?.(); + events.push(`start:${context.hideThinkingBlock}`); + }, + }, + rebuildChatFromMessages: () => { + events.push(`rebuild:${context.hideThinkingBlock}`); + }, + }); + + await interactiveModePrototype.handleReloadCommand.call(context); + + expect(context.hideThinkingBlock).toBe(true); + expect(events).toEqual(["reload", "rebuild:true", "start:true"]); + }); + + it("keeps the reload blocker focused until async reload completes", async () => { + initTheme("dark", false); + const editor = {}; + let focused: unknown; + let chatRestored = false; + let markReloadWaiting!: () => void; + let finishReload!: () => void; + const reloadWaiting = new Promise((resolve) => { + markReloadWaiting = resolve; + }); + const reloadFinished = new Promise((resolve) => { + finishReload = resolve; + }); + + const context = createReloadCommandContext({ + editor, + session: { + reload: async (options) => { + await options?.beforeSessionStart?.(); + markReloadWaiting(); + await reloadFinished; + }, + }, + ui: { + setFocus: (component) => { + focused = component; + }, + }, + rebuildChatFromMessages: () => { + chatRestored = true; + }, + }); + + const reloadPromise = interactiveModePrototype.handleReloadCommand.call(context); + await reloadWaiting; + + expect(chatRestored).toBe(true); + expect(focused).not.toBe(editor); + + finishReload(); + await reloadPromise; + + expect(focused).toBe(editor); + }); +}); From a61137a661e0d9549272e6f89aa978363fc7df66 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 08:41:29 +0200 Subject: [PATCH 131/141] docs(coding-agent): fix plan-mode docs links closes #5957 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/tui.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 341921f2..2610e24c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index c8ff3554..38ef1986 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -742,7 +742,7 @@ ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active")); ctx.ui.setStatus("my-ext", undefined); ``` -**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts) +**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts) ### Pattern 4b: Working Indicator Customization @@ -802,7 +802,7 @@ ctx.ui.setWidget("my-widget", (_tui, theme) => { ctx.ui.setWidget("my-widget", undefined); ``` -**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts) +**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) ### Pattern 6: Custom Footer @@ -919,7 +919,7 @@ export default function (pi: ExtensionAPI) { - **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing - **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls - **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable -- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget +- **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget - **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator - **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats - **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing From 084574048bade4cd7202e1a7d45f89e77e06ebd8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Jun 2026 07:44:05 +0000 Subject: [PATCH 132/141] chore: approve contributor any-victor --- .github/APPROVED_CONTRIBUTORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index a8db269c..7695e96f 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -241,3 +241,5 @@ dangooddd pr Mearman pr dodiego pr + +any-victor pr From 7d0497fdb752a05a9327864951ae7d6fcc4a9617 Mon Sep 17 00:00:00 2001 From: Vegard Stikbakke Date: Mon, 22 Jun 2026 10:14:59 +0200 Subject: [PATCH 133/141] fix(ai): preserve early reasoning details closes #5114 --- packages/ai/CHANGELOG.md | 4 + .../ai/src/providers/openai-completions.ts | 47 +++++-- ...enai-completions-reasoning-details.test.ts | 118 ++++++++++++++++++ 3 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 packages/ai/test/openai-completions-reasoning-details.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 586260cf..828e3433 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI-compatible streaming to preserve encrypted `reasoning_details` that arrive before matching tool call deltas ([#5114](https://github.com/earendil-works/pi/issues/5114)). + ## [0.79.9] - 2026-06-20 ### Added diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 10c8e7e6..b257f96e 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -78,6 +78,20 @@ function isImageContentBlock(block: { type: string }): block is ImageContent { return block.type === "image"; } +function isEncryptedReasoningDetail(detail: unknown): detail is OpenAIEncryptedReasoningDetail { + if (typeof detail !== "object" || detail === null) { + return false; + } + const candidate = detail as Record; + return ( + candidate.type === "reasoning.encrypted" && + typeof candidate.id === "string" && + candidate.id.length > 0 && + typeof candidate.data === "string" && + candidate.data.length > 0 + ); +} + export interface OpenAICompletionsOptions extends StreamOptions { toolChoice?: "auto" | "none" | "required" | { type: "function"; function: { name: string } }; reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh"; @@ -96,6 +110,12 @@ type ResolvedChatTemplateKwargValue = string | number | boolean | null; type ChatCompletionInstructionMessageParam = ChatCompletionDeveloperMessageParam | ChatCompletionSystemMessageParam; +type OpenAIEncryptedReasoningDetail = { + type: "reasoning.encrypted"; + id: string; + data: string; +}; + type ChatCompletionTextPartWithCacheControl = ChatCompletionContentPartText & { cache_control?: OpenAICompatCacheControl; }; @@ -177,6 +197,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA let hasFinishReason = false; const toolCallBlocksByIndex = new Map(); const toolCallBlocksById = new Map(); + const pendingReasoningDetailsByToolCallId = new Map(); const blocks = output.content as StreamingBlock[]; const getContentIndex = (block: StreamingBlock) => blocks.indexOf(block); const finishBlock = (block: StreamingBlock) => { @@ -232,6 +253,16 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA } return thinkingBlock; }; + const applyPendingReasoningDetail = (block: StreamingToolCallBlock) => { + if (!block.id) { + return; + } + const pendingReasoningDetail = pendingReasoningDetailsByToolCallId.get(block.id); + if (pendingReasoningDetail) { + block.thoughtSignature = pendingReasoningDetail; + pendingReasoningDetailsByToolCallId.delete(block.id); + } + }; const ensureToolCallBlock = (toolCall: StreamingToolCallDelta) => { const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined; let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined; @@ -267,6 +298,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (toolCall.id) { toolCallBlocksById.set(toolCall.id, block); } + applyPendingReasoningDetail(block); return block; }; @@ -376,15 +408,16 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA } } - const reasoningDetails = (choice.delta as any).reasoning_details; - if (reasoningDetails && Array.isArray(reasoningDetails)) { + const reasoningDetails = (choice.delta as { reasoning_details?: unknown }).reasoning_details; + if (Array.isArray(reasoningDetails)) { for (const detail of reasoningDetails) { - if (detail.type === "reasoning.encrypted" && detail.id && detail.data) { - const matchingToolCall = output.content.find( - (b) => b.type === "toolCall" && b.id === detail.id, - ) as ToolCall | undefined; + if (isEncryptedReasoningDetail(detail)) { + const serializedDetail = JSON.stringify(detail); + const matchingToolCall = toolCallBlocksById.get(detail.id); if (matchingToolCall) { - matchingToolCall.thoughtSignature = JSON.stringify(detail); + matchingToolCall.thoughtSignature = serializedDetail; + } else { + pendingReasoningDetailsByToolCallId.set(detail.id, serializedDetail); } } } diff --git a/packages/ai/test/openai-completions-reasoning-details.test.ts b/packages/ai/test/openai-completions-reasoning-details.test.ts new file mode 100644 index 00000000..c07b1acf --- /dev/null +++ b/packages/ai/test/openai-completions-reasoning-details.test.ts @@ -0,0 +1,118 @@ +import { Type } from "typebox"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; +import type { AssistantMessage, Model, Tool } from "../src/types.ts"; + +const mockState = vi.hoisted(() => ({ + chunkSets: [] as unknown[][], + payloads: [] as unknown[], +})); + +vi.mock("openai", () => { + class FakeOpenAI { + chat = { + completions: { + create: (payload: unknown) => { + mockState.payloads.push(payload); + const chunks = mockState.chunkSets.shift() ?? []; + const stream = { + async *[Symbol.asyncIterator]() { + for (const chunk of chunks) { + yield chunk; + } + }, + }; + const result = Promise.resolve(stream) as Promise & { + withResponse: () => Promise<{ data: typeof stream; response: { status: number; headers: Headers } }>; + }; + result.withResponse = async () => ({ + data: stream, + response: { status: 200, headers: new Headers() }, + }); + return result; + }, + }, + }; + } + return { default: FakeOpenAI }; +}); + +const reasoningDetail = { type: "reasoning.encrypted", id: "call_1", data: "encrypted-signature" }; +const readTool: Tool = { + name: "read", + description: "Read a file", + parameters: Type.Object({ path: Type.String() }), +}; + +function model(): Model<"openai-completions"> { + return { + id: "google/gemini-test", + name: "Gemini Test", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 100_000, + maxTokens: 4096, + }; +} + +function chunk(delta: Record, finishReason: string | null = null): unknown { + return { + id: "chatcmpl-test", + model: "google/gemini-test", + choices: [{ index: 0, delta, finish_reason: finishReason }], + }; +} + +function toolCallChunk(): unknown { + return chunk({ + tool_calls: [ + { + index: 0, + id: "call_1", + type: "function", + function: { name: "read", arguments: '{"path":"README.md"}' }, + }, + ], + }); +} + +async function runOpenAICompletionsStream(messages: AssistantMessage[] = []): Promise { + return await streamOpenAICompletions(model(), { messages, tools: [readTool] }, { apiKey: "test" }).result(); +} + +function getAssistantPayload(payload: unknown): { reasoning_details?: unknown } | undefined { + const messages = (payload as { messages?: Array<{ role?: string; reasoning_details?: unknown }> }).messages ?? []; + return messages.find((message) => message.role === "assistant"); +} + +describe("openai-completions reasoning_details streaming", () => { + beforeEach(() => { + mockState.chunkSets = []; + mockState.payloads = []; + }); + + it("preserves reasoning_details that arrive before their matching tool call", async () => { + mockState.chunkSets = [ + [chunk({ reasoning_details: [reasoningDetail] }), toolCallChunk(), chunk({}, "tool_calls")], + [chunk({ content: "ok" }), chunk({}, "stop")], + ]; + + const assistantMessage = await runOpenAICompletionsStream(); + const toolCall = assistantMessage.content.find((block) => block.type === "toolCall"); + expect(toolCall).toMatchObject({ + type: "toolCall", + id: "call_1", + name: "read", + arguments: { path: "README.md" }, + thoughtSignature: JSON.stringify(reasoningDetail), + }); + + await runOpenAICompletionsStream([assistantMessage]); + + expect(getAssistantPayload(mockState.payloads[1])?.reasoning_details).toEqual([reasoningDetail]); + }); +}); From 5b9b70d29cdfa3e71b45c26698c2226c21d762f9 Mon Sep 17 00:00:00 2001 From: Enrico <5575513+PizzaMarinara@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:36:16 +0200 Subject: [PATCH 134/141] feat(coding-agent): add compaction reason and willRetry to extension compact events (#5962) session_before_compact and session_compact now carry reason ("manual" | "threshold" | "overflow") and willRetry, matching what the RPC protocol already exposes via auto_compaction_start/end. Extensions can now distinguish manual /compact from threshold auto-compaction and overflow recovery through the public API instead of monkeypatching AgentSession internals. --- packages/coding-agent/docs/compaction.md | 4 +- packages/coding-agent/docs/extensions.md | 7 +- .../coding-agent/src/core/agent-session.ts | 8 ++ .../coding-agent/src/core/extensions/types.ts | 8 ++ .../5217-compaction-reason.test.ts | 95 +++++++++++++++++++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts diff --git a/packages/coding-agent/docs/compaction.md b/packages/coding-agent/docs/compaction.md index babcddc6..5e0d4eef 100644 --- a/packages/coding-agent/docs/compaction.md +++ b/packages/coding-agent/docs/compaction.md @@ -276,7 +276,7 @@ Fired before auto-compaction or `/compact`. Can cancel or provide custom summary ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; // preparation.messagesToSummarize - messages to summarize // preparation.turnPrefixMessages - split turn prefix (if isSplitTurn) @@ -287,6 +287,8 @@ pi.on("session_before_compact", async (event, ctx) => { // preparation.settings - compaction settings // branchEntries - all entries on current branch (for custom state) + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // signal - AbortSignal (pass to LLM calls) // Cancel: diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 5a559813..a9271ee8 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -437,7 +437,10 @@ Fired on compaction. See [compaction.md](compaction.md) for details. ```typescript pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; + const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event; + + // reason - "manual" (/compact), "threshold", or "overflow" + // willRetry - whether the aborted turn is retried after compaction (overflow recovery) // Cancel: return { cancel: true }; @@ -455,6 +458,8 @@ pi.on("session_before_compact", async (event, ctx) => { pi.on("session_compact", async (event, ctx) => { // event.compactionEntry - the saved compaction // event.fromExtension - whether extension provided it + // event.reason - "manual" (/compact), "threshold", or "overflow" + // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery) }); ``` diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index a1033128..395d8161 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1684,6 +1684,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions, + reason: "manual", + willRetry: false, signal: this._compactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -1747,6 +1749,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason: "manual", + willRetry: false, }); } @@ -1945,6 +1949,8 @@ export class AgentSession { preparation, branchEntries: pathEntries, customInstructions: undefined, + reason, + willRetry, signal: this._autoCompactionAbortController.signal, })) as SessionBeforeCompactResult | undefined; @@ -2022,6 +2028,8 @@ export class AgentSession { type: "session_compact", compactionEntry: savedCompactionEntry, fromExtension, + reason, + willRetry, }); } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index a869a55d..7234d4e4 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent { preparation: CompactionPreparation; branchEntries: SessionEntry[]; customInstructions?: string; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; signal: AbortSignal; } @@ -579,6 +583,10 @@ export interface SessionCompactEvent { type: "session_compact"; compactionEntry: CompactionEntry; fromExtension: boolean; + /** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */ + reason: "manual" | "threshold" | "overflow"; + /** True when the aborted turn is retried after this compaction (overflow recovery) */ + willRetry: boolean; } /** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */ diff --git a/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts new file mode 100644 index 00000000..9af206fe --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5217-compaction-reason.test.ts @@ -0,0 +1,95 @@ +import { fauxAssistantMessage } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionFactory } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +type SessionWithCompactionInternals = { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; +}; + +interface RecordedCompactionEvent { + type: "session_before_compact" | "session_compact"; + reason: "manual" | "threshold" | "overflow"; + willRetry: boolean; +} + +function recordingExtension(recorded: RecordedCompactionEvent[]): ExtensionFactory { + return (pi) => { + pi.on("session_before_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + return { + compaction: { + summary: "summary from extension", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: {}, + }, + }; + }); + pi.on("session_compact", async (event) => { + recorded.push({ type: event.type, reason: event.reason, willRetry: event.willRetry }); + }); + }; +} + +async function createCompactionHarness(recorded: RecordedCompactionEvent[]): Promise { + const harness = await createHarness({ + settings: { compaction: { keepRecentTokens: 1 } }, + extensionFactories: [recordingExtension(recorded)], + }); + harness.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two")]); + await harness.session.prompt("first"); + await harness.session.prompt("second"); + return harness; +} + +describe("issue #5217 compaction reason on extension events", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) { + harnesses.pop()?.cleanup(); + } + }); + + it("reports manual reason for compact()", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + + await harness.session.compact(); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "manual", willRetry: false }, + { type: "session_compact", reason: "manual", willRetry: false }, + ]); + }); + + it("reports threshold reason for auto-compaction", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("threshold", false); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "threshold", willRetry: false }, + { type: "session_compact", reason: "threshold", willRetry: false }, + ]); + }); + + it("reports overflow reason and willRetry for overflow recovery", async () => { + const recorded: RecordedCompactionEvent[] = []; + const harness = await createCompactionHarness(recorded); + harnesses.push(harness); + const sessionInternals = harness.session as unknown as SessionWithCompactionInternals; + + await sessionInternals._runAutoCompaction("overflow", true); + + expect(recorded).toEqual([ + { type: "session_before_compact", reason: "overflow", willRetry: true }, + { type: "session_compact", reason: "overflow", willRetry: true }, + ]); + }); +}); From b7908b49748ffae23de019b0111a7a45d828fb0b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 10:37:20 +0200 Subject: [PATCH 135/141] docs(coding-agent): document slash command table closes #5959 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/README.md | 3 ++- packages/coding-agent/docs/usage.md | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2610e24c..0cc0a0e8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). - Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 68e17a62..5dc991cc 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -186,7 +186,8 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist | `/clone` | Duplicate the current active branch into a new session | | `/compact [prompt]` | Manually compact context, optional custom instructions | | `/copy` | Copy last assistant message to clipboard | -| `/export [file]` | Export session to HTML file | +| `/export [file]` | Export session to HTML or JSONL file | +| `/import ` | Import and resume a session from a JSONL file | | `/share` | Upload as private GitHub gist with shareable HTML link | | `/reload` | Reload keybindings, extensions, skills, prompts, and context files (themes hot-reload automatically) | | `/hotkeys` | Show all keyboard shortcuts | diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 5ff0cf73..ccd3ee41 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -44,11 +44,13 @@ Type `/` in the editor to open command completion. Extensions can register custo | `/name ` | Set session display name | | `/session` | Show session file, ID, messages, tokens, and cost | | `/tree` | Jump to any point in the session and continue from there | +| `/trust` | Save project trust decision for future sessions | | `/fork` | Create a new session from a previous user message | | `/clone` | Duplicate the current active branch into a new session | | `/compact [prompt]` | Manually compact context, optionally with custom instructions | | `/copy` | Copy last assistant message to clipboard | -| `/export [file]` | Export session to HTML | +| `/export [file]` | Export session to HTML or JSONL | +| `/import ` | Import and resume a session from a JSONL file | | `/share` | Upload as private GitHub gist with shareable HTML link | | `/reload` | Reload keybindings, extensions, skills, prompts, and context files | | `/hotkeys` | Show all keyboard shortcuts | From 5641d6bac5aa501308f7a700624e81c6cdc52209 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 10:40:46 +0200 Subject: [PATCH 136/141] fix: clear untriaged when no-action is added --- .github/workflows/issue-triage-labels.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/issue-triage-labels.yml b/.github/workflows/issue-triage-labels.yml index ebe5500c..0e6204d5 100644 --- a/.github/workflows/issue-triage-labels.yml +++ b/.github/workflows/issue-triage-labels.yml @@ -53,6 +53,11 @@ jobs: return; } + if (context.payload.action === 'labeled' && context.payload.label?.name === NO_ACTION_LABEL) { + await removeLabelIfPresent(context.issue.number, context.payload.issue, UNTRIAGED_LABEL); + return; + } + if (context.payload.action !== 'labeled' || context.payload.label?.name !== LAST_READ_LABEL) { console.log('Not a last-read label event'); return; From 756a4e8f6f5eb2861555539145b80633e0f1d445 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 10:57:39 +0200 Subject: [PATCH 137/141] fix(coding-agent): respect nested repo ignore boundaries in find closes #5960 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/core/tools/find.ts | 29 ++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0cc0a0e8..12352557 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed the `find` tool to respect nested git repository boundaries when parent `.gitignore` rules ignore the nested repo ([#5960](https://github.com/earendil-works/pi/issues/5960)). - Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). - Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index 6f852f61..e03b728a 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -221,17 +221,24 @@ export function createFindToolDefinition( return; } - // Build fd arguments. --no-require-git makes fd apply hierarchical .gitignore - // semantics whether or not the search path is inside a git repository, without - // leaking sibling-directory rules the way --ignore-file (a global source) would. - const args: string[] = [ - "--glob", - "--color=never", - "--hidden", - "--no-require-git", - "--max-results", - String(effectiveLimit), - ]; + const args: string[] = ["--glob", "--color=never", "--hidden"]; + + // fd normally ignores .gitignore outside git repos, so keep --no-require-git + // there. Inside repos, use fd's default git-aware behavior so parent + // .gitignore rules stop at nested repo boundaries: + // https://github.com/earendil-works/pi/issues/5960 + let insideGitRepo = false; + for (let current = searchPath; ; ) { + if (await pathExists(path.join(current, ".git"))) { + insideGitRepo = true; + break; + } + const parent = path.dirname(current); + if (parent === current) break; + current = parent; + } + if (!insideGitRepo) args.push("--no-require-git"); + args.push("--max-results", String(effectiveLimit)); // fd --glob matches against the basename unless --full-path is set; in --full-path // mode it matches against the absolute candidate path, so a path-containing From 5df5a1ce768de6e1a2b613d148c1357a91d4e58b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 11:11:25 +0200 Subject: [PATCH 138/141] docs(coding-agent): audit unreleased changelog --- packages/coding-agent/CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 12352557..59e4cd27 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,14 +2,25 @@ ## [Unreleased] +### New Features + +- **Extension compaction event context** - Extension `session_before_compact` and `session_compact` events now include `reason` and `willRetry`, so extensions can distinguish manual `/compact`, threshold auto-compaction, and overflow retry flows. See [session_before_compact / session_compact](docs/extensions.md#session_before_compact--session_compact) and [Custom Summarization via Extensions](docs/compaction.md#custom-summarization-via-extensions). +- **Safer update flow** - `pi update` installs the exact checked Pi version, and update notices show the changelog URL, making upgrades more predictable. See [Install and Manage](docs/packages.md#install-and-manage). + +### Added + +- Added `reason` and `willRetry` metadata to extension `session_before_compact` and `session_compact` events so extensions can distinguish manual, threshold, and overflow compaction flows ([#5962](https://github.com/earendil-works/pi/pull/5962) by [@PizzaMarinara](https://github.com/PizzaMarinara)). + ### Fixed - Fixed the `find` tool to respect nested git repository boundaries when parent `.gitignore` rules ignore the nested repo ([#5960](https://github.com/earendil-works/pi/issues/5960)). - Fixed the usage docs slash command table to include `/trust` and `/import` ([#5959](https://github.com/earendil-works/pi/issues/5959)). +- Fixed inherited OpenAI-compatible streaming to preserve encrypted `reasoning_details` that arrive before matching tool call deltas ([#5114](https://github.com/earendil-works/pi/issues/5114)). - Fixed broken TUI documentation links to the plan-mode extension example ([#5957](https://github.com/earendil-works/pi/issues/5957)). - Fixed transient extension UI and session-start messages emitted during session replacement or reload so they remain visible, and kept reload input blocked until reload completes ([#5943](https://github.com/earendil-works/pi/issues/5943)). - Fixed the plan-mode example to preserve active custom tools, skip the action prompt when no plan is found, and queue refinement/execution follow-ups correctly from `agent_end` ([#5940](https://github.com/earendil-works/pi/issues/5940)). - Fixed `pi update` to install the exact version returned by the Pi update check, make `--force` reinstall that checked version, fail instead of falling back to an unversioned reinstall when no version is available, and report both the old and updated versions. +- Fixed update notifications to display the actual changelog URL as the hyperlink text. ## [0.79.9] - 2026-06-20 From 8e1900666f3cb83c281297d8f787fae6ee2bd0e6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 11:14:43 +0200 Subject: [PATCH 139/141] Release v0.79.10 --- package-lock.json | 26 ++--- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 4 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 2 +- packages/ai/src/models.generated.ts | 94 +++++++++---------- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- packages/coding-agent/npm-shrinkwrap.json | 24 ++--- packages/coding-agent/package.json | 8 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 20 files changed, 97 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9bec652f..ba0eac39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5111,10 +5111,10 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.10", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -5463,7 +5463,7 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -5769,12 +5769,12 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -5815,32 +5815,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.79.9" + "version": "0.79.10" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.9.9", + "version": "1.9.10", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "ms": "2.1.3" }, @@ -6136,7 +6136,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index ff48ad2c..86c6fda5 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/agent/package.json b/packages/agent/package.json index 75a73cb8..a3f236a4 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.79.9", + "version": "0.79.10", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.10", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 828e3433..e7778d9c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ### Fixed diff --git a/packages/ai/package.json b/packages/ai/package.json index 77272079..240209ae 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.79.9", + "version": "0.79.10", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index afdea285..cfc69879 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -8885,23 +8885,6 @@ export const MODELS = { contextWindow: 200000, maxTokens: 4096, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", - api: "openai-completions", - provider: "openrouter", - baseUrl: "https://openrouter.ai/api/v1", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, "anthropic/claude-fable-5": { id: "anthropic/claude-fable-5", name: "Anthropic: Claude Fable 5", @@ -9657,7 +9640,7 @@ export const MODELS = { cacheRead: 0.2, cacheWrite: 0.375, }, - contextWindow: 131072, + contextWindow: 65536, maxTokens: 32768, } satisfies Model<"openai-completions">, "google/gemini-3.1-flash-lite": { @@ -9845,7 +9828,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 32768, + maxTokens: 8192, } satisfies Model<"openai-completions">, "ibm-granite/granite-4.1-8b": { id: "ibm-granite/granite-4.1-8b", @@ -9947,8 +9930,8 @@ export const MODELS = { cacheRead: 0.06, cacheWrite: 0, }, - contextWindow: 262144, - maxTokens: 144000, + contextWindow: 256000, + maxTokens: 80000, } satisfies Model<"openai-completions">, "liquid/lfm-2.5-1.2b-thinking:free": { id: "liquid/lfm-2.5-1.2b-thinking:free", @@ -10169,7 +10152,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 4096, + maxTokens: 512000, } satisfies Model<"openai-completions">, "mistralai/codestral-2508": { id: "mistralai/codestral-2508", @@ -10505,7 +10488,7 @@ export const MODELS = { cost: { input: 0.6, output: 2.5, - cacheRead: 0.6, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, @@ -10539,12 +10522,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.66, - output: 3.5, - cacheRead: 0.33, + output: 3.41, + cacheRead: 0.144, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2.7-code": { id: "moonshotai/kimi-k2.7-code", @@ -10578,7 +10561,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262144, + maxTokens: 256000, } satisfies Model<"openai-completions">, "nvidia/llama-3.3-nemotron-super-49b-v1.5": { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", @@ -10884,7 +10867,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 1047576, - maxTokens: 4096, + maxTokens: 32768, } satisfies Model<"openai-completions">, "openai/gpt-4.1-nano": { id: "openai/gpt-4.1-nano", @@ -10914,7 +10897,7 @@ export const MODELS = { cost: { input: 2.5, output: 10, - cacheRead: 1.25, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 128000, @@ -11050,11 +11033,11 @@ export const MODELS = { cost: { input: 0.25, output: 2, - cacheRead: 0.03, + cacheRead: 0.025, cacheWrite: 0, }, contextWindow: 400000, - maxTokens: 4096, + maxTokens: 128000, } satisfies Model<"openai-completions">, "openai/gpt-5-nano": { id: "openai/gpt-5-nano", @@ -11118,11 +11101,11 @@ export const MODELS = { cost: { input: 1.25, output: 10, - cacheRead: 0.125, + cacheRead: 0.13, cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 16384, + maxTokens: 32000, } satisfies Model<"openai-completions">, "openai/gpt-5.1-codex": { id: "openai/gpt-5.1-codex", @@ -11209,7 +11192,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 128000, - maxTokens: 32000, + maxTokens: 16384, } satisfies Model<"openai-completions">, "openai/gpt-5.2-codex": { id: "openai/gpt-5.2-codex", @@ -12422,11 +12405,11 @@ export const MODELS = { cost: { input: 0.14, output: 1, - cacheRead: 0.05, + cacheRead: 0, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 81920, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-397b-a17b": { id: "qwen/qwen3.5-397b-a17b", @@ -12459,8 +12442,8 @@ export const MODELS = { cacheRead: 0, cacheWrite: 0, }, - contextWindow: 256000, - maxTokens: 32768, + contextWindow: 262144, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3.5-flash-02-23": { id: "qwen/qwen3.5-flash-02-23", @@ -13034,11 +13017,11 @@ export const MODELS = { cost: { input: 0.98, output: 3.08, - cacheRead: 0.182, + cacheRead: 0.49, cacheWrite: 0, }, contextWindow: 202752, - maxTokens: 4096, + maxTokens: 65535, } satisfies Model<"openai-completions">, "z-ai/glm-5.2": { id: "z-ai/glm-5.2", @@ -13050,13 +13033,13 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh"}, input: ["text"], cost: { - input: 1.2, - output: 4.1, - cacheRead: 0.2, + input: 1, + output: 4, + cacheRead: 0.18, cacheWrite: 0, }, contextWindow: 1048576, - maxTokens: 131072, + maxTokens: 32768, } satisfies Model<"openai-completions">, "~anthropic/claude-fable-latest": { id: "~anthropic/claude-fable-latest", @@ -13170,12 +13153,12 @@ export const MODELS = { input: ["text", "image"], cost: { input: 0.66, - output: 3.5, - cacheRead: 0.33, + output: 3.41, + cacheRead: 0.144, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 262142, + maxTokens: 262144, } satisfies Model<"openai-completions">, "~openai/gpt-latest": { id: "~openai/gpt-latest", @@ -15936,6 +15919,23 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8000, } satisfies Model<"anthropic-messages">, + "sakana/fugu-ultra": { + id: "sakana/fugu-ultra", + name: "Fugu Ultra", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1000000, + maxTokens: 1000000, + } satisfies Model<"anthropic-messages">, "stepfun/step-3.5-flash": { id: "stepfun/step-3.5-flash", name: "StepFun 3.5 Flash", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 59e4cd27..4014d25b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 9a131279..c138594f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 8b4182e7..11dfb54a 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 779d519c..a1d1808b 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index 01ee2f61..7d7f5932 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index 67a9ac1e..e473cb1a 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index 01aad382..0558de01 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.9.9", + "version": "1.9.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.9.9", + "version": "1.9.10", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index b8dc74ab..5957ad4b 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.9.9", + "version": "1.9.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index 89961b12..dac510c0 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.79.9", + "version": "0.79.10", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 3ed465d2..e23e480f 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.79.9", + "version": "0.79.10", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index c311b556..1e9fa157 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,17 +1,17 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -474,11 +474,11 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.9.tgz", + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.10.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.10", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -488,8 +488,8 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -512,8 +512,8 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.9.tgz", + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.10.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 490a7751..81489b28 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.79.9", + "version": "0.79.10", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -36,9 +36,9 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 96ff6d5c..dd175ffd 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/tui/package.json b/packages/tui/package.json index d9d53e0e..c5d0e480 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.79.9", + "version": "0.79.10", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 329dceb5f3806654c59343949768a2973d752036 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 11:14:46 +0200 Subject: [PATCH 140/141] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 86c6fda5..0d6e2f67 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e7778d9c..93a85598 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4014d25b..a405f908 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index dd175ffd..0a34ae38 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.79.10] - 2026-06-22 ## [0.79.9] - 2026-06-20 From 717a8f958a1ac6ae245091a905672acebd2f3fe4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 22 Jun 2026 12:54:12 +0200 Subject: [PATCH 141/141] fix(ai): revert selective pi-ai base entrypoints --- packages/agent/README.md | 18 - packages/agent/package.json | 4 - packages/agent/src/agent-loop.ts | 2 +- packages/agent/src/agent.ts | 2 +- packages/agent/src/base.ts | 39 -- packages/agent/src/harness/agent-harness.ts | 2 +- .../compaction/branch-summarization.ts | 3 +- .../src/harness/compaction/compaction.ts | 10 +- .../agent/src/harness/compaction/utils.ts | 2 +- packages/agent/src/harness/messages.ts | 2 +- packages/agent/src/harness/session/session.ts | 2 +- packages/agent/src/harness/types.ts | 4 +- packages/agent/src/index.ts | 49 +- packages/agent/src/proxy.ts | 2 +- packages/agent/src/types.ts | 2 +- packages/agent/vitest.config.ts | 6 - packages/agent/vitest.harness.config.ts | 6 - packages/ai/README.md | 32 +- packages/ai/package.json | 12 - packages/ai/src/base.ts | 45 -- packages/ai/src/bedrock-provider.ts | 4 +- packages/ai/src/images-api-registry.ts | 4 - packages/ai/src/images.ts | 2 + packages/ai/src/index.ts | 49 +- packages/ai/src/providers/amazon-bedrock.ts | 9 - packages/ai/src/providers/anthropic.ts | 9 - .../src/providers/azure-openai-responses.ts | 9 - packages/ai/src/providers/google-vertex.ts | 9 - packages/ai/src/providers/google.ts | 9 - .../ai/src/providers/images/openrouter.ts | 8 - .../src/providers/images/register-builtins.ts | 50 ++ packages/ai/src/providers/mistral.ts | 9 - .../src/providers/openai-codex-responses.ts | 9 - .../ai/src/providers/openai-completions.ts | 9 - packages/ai/src/providers/openai-responses.ts | 9 - .../ai/src/providers/register-builtins.ts | 458 +++++++++++------- packages/ai/src/stream.ts | 4 + packages/ai/test/abort.test.ts | 2 +- .../anthropic-eager-tool-input-e2e.test.ts | 2 +- ...ic-empty-thinking-signature-compat.test.ts | 2 +- .../anthropic-force-adaptive-thinking.test.ts | 2 +- ...anthropic-long-cache-retention-e2e.test.ts | 2 +- .../ai/test/anthropic-opus-4-8-smoke.test.ts | 2 +- .../test/anthropic-temperature-compat.test.ts | 2 +- .../test/anthropic-thinking-disable.test.ts | 2 +- .../anthropic-tool-name-normalization.test.ts | 2 +- packages/ai/test/base-entrypoint.test.ts | 68 --- packages/ai/test/bedrock-models.test.ts | 2 +- packages/ai/test/cache-retention.test.ts | 2 +- packages/ai/test/context-overflow.test.ts | 2 +- .../ai/test/cross-provider-handoff.test.ts | 2 +- packages/ai/test/empty.test.ts | 2 +- .../ai/test/google-thinking-disable.test.ts | 2 +- packages/ai/test/interleaved-thinking.test.ts | 2 +- packages/ai/test/lazy-module-load.test.ts | 52 +- .../ai/test/mistral-reasoning-mode.test.ts | 2 +- packages/ai/test/mistral-tool-schema.test.ts | 2 +- .../openai-codex-cache-affinity-e2e.test.ts | 2 +- .../openai-completions-empty-tools.test.ts | 2 +- .../openai-completions-response-model.test.ts | 2 +- .../openai-completions-tool-choice.test.ts | 2 +- ...penai-responses-cache-affinity-e2e.test.ts | 2 +- ...nai-responses-reasoning-replay-e2e.test.ts | 2 +- .../test/openrouter-cache-write-repro.test.ts | 2 +- packages/ai/test/openrouter-images.test.ts | 12 - packages/ai/test/responseid.test.ts | 2 +- packages/ai/test/stream.test.ts | 2 +- packages/ai/test/tokens.test.ts | 2 +- .../test/tool-call-id-normalization.test.ts | 2 +- .../ai/test/tool-call-without-result.test.ts | 2 +- packages/ai/test/total-tokens.test.ts | 2 +- packages/ai/test/unicode-surrogate.test.ts | 2 +- packages/ai/test/xhigh.test.ts | 2 +- ...ms-anthropic-empty-signature-smoke.test.ts | 2 +- packages/ai/test/zen.test.ts | 2 +- packages/coding-agent/vitest.config.ts | 9 - scripts/check-browser-smoke.mjs | 44 -- tsconfig.json | 1 - 78 files changed, 492 insertions(+), 668 deletions(-) delete mode 100644 packages/agent/src/base.ts delete mode 100644 packages/ai/src/base.ts create mode 100644 packages/ai/src/providers/images/register-builtins.ts delete mode 100644 packages/ai/test/base-entrypoint.test.ts diff --git a/packages/agent/README.md b/packages/agent/README.md index ca4c1080..f0fedb81 100644 --- a/packages/agent/README.md +++ b/packages/agent/README.md @@ -31,24 +31,6 @@ agent.subscribe((event) => { await agent.prompt("Hello!"); ``` -## Base Entry Point - -Use `@earendil-works/pi-agent-core/base` with `@earendil-works/pi-ai/base` when bundling applications that should include only selected provider transports: - -```typescript -import { Agent } from "@earendil-works/pi-agent-core/base"; -import { getModel } from "@earendil-works/pi-ai/base"; -import { register } from "@earendil-works/pi-ai/anthropic"; - -register(); - -const agent = new Agent({ - initialState: { model: getModel("anthropic", "claude-sonnet-4-6") }, -}); -``` - -The default `@earendil-works/pi-agent-core` entry point remains batteries-included and registers pi-ai's lazy built-in transports for backward compatibility. - ## Core Concepts ### AgentMessage vs LLM Message diff --git a/packages/agent/package.json b/packages/agent/package.json index a3f236a4..9322d7c6 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -10,10 +10,6 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./base": { - "types": "./dist/base.d.ts", - "import": "./dist/base.js" - }, "./node": { "types": "./dist/node.d.ts", "import": "./dist/node.js" diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 8a8f20f1..77bd9568 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -10,7 +10,7 @@ import { streamSimple, type ToolResultMessage, validateToolArguments, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import type { AgentContext, AgentEvent, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index ef26e213..db6684a8 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -7,7 +7,7 @@ import { type TextContent, type ThinkingBudgets, type Transport, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts"; import type { AfterToolCallContext, diff --git a/packages/agent/src/base.ts b/packages/agent/src/base.ts deleted file mode 100644 index ad1abafa..00000000 --- a/packages/agent/src/base.ts +++ /dev/null @@ -1,39 +0,0 @@ -export * from "./agent.ts"; -export * from "./agent-loop.ts"; -export * from "./harness/agent-harness.ts"; -export { - type BranchPreparation, - type BranchSummaryDetails, - type CollectEntriesResult, - collectEntriesForBranchSummary, - generateBranchSummary, - prepareBranchEntries, -} from "./harness/compaction/branch-summarization.ts"; -export { - calculateContextTokens, - compact, - DEFAULT_COMPACTION_SETTINGS, - estimateContextTokens, - estimateTokens, - findCutPoint, - findTurnStartIndex, - generateSummary, - getLastAssistantUsage, - prepareCompaction, - serializeConversation, - shouldCompact, -} from "./harness/compaction/compaction.ts"; -export * from "./harness/messages.ts"; -export * from "./harness/prompt-templates.ts"; -export * from "./harness/session/jsonl-repo.ts"; -export * from "./harness/session/memory-repo.ts"; -export * from "./harness/session/repo-utils.ts"; -export * from "./harness/session/session.ts"; -export { uuidv7 } from "./harness/session/uuid.ts"; -export * from "./harness/skills.ts"; -export * from "./harness/system-prompt.ts"; -export * from "./harness/types.ts"; -export * from "./harness/utils/shell-output.ts"; -export * from "./harness/utils/truncate.ts"; -export * from "./proxy.ts"; -export * from "./types.ts"; diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index f097ee75..96563465 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -4,7 +4,7 @@ import { type Model, streamSimple, type UserMessage, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import { runAgentLoop } from "../agent-loop.ts"; import type { AgentContext, diff --git a/packages/agent/src/harness/compaction/branch-summarization.ts b/packages/agent/src/harness/compaction/branch-summarization.ts index 60ffe771..c1824ebf 100644 --- a/packages/agent/src/harness/compaction/branch-summarization.ts +++ b/packages/agent/src/harness/compaction/branch-summarization.ts @@ -1,4 +1,5 @@ -import { completeSimple, type Model } from "@earendil-works/pi-ai/base"; +import type { Model } from "@earendil-works/pi-ai"; +import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/compaction.ts b/packages/agent/src/harness/compaction/compaction.ts index d619b34a..dba753d7 100644 --- a/packages/agent/src/harness/compaction/compaction.ts +++ b/packages/agent/src/harness/compaction/compaction.ts @@ -1,11 +1,5 @@ -import { - type AssistantMessage, - completeSimple, - type ImageContent, - type Model, - type TextContent, - type Usage, -} from "@earendil-works/pi-ai/base"; +import type { AssistantMessage, ImageContent, Model, TextContent, Usage } from "@earendil-works/pi-ai"; +import { completeSimple } from "@earendil-works/pi-ai"; import type { AgentMessage, ThinkingLevel } from "../../types.ts"; import { convertToLlm, diff --git a/packages/agent/src/harness/compaction/utils.ts b/packages/agent/src/harness/compaction/utils.ts index 388dd84a..07535b79 100644 --- a/packages/agent/src/harness/compaction/utils.ts +++ b/packages/agent/src/harness/compaction/utils.ts @@ -1,4 +1,4 @@ -import type { Message } from "@earendil-works/pi-ai/base"; +import type { Message } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.ts"; /** File paths touched by a session branch or compaction range. */ diff --git a/packages/agent/src/harness/messages.ts b/packages/agent/src/harness/messages.ts index 8368dd34..36ce96a1 100644 --- a/packages/agent/src/harness/messages.ts +++ b/packages/agent/src/harness/messages.ts @@ -1,4 +1,4 @@ -import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai/base"; +import type { ImageContent, Message, TextContent } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../types.ts"; export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 3fd76c6b..6f136208 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -1,4 +1,4 @@ -import type { ImageContent, TextContent } from "@earendil-works/pi-ai/base"; +import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; import type { AgentMessage } from "../../types.ts"; import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage } from "../messages.ts"; import type { diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 8f281570..4756ca84 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -1,5 +1,5 @@ -import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai/base"; -import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../types.ts"; +import type { ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "@earendil-works/pi-ai"; +import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../index.ts"; import type { Session } from "./session/session.ts"; /** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */ diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 08b7b9cb..fd3c2b9f 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -1,5 +1,44 @@ -// Import the default pi-ai entrypoint so that all built-in providers register -// automatically. Unlike "@earendil-works/pi-ai/base" which does not. -import "@earendil-works/pi-ai"; - -export * from "./base.ts"; +// Core Agent +export * from "./agent.ts"; +// Loop functions +export * from "./agent-loop.ts"; +export * from "./harness/agent-harness.ts"; +export { + type BranchPreparation, + type BranchSummaryDetails, + type CollectEntriesResult, + collectEntriesForBranchSummary, + generateBranchSummary, + prepareBranchEntries, +} from "./harness/compaction/branch-summarization.ts"; +export { + calculateContextTokens, + compact, + DEFAULT_COMPACTION_SETTINGS, + estimateContextTokens, + estimateTokens, + findCutPoint, + findTurnStartIndex, + generateSummary, + getLastAssistantUsage, + prepareCompaction, + serializeConversation, + shouldCompact, +} from "./harness/compaction/compaction.ts"; +export * from "./harness/messages.ts"; +export * from "./harness/prompt-templates.ts"; +export * from "./harness/session/jsonl-repo.ts"; +export * from "./harness/session/memory-repo.ts"; +export * from "./harness/session/repo-utils.ts"; +export * from "./harness/session/session.ts"; +export { uuidv7 } from "./harness/session/uuid.ts"; +export * from "./harness/skills.ts"; +export * from "./harness/system-prompt.ts"; +// Harness +export * from "./harness/types.ts"; +export * from "./harness/utils/shell-output.ts"; +export * from "./harness/utils/truncate.ts"; +// Proxy utilities +export * from "./proxy.ts"; +// Types +export * from "./types.ts"; diff --git a/packages/agent/src/proxy.ts b/packages/agent/src/proxy.ts index 8e5cb2c2..5f0925c9 100644 --- a/packages/agent/src/proxy.ts +++ b/packages/agent/src/proxy.ts @@ -14,7 +14,7 @@ import { type SimpleStreamOptions, type StopReason, type ToolCall, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; // Create stream class matching ProxyMessageEventStream class ProxyMessageEventStream extends EventStream { diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 541d8d63..cb99a79a 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -9,7 +9,7 @@ import type { TextContent, Tool, ToolResultMessage, -} from "@earendil-works/pi-ai/base"; +} from "@earendil-works/pi-ai"; import type { Static, TSchema } from "typebox"; /** diff --git a/packages/agent/vitest.config.ts b/packages/agent/vitest.config.ts index 297fbd50..bcc497fa 100644 --- a/packages/agent/vitest.config.ts +++ b/packages/agent/vitest.config.ts @@ -1,12 +1,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - resolve: { - alias: { - "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, - "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, - }, - }, test: { globals: true, environment: "node", diff --git a/packages/agent/vitest.harness.config.ts b/packages/agent/vitest.harness.config.ts index 37acc34d..9421e5a9 100644 --- a/packages/agent/vitest.harness.config.ts +++ b/packages/agent/vitest.harness.config.ts @@ -1,12 +1,6 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - resolve: { - alias: { - "@earendil-works/pi-ai/base": new URL("../ai/src/base.ts", import.meta.url).pathname, - "@earendil-works/pi-ai": new URL("../ai/src/index.ts", import.meta.url).pathname, - }, - }, test: { globals: true, environment: "node", diff --git a/packages/ai/README.md b/packages/ai/README.md index bec437aa..428ef259 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -8,7 +8,6 @@ Unified LLM API with automatic model discovery, provider configuration, token an - [Supported Providers](#supported-providers) - [Installation](#installation) -- [Base Entry Point](#base-entry-point) - [Quick Start](#quick-start) - [Tools](#tools) - [Defining Tools](#defining-tools) @@ -89,30 +88,6 @@ npm install @earendil-works/pi-ai TypeBox exports are re-exported from `@earendil-works/pi-ai`: `Type`, `Static`, and `TSchema`. -## Base Entry Point - -Use `@earendil-works/pi-ai/base` when a bundler should include only explicitly selected transport implementations. The base entry point exposes model discovery, registries, generic dispatch, environment-key helpers, and provider-neutral utilities without registering built-in transports during module initialization. Generic dispatch still resolves configured environment keys when called. - -Import each selected transport directly and call its `register()` function: - -```typescript -import { getModel, streamSimple } from '@earendil-works/pi-ai/base'; -import { register as registerAnthropic } from '@earendil-works/pi-ai/anthropic'; - -registerAnthropic(); - -const model = getModel('anthropic', 'claude-sonnet-4-6'); -const response = await streamSimple(model, { - messages: [{ role: 'user', content: 'Hello!' }] -}, { - apiKey: process.env.ANTHROPIC_API_KEY -}).result(); -``` - -Direct transport imports bundle their implementation and SDK code. Register only the transports used by the application. For example, OpenRouter, Groq, xAI, and DeepSeek chat models share the `@earendil-works/pi-ai/openai-completions` transport. - -Use `@earendil-works/pi-ai` for the batteries-included behavior. The root entry point remains backward-compatible: it registers lazy wrappers for all built-in transports and resolves environment API keys automatically during dispatch. - ## Quick Start ```typescript @@ -463,7 +438,7 @@ Do not use `stream()` or `complete()` for image generation. Image generation is ### Basic Image Generation ```typescript -import { getImageModel, generateImages } from '@earendil-works/pi-ai'; +import { getImageModel, generateImages } from '@mariozechner/pi-ai'; const model = getImageModel('openrouter', 'google/gemini-2.5-flash-image'); @@ -1123,7 +1098,6 @@ const response = await complete(model, { - OAuth login flows are not supported in browser environments. Use the `@earendil-works/pi-ai/oauth` entry point in Node.js. - In browser builds, Bedrock can still appear in model lists. Calls to Bedrock models fail at runtime. - Use a server-side proxy or backend service if you need Bedrock or OAuth-based auth from a web app. -- Use `@earendil-works/pi-ai/base` plus explicit direct transport registration when a browser bundle should exclude unused provider SDK implementations. ### Environment Variables (Node.js only) @@ -1360,7 +1334,6 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - `stream()` function returning `AssistantMessageEventStream` - `streamSimple()` for `SimpleStreamOptions` mapping -- `register()` for explicit direct transport registration from `@earendil-works/pi-ai/base` - Provider-specific options interface - Message conversion functions to transform `Context` to provider format - Tool conversion if the provider supports tools @@ -1371,8 +1344,7 @@ Create a new provider file (for example `amazon-bedrock.ts`) that exports: - Register the API with `registerApiProvider()` - Add a package subpath export in `package.json` for the provider module (`./dist/providers/.js`) - Add lazy loader wrappers in `src/providers/register-builtins.ts`, do not statically import provider implementation modules there -- Add any root-level `export type` re-exports in `src/index.ts` and `src/base.ts` that should remain available from `@earendil-works/pi-ai` and `@earendil-works/pi-ai/base` -- Keep `src/base.ts` free of built-in registration imports +- Add any root-level `export type` re-exports in `src/index.ts` that should remain available from `@earendil-works/pi-ai` - Add credential detection in `env-api-keys.ts` for the new provider - Ensure `streamSimple` handles auth lookup via `getEnvApiKey()` or provider-specific auth diff --git a/packages/ai/package.json b/packages/ai/package.json index 240209ae..5ba97133 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -10,14 +10,6 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./base": { - "types": "./dist/base.d.ts", - "import": "./dist/base.js" - }, - "./amazon-bedrock": { - "types": "./dist/providers/amazon-bedrock.d.ts", - "import": "./dist/providers/amazon-bedrock.js" - }, "./anthropic": { "types": "./dist/providers/anthropic.d.ts", "import": "./dist/providers/anthropic.js" @@ -50,10 +42,6 @@ "types": "./dist/providers/openai-responses.d.ts", "import": "./dist/providers/openai-responses.js" }, - "./openrouter-images": { - "types": "./dist/providers/images/openrouter.d.ts", - "import": "./dist/providers/images/openrouter.js" - }, "./oauth": { "types": "./dist/oauth.d.ts", "import": "./dist/oauth.js" diff --git a/packages/ai/src/base.ts b/packages/ai/src/base.ts deleted file mode 100644 index f1a8a110..00000000 --- a/packages/ai/src/base.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type { Static, TSchema } from "typebox"; -export { Type } from "typebox"; - -export * from "./api-registry.ts"; -export * from "./env-api-keys.ts"; -export * from "./image-models.ts"; -export * from "./images.ts"; -export * from "./images-api-registry.ts"; -export * from "./models.ts"; -export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.ts"; -export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.ts"; -export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.ts"; -export * from "./providers/faux.ts"; -export type { GoogleOptions } from "./providers/google.ts"; -export type { GoogleThinkingLevel } from "./providers/google-shared.ts"; -export type { GoogleVertexOptions } from "./providers/google-vertex.ts"; -export type { MistralOptions } from "./providers/mistral.ts"; -export type { - OpenAICodexResponsesOptions, - OpenAICodexWebSocketDebugStats, -} from "./providers/openai-codex-responses.ts"; -export type { OpenAICompletionsOptions } from "./providers/openai-completions.ts"; -export type { OpenAIResponsesOptions } from "./providers/openai-responses.ts"; -export * from "./session-resources.ts"; -export * from "./stream.ts"; -export * from "./types.ts"; -export * from "./utils/diagnostics.ts"; -export * from "./utils/event-stream.ts"; -export * from "./utils/json-parse.ts"; -export type { - OAuthAuthInfo, - OAuthCredentials, - OAuthDeviceCodeInfo, - OAuthLoginCallbacks, - OAuthPrompt, - OAuthProvider, - OAuthProviderId, - OAuthProviderInfo, - OAuthProviderInterface, - OAuthSelectOption, - OAuthSelectPrompt, -} from "./utils/oauth/types.ts"; -export * from "./utils/overflow.ts"; -export * from "./utils/typebox-helpers.ts"; -export * from "./utils/validation.ts"; diff --git a/packages/ai/src/bedrock-provider.ts b/packages/ai/src/bedrock-provider.ts index f5aa3998..cf08b33e 100644 --- a/packages/ai/src/bedrock-provider.ts +++ b/packages/ai/src/bedrock-provider.ts @@ -1,6 +1,4 @@ -import { register, streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; - -export { register }; +import { streamBedrock, streamSimpleBedrock } from "./providers/amazon-bedrock.ts"; export const bedrockProviderModule = { streamBedrock, diff --git a/packages/ai/src/images-api-registry.ts b/packages/ai/src/images-api-registry.ts index 5335a926..4758439b 100644 --- a/packages/ai/src/images-api-registry.ts +++ b/packages/ai/src/images-api-registry.ts @@ -51,7 +51,3 @@ export function registerImagesApiProvider, project: string, diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 62ad9cfd..a270792a 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -4,7 +4,6 @@ import { GoogleGenAI, type ThinkingConfig, } from "@google/genai"; -import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, @@ -316,14 +315,6 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt } satisfies GoogleOptions); }; -export function register(): void { - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); -} - function createClient( model: Model<"google-generative-ai">, apiKey?: string, diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/providers/images/openrouter.ts index d1b2c7e8..54caeaf0 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/providers/images/openrouter.ts @@ -6,7 +6,6 @@ import type { ChatCompletionContentPartText, ChatCompletionCreateParamsNonStreaming, } from "openai/resources/chat/completions.js"; -import { registerImagesApiProvider } from "../../images-api-registry.ts"; import type { AssistantImages, ImageContent, @@ -104,13 +103,6 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image } }; -export function register(): void { - registerImagesApiProvider({ - api: "openrouter-images", - generateImages: generateImagesOpenRouter, - }); -} - function createClient( model: ImagesModel<"openrouter-images">, apiKey: string, diff --git a/packages/ai/src/providers/images/register-builtins.ts b/packages/ai/src/providers/images/register-builtins.ts new file mode 100644 index 00000000..e3decbb9 --- /dev/null +++ b/packages/ai/src/providers/images/register-builtins.ts @@ -0,0 +1,50 @@ +import { registerImagesApiProvider } from "../../images-api-registry.ts"; +import type { AssistantImages, ImagesContext, ImagesFunction, ImagesModel, ImagesOptions } from "../../types.ts"; +import type { generateImagesOpenRouter as generateImagesOpenRouterFunction } from "./openrouter.ts"; + +interface OpenRouterImagesProviderModule { + generateImagesOpenRouter: typeof generateImagesOpenRouterFunction; +} + +let openRouterImagesProviderModulePromise: Promise | undefined; + +function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { + return { + api: model.api, + provider: model.provider, + model: model.id, + output: [], + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }; +} + +function loadOpenRouterImagesProviderModule(): Promise { + openRouterImagesProviderModulePromise ||= import("./openrouter.ts").then( + (module) => module as OpenRouterImagesProviderModule, + ); + return openRouterImagesProviderModulePromise; +} + +export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", ImagesOptions> = async ( + model: ImagesModel<"openrouter-images">, + context: ImagesContext, + options?: ImagesOptions, +) => { + try { + const module = await loadOpenRouterImagesProviderModule(); + return await module.generateImagesOpenRouter(model, context, options); + } catch (error) { + return createLazyLoadErrorImages(model, error); + } +}; + +export function registerBuiltInImagesApiProviders(): void { + registerImagesApiProvider({ + api: "openrouter-images", + generateImages: generateImagesOpenRouter, + }); +} + +registerBuiltInImagesApiProviders(); diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index ba331af4..6a132236 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -6,7 +6,6 @@ import type { ContentChunk, FunctionTool, } from "@mistralai/mistralai/models/components"; -import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -131,14 +130,6 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple } satisfies MistralOptions); }; -export function register(): void { - registerApiProvider({ - api: "mistral-conversations", - stream: streamMistral, - streamSimple: streamSimpleMistral, - }); -} - function createOutput(model: Model<"mistral-conversations">): AssistantMessage { return { role: "assistant", diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 1bc2879a..19a2f5d7 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -20,7 +20,6 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts"; import type { @@ -429,14 +428,6 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp } satisfies OpenAICodexResponsesOptions); }; -export function register(): void { - registerApiProvider({ - api: "openai-codex-responses", - stream: streamOpenAICodexResponses, - streamSimple: streamSimpleOpenAICodexResponses, - }); -} - // ============================================================================ // Request Building // ============================================================================ diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index b257f96e..2909678c 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,7 +10,6 @@ import type { ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; -import { registerApiProvider } from "../api-registry.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -486,14 +485,6 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", } satisfies OpenAICompletionsOptions); }; -export function register(): void { - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); -} - function createClient( model: Model<"openai-completions">, context: Context, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index b1692978..014233d5 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,6 +1,5 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { registerApiProvider } from "../api-registry.ts"; import { clampThinkingLevel } from "../models.ts"; import type { Api, @@ -182,14 +181,6 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim } satisfies OpenAIResponsesOptions); }; -export function register(): void { - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); -} - function createClient( model: Model<"openai-responses">, context: Context, diff --git a/packages/ai/src/providers/register-builtins.ts b/packages/ai/src/providers/register-builtins.ts index 6192f8a2..8fdcaaf0 100644 --- a/packages/ai/src/providers/register-builtins.ts +++ b/packages/ai/src/providers/register-builtins.ts @@ -1,14 +1,9 @@ -import { type ApiProvider, clearApiProviders, getApiProvider, registerApiProvider } from "../api-registry.ts"; -import { getImagesApiProvider, type ImagesApiProvider, registerImagesApiProvider } from "../images-api-registry.ts"; +import { clearApiProviders, registerApiProvider } from "../api-registry.ts"; import type { Api, - AssistantImages, AssistantMessage, AssistantMessageEvent, - ImagesApi, - ImagesContext, - ImagesModel, - ImagesOptions, + Context, Model, SimpleStreamOptions, StreamFunction, @@ -25,47 +20,70 @@ import type { OpenAICodexResponsesOptions } from "./openai-codex-responses.ts"; import type { OpenAICompletionsOptions } from "./openai-completions.ts"; import type { OpenAIResponsesOptions } from "./openai-responses.ts"; -interface RegisteringProviderModule { - register(): void; +interface LazyProviderModule< + TApi extends Api, + TOptions extends StreamOptions, + TSimpleOptions extends SimpleStreamOptions, +> { + stream: (model: Model, context: Context, options?: TOptions) => AsyncIterable; + streamSimple: ( + model: Model, + context: Context, + options?: TSimpleOptions, + ) => AsyncIterable; } -function createLazyLoadErrorImages(model: ImagesModel<"openrouter-images">, error: unknown): AssistantImages { - return { - api: model.api, - provider: model.provider, - model: model.id, - output: [], - stopReason: "error", - errorMessage: error instanceof Error ? error.message : String(error), - timestamp: Date.now(), - }; +interface AnthropicProviderModule { + streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>; + streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>; } -function createLazyImagesApiProvider( - api: TApi, - loadModule: () => Promise, -): ImagesApiProvider { - return { - api, - generateImages: async (model: ImagesModel, context: ImagesContext, options?: TOptions) => { - try { - const module = await loadModule(); - module.register(); - const provider = getImagesApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return await provider.generateImages(model, context, options); - } catch (error) { - return createLazyLoadErrorImages(model as ImagesModel<"openrouter-images">, error); - } - }, - }; +interface AzureOpenAIResponsesProviderModule { + streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses", AzureOpenAIResponsesOptions>; + streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-responses", SimpleStreamOptions>; +} + +interface GoogleProviderModule { + streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>; + streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleStreamOptions>; +} + +interface GoogleVertexProviderModule { + streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOptions>; + streamSimpleGoogleVertex: StreamFunction<"google-vertex", SimpleStreamOptions>; +} + +interface MistralProviderModule { + streamMistral: StreamFunction<"mistral-conversations", MistralOptions>; + streamSimpleMistral: StreamFunction<"mistral-conversations", SimpleStreamOptions>; +} + +interface OpenAICodexResponsesProviderModule { + streamOpenAICodexResponses: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions>; + streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-responses", SimpleStreamOptions>; +} + +interface OpenAICompletionsProviderModule { + streamOpenAICompletions: StreamFunction<"openai-completions", OpenAICompletionsOptions>; + streamSimpleOpenAICompletions: StreamFunction<"openai-completions", SimpleStreamOptions>; +} + +interface OpenAIResponsesProviderModule { + streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIResponsesOptions>; + streamSimpleOpenAIResponses: StreamFunction<"openai-responses", SimpleStreamOptions>; } interface BedrockProviderModule { - streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOptions>; - streamSimpleBedrock: StreamFunction<"bedrock-converse-stream", SimpleStreamOptions>; + streamBedrock: ( + model: Model<"bedrock-converse-stream">, + context: Context, + options?: BedrockOptions, + ) => AsyncIterable; + streamSimpleBedrock: ( + model: Model<"bedrock-converse-stream">, + context: Context, + options?: SimpleStreamOptions, + ) => AsyncIterable; } const importNodeOnlyProvider = (specifier: string): Promise => { @@ -73,10 +91,42 @@ const importNodeOnlyProvider = (specifier: string): Promise => { return import(runtimeSpecifier); }; -let bedrockProviderModuleOverride: BedrockProviderModule | undefined; +let anthropicProviderModulePromise: + | Promise> + | undefined; +let azureOpenAIResponsesProviderModulePromise: + | Promise> + | undefined; +let googleProviderModulePromise: + | Promise> + | undefined; +let googleVertexProviderModulePromise: + | Promise> + | undefined; +let mistralProviderModulePromise: + | Promise> + | undefined; +let openAICodexResponsesProviderModulePromise: + | Promise> + | undefined; +let openAICompletionsProviderModulePromise: + | Promise> + | undefined; +let openAIResponsesProviderModulePromise: + | Promise> + | undefined; +let bedrockProviderModuleOverride: + | LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> + | undefined; +let bedrockProviderModulePromise: + | Promise> + | undefined; export function setBedrockProviderModule(module: BedrockProviderModule): void { - bedrockProviderModuleOverride = module; + bedrockProviderModuleOverride = { + stream: module.streamBedrock, + streamSimple: module.streamSimpleBedrock, + }; } function forwardStream(target: AssistantMessageEventStream, source: AsyncIterable): void { @@ -109,29 +159,15 @@ function createLazyLoadErrorMessage(model: Model, error: }; } -async function loadAndRegisterProvider( - api: TApi, - loadModule: () => Promise, -) { - const module = await loadModule(); - module.register(); - const provider = getApiProvider(api); - if (!provider) { - throw new Error(`No API provider registered for api: ${api}`); - } - return provider; -} - -function createLazyStream( - api: TApi, - loadModule: () => Promise, +function createLazyStream( + loadModule: () => Promise>, ): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadAndRegisterProvider(api, loadModule) - .then((provider) => { - const inner = provider.stream(model, context, options); + loadModule() + .then((module) => { + const inner = module.stream(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -144,16 +180,17 @@ function createLazyStream( }; } -function createLazySimpleStream( - api: TApi, - loadModule: () => Promise, -): StreamFunction { +function createLazySimpleStream< + TApi extends Api, + TOptions extends StreamOptions, + TSimpleOptions extends SimpleStreamOptions, +>(loadModule: () => Promise>): StreamFunction { return (model, context, options) => { const outer = new AssistantMessageEventStream(); - loadAndRegisterProvider(api, loadModule) - .then((provider) => { - const inner = provider.streamSimple(model, context, options); + loadModule() + .then((module) => { + const inner = module.streamSimple(model, context, options); forwardStream(outer, inner); }) .catch((error) => { @@ -166,112 +203,199 @@ function createLazySimpleStream( }; } -function createLazyApiProvider( - api: TApi, - loadModule: () => Promise, -): ApiProvider { - return { - api, - stream: createLazyStream(api, loadModule), - streamSimple: createLazySimpleStream(api, loadModule), - }; -} - -function registerBedrockProviderModule(module: BedrockProviderModule): void { - registerApiProvider({ - api: "bedrock-converse-stream", - stream: module.streamBedrock, - streamSimple: module.streamSimpleBedrock, +function loadAnthropicProviderModule(): Promise< + LazyProviderModule<"anthropic-messages", AnthropicOptions, SimpleStreamOptions> +> { + anthropicProviderModulePromise ||= import("./anthropic.ts").then((module) => { + const provider = module as AnthropicProviderModule; + return { + stream: provider.streamAnthropic, + streamSimple: provider.streamSimpleAnthropic, + }; }); + return anthropicProviderModulePromise; } -function loadBedrockProviderModule(): Promise { - const module = bedrockProviderModuleOverride; - if (module) { - return Promise.resolve({ register: () => registerBedrockProviderModule(module) }); +function loadAzureOpenAIResponsesProviderModule(): Promise< + LazyProviderModule<"azure-openai-responses", AzureOpenAIResponsesOptions, SimpleStreamOptions> +> { + azureOpenAIResponsesProviderModulePromise ||= import("./azure-openai-responses.ts").then((module) => { + const provider = module as AzureOpenAIResponsesProviderModule; + return { + stream: provider.streamAzureOpenAIResponses, + streamSimple: provider.streamSimpleAzureOpenAIResponses, + }; + }); + return azureOpenAIResponsesProviderModulePromise; +} + +function loadGoogleProviderModule(): Promise< + LazyProviderModule<"google-generative-ai", GoogleOptions, SimpleStreamOptions> +> { + googleProviderModulePromise ||= import("./google.ts").then((module) => { + const provider = module as GoogleProviderModule; + return { + stream: provider.streamGoogle, + streamSimple: provider.streamSimpleGoogle, + }; + }); + return googleProviderModulePromise; +} + +function loadGoogleVertexProviderModule(): Promise< + LazyProviderModule<"google-vertex", GoogleVertexOptions, SimpleStreamOptions> +> { + googleVertexProviderModulePromise ||= import("./google-vertex.ts").then((module) => { + const provider = module as GoogleVertexProviderModule; + return { + stream: provider.streamGoogleVertex, + streamSimple: provider.streamSimpleGoogleVertex, + }; + }); + return googleVertexProviderModulePromise; +} + +function loadMistralProviderModule(): Promise< + LazyProviderModule<"mistral-conversations", MistralOptions, SimpleStreamOptions> +> { + mistralProviderModulePromise ||= import("./mistral.ts").then((module) => { + const provider = module as MistralProviderModule; + return { + stream: provider.streamMistral, + streamSimple: provider.streamSimpleMistral, + }; + }); + return mistralProviderModulePromise; +} + +function loadOpenAICodexResponsesProviderModule(): Promise< + LazyProviderModule<"openai-codex-responses", OpenAICodexResponsesOptions, SimpleStreamOptions> +> { + openAICodexResponsesProviderModulePromise ||= import("./openai-codex-responses.ts").then((module) => { + const provider = module as OpenAICodexResponsesProviderModule; + return { + stream: provider.streamOpenAICodexResponses, + streamSimple: provider.streamSimpleOpenAICodexResponses, + }; + }); + return openAICodexResponsesProviderModulePromise; +} + +function loadOpenAICompletionsProviderModule(): Promise< + LazyProviderModule<"openai-completions", OpenAICompletionsOptions, SimpleStreamOptions> +> { + openAICompletionsProviderModulePromise ||= import("./openai-completions.ts").then((module) => { + const provider = module as OpenAICompletionsProviderModule; + return { + stream: provider.streamOpenAICompletions, + streamSimple: provider.streamSimpleOpenAICompletions, + }; + }); + return openAICompletionsProviderModulePromise; +} + +function loadOpenAIResponsesProviderModule(): Promise< + LazyProviderModule<"openai-responses", OpenAIResponsesOptions, SimpleStreamOptions> +> { + openAIResponsesProviderModulePromise ||= import("./openai-responses.ts").then((module) => { + const provider = module as OpenAIResponsesProviderModule; + return { + stream: provider.streamOpenAIResponses, + streamSimple: provider.streamSimpleOpenAIResponses, + }; + }); + return openAIResponsesProviderModulePromise; +} + +function loadBedrockProviderModule(): Promise< + LazyProviderModule<"bedrock-converse-stream", BedrockOptions, SimpleStreamOptions> +> { + if (bedrockProviderModuleOverride) { + return Promise.resolve(bedrockProviderModuleOverride); } - return importNodeOnlyProvider("./amazon-bedrock.ts").then((provider) => provider as RegisteringProviderModule); + bedrockProviderModulePromise ||= importNodeOnlyProvider("./amazon-bedrock.ts").then((module) => { + const provider = module as BedrockProviderModule; + return { + stream: provider.streamBedrock, + streamSimple: provider.streamSimpleBedrock, + }; + }); + return bedrockProviderModulePromise; } -const anthropicProvider = createLazyApiProvider<"anthropic-messages", AnthropicOptions>( - "anthropic-messages", - () => import("./anthropic.ts"), -); -const azureOpenAIResponsesProvider = createLazyApiProvider<"azure-openai-responses", AzureOpenAIResponsesOptions>( - "azure-openai-responses", - () => import("./azure-openai-responses.ts"), -); -const googleProvider = createLazyApiProvider<"google-generative-ai", GoogleOptions>( - "google-generative-ai", - () => import("./google.ts"), -); -const googleVertexProvider = createLazyApiProvider<"google-vertex", GoogleVertexOptions>( - "google-vertex", - () => import("./google-vertex.ts"), -); -const mistralProvider = createLazyApiProvider<"mistral-conversations", MistralOptions>( - "mistral-conversations", - () => import("./mistral.ts"), -); -const openAICodexResponsesProvider = createLazyApiProvider<"openai-codex-responses", OpenAICodexResponsesOptions>( - "openai-codex-responses", - () => import("./openai-codex-responses.ts"), -); -const openAICompletionsProvider = createLazyApiProvider<"openai-completions", OpenAICompletionsOptions>( - "openai-completions", - () => import("./openai-completions.ts"), -); -const openAIResponsesProvider = createLazyApiProvider<"openai-responses", OpenAIResponsesOptions>( - "openai-responses", - () => import("./openai-responses.ts"), -); -const bedrockProvider = createLazyApiProvider<"bedrock-converse-stream", BedrockOptions>( - "bedrock-converse-stream", - loadBedrockProviderModule, -); -const openRouterImagesProvider = createLazyImagesApiProvider( - "openrouter-images", - () => import("./images/openrouter.ts"), -); - -export const generateImagesOpenRouter = openRouterImagesProvider.generateImages; -export const streamAnthropic = anthropicProvider.stream; -export const streamSimpleAnthropic = anthropicProvider.streamSimple; -export const streamAzureOpenAIResponses = azureOpenAIResponsesProvider.stream; -export const streamSimpleAzureOpenAIResponses = azureOpenAIResponsesProvider.streamSimple; -export const streamGoogle = googleProvider.stream; -export const streamSimpleGoogle = googleProvider.streamSimple; -export const streamGoogleVertex = googleVertexProvider.stream; -export const streamSimpleGoogleVertex = googleVertexProvider.streamSimple; -export const streamMistral = mistralProvider.stream; -export const streamSimpleMistral = mistralProvider.streamSimple; -export const streamOpenAICodexResponses = openAICodexResponsesProvider.stream; -export const streamSimpleOpenAICodexResponses = openAICodexResponsesProvider.streamSimple; -export const streamOpenAICompletions = openAICompletionsProvider.stream; -export const streamSimpleOpenAICompletions = openAICompletionsProvider.streamSimple; -export const streamOpenAIResponses = openAIResponsesProvider.stream; -export const streamSimpleOpenAIResponses = openAIResponsesProvider.streamSimple; - -const registerBuiltInApiProviderFunctions = [ - () => registerApiProvider(anthropicProvider), - () => registerApiProvider(openAICompletionsProvider), - () => registerApiProvider(mistralProvider), - () => registerApiProvider(openAIResponsesProvider), - () => registerApiProvider(azureOpenAIResponsesProvider), - () => registerApiProvider(openAICodexResponsesProvider), - () => registerApiProvider(googleProvider), - () => registerApiProvider(googleVertexProvider), - () => registerApiProvider(bedrockProvider), -]; - -export function registerBuiltInImagesApiProviders(): void { - registerImagesApiProvider(openRouterImagesProvider); -} +export const streamAnthropic = createLazyStream(loadAnthropicProviderModule); +export const streamSimpleAnthropic = createLazySimpleStream(loadAnthropicProviderModule); +export const streamAzureOpenAIResponses = createLazyStream(loadAzureOpenAIResponsesProviderModule); +export const streamSimpleAzureOpenAIResponses = createLazySimpleStream(loadAzureOpenAIResponsesProviderModule); +export const streamGoogle = createLazyStream(loadGoogleProviderModule); +export const streamSimpleGoogle = createLazySimpleStream(loadGoogleProviderModule); +export const streamGoogleVertex = createLazyStream(loadGoogleVertexProviderModule); +export const streamSimpleGoogleVertex = createLazySimpleStream(loadGoogleVertexProviderModule); +export const streamMistral = createLazyStream(loadMistralProviderModule); +export const streamSimpleMistral = createLazySimpleStream(loadMistralProviderModule); +export const streamOpenAICodexResponses = createLazyStream(loadOpenAICodexResponsesProviderModule); +export const streamSimpleOpenAICodexResponses = createLazySimpleStream(loadOpenAICodexResponsesProviderModule); +export const streamOpenAICompletions = createLazyStream(loadOpenAICompletionsProviderModule); +export const streamSimpleOpenAICompletions = createLazySimpleStream(loadOpenAICompletionsProviderModule); +export const streamOpenAIResponses = createLazyStream(loadOpenAIResponsesProviderModule); +export const streamSimpleOpenAIResponses = createLazySimpleStream(loadOpenAIResponsesProviderModule); +const streamBedrockLazy = createLazyStream(loadBedrockProviderModule); +const streamSimpleBedrockLazy = createLazySimpleStream(loadBedrockProviderModule); export function registerBuiltInApiProviders(): void { - for (const register of registerBuiltInApiProviderFunctions) { - register(); - } + registerApiProvider({ + api: "anthropic-messages", + stream: streamAnthropic, + streamSimple: streamSimpleAnthropic, + }); + + registerApiProvider({ + api: "openai-completions", + stream: streamOpenAICompletions, + streamSimple: streamSimpleOpenAICompletions, + }); + + registerApiProvider({ + api: "mistral-conversations", + stream: streamMistral, + streamSimple: streamSimpleMistral, + }); + + registerApiProvider({ + api: "openai-responses", + stream: streamOpenAIResponses, + streamSimple: streamSimpleOpenAIResponses, + }); + + registerApiProvider({ + api: "azure-openai-responses", + stream: streamAzureOpenAIResponses, + streamSimple: streamSimpleAzureOpenAIResponses, + }); + + registerApiProvider({ + api: "openai-codex-responses", + stream: streamOpenAICodexResponses, + streamSimple: streamSimpleOpenAICodexResponses, + }); + + registerApiProvider({ + api: "google-generative-ai", + stream: streamGoogle, + streamSimple: streamSimpleGoogle, + }); + + registerApiProvider({ + api: "google-vertex", + stream: streamGoogleVertex, + streamSimple: streamSimpleGoogleVertex, + }); + + registerApiProvider({ + api: "bedrock-converse-stream", + stream: streamBedrockLazy, + streamSimple: streamSimpleBedrockLazy, + }); } export function resetApiProviders(): void { diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 7b6b28c6..3f333d9e 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,3 +1,5 @@ +import "./providers/register-builtins.ts"; + import { getApiProvider } from "./api-registry.ts"; import { getEnvApiKey } from "./env-api-keys.ts"; import type { @@ -11,6 +13,8 @@ import type { StreamOptions, } from "./types.ts"; +export { getEnvApiKey } from "./env-api-keys.ts"; + function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { return typeof apiKey === "string" && apiKey.trim().length > 0; } diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 303044ad..27c274aa 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete, stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts index a9862485..2483e1a4 100644 --- a/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts +++ b/packages/ai/test/anthropic-eager-tool-input-e2e.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts index 5663e2a8..69e58e27 100644 --- a/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts +++ b/packages/ai/test/anthropic-empty-thinking-signature-compat.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; +import { streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; interface AnthropicPayload { diff --git a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts index cef2ca48..e797c3a9 100644 --- a/packages/ai/test/anthropic-force-adaptive-thinking.test.ts +++ b/packages/ai/test/anthropic-force-adaptive-thinking.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts index 0c9d590f..2b7667d7 100644 --- a/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts +++ b/packages/ai/test/anthropic-long-cache-retention-e2e.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { complete } from "../src/index.ts"; import { getModels, getProviders } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, KnownProvider, Model, ProviderStreamOptions } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts index 0c1288b3..bb4b739b 100644 --- a/packages/ai/test/anthropic-opus-4-8-smoke.test.ts +++ b/packages/ai/test/anthropic-opus-4-8-smoke.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-temperature-compat.test.ts b/packages/ai/test/anthropic-temperature-compat.test.ts index 06a05698..00161ab8 100644 --- a/packages/ai/test/anthropic-temperature-compat.test.ts +++ b/packages/ai/test/anthropic-temperature-compat.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicTemperaturePayload { diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 581d3a34..13d333e5 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface AnthropicThinkingPayload { diff --git a/packages/ai/test/anthropic-tool-name-normalization.test.ts b/packages/ai/test/anthropic-tool-name-normalization.test.ts index d52ff50e..bb454081 100644 --- a/packages/ai/test/anthropic-tool-name-normalization.test.ts +++ b/packages/ai/test/anthropic-tool-name-normalization.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; import type { Context, Tool } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/base-entrypoint.test.ts b/packages/ai/test/base-entrypoint.test.ts deleted file mode 100644 index d54fa492..00000000 --- a/packages/ai/test/base-entrypoint.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { clearApiProviders, complete, getApiProvider, getApiProviders } from "../src/base.ts"; -import { register as registerAmazonBedrock } from "../src/providers/amazon-bedrock.ts"; -import { register as registerAnthropic } from "../src/providers/anthropic.ts"; -import { register as registerAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts"; -import { fauxAssistantMessage, registerFauxProvider } from "../src/providers/faux.ts"; -import { register as registerGoogle } from "../src/providers/google.ts"; -import { register as registerGoogleVertex } from "../src/providers/google-vertex.ts"; -import { register as registerMistral } from "../src/providers/mistral.ts"; -import { register as registerOpenAICodexResponses } from "../src/providers/openai-codex-responses.ts"; -import { register as registerOpenAICompletions } from "../src/providers/openai-completions.ts"; -import { register as registerOpenAIResponses } from "../src/providers/openai-responses.ts"; - -const registrations = [ - ["bedrock-converse-stream", registerAmazonBedrock], - ["anthropic-messages", registerAnthropic], - ["azure-openai-responses", registerAzureOpenAIResponses], - ["google-generative-ai", registerGoogle], - ["google-vertex", registerGoogleVertex], - ["mistral-conversations", registerMistral], - ["openai-codex-responses", registerOpenAICodexResponses], - ["openai-completions", registerOpenAICompletions], - ["openai-responses", registerOpenAIResponses], -] as const; - -afterEach(() => { - clearApiProviders(); -}); - -describe("base entrypoint", () => { - it("starts without built-in provider registrations", () => { - expect(getApiProviders()).toEqual([]); - }); - - it.each(registrations)("registers the %s transport explicitly", (api, register) => { - register(); - expect(getApiProvider(api)?.api).toBe(api); - }); - - it("dispatches custom providers", async () => { - const registration = registerFauxProvider(); - registration.setResponses([fauxAssistantMessage("hello")]); - const response = await complete(registration.getModel(), { - messages: [{ role: "user", content: "hi", timestamp: Date.now() }], - }); - expect(response.content).toEqual([{ type: "text", text: "hello" }]); - }); - - it("fails clearly when no transport is registered", async () => { - await expect( - complete( - { - id: "missing-model", - name: "Missing Model", - api: "missing-api", - provider: "missing-provider", - baseUrl: "https://example.com", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 1, - maxTokens: 1, - }, - { messages: [] }, - ), - ).rejects.toThrow("No API provider registered for api: missing-api"); - }); -}); diff --git a/packages/ai/test/bedrock-models.test.ts b/packages/ai/test/bedrock-models.test.ts index 5bd5a1f3..2cfd8fb9 100644 --- a/packages/ai/test/bedrock-models.test.ts +++ b/packages/ai/test/bedrock-models.test.ts @@ -17,8 +17,8 @@ */ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModels } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/cache-retention.test.ts b/packages/ai/test/cache-retention.test.ts index b5c55198..6e2c1a5b 100644 --- a/packages/ai/test/cache-retention.test.ts +++ b/packages/ai/test/cache-retention.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; import { getModel } from "../src/models.ts"; import { streamAnthropic } from "../src/providers/anthropic.ts"; import { streamOpenAICompletions } from "../src/providers/openai-completions.ts"; import { streamOpenAIResponses } from "../src/providers/openai-responses.ts"; +import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; class PayloadCaptured extends Error { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 54087a29..9f021a41 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -14,8 +14,8 @@ import type { ChildProcess } from "child_process"; import { execSync, spawn } from "child_process"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { AssistantMessage, Context, Model, Usage } from "../src/types.ts"; import { isContextOverflow } from "../src/utils/overflow.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 73fac229..23593394 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -25,8 +25,8 @@ import { writeFileSync } from "fs"; import { Type } from "typebox"; import { beforeAll, describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.ts"; import { hasAzureOpenAICredentials } from "./azure-utils.ts"; import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.ts"; diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index fadd67a9..a8453dcc 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, AssistantMessage, Context, Model, StreamOptions, UserMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/google-thinking-disable.test.ts b/packages/ai/test/google-thinking-disable.test.ts index 3920f4c8..30df3638 100644 --- a/packages/ai/test/google-thinking-disable.test.ts +++ b/packages/ai/test/google-thinking-disable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.ts"; type SimpleOptionsWithExtras = SimpleStreamOptions & Record; diff --git a/packages/ai/test/interleaved-thinking.test.ts b/packages/ai/test/interleaved-thinking.test.ts index a2d3f5a6..4cf387ce 100644 --- a/packages/ai/test/interleaved-thinking.test.ts +++ b/packages/ai/test/interleaved-thinking.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getEnvApiKey } from "../src/env-api-keys.ts"; -import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple } from "../src/stream.ts"; import type { Api, Context, Model, StopReason, Tool, ToolCall, ToolResultMessage } from "../src/types.ts"; import { StringEnum } from "../src/utils/typebox-helpers.ts"; import { hasBedrockCredentials } from "./bedrock-utils.ts"; diff --git a/packages/ai/test/lazy-module-load.test.ts b/packages/ai/test/lazy-module-load.test.ts index 14bccff0..e21f0d12 100644 --- a/packages/ai/test/lazy-module-load.test.ts +++ b/packages/ai/test/lazy-module-load.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from "vitest"; const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const aiEntryUrl = new URL("../src/index.ts", import.meta.url).href; -const baseEntryUrl = new URL("../src/base.ts", import.meta.url).href; const SDK_SPECIFIERS = [ "@anthropic-ai/sdk", @@ -17,10 +16,9 @@ const SDK_SPECIFIERS = [ type ProbeResult = { loadedSpecifiers: string[]; - value?: unknown; }; -function runProbe(action: string, entryUrl = aiEntryUrl): ProbeResult { +function runProbe(action: string): ProbeResult { const script = ` import { registerHooks } from "node:module"; @@ -36,11 +34,9 @@ function runProbe(action: string, entryUrl = aiEntryUrl): ProbeResult { }, }); - const mod = await import(${JSON.stringify(entryUrl)}); - const value = await (async () => { - ${action} - })(); - console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)], value })); + const mod = await import(${JSON.stringify(aiEntryUrl)}); + ${action} + console.log(JSON.stringify({ loadedSpecifiers: [...new Set(loaded)] })); `; const result = spawnSync(process.execPath, ["--input-type=module", "--eval", script], { @@ -70,33 +66,6 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual([]); }); - it("registers built-in transports when importing the root barrel", () => { - const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api).sort();`); - expect(result.value).toEqual([ - "anthropic-messages", - "azure-openai-responses", - "bedrock-converse-stream", - "google-generative-ai", - "google-vertex", - "mistral-conversations", - "openai-codex-responses", - "openai-completions", - "openai-responses", - ]); - }); - - it("registers built-in image transports when importing the root barrel", () => { - const result = runProbe(`return mod.getImagesApiProvider("openrouter-images")?.api;`); - expect(result.loadedSpecifiers).toEqual([]); - expect(result.value).toBe("openrouter-images"); - }); - - it("does not load provider SDKs or register transports when importing the base barrel", () => { - const result = runProbe(`return mod.getApiProviders().map((provider) => provider.api);`, baseEntryUrl); - expect(result.loadedSpecifiers).toEqual([]); - expect(result.value).toEqual([]); - }); - it("loads only the Anthropic SDK when calling the root lazy wrapper", () => { const result = runProbe(` const model = { @@ -127,17 +96,4 @@ describe("lazy provider module loading", () => { expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); }); - - it("dispatches through a lazy wrapper again after resetting providers", () => { - const result = runProbe(` - const model = mod.getModel("anthropic", "claude-sonnet-4-6"); - const context = { messages: [{ role: "user", content: "hi" }] }; - await mod.streamSimple(model, context).result(); - mod.resetApiProviders(); - return (await mod.streamSimple(model, context).result()).stopReason; - `); - - expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]); - expect(result.value).toBe("error"); - }); }); diff --git a/packages/ai/test/mistral-reasoning-mode.test.ts b/packages/ai/test/mistral-reasoning-mode.test.ts index 0e2f45c5..d197a56a 100644 --- a/packages/ai/test/mistral-reasoning-mode.test.ts +++ b/packages/ai/test/mistral-reasoning-mode.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; import type { Context, Model, SimpleStreamOptions } from "../src/types.ts"; interface MistralPayload { diff --git a/packages/ai/test/mistral-tool-schema.test.ts b/packages/ai/test/mistral-tool-schema.test.ts index 3c490c6d..c6898fc6 100644 --- a/packages/ai/test/mistral-tool-schema.test.ts +++ b/packages/ai/test/mistral-tool-schema.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; interface MistralToolPayload { diff --git a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts index 62245dcf..0abb46f6 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 0ecf8e13..be233670 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { streamSimple } from "../src/stream.ts"; // Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible // backends (e.g. DashScope / Aliyun Qwen via compatible-mode) reject the request with diff --git a/packages/ai/test/openai-completions-response-model.test.ts b/packages/ai/test/openai-completions-response-model.test.ts index ec2abea1..d8e4d2f9 100644 --- a/packages/ai/test/openai-completions-response-model.test.ts +++ b/packages/ai/test/openai-completions-response-model.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { complete } from "../src/index.ts"; +import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; // Router/virtual ids (e.g. OpenRouter `auto`) keep `model` pinned to the diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 41601281..e3b34681 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -1,8 +1,8 @@ import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { stream, streamSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; import { convertMessages } from "../src/providers/openai-completions.ts"; +import { stream, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Model, SimpleStreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ diff --git a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts index 0d247b0b..d173694f 100644 --- a/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity-e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Context } from "../src/types.ts"; describe.skipIf(!process.env.OPENAI_API_KEY)("openai responses cache affinity e2e", () => { diff --git a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts index 87945bc2..aa6f2e23 100644 --- a/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts +++ b/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Context, Message, Tool, ToolCall } from "../src/types.ts"; const testToolSchema = Type.Object({ diff --git a/packages/ai/test/openrouter-cache-write-repro.test.ts b/packages/ai/test/openrouter-cache-write-repro.test.ts index 2cf2a791..4bdeb286 100644 --- a/packages/ai/test/openrouter-cache-write-repro.test.ts +++ b/packages/ai/test/openrouter-cache-write-repro.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { completeSimple } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple } from "../src/stream.ts"; function createLongSystemPrompt(): string { const nonce = `${Date.now()}-${Math.random()}`; diff --git a/packages/ai/test/openrouter-images.test.ts b/packages/ai/test/openrouter-images.test.ts index 5811e6f4..31882bec 100644 --- a/packages/ai/test/openrouter-images.test.ts +++ b/packages/ai/test/openrouter-images.test.ts @@ -1,8 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { generateImages } from "../src/images.ts"; -import { clearImagesApiProviders, getImagesApiProvider } from "../src/images-api-registry.ts"; -import { register as registerOpenRouterImages } from "../src/providers/images/openrouter.ts"; -import { registerBuiltInImagesApiProviders } from "../src/providers/register-builtins.ts"; import type { ImagesContext, ImagesModel } from "../src/types.ts"; const mockState = vi.hoisted(() => ({ @@ -65,15 +62,6 @@ describe("openrouter images", () => { beforeEach(() => { mockState.lastParams = undefined; mockState.lastRequestOptions = undefined; - clearImagesApiProviders(); - registerBuiltInImagesApiProviders(); - }); - - it("registers the direct OpenRouter images transport explicitly", () => { - clearImagesApiProviders(); - expect(getImagesApiProvider("openrouter-images")).toBeUndefined(); - registerOpenRouterImages(); - expect(getImagesApiProvider("openrouter-images")?.api).toBe("openrouter-images"); }); it("returns text plus images in final output", async () => { diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index bc5d6c54..d250f5f4 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 3a381966..9a70b6c2 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -4,8 +4,8 @@ import { dirname, join } from "path"; import { Type } from "typebox"; import { fileURLToPath } from "url"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { complete, stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete, stream } from "../src/stream.ts"; import type { Api, Context, ImageContent, Model, StreamOptions, Tool, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index fdf45b94..676f5a29 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { getModel, getModels } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 622e5127..0672181b 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -12,8 +12,8 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { completeSimple, getEnvApiKey } from "../src/stream.ts"; import type { AssistantMessage, Message, Tool, ToolResultMessage } from "../src/types.ts"; import { resolveApiKey } from "./oauth.ts"; diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index b21c1ce8..11b832f3 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Tool } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 4df4b2e1..972913a7 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -13,8 +13,8 @@ */ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, Usage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index d8b02392..9cdddefa 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -1,7 +1,7 @@ import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { complete } from "../src/stream.ts"; import type { Api, Context, Model, StreamOptions, ToolResultMessage } from "../src/types.ts"; type StreamOptionsWithExtras = StreamOptions & Record; diff --git a/packages/ai/test/xhigh.test.ts b/packages/ai/test/xhigh.test.ts index 1bda5c14..3c279863 100644 --- a/packages/ai/test/xhigh.test.ts +++ b/packages/ai/test/xhigh.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { stream } from "../src/index.ts"; import { getModel } from "../src/models.ts"; +import { stream } from "../src/stream.ts"; import type { Context, Model } from "../src/types.ts"; function makeContext(): Context { diff --git a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts index 9a371b60..ab2fec30 100644 --- a/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts +++ b/packages/ai/test/xiaomi-token-plan-ams-anthropic-empty-signature-smoke.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { completeSimple, getEnvApiKey, streamSimple } from "../src/index.ts"; +import { completeSimple, getEnvApiKey, streamSimple } from "../src/stream.ts"; import type { AssistantMessage, Context, Model } from "../src/types.ts"; const provider = "xiaomi-token-plan-ams"; diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts index 82a2079f..8ca80014 100644 --- a/packages/ai/test/zen.test.ts +++ b/packages/ai/test/zen.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { complete } from "../src/index.ts"; import { MODELS } from "../src/models.generated.ts"; +import { complete } from "../src/stream.ts"; import type { Model } from "../src/types.ts"; describe.skipIf(!process.env.OPENCODE_API_KEY)("OpenCode Models Smoke Test", () => { diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index 5759e3c4..67ce0fca 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -2,11 +2,8 @@ import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; const aiSrcIndex = fileURLToPath(new URL("../ai/src/index.ts", import.meta.url)); -const aiSrcBase = fileURLToPath(new URL("../ai/src/base.ts", import.meta.url)); const aiSrcOAuth = fileURLToPath(new URL("../ai/src/oauth.ts", import.meta.url)); -const aiOpenRouterImages = fileURLToPath(new URL("../ai/src/providers/images/openrouter.ts", import.meta.url)); const agentSrcIndex = fileURLToPath(new URL("../agent/src/index.ts", import.meta.url)); -const agentSrcBase = fileURLToPath(new URL("../agent/src/base.ts", import.meta.url)); const tuiSrcIndex = fileURLToPath(new URL("../tui/src/index.ts", import.meta.url)); export default defineConfig({ @@ -23,18 +20,12 @@ export default defineConfig({ resolve: { alias: [ { find: /^@earendil-works\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@earendil-works\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@earendil-works\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@earendil-works\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@earendil-works\/pi-agent-core$/, replacement: agentSrcIndex }, - { find: /^@earendil-works\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@earendil-works\/pi-tui$/, replacement: tuiSrcIndex }, { find: /^@mariozechner\/pi-ai$/, replacement: aiSrcIndex }, - { find: /^@mariozechner\/pi-ai\/base$/, replacement: aiSrcBase }, { find: /^@mariozechner\/pi-ai\/oauth$/, replacement: aiSrcOAuth }, - { find: /^@mariozechner\/pi-ai\/openrouter-images$/, replacement: aiOpenRouterImages }, { find: /^@mariozechner\/pi-agent-core$/, replacement: agentSrcIndex }, - { find: /^@mariozechner\/pi-agent-core\/base$/, replacement: agentSrcBase }, { find: /^@mariozechner\/pi-tui$/, replacement: tuiSrcIndex }, ], }, diff --git a/scripts/check-browser-smoke.mjs b/scripts/check-browser-smoke.mjs index 906eccd3..d590988d 100644 --- a/scripts/check-browser-smoke.mjs +++ b/scripts/check-browser-smoke.mjs @@ -4,21 +4,7 @@ import { join } from "node:path"; import { build } from "esbuild"; const outputPath = join(tmpdir(), "pi-browser-smoke.js"); -const baseOutputPath = join(tmpdir(), "pi-browser-base-smoke.js"); -const selectiveOutputPath = join(tmpdir(), "pi-browser-selective-smoke.js"); const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log"); -const providerImplementationInputs = [ - "packages/ai/src/providers/amazon-bedrock.ts", - "packages/ai/src/providers/anthropic.ts", - "packages/ai/src/providers/azure-openai-responses.ts", - "packages/ai/src/providers/google.ts", - "packages/ai/src/providers/google-vertex.ts", - "packages/ai/src/providers/images/openrouter.ts", - "packages/ai/src/providers/mistral.ts", - "packages/ai/src/providers/openai-codex-responses.ts", - "packages/ai/src/providers/openai-completions.ts", - "packages/ai/src/providers/openai-responses.ts", -]; try { await build({ @@ -29,36 +15,6 @@ try { logLevel: "silent", outfile: outputPath, }); - const baseBuild = await build({ - stdin: { - contents: `import { complete } from "@earendil-works/pi-ai/base";\nimport { Agent } from "@earendil-works/pi-agent-core/base";\nconsole.log(typeof complete, typeof Agent);\n`, - resolveDir: process.cwd(), - sourcefile: "pi-browser-base-smoke-entry.ts", - }, - bundle: true, - platform: "browser", - format: "esm", - logLevel: "silent", - metafile: true, - outfile: baseOutputPath, - }); - const bundledInputs = new Set(Object.keys(baseBuild.metafile.inputs)); - const reachableProviderImplementations = providerImplementationInputs.filter((input) => bundledInputs.has(input)); - if (reachableProviderImplementations.length > 0) { - throw new Error(`Base browser bundle reached provider implementations:\n${reachableProviderImplementations.join("\n")}`); - } - await build({ - stdin: { - contents: `import { register as registerAnthropic } from "@earendil-works/pi-ai/anthropic";\nimport { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";\nimport { register as registerOpenRouterImages } from "@earendil-works/pi-ai/openrouter-images";\nconsole.log(typeof registerAnthropic, typeof registerOpenAICompletions, typeof registerOpenRouterImages);\n`, - resolveDir: process.cwd(), - sourcefile: "pi-browser-selective-smoke-entry.ts", - }, - bundle: true, - platform: "browser", - format: "esm", - logLevel: "silent", - outfile: selectiveOutputPath, - }); process.exit(0); } catch (error) { let detailedErrors = ""; diff --git a/tsconfig.json b/tsconfig.json index bcb12eee..903f80f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "*": ["./*"], "@earendil-works/pi-ai": ["./packages/ai/src/index.ts"], "@earendil-works/pi-ai/oauth": ["./packages/ai/src/oauth.ts"], - "@earendil-works/pi-ai/openrouter-images": ["./packages/ai/src/providers/images/openrouter.ts"], "@earendil-works/pi-ai/*": ["./packages/ai/src/*.ts", "./packages/ai/src/providers/*.ts"], "@earendil-works/pi-ai/dist/*": ["./packages/ai/src/*"], "@earendil-works/pi-agent-core": ["./packages/agent/src/index.ts"],