@hifullmoon/aicommit 2.2.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,53 @@
1
1
  # Provider compatibility / Provider 兼容表
2
2
 
3
- Built-in setup defaults choose an adapter and endpoint; adapters own request/response dialects; the core owns Git state, user interaction, HTTPS enforcement, retry, timeout, authorization, and machine output.
3
+ AICommit uses **Pi AI** (`@earendil-works/pi-ai`, pinned to 0.85.0) for model request construction, message conversion, SSE decoding, thinking events, and normalized results. Node.js **>=22.19.0** is required. See the [Pi AI documentation](https://github.com/earendil-works/pi/tree/main/packages/ai).
4
4
 
5
- 内置 setup 默认值负责选择 adapter 和端点;adapter 负责请求/响应方言;核心负责 Git 状态、用户交互、HTTPS、重试、超时、鉴权与机器输出。
5
+ AICommit 使用 **Pi AI**(`@earendil-works/pi-ai`,固定为 0.85.0)构造模型请求、转换消息、解析 SSE,并读取统一的推理事件和结果。要求 Node.js **>=22.19.0**。
6
6
 
7
- | Provider / adapter | Endpoint and auth / 端点与鉴权 | Streaming / 流式 | Reasoning / 推理 | Token and usage mapping / token 与 usage | Notes / 说明 |
8
- | ---------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
9
- | OpenAI / `openai` | Official HTTPS Chat Completions; Bearer key / 官方 HTTPS;Bearer key | SSE | Native for recognized `o*`/`gpt-5*`; model-dependent otherwise / 已识别推理模型原生支持 | `max_completion_tokens` for reasoning models, otherwise `max_tokens`; OpenAI usage | Unsupported effort is rejected locally for known model generations / 已知模型不支持的 effort 会本地拒绝 |
10
- | OpenRouter / `openrouter` | OpenRouter HTTPS; Bearer key; `X-Title: aicommit` | SSE | `reasoning.effort` | `max_tokens`; OpenAI-style usage | Model IDs commonly include vendor prefix / model 通常含厂商前缀 |
11
- | DeepSeek / `deepseek` | DeepSeek HTTPS; Bearer key | SSE | `thinking.type` plus normalized effort / thinking 与归一 effort | `max_tokens`; compatible usage | `medium`/`xhigh` map to supported high behavior where required / 必要时映射为 high |
12
- | MiniMax / `minimax` | MiniMax HTTPS; Bearer key | SSE | `reasoning_split` and thinking switch | `max_tokens`; compatible usage | Adapter removes conflicting switches before send / 发送前移除冲突开关 |
13
- | Kimi Code / `custom` | Kimi Code OpenAI-compatible HTTPS; Bearer key / Kimi Code OpenAI 兼容 HTTPS | SSE | Server default; no vendor fields injected / 使用服务端默认值,不注入厂商字段 | `max_tokens`; OpenAI-style usage | Bundled preset uses `kimi-for-coding`; membership and Platform keys are distinct / 会员与开放平台 key 不通用 |
14
- | Ollama native / `ollama` | Loopback `/api/chat` or `/api/generate`; normally keyless HTTP | Complete JSON in v1; native NDJSON streaming is not consumed / v1 使用完整 JSON | Native `think` boolean | `options.num_predict`; `prompt_eval_count` + `eval_count` | OpenAI-compatible `/v1/chat/completions` uses the compatible shape instead / `/v1` 使用兼容方言 |
15
- | Custom compatible / `custom` | HTTPS remote or loopback HTTP; optional core Bearer key | SSE when endpoint supports Chat Completions events | No vendor fields by default; explicit `enabledBody`/`disabledBody` / 默认不注入厂商字段 | `max_tokens`; common OpenAI/Anthropic/Ollama usage fields normalized | Validate the endpoint before trusting it with code or credentials / 发送代码前验证端点 |
7
+ The runtime keeps the existing Provider/Model config format. `providers.js` selects Pi model metadata and applies configuration overrides; `model-client.js` calls Pi's `openai-completions` implementation. `provider-response.js` normalizes legacy response fields before Pi; its SSE framing uses the lightweight [eventsource-parser](https://github.com/rexxars/eventsource-parser) dependency. `api.js` retains commit prompts, policy validation, and recovery. Presets remain setup data, not executable adapters.
16
8
 
17
- ## Compatibility contract / 兼容契约
9
+ 现有 Provider/Model 配置格式保持不变。`providers.js` 选择 Pi 模型元数据并应用配置覆盖;`model-client.js` 调用 Pi 的 `openai-completions` 实现;`provider-response.js` 在进入 Pi 前统一旧版响应字段,SSE 分帧使用轻量依赖 `eventsource-parser`;`api.js` 保留提交提示词、规则校验与恢复。预设仍是 setup 数据,不是可执行适配器。
18
10
 
19
- All built-in adapters return `content`, optional `reasoning`, normalized usage, finish reason, raw response, capabilities, attempts, and latency. Retries cover 429, selected 5xx, and network/body interruption; authentication, invalid parameters, and safety failures are not retried.
11
+ | Provider / adapter | Pi integration / Pi 接入 | Reasoning / 推理 | Token budget / 输出预算 |
12
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
13
+ | OpenAI / `openai` | Chat Completions; bundled model metadata / Chat Completions 与内置模型元数据 | Pi thinking-level map; unsupported known efforts rejected locally / 使用 Pi 强度映射,已知不支持的强度本地拒绝 | Reasoning: `max_completion_tokens`; otherwise `max_tokens` |
14
+ | OpenRouter / `openrouter` | Chat Completions; `X-Title: aicommit` | Pi `reasoning.effort` and catalog capability checks / Pi 参数映射与模型能力检查 | `max_tokens` |
15
+ | DeepSeek / `deepseek` | Chat Completions; bundled model metadata / 内置模型元数据 | Pi `thinking.type` and effort mapping; enabled thinking omits temperature / Pi 映射推理参数,开启时省略 temperature | `max_tokens` |
16
+ | MiniMax / `minimax` | Chat Completions / 兼容接口 | Small compatibility override for `reasoning_split` and thinking switches / 保留少量开关兼容逻辑 | `max_tokens` |
17
+ | Kimi Code / `custom` | Existing OpenAI-compatible endpoint and `kimi-for-coding` preset / 保留现有兼容端点与预设 | Server defaults; configurable body switches / 服务端默认值或自定义请求体 | `max_tokens` |
18
+ | Ollama / `ollama` | Native JSON bridge for `/api/chat` and `/api/generate`; compatible `/v1/chat/completions` uses Pi directly / 原生端点使用 JSON 桥接,兼容端点直接使用 Pi | Native `think` switch / 原生开关 | Native: `options.num_predict`; compatible: `max_tokens` |
19
+ | Custom / `custom` | Arbitrary OpenAI-compatible model IDs and full endpoint URLs / 任意兼容模型 ID 与完整端点 URL | `enabledBody` / `disabledBody`; no inferred vendor fields / 不自动注入厂商开关 | `max_tokens` |
20
20
 
21
- 所有内置 adapter 返回 `content`、可选 `reasoning`、标准 usage、finish reason、raw response、capability、attempts 与 latency。仅 429、部分 5xx、网络/响应中断会重试;鉴权、参数与安全错误不会重试。
21
+ ## Configuration and model metadata / 配置与模型元数据
22
22
 
23
- Use `aicommit doctor -p provider-name` to verify the selected adapter and live endpoint. Reuse a built-in adapter when only setup defaults change, and use `custom` for an OpenAI-compatible endpoint with optional body switches. A protocol requiring a different transport, streaming parser, or credential scheme needs core support.
23
+ - `apiUrl` is still the **complete endpoint**, not a base URL. Proxy paths and query parameters are preserved. Requests use only the resolved AICommit credential. Pi environment-key discovery and OAuth are not invoked, and redirects are rejected.
24
+ - `modelId` need not exist in Pi's catalog. Known OpenAI, DeepSeek, and OpenRouter models use the bundled metadata; unknown IDs retain a compatible fallback. No online model discovery runs during setup or generation.
25
+ - `reasoning.mode: auto` preserves server defaults and explicit `extraBody`. `on` / `off` applies the selected mode after extras. Setup filters known supported effort levels. DeepSeek's legacy unsupported effort values are normalized by Pi: for the pinned V4 Flash catalog, `medium` becomes `high`, and `xhigh` becomes `max`.
26
+ - Pi requests SSE by default, even when the CLI does not display reasoning. Complete JSON responses are bridged into Pi events. Set `extraBody: { "stream": false }` for endpoints that reject streaming requests; streaming-only options are removed automatically.
27
+ - Native Ollama remains non-streaming. `/api/generate` receives `system` and `prompt`, while `/api/chat` receives `messages`; existing `options` are retained.
24
28
 
25
- 使用 `aicommit doctor -p provider-name` 校验所选 adapter 与在线端点。若只改变 setup 默认值,应复用内置 adapter;OpenAI-compatible endpoint 及少量 body 开关使用 `custom`。需要不同传输、流解析或鉴权方案的协议必须由核心直接支持。
29
+ 对应行为:
30
+
31
+ - `apiUrl` 仍填写**完整接口地址**,代理路径与查询参数会保留。鉴权只使用 AICommit 已解析的凭据,不调用 Pi 的环境变量凭据发现或 OAuth,也不跟随重定向。
32
+ - Pi 目录中没有的 `modelId` 也可以配置;已知 OpenAI、DeepSeek、OpenRouter 模型使用随依赖发布的元数据,未知模型走兼容路径。setup 与生成过程不在线拉取模型目录。
33
+ - `reasoning.mode: auto` 保留服务端默认值及显式 `extraBody`;`on` / `off` 在 extras 之后应用。setup 根据已知能力过滤强度。DeepSeek 的旧配置由 Pi 归一:当前 V4 Flash 目录中 `medium` 映射为 `high`,`xhigh` 映射为 `max`。
34
+ - Pi 默认请求 SSE,包括终端不展示推理的场景;完整 JSON 响应通过桥接交给 Pi。若服务拒绝流式请求,可配置 `extraBody: { "stream": false }`,流式专用参数会自动移除。
35
+ - Ollama 原生端点继续使用非流式响应,保留 `options`;`/api/generate` 使用 `system` / `prompt`,`/api/chat` 使用 `messages`。
36
+
37
+ ## Result and retry contract / 结果与重试契约
38
+
39
+ Callers receive `content`, optional `reasoning`, normalized usage, finish reason, capabilities, attempts, and latency. Cached input tokens are included once in `inputTokens`; reasoning tokens are already part of output usage. `piMessage` exposes Pi's normalized assistant result. `raw` retains the actual complete JSON response, or a reconstructed Chat Completions result for SSE, preserving `callAPI` compatibility.
40
+
41
+ Retries remain owned by AICommit; Pi and the underlying SDK's automatic retries are disabled. Only 429, selected 5xx, and network failures **before an accepted response** can retry. Accepted-body interruptions, malformed responses, authentication, invalid parameters, and safety failures are never automatically replayed. Oversized `Retry-After` fails instead of retrying early.
42
+
43
+ SSE must contain a `finish_reason`. A clean EOF or `[DONE]` alone is rejected, and partial content is not returned as a successful generation. Token-limit aliases (`max_tokens`, `max_output_tokens`, `token_limit`) are normalized to `length` before Pi so recovery can run. Textual `reasoning_details`, including legacy shapes, are combined with ordinary reasoning deltas in arrival order. Duplicate representations within one event are emitted once; repeated text in later events is retained. Encrypted metadata remains opaque.
44
+
45
+ 业务仍获得 `content`、可选 `reasoning`、归一 usage、结束原因、能力、尝试次数与耗时。缓存输入 token 只计入一次,推理 token 已包含在输出中。`piMessage` 提供 Pi 的统一 assistant 结果;完整 JSON 的 `raw` 保留原响应,SSE 的 `raw` 则重建兼容 Chat Completions 结构。
46
+
47
+ 重试由 AICommit 负责,Pi 与底层 SDK 的自动重试均已关闭。只有 429、部分 5xx,以及**收到成功响应前**的网络失败可重试;已接受请求后的响应中断、格式错误、鉴权、参数及安全错误不会自动重放。超过上限的 `Retry-After` 会直接报错,不提前重试。
48
+
49
+ SSE 必须带 `finish_reason`。只有 EOF 或 `[DONE]` 时会拒绝结果,不将半截内容当作成功生成。输出上限别名(`max_tokens`、`max_output_tokens`、`token_limit`)在进入 Pi 前归一为 `length`,保留补全恢复流程。`reasoning_details`(含旧格式)中的文本与普通推理分片按到达顺序合并;同一事件中的重复表示只展示一次,后续事件中重复出现的文本则保留。加密元数据不会作为推理文本输出。
50
+
51
+ Use `aicommit doctor -p provider-name -m model-name` to verify a configured connection. This integration covers the existing six adapter types; installing Pi does **not** automatically expose every Pi provider, OAuth flow, or native Anthropic/Gemini/Responses endpoint. Those protocols require explicit routing and configuration support.
52
+
53
+ 使用 `aicommit doctor -p provider-name -m model-name` 检查配置的连接。本次接入覆盖现有六种适配类型;安装 Pi **不会自动开放**其全部供应商、OAuth 或 Anthropic/Gemini/Responses 原生端点,这些协议需要显式扩展路由与配置。
@@ -26,8 +26,20 @@ JSON 模式保证 stdout 只有一个机器对象,诊断进入 stderr。`error
26
26
  | `split run --scope=all --yes` stops before API call / split 非交互在 API 前停止 | `sensitive_data` / `7` | Complete untracked scan found sensitive-looking data / 完整未跟踪扫描发现疑似敏感数据 | Review/stage intended files explicitly; do not bypass without checking the actual content |
27
27
  | Commit aborts after generation / 生成后提交中止 | `concurrent_modification` / `8` | Index/worktree changed during the protected window / 受保护窗口中 index/worktree 被修改 | Review `git status`, restore the intended snapshot, and generate again |
28
28
  | Split stopped after one or more commits / split 部分提交后停止 | reported Git failure | Hook, crash, SIGINT, or concurrent pending edit / hook、崩溃、中断或待处理文件变化 | Run `aicommit split resume`; if another Git workflow already replaced the transaction, use `aicommit split abort`(只删除恢复元数据,不改提交或工作区) |
29
+ | `aicommit update` refuses the installation / update 拒绝当前安装 | `config` / `2` | Source checkout, npm link, or another active Node/npm environment / 源码、npm link 或当前是另一套 Node/npm 环境 | Activate the Node environment that installed AICommit, or run `npm install --global @hifullmoon/aicommit@latest` manually |
30
+ | `aicommit update` cannot reach npm / update 无法访问 npm | `network` / `4` | Registry authentication, proxy, DNS, or network failure / registry 鉴权、代理、DNS 或网络失败 | Check `npm config get registry` and npm authentication/proxy settings, then retry |
29
31
  | npm provenance is absent or invalid / npm provenance 缺失或失败 | npm audit failure | Old npm CLI, non-trusted release, or wrong version / npm 过旧、非可信发布或版本错误 | Upgrade npm; run `npm audit signatures`; install only a version linked to the official workflow |
30
32
 
31
33
  If a failure remains, capture `aicommit doctor --output=json`, Node/Git versions, the error category, and redacted config sources. Never attach a diff, commit message, config file, API key, reasoning trace, or credential-helper output to a public issue.
32
34
 
33
35
  若问题仍未解决,请记录 `aicommit doctor --output=json`、Node/Git 版本、错误分类与脱敏后的配置来源。不要在公开 issue 中附加 diff、commit message、配置文件、API key、reasoning 或 credential-helper 输出。
36
+
37
+ ## Large-change limits / 大变更限制
38
+
39
+ Default `auto` analysis builds a local inventory and selects bounded excerpts without model reduction calls. If a complete split candidate inventory cannot fit one request, stage a smaller logical change or explicitly choose personal `largeChange.strategy: "deep"`. Deep analysis can reach its fixed request (256) or depth (8) limits; increasing token or time budgets does not raise those limits. Token/time limits remain configurable in personal settings. Unknown model token counts use conservative estimates; an oversized request fails before dispatch, and provider-context errors are not blindly replayed.
40
+
41
+ 默认 `auto` 在本地建立清单并选择受限片段,不调用模型递归汇总。完整拆分候选清单放不进一次请求时,请暂存更小的逻辑变更,或明确在个人配置中选择 `largeChange.strategy: "deep"`。深度分析的请求数(256)和汇总层级(8)上限固定,提高 token 或时间预算不会改变它们;token 和时间预算可在个人配置中调整。未知模型采用保守 token 估算,输入估算超限会在发送前失败;Provider 上下文超限不会盲目重试。
42
+
43
+ Text lines over 1 MiB fail explicitly. Large-change hunk plans are unsupported; use file-level split. Metadata-only files remain in the complete plan. Invalid, duplicate, or missing IDs are response-format errors, not automatic catch-all groups.
44
+
45
+ 单行超过 1 MiB 会明确报错。大变更暂不支持 hunk 规划,请使用文件级拆分。仅统计的文件仍纳入完整计划。无效、重复或遗漏 ID 会报响应格式错误,不会自动归入兜底组。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifullmoon/aicommit",
3
- "version": "2.2.3",
3
+ "version": "2.4.0",
4
4
  "description": "Safe, local-first AI commit message generator for Git workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,6 +48,7 @@
48
48
  "lines": 70
49
49
  },
50
50
  "dependencies": {
51
+ "@earendil-works/pi-ai": "0.85.0",
51
52
  "@inquirer/checkbox": "^4.3.2",
52
53
  "@inquirer/confirm": "^5.1.0",
53
54
  "@inquirer/core": "^10.3.2",
@@ -57,6 +58,7 @@
57
58
  "@inquirer/select": "^4.1.0",
58
59
  "boxen": "^8.0.1",
59
60
  "chalk": "^5.4.0",
61
+ "eventsource-parser": "4.1.0",
60
62
  "ora": "^8.2.0",
61
63
  "wrap-ansi": "^9.0.2"
62
64
  },
@@ -90,7 +92,7 @@
90
92
  }
91
93
  },
92
94
  "engines": {
93
- "node": ">=18"
95
+ "node": ">=22.19.0"
94
96
  },
95
97
  "devDependencies": {
96
98
  "c8": "10.1.3",
@@ -19,9 +19,15 @@
19
19
  "error"
20
20
  ],
21
21
  "properties": {
22
- "schemaVersion": { "const": "1.0" },
23
- "ok": { "type": "boolean" },
24
- "message": { "type": ["string", "null"] },
22
+ "schemaVersion": {
23
+ "const": "1.0"
24
+ },
25
+ "ok": {
26
+ "type": "boolean"
27
+ },
28
+ "message": {
29
+ "type": ["string", "null"]
30
+ },
25
31
  "plan": {
26
32
  "type": ["array", "null"],
27
33
  "items": {
@@ -29,8 +35,15 @@
29
35
  "additionalProperties": false,
30
36
  "required": ["message", "files"],
31
37
  "properties": {
32
- "message": { "type": "string" },
33
- "files": { "type": "array", "items": { "type": "string" } },
38
+ "message": {
39
+ "type": "string"
40
+ },
41
+ "files": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string"
45
+ }
46
+ },
34
47
  "hunks": {
35
48
  "type": "array",
36
49
  "items": {
@@ -38,29 +51,62 @@
38
51
  "additionalProperties": false,
39
52
  "required": ["path", "ids"],
40
53
  "properties": {
41
- "path": { "type": "string" },
42
- "ids": { "type": "array", "items": { "type": "string" } }
54
+ "path": {
55
+ "type": "string"
56
+ },
57
+ "ids": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string"
61
+ }
62
+ }
43
63
  }
44
64
  }
45
65
  }
46
66
  }
47
67
  }
48
68
  },
49
- "provider": { "type": ["string", "null"] },
50
- "model": { "type": ["string", "null"] },
51
- "latencyMs": { "type": ["number", "null"], "minimum": 0 },
69
+ "provider": {
70
+ "type": ["string", "null"]
71
+ },
72
+ "model": {
73
+ "type": ["string", "null"]
74
+ },
75
+ "latencyMs": {
76
+ "type": ["number", "null"],
77
+ "minimum": 0
78
+ },
52
79
  "usage": {
53
80
  "type": ["object", "null"],
54
81
  "additionalProperties": false,
55
82
  "properties": {
56
- "inputTokens": { "type": "number", "minimum": 0 },
57
- "outputTokens": { "type": "number", "minimum": 0 },
58
- "totalTokens": { "type": "number", "minimum": 0 }
83
+ "inputTokens": {
84
+ "type": "number",
85
+ "minimum": 0
86
+ },
87
+ "outputTokens": {
88
+ "type": "number",
89
+ "minimum": 0
90
+ },
91
+ "totalTokens": {
92
+ "type": "number",
93
+ "minimum": 0
94
+ }
59
95
  }
60
96
  },
61
- "warnings": { "type": "array", "items": { "type": "string" } },
62
- "exitReason": { "type": "string", "minLength": 1 },
63
- "committed": { "type": "boolean" },
97
+ "warnings": {
98
+ "type": "array",
99
+ "items": {
100
+ "type": "string"
101
+ }
102
+ },
103
+ "exitReason": {
104
+ "type": "string",
105
+ "minLength": 1
106
+ },
107
+ "committed": {
108
+ "type": "boolean"
109
+ },
64
110
  "error": {
65
111
  "type": ["object", "null"],
66
112
  "additionalProperties": false,
@@ -78,9 +124,65 @@
78
124
  "internal"
79
125
  ]
80
126
  },
81
- "message": { "type": "string" }
127
+ "message": {
128
+ "type": "string"
129
+ }
82
130
  }
83
131
  },
84
- "data": { "type": "object" }
132
+ "data": {
133
+ "type": "object",
134
+ "properties": {
135
+ "analysis": {
136
+ "type": "object",
137
+ "description": "Large-change coverage and shared request budget; never contains diff or intermediate summaries.",
138
+ "properties": {
139
+ "totalFiles": {
140
+ "type": "number",
141
+ "minimum": 0
142
+ },
143
+ "analyzedFiles": {
144
+ "type": "number",
145
+ "minimum": 0
146
+ },
147
+ "sampledFiles": {
148
+ "type": "number",
149
+ "minimum": 0,
150
+ "description": "Files represented by partial excerpts; not fully analyzed files."
151
+ },
152
+ "strategy": {
153
+ "enum": ["auto", "deep"]
154
+ },
155
+ "metadataOnlyFiles": {
156
+ "type": "number",
157
+ "minimum": 0
158
+ },
159
+ "failedFiles": {
160
+ "type": "number",
161
+ "minimum": 0
162
+ },
163
+ "completedChunks": {
164
+ "type": "number",
165
+ "minimum": 0
166
+ },
167
+ "requests": {
168
+ "type": "number",
169
+ "minimum": 0
170
+ },
171
+ "budgetedTokens": {
172
+ "type": "number",
173
+ "minimum": 0
174
+ },
175
+ "maxTotalTokens": {
176
+ "type": "number",
177
+ "minimum": 0
178
+ },
179
+ "elapsedMs": {
180
+ "type": "number",
181
+ "minimum": 0
182
+ }
183
+ }
184
+ }
185
+ }
186
+ }
85
187
  }
86
188
  }
@@ -0,0 +1,76 @@
1
+ import { ERROR_CATEGORIES, fail } from './errors.js';
2
+
3
+ export const DEFAULT_LARGE_CHANGE = Object.freeze({
4
+ strategy: 'auto',
5
+ chunkInputTokens: 12000,
6
+ maxTotalTokens: 200000,
7
+ concurrency: 2,
8
+ timeoutMs: 180000,
9
+ });
10
+
11
+ export function estimateTokens(text) {
12
+ // Deliberately conservative fallback for providers without a tokenizer.
13
+ return Math.ceil(Buffer.byteLength(String(text), 'utf8') / 2);
14
+ }
15
+
16
+ export function createAnalysisBudget(settings = {}) {
17
+ const limits = { ...DEFAULT_LARGE_CHANGE, ...settings };
18
+ const started = Date.now();
19
+ let charged = 0;
20
+ let requests = 0;
21
+ let inputTokens = 0;
22
+ let outputTokens = 0;
23
+ let reserveFinal = 0;
24
+ return {
25
+ limits,
26
+ signal: AbortSignal.timeout(limits.timeoutMs),
27
+ reserveFinal(tokens) {
28
+ reserveFinal = tokens;
29
+ },
30
+ remainingMs() {
31
+ return Math.max(0, limits.timeoutMs - (Date.now() - started));
32
+ },
33
+ snapshot() {
34
+ return {
35
+ requests,
36
+ budgetedTokens: charged,
37
+ maxTotalTokens: limits.maxTotalTokens,
38
+ elapsedMs: Date.now() - started,
39
+ usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
40
+ };
41
+ },
42
+ reserve(input, output) {
43
+ if (
44
+ !this.remainingMs() ||
45
+ charged + input + output + reserveFinal > limits.maxTotalTokens ||
46
+ requests >= 256
47
+ ) {
48
+ throw fail(
49
+ ERROR_CATEGORIES.PROVIDER,
50
+ 'Large-change analysis reached its token, request, or time budget. No incomplete plan will be committed. Increase the personal largeChange budget or stage a smaller change.',
51
+ { data: { analysis: this.snapshot() } },
52
+ );
53
+ }
54
+ if (input > limits.chunkInputTokens) {
55
+ throw fail(
56
+ ERROR_CATEGORIES.PROVIDER,
57
+ 'Analysis request exceeds largeChange.chunkInputTokens; shorten repository context or increase the personal input budget.',
58
+ );
59
+ }
60
+ charged += input + output;
61
+ requests += 1;
62
+ inputTokens += input;
63
+ outputTokens += output;
64
+ return { input, output };
65
+ },
66
+ settle(ticket, usage) {
67
+ if (!ticket || !usage) return;
68
+ const actualInput = usage.inputTokens ?? ticket.input;
69
+ const actualOutput = usage.outputTokens ?? ticket.output;
70
+ charged +=
71
+ Math.max(usage.totalTokens || 0, actualInput + actualOutput) - ticket.input - ticket.output;
72
+ inputTokens += actualInput - ticket.input;
73
+ outputTokens += actualOutput - ticket.output;
74
+ },
75
+ };
76
+ }