Merge branch 'main' into dev

This commit is contained in:
Mario Zechner 2026-09-02 12:21:58 +02:00
commit 7123410467
4 changed files with 66 additions and 2 deletions

View file

@ -74,7 +74,7 @@ function getCacheControl(
}
// Stealth mode: Mimic Claude Code's tool naming exactly
const claudeCodeVersion = "2.1.75";
const claudeCodeVersion = "2.1.251";
// Claude Code 2.x tool names (canonical casing)
// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed proxied plain-HTTP provider requests hanging after a tool call by tunneling them with CONNECT ([#8134](https://github.com/earendil-works/pi/issues/8134)).
## [0.84.4] - 2026-08-28
### New Features

View file

@ -86,6 +86,8 @@ export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TI
const dispatcher = withUndiciErrorListener(
new undici.EnvHttpProxyAgent({
allowH2: false,
// Keep HTTP origins on CONNECT tunnels as they were before Undici 8.7.
proxyTunnel: true,
bodyTimeout: normalizedTimeoutMs,
connect: {
autoSelectFamilyAttemptTimeout: DEFAULT_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT_MS,

View file

@ -1,3 +1,4 @@
import http from "node:http";
import net from "node:net";
import tls from "node:tls";
import * as undici from "undici";
@ -5,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { applyHttpProxySettings, configureHttpDispatcher } from "../src/core/http-dispatcher.ts";
const PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY"] as const;
const DISPATCHER_PROXY_ENV_KEYS = [...PROXY_ENV_KEYS, "http_proxy", "https_proxy"] as const;
const DISPATCHER_PROXY_ENV_KEYS = [...PROXY_ENV_KEYS, "http_proxy", "https_proxy", "NO_PROXY", "no_proxy"] as const;
describe("http proxy settings", () => {
let savedEnv: Record<(typeof PROXY_ENV_KEYS)[number], string | undefined>;
@ -89,6 +90,63 @@ describe("http dispatcher", () => {
vi.restoreAllMocks();
});
it("tunnels proxied HTTP origins", async () => {
const origin = http.createServer((_request, response) => {
response.end("origin");
});
await new Promise<void>((resolve) => origin.listen(0, "127.0.0.1", resolve));
const originAddress = origin.address();
if (!originAddress || typeof originAddress === "string") {
throw new Error("Origin did not bind to a TCP port");
}
const proxyRequestLines: string[] = [];
const proxy = net.createServer((client) => {
client.once("data", (data) => {
const [requestLine = ""] = data.toString().split("\r\n");
proxyRequestLines.push(requestLine);
if (!requestLine.startsWith("CONNECT ")) {
client.end("HTTP/1.1 501 Not Implemented\r\ncontent-length: 0\r\nconnection: close\r\n\r\n");
return;
}
const upstream = net.connect(originAddress.port, "127.0.0.1", () => {
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
client.pipe(upstream).pipe(client);
});
upstream.on("error", () => client.destroy());
client.on("error", () => upstream.destroy());
});
});
await new Promise<void>((resolve) => proxy.listen(0, "127.0.0.1", resolve));
const proxyAddress = proxy.address();
if (!proxyAddress || typeof proxyAddress === "string") {
throw new Error("Proxy did not bind to a TCP port");
}
process.env.HTTP_PROXY = `http://127.0.0.1:${proxyAddress.port}`;
configureHttpDispatcher();
const dispatcher = undici.getGlobalDispatcher();
try {
const originUrl = `http://127.0.0.1:${originAddress.port}/v1/chat/completions`;
await expect(undici.fetch(originUrl).then((response) => response.text())).resolves.toBe("origin");
await expect(undici.fetch(originUrl).then((response) => response.text())).resolves.toBe("origin");
expect(proxyRequestLines).not.toHaveLength(0);
expect(proxyRequestLines).toEqual(
expect.arrayContaining([
expect.stringMatching(`^CONNECT 127\\.0\\.0\\.1:${originAddress.port} HTTP/1\\.1$`),
]),
);
} finally {
await dispatcher.close();
undici.setGlobalDispatcher(originalDispatcher);
await Promise.all([
new Promise<void>((resolve) => proxy.close(() => resolve())),
new Promise<void>((resolve) => origin.close(() => resolve())),
]);
}
});
it("allows two seconds for HTTPS connection attempts without changing the Node default", async () => {
// Preserve a deliberate host fetch override while testing the dispatcher itself.
globalThis.fetch = async () => {