diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index fe4bb356e..0461e0645 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -12,6 +12,7 @@ - Fixed OpenCode Go Claude Haiku 5.5 failing with a 400 (`Invalid signature in thinking block ... bound to a different conversation`) after the tool list, system prompt, or earlier history changed, which left the session failing on every retry. OpenCode Go and Zen Claude 5.5 models, Opus 5, and Fable 5.1 now use per-message effort and send `thinking.block_binding: drop_block`, like the Anthropic provider ([#10954](https://github.com/earendil-works/pi/issues/10954)) - Fixed Anthropic non-strict tool schemas dropping root `$defs` and `definitions`, which left property `$ref`s pointing at nothing. Claude then produced wrong arguments for such tools (common in MCP servers), for example a JSON string instead of an object, and some Anthropic-compatible gateways rejected the request outright ([#9557](https://github.com/earendil-works/pi/issues/9557)) - Fixed OpenRouter rejecting requests with a 400 (`This endpoint's maximum context length is ...`) for models whose output limit is close to the context window, such as DeepSeek flash. OpenRouter counts the input with its own tokenizer, which can be several percent higher than the provider's count, and pi left only 4,096 tokens spare. pi then treated the 400 as a context overflow and compacted the session, even at around 20% of the window. The default output limit now leaves 15% of the estimated input unused, and near a full window at most 32,768 output tokens are requested ([#10497](https://github.com/earendil-works/pi/issues/10497)) +- Fixed Azure GPT-6 Astra, Luna, Sol, GPT-6.1 Sol, and GPT-6.1 Terra using a 272K context window instead of the 1,050,000 tokens Azure Foundry deploys them with ([#11742](https://github.com/earendil-works/pi/issues/11742)) ## [1.5.0] - 2026-11-02 diff --git a/packages/ai/catalog/providers/azure.ts b/packages/ai/catalog/providers/azure.ts index 94bd77a65..8f66f7756 100644 --- a/packages/ai/catalog/providers/azure.ts +++ b/packages/ai/catalog/providers/azure.ts @@ -10,6 +10,11 @@ const CONTEXT_WINDOW_OVERRIDES: Record = { "gpt-5.6-luna": 1050000, "gpt-5.6-sol": 1050000, "gpt-5.6-terra": 1050000, + "gpt-6-astra": 1050000, + "gpt-6-luna": 1050000, + "gpt-6-sol": 1050000, + "gpt-6.1-sol": 1050000, + "gpt-6.1-terra": 1050000, }; // Azure resells DeepSeek at its own rates. US data zone, checked 2026-09-16. diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index 91f5c72f0..3e2d339ea 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -169,6 +169,13 @@ describe("getSupportedThinkingLevels", () => { } }); + it.each(["gpt-6-astra", "gpt-6-luna", "gpt-6-sol", "gpt-6.1-sol", "gpt-6.1-terra"] as const)( + "uses the Azure Foundry 1.05M context window for %s", + (modelId) => { + expect(getModel("azure", modelId)?.contextWindow).toBe(1050000); + }, + ); + it("includes only medium/high/xhigh for OpenAI GPT-5.5 Pro", () => { const model = getModel("openai", "gpt-5.5-pro"); expect(model).toBeDefined(); diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index 34e4d68b4..bdb8d3701 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -126,7 +126,7 @@ In fullscreen mode, these actions control the transcript and take precedence ove | `app.exit` | `ctrl+d` | Exit (when editor empty) | | `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background | | `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) | -| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard | +| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v`, `ctrl+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard | On native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior. diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index 75e540b37..f06a2e6f1 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -73,7 +73,7 @@ const APP_KEYBINDINGS = { description: "Restore queued messages", }, "app.clipboard.pasteImage": { - defaultKeys: windowsKeybindings ? "alt+v" : "ctrl+v", + defaultKeys: windowsKeybindings ? ["alt+v", "ctrl+v"] : "ctrl+v", description: "Paste files on macOS, images, or text from clipboard", }, "app.session.new": { defaultKeys: [], description: "Start a new session" }, diff --git a/packages/coding-agent/src/extensions/llama/client.ts b/packages/coding-agent/src/extensions/llama/client.ts index 3233295a8..67be757d8 100644 --- a/packages/coding-agent/src/extensions/llama/client.ts +++ b/packages/coding-agent/src/extensions/llama/client.ts @@ -158,6 +158,13 @@ export function llamaInferenceUrl(serverUrl: string): string { return `${normalizeLlamaServerUrl(serverUrl)}/v1`; } +function connectionErrorDetail(error: unknown): string { + const cause = error instanceof Error ? error.cause : undefined; + if (cause instanceof Error && cause.message) return cause.message; + if (cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string") return cause.code; + return error instanceof Error ? error.message : String(error); +} + export class LlamaClient { readonly serverUrl: string; private readonly apiKey: string | undefined; @@ -167,13 +174,25 @@ export class LlamaClient { this.apiKey = apiKey; } + /** Fetches from the server, naming the server and the network-level cause when the connection fails. */ + private async fetchServer(path: string, init: RequestInit): Promise { + try { + return await fetch(`${this.serverUrl}${path}`, init); + } catch (error) { + if (init.signal?.aborted) throw error; + throw new Error(`Could not reach llama.cpp server at ${this.serverUrl}: ${connectionErrorDetail(error)}`, { + cause: error, + }); + } + } + private async request(path: string, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); if (init.body !== undefined) headers.set("Content-Type", "application/json"); if (this.apiKey) headers.set("Authorization", `Bearer ${this.apiKey}`); const timeout = AbortSignal.timeout(15_000); const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout; - const response = await fetch(`${this.serverUrl}${path}`, { ...init, headers, signal }); + const response = await this.fetchServer(path, { ...init, headers, signal }); let payload: unknown; try { payload = await response.json(); @@ -229,7 +248,7 @@ export class LlamaClient { async watch(onEvent: (event: LlamaModelEvent) => void, signal?: AbortSignal): Promise { const headers = new Headers(); if (this.apiKey) headers.set("Authorization", `Bearer ${this.apiKey}`); - const response = await fetch(`${this.serverUrl}/models/sse`, { headers, signal }); + const response = await this.fetchServer("/models/sse", { headers, signal }); if (!response.ok || !response.body) throw new Error(`llama.cpp SSE returned HTTP ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); diff --git a/packages/coding-agent/test/keybindings.test.ts b/packages/coding-agent/test/keybindings.test.ts index cb505eede..cdc089d72 100644 --- a/packages/coding-agent/test/keybindings.test.ts +++ b/packages/coding-agent/test/keybindings.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { KEYBINDINGS, useWindowsKeybindings } from "../src/core/keybindings.ts"; describe("Windows keybinding defaults", () => { @@ -20,11 +20,26 @@ describe("Windows keybinding defaults", () => { expect(useWindowsKeybindings("darwin", {})).toBe(false); }); + it("keeps ctrl+v as a paste key on WSL", async () => { + if (process.platform !== "linux") return; + vi.stubEnv("WSL_DISTRO_NAME", "Ubuntu"); + vi.resetModules(); + try { + const { KEYBINDINGS: wslKeybindings } = await import("../src/core/keybindings.ts"); + expect(wslKeybindings["app.clipboard.pasteImage"].defaultKeys).toEqual(["alt+v", "ctrl+v"]); + } finally { + vi.unstubAllEnvs(); + vi.resetModules(); + } + }); + it("applies the detected defaults consistently", () => { const windowsKeybindings = useWindowsKeybindings(); const nativeWindows = process.platform === "win32"; - expect(KEYBINDINGS["app.clipboard.pasteImage"].defaultKeys).toBe(windowsKeybindings ? "alt+v" : "ctrl+v"); + expect(KEYBINDINGS["app.clipboard.pasteImage"].defaultKeys).toEqual( + windowsKeybindings ? ["alt+v", "ctrl+v"] : "ctrl+v", + ); expect(KEYBINDINGS["tui.altScreen.search"].defaultKeys).toBe(windowsKeybindings ? "ctrl+f" : "ctrl+shift+f"); expect(KEYBINDINGS["app.message.followUp"].defaultKeys).toBe(windowsKeybindings ? "ctrl+q" : "alt+enter"); expect(KEYBINDINGS["app.model.cycleBackward"].defaultKeys).toBe(windowsKeybindings ? "alt+p" : "shift+ctrl+p"); diff --git a/packages/coding-agent/test/llama-extension.test.ts b/packages/coding-agent/test/llama-extension.test.ts index f211d6210..ce7faa453 100644 --- a/packages/coding-agent/test/llama-extension.test.ts +++ b/packages/coding-agent/test/llama-extension.test.ts @@ -59,6 +59,19 @@ describe("llama.cpp extension", () => { expect(() => normalizeLlamaServerUrl("file:///tmp/llama")).toThrow("http or https"); }); + // #11745: a refused connection showed only "fetch failed" + it("names the server and the cause when it cannot be reached", async () => { + const server = createServer(); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const port = (server.address() as AddressInfo).port; + await new Promise((resolve) => server.close(() => resolve())); + + await expect(new LlamaClient(`http://127.0.0.1:${port}`).list()).rejects.toThrow( + `Could not reach llama.cpp server at http://127.0.0.1:${port}: connect ECONNREFUSED 127.0.0.1:${port}`, + ); + }); + it("exposes loaded and sleeping models with router metadata", () => { const controller = createLlamaProvider(); controller.setCatalog(