Merge branch 'main' into stream-fn-in-compaction

This commit is contained in:
Mario Zechner
2026-05-17 20:59:53 +02:00
committed by GitHub
36 changed files with 396 additions and 343 deletions

View File

@@ -209,3 +209,5 @@ maximilianzuern pr
brianmichel pr brianmichel pr
abhinavmathur-atlan pr abhinavmathur-atlan pr
mattiacerutti pr

View File

@@ -17,11 +17,8 @@ jobs:
script: | script: |
const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS'; const APPROVED_FILE = '.github/APPROVED_CONTRIBUTORS';
const VALID_CAPABILITIES = new Set(['issue', 'pr']); 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 issueAuthor = context.payload.issue.user.login;
const defaultBranch = context.payload.repository.default_branch; 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]') { if (issueAuthor.endsWith('[bot]') || issueAuthor === 'dependabot[bot]') {
console.log(`Skipping bot: ${issueAuthor}`); console.log(`Skipping bot: ${issueAuthor}`);
@@ -97,36 +94,15 @@ jobs:
return; return;
} }
function buildNormalGateMessage() { const message = [
return [ 'This issue was auto-closed. All issues from new contributors are auto-closed by default.',
'This issue was auto-closed. All issues from new contributors are auto-closed by default.', '',
...(isFridayThroughSunday `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.`,
? [ '',
'Issues submitted Friday through Sunday are not reviewed. If this is urgent, ask on Discord: https://discord.com/invite/3cU7Bz4UPx', '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');
`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();
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner: context.repo.owner, owner: context.repo.owner,
@@ -135,33 +111,6 @@ jobs:
body: message, 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({ await github.rest.issues.update({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,

20
package-lock.json generated
View File

@@ -31,7 +31,7 @@
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -6842,12 +6842,12 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "7.25.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz",
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20.18.1" "node": ">=22.19.0"
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
@@ -7787,7 +7787,7 @@
"vitest": "^3.2.4" "vitest": "^3.2.4"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
} }
}, },
"packages/agent/node_modules/@types/node": { "packages/agent/node_modules/@types/node": {
@@ -7831,7 +7831,7 @@
"vitest": "^3.2.4" "vitest": "^3.2.4"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
} }
}, },
"packages/ai/node_modules/@types/node": { "packages/ai/node_modules/@types/node": {
@@ -7870,7 +7870,7 @@
"minimatch": "^10.2.3", "minimatch": "^10.2.3",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"typebox": "^1.1.24", "typebox": "^1.1.24",
"undici": "^7.19.1", "undici": "^8.3.0",
"yaml": "^2.8.2" "yaml": "^2.8.2"
}, },
"bin": { "bin": {
@@ -7887,7 +7887,7 @@
"vitest": "^3.2.4" "vitest": "^3.2.4"
}, },
"engines": { "engines": {
"node": ">=20.6.0" "node": ">=22.19.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@mariozechner/clipboard": "^0.3.6" "@mariozechner/clipboard": "^0.3.6"
@@ -7961,7 +7961,7 @@
"chalk": "^5.5.0" "chalk": "^5.5.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"koffi": "^2.9.0" "koffi": "^2.9.0"

View File

@@ -45,7 +45,7 @@
"shx": "^0.4.0" "shx": "^0.4.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
}, },
"version": "0.0.3", "version": "0.0.3",
"dependencies": { "dependencies": {

View File

@@ -1,5 +1,11 @@
# Changelog # Changelog
## [Unreleased]
### Breaking Changes
- Raised the minimum supported Node.js version to 22.19.0.
## [0.74.1] - 2026-05-16 ## [0.74.1] - 2026-05-16
## [0.74.0] - 2026-05-07 ## [0.74.0] - 2026-05-07

View File

@@ -50,7 +50,7 @@
"directory": "packages/agent" "directory": "packages/agent"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.3.0",

View File

@@ -1,5 +1,15 @@
# Changelog # 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 ## [0.74.1] - 2026-05-16
### Added ### Added

View File

@@ -97,7 +97,7 @@
"directory": "packages/ai" "directory": "packages/ai"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.3.0",

View File

@@ -203,6 +203,9 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
) { ) {
mergeThinkingLevelMap(model, { off: null }); mergeThinkingLevelMap(model, { off: null });
} }
if (model.provider === "github-copilot" && model.id.startsWith("gpt-5")) {
mergeThinkingLevelMap(model, { minimal: "low" });
}
if ( if (
model.api === "openai-responses" && model.api === "openai-responses" &&
model.provider === "openai" && model.provider === "openai" &&
@@ -237,9 +240,6 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) { if (model.provider === "openai-codex" && supportsOpenAiXhigh(model.id)) {
mergeThinkingLevelMap(model, { minimal: "low" }); 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")) { if (model.provider === "openrouter" && model.id.startsWith("inception/mercury-2")) {
// Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling. // Mercury 2 in instant mode (reasoning_effort: "none") disables tool calling.
// Mark "off" unsupported so the openai-completions provider omits the reasoning param // 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_CONTEXT = 272000;
const CODEX_MAX_TOKENS = 128000; const CODEX_MAX_TOKENS = 128000;
const codexModels: Model<"openai-codex-responses">[] = [ 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", id: "gpt-5.2",
name: "GPT-5.2", name: "GPT-5.2",
@@ -1545,18 +1509,6 @@ async function generateModels() {
contextWindow: CODEX_CONTEXT, contextWindow: CODEX_CONTEXT,
maxTokens: CODEX_MAX_TOKENS, 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", id: "gpt-5.3-codex",
name: "GPT-5.3 Codex", name: "GPT-5.3 Codex",
@@ -1569,6 +1521,18 @@ async function generateModels() {
contextWindow: CODEX_CONTEXT, contextWindow: CODEX_CONTEXT,
maxTokens: CODEX_MAX_TOKENS, 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", id: "gpt-5.4",
name: "GPT-5.4", name: "GPT-5.4",
@@ -1581,6 +1545,42 @@ async function generateModels() {
contextWindow: CODEX_CONTEXT, contextWindow: CODEX_CONTEXT,
maxTokens: CODEX_MAX_TOKENS, 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", id: "gpt-5.5",
name: "GPT-5.5", name: "GPT-5.5",
@@ -1594,29 +1594,17 @@ async function generateModels() {
maxTokens: CODEX_MAX_TOKENS, maxTokens: CODEX_MAX_TOKENS,
}, },
{ {
id: "gpt-5.4-mini", id: "gpt-5.5-fast",
name: "GPT-5.4 Mini", name: "GPT-5.5 Fast",
api: "openai-codex-responses", api: "openai-codex-responses",
provider: "openai-codex", provider: "openai-codex",
baseUrl: CODEX_BASE_URL, baseUrl: CODEX_BASE_URL,
reasoning: true, reasoning: true,
input: ["text", "image"], 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, contextWindow: CODEX_CONTEXT,
maxTokens: CODEX_MAX_TOKENS, 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); allModels.push(...codexModels);

View File

@@ -4005,7 +4005,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null}, thinkingLevelMap: {"off":null,"minimal":"low"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -4024,7 +4024,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -4043,7 +4043,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -4062,7 +4062,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -4081,7 +4081,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -4100,7 +4100,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -4119,7 +4119,7 @@ export const MODELS = {
baseUrl: "https://api.individual.githubcopilot.com", 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"}, 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, reasoning: true,
thinkingLevelMap: {"off":null,"xhigh":"xhigh"}, thinkingLevelMap: {"off":null,"minimal":"low","xhigh":"xhigh"},
input: ["text", "image"], input: ["text", "image"],
cost: { cost: {
input: 0, input: 0,
@@ -7155,58 +7155,6 @@ export const MODELS = {
} satisfies Model<"openai-responses">, } satisfies Model<"openai-responses">,
}, },
"openai-codex": { "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": { "gpt-5.2": {
id: "gpt-5.2", id: "gpt-5.2",
name: "GPT-5.2", name: "GPT-5.2",
@@ -7225,24 +7173,6 @@ export const MODELS = {
contextWindow: 272000, contextWindow: 272000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"openai-codex-responses">, } 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": { "gpt-5.3-codex": {
id: "gpt-5.3-codex", id: "gpt-5.3-codex",
name: "GPT-5.3 Codex", name: "GPT-5.3 Codex",
@@ -7271,12 +7201,12 @@ export const MODELS = {
thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"}, thinkingLevelMap: {"xhigh":"xhigh","minimal":"low"},
input: ["text"], input: ["text"],
cost: { cost: {
input: 0, input: 1.75,
output: 0, output: 14,
cacheRead: 0, cacheRead: 0.175,
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 128000, contextWindow: 272000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"openai-codex-responses">, } satisfies Model<"openai-codex-responses">,
"gpt-5.4": { "gpt-5.4": {
@@ -7297,9 +7227,27 @@ export const MODELS = {
contextWindow: 272000, contextWindow: 272000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"openai-codex-responses">, } 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": { "gpt-5.4-mini": {
id: "gpt-5.4-mini", id: "gpt-5.4-mini",
name: "GPT-5.4 Mini", name: "GPT-5.4 mini",
api: "openai-codex-responses", api: "openai-codex-responses",
provider: "openai-codex", provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api", baseUrl: "https://chatgpt.com/backend-api",
@@ -7315,6 +7263,24 @@ export const MODELS = {
contextWindow: 272000, contextWindow: 272000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"openai-codex-responses">, } 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": { "gpt-5.5": {
id: "gpt-5.5", id: "gpt-5.5",
name: "GPT-5.5", name: "GPT-5.5",
@@ -7333,6 +7299,24 @@ export const MODELS = {
contextWindow: 272000, contextWindow: 272000,
maxTokens: 128000, maxTokens: 128000,
} satisfies Model<"openai-codex-responses">, } 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": { "opencode": {
"big-pickle": { "big-pickle": {

View File

@@ -1,9 +1,19 @@
import type { Api, Model, SimpleStreamOptions, StreamOptions, ThinkingBudgets, ThinkingLevel } from "../types.js"; 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<Api>, options?: SimpleStreamOptions, apiKey?: string): StreamOptions { export function buildBaseOptions(model: Model<Api>, 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 { return {
temperature: options?.temperature, temperature: options?.temperature,
maxTokens: options?.maxTokens ?? (model.maxTokens > 0 ? model.maxTokens : undefined), maxTokens: options?.maxTokens ?? defaultMaxTokens,
signal: options?.signal, signal: options?.signal,
apiKey: apiKey || options?.apiKey, apiKey: apiKey || options?.apiKey,
transport: options?.transport, transport: options?.transport,

View File

@@ -276,12 +276,12 @@ describe("AI Providers Abort Tests", () => {
describe("OpenAI Codex Provider Abort", () => { describe("OpenAI Codex Provider Abort", () => {
it.skipIf(!openaiCodexToken)("should abort mid-stream", { retry: 3 }, async () => { 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 }); await testAbortSignal(llm, { apiKey: openaiCodexToken });
}); });
it.skipIf(!openaiCodexToken)("should handle immediate abort", { retry: 3 }, async () => { 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 }); await testImmediateAbort(llm, { apiKey: openaiCodexToken });
}); });
}); });

View File

@@ -228,9 +228,9 @@ describe("Context overflow error handling", () => {
describe("OpenAI Codex (OAuth)", () => { describe("OpenAI Codex (OAuth)", () => {
it.skipIf(!openaiCodexToken)( it.skipIf(!openaiCodexToken)(
"gpt-5.2-codex - should detect overflow via isContextOverflow", "gpt-5.5 - should detect overflow via isContextOverflow",
async () => { async () => {
const model = getModel("openai-codex", "gpt-5.2-codex"); const model = getModel("openai-codex", "gpt-5.5");
const result = await testContextOverflow(model, openaiCodexToken!); const result = await testContextOverflow(model, openaiCodexToken!);
logResult(result); logResult(result);

View File

@@ -67,7 +67,7 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [
{ provider: "openai", model: "gpt-5-mini", label: "openai-responses-gpt-5-mini" }, { 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" }, { provider: "azure-openai-responses", model: "gpt-4o-mini", label: "azure-openai-responses-gpt-4o-mini" },
// OpenAI Codex // 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 // GitHub Copilot
{ provider: "github-copilot", model: "claude-sonnet-4.5", label: "copilot-claude-sonnet-4.5" }, { 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" }, { provider: "github-copilot", model: "gpt-5.1-codex", label: "copilot-gpt-5.1-codex" },

View File

@@ -704,37 +704,37 @@ describe("AI Providers Empty Message Tests", () => {
describe("OpenAI Codex Provider Empty Messages", () => { describe("OpenAI Codex Provider Empty Messages", () => {
it.skipIf(!openaiCodexToken)( it.skipIf(!openaiCodexToken)(
"gpt-5.2-codex - should handle empty content array", "gpt-5.5 - should handle empty content array",
{ retry: 3, timeout: 30000 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testEmptyMessage(llm, { apiKey: openaiCodexToken }); await testEmptyMessage(llm, { apiKey: openaiCodexToken });
}, },
); );
it.skipIf(!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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testEmptyStringMessage(llm, { apiKey: openaiCodexToken }); await testEmptyStringMessage(llm, { apiKey: openaiCodexToken });
}, },
); );
it.skipIf(!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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testWhitespaceOnlyMessage(llm, { apiKey: openaiCodexToken }); await testWhitespaceOnlyMessage(llm, { apiKey: openaiCodexToken });
}, },
); );
it.skipIf(!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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testEmptyAssistantMessage(llm, { apiKey: openaiCodexToken }); await testEmptyAssistantMessage(llm, { apiKey: openaiCodexToken });
}, },
); );

View File

@@ -481,19 +481,19 @@ describe("Tool Results with Images", () => {
describe("OpenAI Codex Provider", () => { describe("OpenAI Codex Provider", () => {
it.skipIf(!openaiCodexToken)( 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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await handleToolWithImageResult(llm, { apiKey: openaiCodexToken }); await handleToolWithImageResult(llm, { apiKey: openaiCodexToken });
}, },
); );
it.skipIf(!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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await handleToolWithTextAndImageResult(llm, { apiKey: openaiCodexToken }); await handleToolWithTextAndImageResult(llm, { apiKey: openaiCodexToken });
}, },
); );

View File

@@ -8,7 +8,7 @@ const codexToken = await resolveApiKey("openai-codex");
describe("openai-codex cache affinity e2e", () => { describe("openai-codex cache affinity e2e", () => {
it.skipIf(!codexToken)("handles SSE requests with aligned cache-affinity identifiers", async () => { 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 sessionId = "0195d6e4-4cf9-7f44-a2d8-f8f7f49ee9d3";
const context: Context = { const context: Context = {
systemPrompt: "You are a helpful assistant. Reply exactly as requested.", systemPrompt: "You are a helpful assistant. Reply exactly as requested.",

View File

@@ -18,7 +18,7 @@ const usage: Usage = {
describe("OpenAI Responses foreign tool call ID normalization", () => { describe("OpenAI Responses foreign tool call ID normalization", () => {
it("hashes foreign Copilot tool item IDs into a bounded Codex-safe fc_<hash> shape", () => { it("hashes foreign Copilot tool item IDs into a bounded Codex-safe fc_<hash> shape", () => {
const model = getModel("openai-codex", "gpt-5.3-codex"); const model = getModel("openai-codex", "gpt-5.5");
const assistant: AssistantMessage = { const assistant: AssistantMessage = {
role: "assistant", role: "assistant",
content: [ content: [
@@ -31,7 +31,7 @@ describe("OpenAI Responses foreign tool call ID normalization", () => {
], ],
api: "openai-responses", api: "openai-responses",
provider: "github-copilot", provider: "github-copilot",
model: "gpt-5.3-codex", model: "gpt-5.5",
usage, usage,
stopReason: "toolUse", stopReason: "toolUse",
timestamp: Date.now() - 2000, timestamp: Date.now() - 2000,

View File

@@ -183,8 +183,8 @@ describe("Responses API tool result images", () => {
); );
}); });
describe("OpenAI Codex Responses Provider (gpt-5.2-codex)", () => { describe("OpenAI Codex Responses Provider (gpt-5.5)", () => {
const model = getModel("openai-codex", "gpt-5.2-codex"); const model = getModel("openai-codex", "gpt-5.5");
it.skipIf(!openaiCodexToken)( it.skipIf(!openaiCodexToken)(
"should send tool result images in function_call_output", "should send tool result images in function_call_output",

View File

@@ -114,7 +114,7 @@ describe("responseId E2E Tests", () => {
describe("OpenAI Codex Provider", () => { describe("OpenAI Codex Provider", () => {
it.skipIf(!openaiCodexToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => { 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 }); await expectResponseId(llm, { apiKey: openaiCodexToken });
}); });
}); });

View File

@@ -309,10 +309,10 @@ describe("Token Statistics on Abort", () => {
describe("OpenAI Codex Provider", () => { describe("OpenAI Codex Provider", () => {
it.skipIf(!openaiCodexToken)( 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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testTokensOnAbort(llm, { apiKey: openaiCodexToken }); await testTokensOnAbort(llm, { apiKey: openaiCodexToken });
}, },
); );

View File

@@ -38,7 +38,7 @@ const echoTool: Tool<typeof echoToolSchema> = {
* *
* 1. Use github-copilot gpt-5.2-codex to generate a tool call * 1. Use github-copilot gpt-5.2-codex to generate a tool call
* 2. Switch to openrouter openai/gpt-5.2-codex and complete * 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. * 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", "github-copilot -> openai-codex should normalize pipe-separated IDs",
async () => { async () => {
const copilotModel = getModel("github-copilot", "gpt-5.2-codex"); 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 // Step 1: Generate tool call with github-copilot
const userMessage: Message = { const userMessage: Message = {
@@ -266,7 +266,7 @@ describe("Tool Call ID Normalization - Prefilled Context", () => {
it.skipIf(!codexToken)( it.skipIf(!codexToken)(
"openai-codex should handle prefilled context with long pipe-separated IDs", "openai-codex should handle prefilled context with long pipe-separated IDs",
async () => { async () => {
const model = getModel("openai-codex", "gpt-5.2-codex"); const model = getModel("openai-codex", "gpt-5.5");
const messages = buildPrefilledMessages(); const messages = buildPrefilledMessages();
const response = await completeSimple( const response = await completeSimple(

View File

@@ -317,10 +317,10 @@ describe("Tool Call Without Result Tests", () => {
describe("OpenAI Codex Provider", () => { describe("OpenAI Codex Provider", () => {
it.skipIf(!openaiCodexToken)( 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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const model = getModel("openai-codex", "gpt-5.2-codex"); const model = getModel("openai-codex", "gpt-5.5");
await testToolCallWithoutResult(model, { apiKey: openaiCodexToken }); await testToolCallWithoutResult(model, { apiKey: openaiCodexToken });
}, },
); );

View File

@@ -771,10 +771,10 @@ describe("totalTokens field", () => {
describe("OpenAI Codex (OAuth)", () => { describe("OpenAI Codex (OAuth)", () => {
it.skipIf(!openaiCodexToken)( 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 }, { retry: 3, timeout: 60000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
console.log(`\nOpenAI Codex / ${llm.id}:`); console.log(`\nOpenAI Codex / ${llm.id}:`);
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: openaiCodexToken }); const { first, second } = await testTotalTokensWithCache(llm, { apiKey: openaiCodexToken });

View File

@@ -746,28 +746,28 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
describe("OpenAI Codex Provider Unicode Handling", () => { describe("OpenAI Codex Provider Unicode Handling", () => {
it.skipIf(!openaiCodexToken)( 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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testEmojiInToolResults(llm, { apiKey: openaiCodexToken }); await testEmojiInToolResults(llm, { apiKey: openaiCodexToken });
}, },
); );
it.skipIf(!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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testRealWorldLinkedInData(llm, { apiKey: openaiCodexToken }); await testRealWorldLinkedInData(llm, { apiKey: openaiCodexToken });
}, },
); );
it.skipIf(!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 }, { retry: 3, timeout: 30000 },
async () => { async () => {
const llm = getModel("openai-codex", "gpt-5.2-codex"); const llm = getModel("openai-codex", "gpt-5.5");
await testUnpairedHighSurrogate(llm, { apiKey: openaiCodexToken }); await testUnpairedHighSurrogate(llm, { apiKey: openaiCodexToken });
}, },
); );

View File

@@ -2,9 +2,16 @@
## [Unreleased] ## [Unreleased]
### Breaking Changes
- Raised the minimum supported Node.js version to 22.19.0.
### Fixed ### 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 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 ## [0.74.1] - 2026-05-16

View File

@@ -406,7 +406,7 @@ pi update npm:@foo/pi-tools # update one package
pi config # enable/disable extensions, skills, prompts, themes 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`: Create a package by adding a `pi` key to `package.json`:

View File

@@ -36,7 +36,7 @@ pi update npm:@foo/bar # update one package
pi update --extension npm:@foo/bar 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: 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`). - 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/`. - 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`. - Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`.

View File

@@ -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. `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.
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`.
### Sessions ### Sessions

View File

@@ -52,7 +52,7 @@
"minimatch": "^10.2.3", "minimatch": "^10.2.3",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"typebox": "^1.1.24", "typebox": "^1.1.24",
"undici": "^7.19.1", "undici": "^8.3.0",
"yaml": "^2.8.2" "yaml": "^2.8.2"
}, },
"overrides": { "overrides": {
@@ -90,6 +90,6 @@
"directory": "packages/coding-agent" "directory": "packages/coding-agent"
}, },
"engines": { "engines": {
"node": ">=20.6.0" "node": ">=22.19.0"
} }
} }

