diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index 2845d9d8f..253403d94 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -209,3 +209,5 @@ maximilianzuern pr brianmichel pr abhinavmathur-atlan pr + +mattiacerutti pr diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index 620183996..626639405 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -17,11 +17,8 @@ jobs: script: | const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS'; const VALID_CAPABILITIES = new Set(['issue', 'pr']); - const ISSUE_GATE_MESSAGE_MODE = 'refactor'; // Switch to 'normal' to restore the standard auto-close message. const issueAuthor = context.payload.issue.user.login; const defaultBranch = context.payload.repository.default_branch; - const issueCreatedDay = new Date(context.payload.issue.created_at).getUTCDay(); - const isFridayThroughSunday = issueCreatedDay === 5 || issueCreatedDay === 6 || issueCreatedDay === 0; if (issueAuthor.endsWith('[bot]') || issueAuthor === 'dependabot[bot]') { console.log(`Skipping bot: ${issueAuthor}`); @@ -97,36 +94,15 @@ jobs: return; } - function buildNormalGateMessage() { - return [ - 'This issue was auto-closed. All issues from new contributors are auto-closed by default.', - ...(isFridayThroughSunday - ? [ - 'Issues submitted Friday through Sunday are not reviewed. If this 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 in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`, - '', - 'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.', - '', - `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`, - ].join('\n'); - } - - function buildRefactorGateMessage() { - return [ - 'This issue was auto-closed. All issues will be closed until 2026-05-17 while the project refactor is being completed.', - '', - `The refactor is happening on \`${defaultBranch}\`: https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${defaultBranch}`, - '', - 'Issues closed during this period will not be reviewed. The reason is that the refactor will not get done otherwise, because issue triage has been taking about 8 hours per day.', - '', - 'In case of emergency, ask on Discord: https://discord.com/invite/3cU7Bz4UPx', - ].join('\n'); - } - - const message = ISSUE_GATE_MESSAGE_MODE === 'refactor' ? buildRefactorGateMessage() : buildNormalGateMessage(); + const message = [ + 'This issue was auto-closed. All issues from new contributors are auto-closed by default.', + '', + `Maintainers review auto-closed issues daily and reopen worthwhile ones. Issues that do not meet the quality bar in [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md) will not be reopened or receive a reply.`, + '', + 'If a maintainer replies `lgtmi` on one of your issues, your future issues will stay open. If a maintainer replies `lgtm`, your future issues and PRs will stay open.', + '', + `See [CONTRIBUTING.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md).`, + ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, @@ -135,33 +111,6 @@ jobs: body: message, }); - const labelsToAdd = []; - if (isFridayThroughSunday) labelsToAdd.push('closed-because-weekend'); - if (ISSUE_GATE_MESSAGE_MODE === 'refactor') labelsToAdd.push('closed-because-refactor'); - - if (labelsToAdd.includes('closed-because-refactor')) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: 'closed-because-refactor', - color: '5319e7', - description: 'Closed while the project refactor is in progress', - }); - } catch (error) { - if (error.status !== 422) throw error; - } - } - - if (labelsToAdd.length > 0) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: labelsToAdd, - }); - } - await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/package-lock.json b/package-lock.json index 08d0d5ae3..ba9678e72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "typescript": "^5.9.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, "node_modules/@ampproject/remapping": { @@ -6842,12 +6842,12 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", + "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { @@ -7787,7 +7787,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, "packages/agent/node_modules/@types/node": { @@ -7831,7 +7831,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" } }, "packages/ai/node_modules/@types/node": { @@ -7870,7 +7870,7 @@ "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "typebox": "^1.1.24", - "undici": "^7.19.1", + "undici": "^8.3.0", "yaml": "^2.8.2" }, "bin": { @@ -7887,7 +7887,7 @@ "vitest": "^3.2.4" }, "engines": { - "node": ">=20.6.0" + "node": ">=22.19.0" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.6" @@ -7961,7 +7961,7 @@ "chalk": "^5.5.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "optionalDependencies": { "koffi": "^2.9.0" diff --git a/package.json b/package.json index 288ed3a28..af4860995 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "shx": "^0.4.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "version": "0.0.3", "dependencies": { diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 923f7298c..215358837 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ## [0.74.1] - 2026-05-16 ## [0.74.0] - 2026-05-07 diff --git a/packages/agent/package.json b/packages/agent/package.json index b307e7622..41b922762 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -50,7 +50,7 @@ "directory": "packages/agent" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ffa59a3ca..b8a94e211 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + +### Fixed + +- Fixed `streamSimple()` defaults for models whose advertised output limit is effectively their full context window to avoid impossible default requests ([#4614](https://github.com/earendil-works/pi/issues/4614)). + ## [0.74.1] - 2026-05-16 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 6d3f3fefe..b72af4d5d 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -97,7 +97,7 @@ "directory": "packages/ai" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 56dcaa6d4..b86d117b6 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -203,6 +203,9 @@ function applyThinkingLevelMetadata(model: Model): void { ) { mergeThinkingLevelMap(model, { off: null }); } + if (model.provider === "github-copilot" && model.id.startsWith("gpt-5")) { + mergeThinkingLevelMap(model, { minimal: "low" }); + } if ( model.api === "openai-responses" && model.provider === "openai" && @@ -237,9 +240,6 @@ function applyThinkingLevelMetadata(model: Model): void { if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) { mergeThinkingLevelMap(model, { minimal: "low" }); } - if (model.provider === "openai-codex" && model.id === "gpt-5.1-codex-mini") { - mergeThinkingLevelMap(model, { minimal: "medium", low: "medium", medium: "medium", high: "high" }); - } 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 @@ -1497,42 +1497,6 @@ async function generateModels() { const CODEX_CONTEXT = 272000; const CODEX_MAX_TOKENS = 128000; const codexModels: Model<"openai-codex-responses">[] = [ - { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, - { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, { id: "gpt-5.2", name: "GPT-5.2", @@ -1545,18 +1509,6 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text", "image"], - cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, - contextWindow: CODEX_CONTEXT, - maxTokens: CODEX_MAX_TOKENS, - }, { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", @@ -1569,6 +1521,18 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, + { + id: "gpt-5.3-codex-spark", + name: "GPT-5.3 Codex Spark", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text"], + cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, { id: "gpt-5.4", name: "GPT-5.4", @@ -1581,6 +1545,42 @@ async function generateModels() { contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, + { + id: "gpt-5.4-fast", + name: "GPT-5.4 Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, + { + id: "gpt-5.4-mini", + name: "GPT-5.4 mini", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, + { + id: "gpt-5.4-mini-fast", + name: "GPT-5.4 mini Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: CODEX_BASE_URL, + reasoning: true, + input: ["text", "image"], + cost: { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 }, + contextWindow: CODEX_CONTEXT, + maxTokens: CODEX_MAX_TOKENS, + }, { id: "gpt-5.5", name: "GPT-5.5", @@ -1594,29 +1594,17 @@ async function generateModels() { maxTokens: CODEX_MAX_TOKENS, }, { - id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", + id: "gpt-5.5-fast", + name: "GPT-5.5 Fast", api: "openai-codex-responses", provider: "openai-codex", baseUrl: CODEX_BASE_URL, reasoning: true, input: ["text", "image"], - cost: { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }, + cost: { input: 12.5, output: 75, cacheRead: 1.25, cacheWrite: 0 }, contextWindow: CODEX_CONTEXT, maxTokens: CODEX_MAX_TOKENS, }, - { - id: "gpt-5.3-codex-spark", - name: "GPT-5.3 Codex Spark", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: CODEX_BASE_URL, - reasoning: true, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: CODEX_MAX_TOKENS, - }, ]; allModels.push(...codexModels); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 63bd4e5f7..2d4de7b4f 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -4005,7 +4005,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null}, + thinkingLevelMap: {"off":null,"minimal":"low"}, input: ["text", "image"], cost: { input: 0, @@ -4024,7 +4024,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4043,7 +4043,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4062,7 +4062,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4081,7 +4081,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4100,7 +4100,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -4119,7 +4119,7 @@ export const MODELS = { 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"}, reasoning: true, - thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, + thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"}, input: ["text", "image"], cost: { input: 0, @@ -7155,58 +7155,6 @@ export const MODELS = { } satisfies Model<"openai-responses">, }, "openai-codex": { - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.1-codex-max": { - id: "gpt-5.1-codex-max", - name: "GPT-5.1 Codex Max", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, - "gpt-5.1-codex-mini": { - id: "gpt-5.1-codex-mini", - name: "GPT-5.1 Codex Mini", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"minimal":"medium","low":"medium","medium":"medium","high":"high"}, - input: ["text", "image"], - cost: { - input: 0.25, - output: 2, - cacheRead: 0.025, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, "gpt-5.2": { id: "gpt-5.2", name: "GPT-5.2", @@ -7225,24 +7173,6 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-codex-responses", - provider: "openai-codex", - baseUrl: "https://chatgpt.com/backend-api", - reasoning: true, - thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 272000, - maxTokens: 128000, - } satisfies Model<"openai-codex-responses">, "gpt-5.3-codex": { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", @@ -7271,12 +7201,12 @@ export const MODELS = { thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, input: ["text"], cost: { - input: 0, - output: 0, - cacheRead: 0, + input: 1.75, + output: 14, + cacheRead: 0.175, cacheWrite: 0, }, - contextWindow: 128000, + contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, "gpt-5.4": { @@ -7297,9 +7227,27 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, + "gpt-5.4-fast": { + id: "gpt-5.4-fast", + name: "GPT-5.4 Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, "gpt-5.4-mini": { id: "gpt-5.4-mini", - name: "GPT-5.4 Mini", + name: "GPT-5.4 mini", api: "openai-codex-responses", provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", @@ -7315,6 +7263,24 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, + "gpt-5.4-mini-fast": { + id: "gpt-5.4-mini-fast", + name: "GPT-5.4 mini Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 1.5, + output: 9, + cacheRead: 0.15, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, "gpt-5.5": { id: "gpt-5.5", name: "GPT-5.5", @@ -7333,6 +7299,24 @@ export const MODELS = { contextWindow: 272000, maxTokens: 128000, } satisfies Model<"openai-codex-responses">, + "gpt-5.5-fast": { + id: "gpt-5.5-fast", + name: "GPT-5.5 Fast", + api: "openai-codex-responses", + provider: "openai-codex", + baseUrl: "https://chatgpt.com/backend-api", + reasoning: true, + thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, + input: ["text", "image"], + cost: { + input: 12.5, + output: 75, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 272000, + maxTokens: 128000, + } satisfies Model<"openai-codex-responses">, }, "opencode": { "big-pickle": { diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index 288f2c52b..d9dcc3481 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -1,9 +1,19 @@ import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js"; +const DEFAULT_MAX_OUTPUT_TOKENS = 32000; +const CONTEXT_WINDOW_OUTPUT_TOLERANCE = 1024; + export function buildBaseOptions(model: Model, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { + const defaultMaxTokens = + model.maxTokens > 0 + ? model.maxTokens >= model.contextWindow - CONTEXT_WINDOW_OUTPUT_TOLERANCE + ? Math.min(model.maxTokens, DEFAULT_MAX_OUTPUT_TOKENS) + : model.maxTokens + : undefined; + return { temperature: options?.temperature, - maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? model.maxTokens : undefined), + maxTokens: options?.maxTokens ?? defaultMaxTokens, signal: options?.signal, apiKey: apiKey || options?.apiKey, transport: options?.transport, diff --git a/packages/ai/test/abort.test.ts b/packages/ai/test/abort.test.ts index 57bf39d10..cd46a6297 100644 --- a/packages/ai/test/abort.test.ts +++ b/packages/ai/test/abort.test.ts @@ -276,12 +276,12 @@ describe("AI Providers Abort Tests", () => { describe("OpenAI Codex Provider Abort", () => { it.skipIf(!openaiCodexToken)("should abort mid-stream", { retry: 3 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testAbortSignal(llm, { apiKey: openaiCodexToken }); }); it.skipIf(!openaiCodexToken)("should handle immediate abort", { retry: 3 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testImmediateAbort(llm, { apiKey: openaiCodexToken }); }); }); diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index f84c3f47b..718800bf1 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -228,9 +228,9 @@ describe("Context overflow error handling", () => { describe("OpenAI Codex (OAuth)", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should detect overflow via isContextOverflow", + "gpt-5.5 - should detect overflow via isContextOverflow", async () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const result = await testContextOverflow(model, openaiCodexToken!); logResult(result); diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 34325f89a..a133241a5 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -67,7 +67,7 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ { provider: "openai", model: "gpt-5-mini", label: "openai-responses-gpt-5-mini" }, { provider: "azure-openai-responses", model: "gpt-4o-mini", label: "azure-openai-responses-gpt-4o-mini" }, // OpenAI Codex - { provider: "openai-codex", model: "gpt-5.2-codex", label: "openai-codex-gpt-5.2-codex" }, + { provider: "openai-codex", model: "gpt-5.5", label: "openai-codex-gpt-5.5" }, // GitHub Copilot { provider: "github-copilot", model: "claude-sonnet-4.5", label: "copilot-claude-sonnet-4.5" }, { provider: "github-copilot", model: "gpt-5.1-codex", label: "copilot-gpt-5.1-codex" }, diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index eaf0e666c..121c07b97 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -704,37 +704,37 @@ describe("AI Providers Empty Message Tests", () => { describe("OpenAI Codex Provider Empty Messages", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle empty content array", + "gpt-5.5 - should handle empty content array", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmptyMessage(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle empty string content", + "gpt-5.5 - should handle empty string content", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmptyStringMessage(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle whitespace-only content", + "gpt-5.5 - should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testWhitespaceOnlyMessage(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle empty assistant message in conversation", + "gpt-5.5 - should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmptyAssistantMessage(llm, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/image-tool-result.test.ts b/packages/ai/test/image-tool-result.test.ts index 72bd0414a..9d1276d59 100644 --- a/packages/ai/test/image-tool-result.test.ts +++ b/packages/ai/test/image-tool-result.test.ts @@ -481,19 +481,19 @@ describe("Tool Results with Images", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle tool result with only image", + "gpt-5.5 - should handle tool result with only image", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await handleToolWithImageResult(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle tool result with text and image", + "gpt-5.5 - should handle tool result with text and image", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await handleToolWithTextAndImageResult(llm, { apiKey: openaiCodexToken }); }, ); 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 cf0ddf396..0d929724c 100644 --- a/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts +++ b/packages/ai/test/openai-codex-cache-affinity-e2e.test.ts @@ -8,7 +8,7 @@ const codexToken = await resolveApiKey("openai-codex"); describe("openai-codex cache affinity e2e", () => { it.skipIf(!codexToken)("handles SSE requests with aligned cache-affinity identifiers", async () => { - const model = getModel("openai-codex", "gpt-5.3-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const sessionId = "0195d6e4-4cf9-7f44-a2d8-f8f7f49ee9d3"; const context: Context = { systemPrompt: "You are a helpful assistant. Reply exactly as requested.", diff --git a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts index c50fcc888..52de262b0 100644 --- a/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts +++ b/packages/ai/test/openai-responses-foreign-toolcall-id.test.ts @@ -18,7 +18,7 @@ const usage: Usage = { describe("OpenAI Responses foreign tool call ID normalization", () => { it("hashes foreign Copilot tool item IDs into a bounded Codex-safe fc_ shape", () => { - const model = getModel("openai-codex", "gpt-5.3-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const assistant: AssistantMessage = { role: "assistant", content: [ @@ -31,7 +31,7 @@ describe("OpenAI Responses foreign tool call ID normalization", () => { ], api: "openai-responses", provider: "github-copilot", - model: "gpt-5.3-codex", + model: "gpt-5.5", usage, stopReason: "toolUse", timestamp: Date.now() - 2000, diff --git a/packages/ai/test/openai-responses-tool-result-images.test.ts b/packages/ai/test/openai-responses-tool-result-images.test.ts index 05d9c16ff..48ab8d08b 100644 --- a/packages/ai/test/openai-responses-tool-result-images.test.ts +++ b/packages/ai/test/openai-responses-tool-result-images.test.ts @@ -183,8 +183,8 @@ describe("Responses API tool result images", () => { ); }); - describe("OpenAI Codex Responses Provider (gpt-5.2-codex)", () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + describe("OpenAI Codex Responses Provider (gpt-5.5)", () => { + const model = getModel("openai-codex", "gpt-5.5"); it.skipIf(!openaiCodexToken)( "should send tool result images in function_call_output", diff --git a/packages/ai/test/responseid.test.ts b/packages/ai/test/responseid.test.ts index 00e6cc5df..61df764b9 100644 --- a/packages/ai/test/responseid.test.ts +++ b/packages/ai/test/responseid.test.ts @@ -114,7 +114,7 @@ describe("responseId E2E Tests", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await expectResponseId(llm, { apiKey: openaiCodexToken }); }); }); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index 00c2b5677..86a70cd23 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -309,10 +309,10 @@ describe("Token Statistics on Abort", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should include token stats when aborted mid-stream", + "gpt-5.5 - should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testTokensOnAbort(llm, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/tool-call-id-normalization.test.ts b/packages/ai/test/tool-call-id-normalization.test.ts index 0655f7548..33ea327ff 100644 --- a/packages/ai/test/tool-call-id-normalization.test.ts +++ b/packages/ai/test/tool-call-id-normalization.test.ts @@ -38,7 +38,7 @@ const echoTool: Tool = { * * 1. Use github-copilot gpt-5.2-codex to generate a tool call * 2. Switch to openrouter openai/gpt-5.2-codex and complete - * 3. Switch to openai-codex gpt-5.2-codex and complete + * 3. Switch to openai-codex gpt-5.5 and complete * * Both should succeed without "call_id too long" errors. */ @@ -117,7 +117,7 @@ describe("Tool Call ID Normalization - Live Handoff", () => { "github-copilot -> openai-codex should normalize pipe-separated IDs", async () => { const copilotModel = getModel("github-copilot", "gpt-5.2-codex"); - const codexModel = getModel("openai-codex", "gpt-5.2-codex"); + const codexModel = getModel("openai-codex", "gpt-5.5"); // Step 1: Generate tool call with github-copilot const userMessage: Message = { @@ -266,7 +266,7 @@ describe("Tool Call ID Normalization - Prefilled Context", () => { it.skipIf(!codexToken)( "openai-codex should handle prefilled context with long pipe-separated IDs", async () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + const model = getModel("openai-codex", "gpt-5.5"); const messages = buildPrefilledMessages(); const response = await completeSimple( diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 18a21f995..fdab93944 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -317,10 +317,10 @@ describe("Tool Call Without Result Tests", () => { describe("OpenAI Codex Provider", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should filter out tool calls without corresponding tool results", + "gpt-5.5 - should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { - const model = getModel("openai-codex", "gpt-5.2-codex"); + const model = getModel("openai-codex", "gpt-5.5"); await testToolCallWithoutResult(model, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index f634a5c33..47303da4c 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -771,10 +771,10 @@ describe("totalTokens field", () => { describe("OpenAI Codex (OAuth)", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should return totalTokens equal to sum of components", + "gpt-5.5 - should return totalTokens equal to sum of components", { retry: 3, timeout: 60000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); console.log(`\nOpenAI Codex / ${llm.id}:`); const { first, second } = await testTotalTokensWithCache(llm, { apiKey: openaiCodexToken }); diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index 5b348ca55..568de1747 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -746,28 +746,28 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { describe("OpenAI Codex Provider Unicode Handling", () => { it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle emoji in tool results", + "gpt-5.5 - should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testEmojiInToolResults(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle real-world LinkedIn comment data with emoji", + "gpt-5.5 - should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testRealWorldLinkedInData(llm, { apiKey: openaiCodexToken }); }, ); it.skipIf(!openaiCodexToken)( - "gpt-5.2-codex - should handle unpaired high surrogate (0xD83D) in tool results", + "gpt-5.5 - should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { - const llm = getModel("openai-codex", "gpt-5.2-codex"); + const llm = getModel("openai-codex", "gpt-5.5"); await testUnpairedHighSurrogate(llm, { apiKey: openaiCodexToken }); }, ); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0f86cf10b..4d2115c0e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,9 +2,16 @@ ## [Unreleased] +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ### Fixed - Fixed compaction summary calls to use custom agent stream functions, preserving proxy-backed LLM routing ([#4484](https://github.com/earendil-works/pi/issues/4484)). +- Fixed user-scoped npm pi packages to install under `~/.pi/agent/npm/` instead of npm's global package root, avoiding permission errors with system-managed Node installs ([#4587](https://github.com/earendil-works/pi/issues/4587)). +- Fixed Mistral requests failing after the global fetch proxy/timeout workaround by removing the custom fetch override and using undici 8 dispatcher support instead ([#4619](https://github.com/earendil-works/pi/issues/4619)). +- Fixed default output token requests for models whose advertised output limit is effectively their full context window, avoiding impossible provider requests inherited from `@earendil-works/pi-ai` ([#4614](https://github.com/earendil-works/pi/issues/4614)). ## [0.74.1] - 2026-05-16 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 44e1bce1d..fedc53d26 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -406,7 +406,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 global npm. Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). 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 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`: diff --git a/packages/coding-agent/docs/packages.md b/packages/coding-agent/docs/packages.md index e0a2f2c2b..575f0accf 100644 --- a/packages/coding-agent/docs/packages.md +++ b/packages/coding-agent/docs/packages.md @@ -36,7 +36,7 @@ pi update npm:@foo/bar # update one package pi update --extension npm:@foo/bar ``` -By default, `install` and `remove` write to global 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. +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. To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only: @@ -57,7 +57,7 @@ npm:pkg ``` - Versioned specs are pinned and skipped by package updates (`pi update`, `pi update --extensions`). -- Global installs use `npm install -g`. +- 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`. diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 05b38fa17..befd4da34 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -153,9 +153,7 @@ When a provider requests a retry delay longer than `retry.provider.maxRetryDelay } ``` -`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers. - -Normally the package manager's global modules location is queried using `root -g`. As a special case, if the first element of `npmCommand` is `"bun"`, the modules location will instead be queried with `pm bin -g`. +`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. User-scoped npm packages install under `~/.pi/agent/npm/`; project-scoped npm packages install under `.pi/npm/`. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers. ### Sessions diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 98e50c6e1..356b42c1f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -52,7 +52,7 @@ "minimatch": "^10.2.3", "proper-lockfile": "^4.1.2", "typebox": "^1.1.24", - "undici": "^7.19.1", + "undici": "^8.3.0", "yaml": "^2.8.2" }, "overrides": { @@ -90,6 +90,6 @@ "directory": "packages/coding-agent" }, "engines": { - "node": ">=20.6.0" + "node": ">=22.19.0" } } diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index dcfb4bba0..6e0e8403a 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,7 +5,7 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -import { EnvHttpProxyAgent, setGlobalDispatcher, fetch as undiciFetch } from "undici"; +import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; import { APP_NAME } from "./config.js"; import { main } from "./main.js"; @@ -17,15 +17,6 @@ process.emitWarning = (() => {}) as typeof process.emitWarning; // (e.g. vLLM buffering a large tool call) exceed that and abort the SSE stream // with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own // AbortController-based deadlines via retry.provider.timeoutMs. -// Node 26 uses an internal undici for globalThis.fetch that does not honor npm -// undici's global dispatcher, so route global fetch through npm undici as well. -const dispatcher = new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }); -setGlobalDispatcher(dispatcher); -const fetchWithDispatcher = undiciFetch as unknown as typeof fetch; -globalThis.fetch = (input, init) => - fetchWithDispatcher(input, { - ...init, - dispatcher, - } as unknown as RequestInit); +setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 })); main(process.argv.slice(2)); diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 27c18fd48..92f9bedee 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1084,7 +1084,7 @@ export class DefaultPackageManager implements PackageManager { } private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise { - const installedPath = this.getNpmInstallPath(source, scope); + const installedPath = this.getManagedNpmInstallPath(source, scope); const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined; if (!installedVersion) { return true; @@ -1114,13 +1114,9 @@ export class DefaultPackageManager implements PackageManager { } private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise { - if (scope === "user") { - await this.runNpmCommand(["install", "-g", ...specs]); - return; - } const installRoot = this.getNpmInstallRoot(scope, false); this.ensureNpmProject(installRoot); - await this.runNpmCommand(["install", ...specs, "--prefix", installRoot]); + await this.runNpmCommand(this.getNpmInstallArgs(specs, installRoot)); } async checkForAvailableUpdates(): Promise { @@ -1669,6 +1665,14 @@ export class DefaultPackageManager implements PackageManager { return { command, args }; } + private getPackageManagerName(): string { + const npmCommand = this.getNpmCommand(); + const commandParts = [npmCommand.command, ...npmCommand.args]; + const separatorIndex = commandParts.lastIndexOf("--"); + const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command; + return packageManagerCommand ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") : ""; + } + private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise { const npmCommand = this.getNpmCommand(); await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options); @@ -1687,25 +1691,32 @@ export class DefaultPackageManager implements PackageManager { return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]); } - private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise { - if (scope === "user" && !temporary) { - await this.runNpmCommand(["install", "-g", source.spec]); - return; + private getNpmInstallArgs(specs: string[], installRoot: string): string[] { + const packageManagerName = this.getPackageManagerName(); + if (packageManagerName === "bun") { + return ["install", ...specs, "--cwd", installRoot]; } + if (packageManagerName === "pnpm") { + return ["install", ...specs, "--prefix", installRoot, "--config.strict-dep-builds=false"]; + } + return ["install", ...specs, "--prefix", installRoot]; + } + + private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise { const installRoot = this.getNpmInstallRoot(scope, temporary); this.ensureNpmProject(installRoot); - await this.runNpmCommand(["install", source.spec, "--prefix", installRoot]); + await this.runNpmCommand(this.getNpmInstallArgs([source.spec], installRoot)); } private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise { - if (scope === "user") { - await this.runNpmCommand(["uninstall", "-g", source.name]); - return; - } const installRoot = this.getNpmInstallRoot(scope, false); if (!existsSync(installRoot)) { return; } + if (this.getPackageManagerName() === "bun") { + await this.runNpmCommand(["uninstall", source.name, "--cwd", installRoot]); + return; + } await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]); } @@ -1836,7 +1847,7 @@ export class DefaultPackageManager implements PackageManager { if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm"); } - return join(this.getGlobalNpmRoot(), ".."); + return join(this.agentDir, "npm"); } private getGlobalNpmRoot(): string { @@ -1845,8 +1856,7 @@ export class DefaultPackageManager implements PackageManager { if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) { return this.globalNpmRoot; } - const isBunPackageManager = npmCommand.command === "bun"; - if (isBunPackageManager) { + if (this.getPackageManagerName() === "bun") { const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim(); this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules"); } else { @@ -1857,14 +1867,7 @@ export class DefaultPackageManager implements PackageManager { } private getPnpmGlobalPackagePath(packageName: string): string | undefined { - const npmCommand = this.getNpmCommand(); - const commandParts = [npmCommand.command, ...npmCommand.args]; - const separatorIndex = commandParts.lastIndexOf("--"); - const packageManagerCommand = separatorIndex >= 0 ? commandParts[separatorIndex + 1] : npmCommand.command; - const packageManagerName = packageManagerCommand - ? basename(packageManagerCommand).replace(/\.(cmd|exe)$/i, "") - : ""; - if (packageManagerName !== "pnpm") { + if (this.getPackageManagerName() !== "pnpm") { return undefined; } @@ -1877,14 +1880,31 @@ export class DefaultPackageManager implements PackageManager { return undefined; } - private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { + private getManagedNpmInstallPath(source: NpmSource, scope: SourceScope): string { if (scope === "temporary") { return join(this.getTemporaryDir("npm"), "node_modules", source.name); } if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name); } - return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name); + return join(this.agentDir, "npm", "node_modules", source.name); + } + + private getLegacyGlobalNpmInstallPath(source: NpmSource): string | undefined { + try { + return this.getPnpmGlobalPackagePath(source.name) ?? join(this.getGlobalNpmRoot(), source.name); + } catch { + return undefined; + } + } + + private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { + const managedPath = this.getManagedNpmInstallPath(source, scope); + if (scope !== "user" || existsSync(managedPath)) { + return managedPath; + } + const legacyPath = this.getLegacyGlobalNpmInstallPath(source); + return legacyPath && existsSync(legacyPath) ? legacyPath : managedPath; } private getGitInstallPath(source: GitSource, scope: SourceScope): string { diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 6e4749926..3675e4133 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -59,11 +59,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string { // Append project context files if (contextFiles.length > 0) { - prompt += "\n\n# Project Context\n\n"; + prompt += "\n\n\n\n"; prompt += "Project-specific instructions and guidelines:\n\n"; for (const { path: filePath, content } of contextFiles) { - prompt += `## ${filePath}\n\n${content}\n\n`; + prompt += `\n${content}\n\n\n`; } + prompt += "\n"; } // Append skills section (only if read tool is available) diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 7c0fef674..edd2ce959 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -645,7 +645,28 @@ Content`, expect(runCommandSpy).toHaveBeenCalledWith( "mise", - ["exec", "node@20", "--", "npm", "install", "-g", "@scope/pkg"], + ["exec", "node@20", "--", "npm", "install", "@scope/pkg", "--prefix", join(agentDir, "npm")], + undefined, + ); + }); + + it("should use bun --cwd for npm package installs", async () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["mise", "exec", "bun@1", "--", "bun"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); + + await packageManager.install("npm:@scope/pkg"); + + expect(runCommandSpy).toHaveBeenCalledWith( + "mise", + ["exec", "bun@1", "--", "bun", "install", "@scope/pkg", "--cwd", join(agentDir, "npm")], undefined, ); }); @@ -799,7 +820,7 @@ Content`, expect(runCommandSyncSpy).toHaveBeenNthCalledWith(2, "mise", ["exec", "node@22", "--", "npm", "root", "-g"]); }); - it("should resolve pnpm global package paths from pnpm list output", async () => { + it("should install user npm packages into the pi-managed npm root", async () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["pnpm"], packages: ["npm:pnpm-pkg"], @@ -810,38 +831,25 @@ Content`, settingsManager, }); - const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); - const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); - let installed = false; - - vi.spyOn(packageManager as any, "runCommandSync").mockImplementation((...callArgs: unknown[]) => { - const [command, args] = callArgs as [string, string[]]; - if (command !== "pnpm") { - throw new Error(`unexpected command ${command}`); - } - if (args.join(" ") === "list -g --depth 0 --json") { - return JSON.stringify([ - { - path: pnpmRoot, - dependencies: installed ? { "pnpm-pkg": { version: "1.0.0", path: packagePath } } : {}, - }, - ]); - } - if (args.join(" ") === "root -g") { - return pnpmRoot; - } - throw new Error(`unexpected args ${args.join(" ")}`); + const packagePath = join(agentDir, "npm", "node_modules", "pnpm-pkg"); + vi.spyOn(packageManager as any, "runCommandSync").mockImplementation(() => { + throw new Error("legacy lookup unavailable"); }); const runCommandSpy = vi .spyOn(packageManager as any, "runCommand") .mockImplementation(async (...callArgs: unknown[]) => { const [command, args] = callArgs as [string, string[]]; expect(command).toBe("pnpm"); - expect(args).toEqual(["install", "-g", "pnpm-pkg"]); + expect(args).toEqual([ + "install", + "pnpm-pkg", + "--prefix", + join(agentDir, "npm"), + "--config.strict-dep-builds=false", + ]); mkdirSync(join(packagePath, "extensions"), { recursive: true }); writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" })); writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};"); - installed = true; }); const first = await packageManager.resolve(); @@ -857,6 +865,49 @@ Content`, expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); }); + it("should load legacy pnpm global package paths from pnpm list output", async () => { + settingsManager = SettingsManager.inMemory({ + npmCommand: ["pnpm"], + packages: ["npm:pnpm-pkg"], + }); + packageManager = new DefaultPackageManager({ + cwd: tempDir, + agentDir, + settingsManager, + }); + + const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); + const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); + mkdirSync(join(packagePath, "extensions"), { recursive: true }); + writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" })); + writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};"); + + vi.spyOn(packageManager as any, "runCommandSync").mockImplementation((...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + if (command !== "pnpm") { + throw new Error(`unexpected command ${command}`); + } + if (args.join(" ") === "list -g --depth 0 --json") { + return JSON.stringify([ + { + path: pnpmRoot, + dependencies: { "pnpm-pkg": { version: "1.0.0", path: packagePath } }, + }, + ]); + } + throw new Error(`unexpected args ${args.join(" ")}`); + }); + const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); + + const result = await packageManager.resolve(); + + expect( + result.extensions.some((r) => r.path === join(packagePath, "extensions", "index.ts") && r.enabled), + ).toBe(true); + expect(runCommandSpy).not.toHaveBeenCalled(); + expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); + }); + it("should resolve wrapped pnpm global package paths from pnpm list output", () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["mise", "exec", "node@20", "--", "pnpm"], @@ -883,7 +934,7 @@ Content`, expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); }); - it("should fail when pnpm global package list is malformed", () => { + it("should ignore malformed legacy pnpm global package lists", () => { settingsManager = SettingsManager.inMemory({ npmCommand: ["pnpm"], }); @@ -895,7 +946,7 @@ Content`, vi.spyOn(packageManager as any, "runCommandSync").mockReturnValue("not json"); - expect(() => packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toThrow(); + expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBeUndefined(); }); }); @@ -1829,12 +1880,42 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(runCommandSpy).not.toHaveBeenCalled(); }); - it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => { - vi.spyOn(packageManager as any, "getGlobalNpmRoot").mockReturnValue(join(agentDir, "node_modules")); + it("should migrate legacy user npm installs into the managed npm root during update", async () => { + const legacyRoot = join(tempDir, "legacy-global", "node_modules"); + const legacyPath = join(legacyRoot, "legacy-pkg"); + const managedPath = join(agentDir, "npm", "node_modules", "legacy-pkg"); + mkdirSync(legacyPath, { recursive: true }); + writeFileSync(join(legacyPath, "package.json"), JSON.stringify({ name: "legacy-pkg", version: "1.0.0" })); + settingsManager.setPackages(["npm:legacy-pkg"]); - const userOldPath = join(agentDir, "node_modules", "user-old"); - const userCurrentPath = join(agentDir, "node_modules", "user-current"); - const userUnknownPath = join(agentDir, "node_modules", "user-unknown"); + vi.spyOn(packageManager as any, "getGlobalNpmRoot").mockReturnValue(legacyRoot); + const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.0.0"'); + const runCommandSpy = vi + .spyOn(packageManager as any, "runCommand") + .mockImplementation(async (...callArgs: unknown[]) => { + const [command, args] = callArgs as [string, string[]]; + expect(command).toBe("npm"); + expect(args).toEqual(["install", "legacy-pkg@latest", "--prefix", join(agentDir, "npm")]); + mkdirSync(managedPath, { recursive: true }); + writeFileSync( + join(managedPath, "package.json"), + JSON.stringify({ name: "legacy-pkg", version: "1.0.0" }), + ); + }); + + expect(packageManager.getInstalledPath("npm:legacy-pkg", "user")).toBe(legacyPath); + + await packageManager.update("npm:legacy-pkg"); + + expect(runCommandCaptureSpy).not.toHaveBeenCalled(); + expect(runCommandSpy).toHaveBeenCalledTimes(1); + expect(packageManager.getInstalledPath("npm:legacy-pkg", "user")).toBe(managedPath); + }); + + it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => { + const userOldPath = join(agentDir, "npm", "node_modules", "user-old"); + const userCurrentPath = join(agentDir, "npm", "node_modules", "user-current"); + const userUnknownPath = join(agentDir, "npm", "node_modules", "user-unknown"); const projectOldPath = join(tempDir, ".pi", "npm", "node_modules", "project-old"); const projectCurrentPath = join(tempDir, ".pi", "npm", "node_modules", "project-current"); const installPaths = [userOldPath, userCurrentPath, userUnknownPath, projectOldPath, projectCurrentPath]; @@ -1924,7 +2005,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(runCommandSpy).toHaveBeenNthCalledWith( 1, "npm", - ["install", "-g", "user-old@latest", "user-unknown@latest"], + ["install", "user-old@latest", "user-unknown@latest", "--prefix", join(agentDir, "npm")], undefined, ); expect(runCommandSpy).toHaveBeenNthCalledWith( diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 44b716f9d..08fe3e5ed 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Breaking Changes + +- Raised the minimum supported Node.js version to 22.19.0. + ## [0.74.1] - 2026-05-16 ### Added diff --git a/packages/tui/package.json b/packages/tui/package.json index 3896eb889..c52be27fd 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -32,7 +32,7 @@ "directory": "packages/tui" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.19.0" }, "types": "./dist/index.d.ts", "dependencies": {