View File

@@ -5,7 +5,7 @@
* *
* Test with: npx tsx src/cli-new.ts [args...] * 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 { APP_NAME } from "./config.js";
import { main } from "./main.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 // (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 // with UND_ERR_BODY_TIMEOUT. Disable both — provider SDKs enforce their own
// AbortController-based deadlines via retry.provider.timeoutMs. // AbortController-based deadlines via retry.provider.timeoutMs.
// Node 26 uses an internal undici for globalThis.fetch that does not honor npm setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }));
// 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);
main(process.argv.slice(2)); main(process.argv.slice(2));

View File

@@ -1084,7 +1084,7 @@ export class DefaultPackageManager implements PackageManager {
} }
private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise<boolean> { private async shouldUpdateNpmSource(source: NpmSource, scope: InstalledSourceScope): Promise<boolean> {
const installedPath = this.getNpmInstallPath(source, scope); const installedPath = this.getManagedNpmInstallPath(source, scope);
const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined; const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined;
if (!installedVersion) { if (!installedVersion) {
return true; return true;
@@ -1114,13 +1114,9 @@ export class DefaultPackageManager implements PackageManager {
} }
private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise<void> { private async installNpmBatch(specs: string[], scope: InstalledSourceScope): Promise<void> {
if (scope === "user") {
await this.runNpmCommand(["install", "-g", ...specs]);
return;
}
const installRoot = this.getNpmInstallRoot(scope, false); const installRoot = this.getNpmInstallRoot(scope, false);
this.ensureNpmProject(installRoot); this.ensureNpmProject(installRoot);
await this.runNpmCommand(["install", ...specs, "--prefix", installRoot]); await this.runNpmCommand(this.getNpmInstallArgs(specs, installRoot));
} }
async checkForAvailableUpdates(): Promise<PackageUpdate[]> { async checkForAvailableUpdates(): Promise<PackageUpdate[]> {
@@ -1669,6 +1665,14 @@ export class DefaultPackageManager implements PackageManager {
return { command, args }; 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<void> { private async runNpmCommand(args: string[], options?: { cwd?: string }): Promise<void> {
const npmCommand = this.getNpmCommand(); const npmCommand = this.getNpmCommand();
await this.runCommand(npmCommand.command, [...npmCommand.args, ...args], options); 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]); return this.runCommandSync(npmCommand.command, [...npmCommand.args, ...args]);
} }
private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise<void> { private getNpmInstallArgs(specs: string[], installRoot: string): string[] {
if (scope === "user" && !temporary) { const packageManagerName = this.getPackageManagerName();
await this.runNpmCommand(["install", "-g", source.spec]); if (packageManagerName === "bun") {
return; 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<void> {
const installRoot = this.getNpmInstallRoot(scope, temporary); const installRoot = this.getNpmInstallRoot(scope, temporary);
this.ensureNpmProject(installRoot); 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<void> { private async uninstallNpm(source: NpmSource, scope: SourceScope): Promise<void> {
if (scope === "user") {
await this.runNpmCommand(["uninstall", "-g", source.name]);
return;
}
const installRoot = this.getNpmInstallRoot(scope, false); const installRoot = this.getNpmInstallRoot(scope, false);
if (!existsSync(installRoot)) { if (!existsSync(installRoot)) {
return; return;
} }
if (this.getPackageManagerName() === "bun") {
await this.runNpmCommand(["uninstall", source.name, "--cwd", installRoot]);
return;
}
await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]); await this.runNpmCommand(["uninstall", source.name, "--prefix", installRoot]);
} }
@@ -1836,7 +1847,7 @@ export class DefaultPackageManager implements PackageManager {
if (scope === "project") { if (scope === "project") {
return join(this.cwd, CONFIG_DIR_NAME, "npm"); return join(this.cwd, CONFIG_DIR_NAME, "npm");
} }
return join(this.getGlobalNpmRoot(), ".."); return join(this.agentDir, "npm");
} }
private getGlobalNpmRoot(): string { private getGlobalNpmRoot(): string {
@@ -1845,8 +1856,7 @@ export class DefaultPackageManager implements PackageManager {
if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) { if (this.globalNpmRoot && this.globalNpmRootCommandKey === commandKey) {
return this.globalNpmRoot; return this.globalNpmRoot;
} }
const isBunPackageManager = npmCommand.command === "bun"; if (this.getPackageManagerName() === "bun") {
if (isBunPackageManager) {
const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim(); const binDir = this.runNpmCommandSync(["pm", "bin", "-g"]).trim();
this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules"); this.globalNpmRoot = join(dirname(binDir), "install", "global", "node_modules");
} else { } else {
@@ -1857,14 +1867,7 @@ export class DefaultPackageManager implements PackageManager {
} }
private getPnpmGlobalPackagePath(packageName: string): string | undefined { private getPnpmGlobalPackagePath(packageName: string): string | undefined {
const npmCommand = this.getNpmCommand(); if (this.getPackageManagerName() !== "pnpm") {
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") {
return undefined; return undefined;
} }
@@ -1877,14 +1880,31 @@ export class DefaultPackageManager implements PackageManager {
return undefined; return undefined;
} }
private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { private getManagedNpmInstallPath(source: NpmSource, scope: SourceScope): string {
if (scope === "temporary") { if (scope === "temporary") {
return join(this.getTemporaryDir("npm"), "node_modules", source.name); return join(this.getTemporaryDir("npm"), "node_modules", source.name);
} }
if (scope === "project") { if (scope === "project") {
return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name); 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 { private getGitInstallPath(source: GitSource, scope: SourceScope): string {

View File

@@ -59,11 +59,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
// Append project context files // Append project context files
if (contextFiles.length > 0) { if (contextFiles.length > 0) {
prompt += "\n\n# Project Context\n\n"; prompt += "\n\n<project_context>\n\n";
prompt += "Project-specific instructions and guidelines:\n\n"; prompt += "Project-specific instructions and guidelines:\n\n";
for (const { path: filePath, content } of contextFiles) { for (const { path: filePath, content } of contextFiles) {
prompt += `## ${filePath}\n\n${content}\n\n`; prompt += `<project_instructions path="${filePath}">\n${content}\n</project_instructions>\n\n`;
} }
prompt += "</project_context>\n";
} }
// Append skills section (only if read tool is available) // Append skills section (only if read tool is available)

View File

@@ -645,7 +645,28 @@ Content`,
expect(runCommandSpy).toHaveBeenCalledWith( expect(runCommandSpy).toHaveBeenCalledWith(
"mise", "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, undefined,
); );
}); });
@@ -799,7 +820,7 @@ Content`,
expect(runCommandSyncSpy).toHaveBeenNthCalledWith(2, "mise", ["exec", "node@22", "--", "npm", "root", "-g"]); 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({ settingsManager = SettingsManager.inMemory({
npmCommand: ["pnpm"], npmCommand: ["pnpm"],
packages: ["npm:pnpm-pkg"], packages: ["npm:pnpm-pkg"],
@@ -810,38 +831,25 @@ Content`,
settingsManager, settingsManager,
}); });
const pnpmRoot = join(tempDir, "pnpm", "global", "v11"); const packagePath = join(agentDir, "npm", "node_modules", "pnpm-pkg");
const packagePath = join(pnpmRoot, "20-hash", "node_modules", "pnpm-pkg"); vi.spyOn(packageManager as any, "runCommandSync").mockImplementation(() => {
let installed = false; throw new Error("legacy lookup unavailable");
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 runCommandSpy = vi const runCommandSpy = vi
.spyOn(packageManager as any, "runCommand") .spyOn(packageManager as any, "runCommand")
.mockImplementation(async (...callArgs: unknown[]) => { .mockImplementation(async (...callArgs: unknown[]) => {
const [command, args] = callArgs as [string, string[]]; const [command, args] = callArgs as [string, string[]];
expect(command).toBe("pnpm"); 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 }); mkdirSync(join(packagePath, "extensions"), { recursive: true });
writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" })); writeFileSync(join(packagePath, "package.json"), JSON.stringify({ name: "pnpm-pkg", version: "1.0.0" }));
writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};"); writeFileSync(join(packagePath, "extensions", "index.ts"), "export default function() {};");
installed = true;
}); });
const first = await packageManager.resolve(); const first = await packageManager.resolve();
@@ -857,6 +865,49 @@ Content`,
expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); 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", () => { it("should resolve wrapped pnpm global package paths from pnpm list output", () => {
settingsManager = SettingsManager.inMemory({ settingsManager = SettingsManager.inMemory({
npmCommand: ["mise", "exec", "node@20", "--", "pnpm"], npmCommand: ["mise", "exec", "node@20", "--", "pnpm"],
@@ -883,7 +934,7 @@ Content`,
expect(packageManager.getInstalledPath("npm:pnpm-pkg", "user")).toBe(packagePath); 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({ settingsManager = SettingsManager.inMemory({
npmCommand: ["pnpm"], npmCommand: ["pnpm"],
}); });
@@ -895,7 +946,7 @@ Content`,
vi.spyOn(packageManager as any, "runCommandSync").mockReturnValue("not json"); 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(); expect(runCommandSpy).not.toHaveBeenCalled();
}); });
it("should batch npm updates per scope and run git updates in parallel while skipping pinned and current packages", async () => { it("should migrate legacy user npm installs into the managed npm root during update", async () => {
vi.spyOn(packageManager as any, "getGlobalNpmRoot").mockReturnValue(join(agentDir, "node_modules")); 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"); vi.spyOn(packageManager as any, "getGlobalNpmRoot").mockReturnValue(legacyRoot);
const userCurrentPath = join(agentDir, "node_modules", "user-current"); const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.0.0"');
const userUnknownPath = join(agentDir, "node_modules", "user-unknown"); 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 projectOldPath = join(tempDir, ".pi", "npm", "node_modules", "project-old");
const projectCurrentPath = join(tempDir, ".pi", "npm", "node_modules", "project-current"); const projectCurrentPath = join(tempDir, ".pi", "npm", "node_modules", "project-current");
const installPaths = [userOldPath, userCurrentPath, userUnknownPath, projectOldPath, projectCurrentPath]; const installPaths = [userOldPath, userCurrentPath, userUnknownPath, projectOldPath, projectCurrentPath];
@@ -1924,7 +2005,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te
expect(runCommandSpy).toHaveBeenNthCalledWith( expect(runCommandSpy).toHaveBeenNthCalledWith(
1, 1,
"npm", "npm",
["install", "-g", "user-old@latest", "user-unknown@latest"], ["install", "user-old@latest", "user-unknown@latest", "--prefix", join(agentDir, "npm")],
undefined, undefined,
); );
expect(runCommandSpy).toHaveBeenNthCalledWith( expect(runCommandSpy).toHaveBeenNthCalledWith(

View File

@@ -1,5 +1,11 @@
# Changelog # Changelog
## [Unreleased]
### Breaking Changes
- Raised the minimum supported Node.js version to 22.19.0.
## [0.74.1] - 2026-05-16 ## [0.74.1] - 2026-05-16
### Added ### Added

View File

@@ -32,7 +32,7 @@
"directory": "packages/tui" "directory": "packages/tui"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=22.19.0"
}, },
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"dependencies": { "dependencies": {