kodevu 0.1.49 → 0.1.51
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.
- package/README.md +29 -1
- package/package.json +1 -1
- package/src/config.js +92 -8
- package/src/index.js +4 -0
- package/src/report-generator.js +0 -18
- package/src/reviewers.js +115 -0
- package/src/token-usage.js +23 -1
package/README.md
CHANGED
|
@@ -29,13 +29,18 @@ npx kodevu [target] [options]
|
|
|
29
29
|
### Options
|
|
30
30
|
|
|
31
31
|
- `target`: Repository path (Git) or SVN URL/Working copy (default: `.`).
|
|
32
|
-
- `--reviewer, -r`: `codex`, `gemini`, `copilot`, or `auto` (default: `auto`).
|
|
32
|
+
- `--reviewer, -r`: `codex`, `gemini`, `copilot`, `openai`, or `auto` (default: `auto`).
|
|
33
33
|
- `--rev, -v`: A specific revision or commit hash to review.
|
|
34
34
|
- `--last, -n`: Number of latest revisions to review (default: 1). Use negative values (e.g., `-3`) to review only the 3rd commit from the top.
|
|
35
35
|
- `--lang, -l`: Output language (e.g., `zh`, `en`, `auto`).
|
|
36
36
|
- `--prompt, -p`: Additional instructions for the reviewer. Use `@file.txt` to read from a file.
|
|
37
37
|
- `--output, -o`: Report output directory (default: `~/.kodevu`).
|
|
38
38
|
- `--format, -f`: Output formats (e.g., `markdown`, `json`, or `markdown,json`).
|
|
39
|
+
- `--openai-api-key`: API key used when `--reviewer openai`.
|
|
40
|
+
- `--openai-base-url`: Base URL used when `--reviewer openai` (default: `https://api.openai.com/v1`).
|
|
41
|
+
- `--openai-model`: Model used when `--reviewer openai` (default: `gpt-5-mini`).
|
|
42
|
+
- `--openai-org`: Optional OpenAI organization ID.
|
|
43
|
+
- `--openai-project`: Optional OpenAI project ID.
|
|
39
44
|
- `--debug, -d`: Print debug information.
|
|
40
45
|
- `--version, -V`: Print the current version and exit.
|
|
41
46
|
|
|
@@ -51,6 +56,11 @@ You can set these in your shell to change default behavior without typing flags
|
|
|
51
56
|
- `KODEVU_OUTPUT_DIR`: Default output directory.
|
|
52
57
|
- `KODEVU_PROMPT`: Default prompt instructions.
|
|
53
58
|
- `KODEVU_TIMEOUT`: Reviewer execution timeout in milliseconds.
|
|
59
|
+
- `KODEVU_OPENAI_API_KEY`: API key for `openai`.
|
|
60
|
+
- `KODEVU_OPENAI_BASE_URL`: Base URL for `openai`.
|
|
61
|
+
- `KODEVU_OPENAI_MODEL`: Model for `openai`.
|
|
62
|
+
- `KODEVU_OPENAI_ORG`: Optional organization ID for `openai`.
|
|
63
|
+
- `KODEVU_OPENAI_PROJECT`: Optional project ID for `openai`.
|
|
54
64
|
|
|
55
65
|
## Examples
|
|
56
66
|
|
|
@@ -91,11 +101,29 @@ export KODEVU_REVIEWER=gemini
|
|
|
91
101
|
npx kodevu .
|
|
92
102
|
```
|
|
93
103
|
|
|
104
|
+
Use the OpenAI API directly with a small set of extra settings:
|
|
105
|
+
```bash
|
|
106
|
+
export KODEVU_REVIEWER=openai
|
|
107
|
+
export KODEVU_OPENAI_API_KEY=sk-...
|
|
108
|
+
export KODEVU_OPENAI_MODEL=gpt-5-mini
|
|
109
|
+
npx kodevu .
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use a custom OpenAI-compatible endpoint:
|
|
113
|
+
```bash
|
|
114
|
+
npx kodevu . \
|
|
115
|
+
--reviewer openai \
|
|
116
|
+
--openai-api-key sk-... \
|
|
117
|
+
--openai-base-url https://your-gateway.example.com/v1 \
|
|
118
|
+
--openai-model gpt-5-mini
|
|
119
|
+
```
|
|
120
|
+
|
|
94
121
|
## How it Works
|
|
95
122
|
|
|
96
123
|
- **Git Targets**: `target` must be a local repository or subdirectory.
|
|
97
124
|
- **SVN Targets**: `target` can be a working copy path or repository URL.
|
|
98
125
|
- **Reviewer "auto"**: Probes `codex`, `gemini`, and `copilot` in your `PATH` and selects one.
|
|
126
|
+
- **Reviewer "openai"**: Calls the OpenAI Chat Completions API directly. `auto` does not select `openai`, so API-based use stays explicit.
|
|
99
127
|
- **Contextual Review**: For local repositories, the reviewer can inspect related files beyond the diff to provide deeper insights.
|
|
100
128
|
|
|
101
129
|
## License
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -8,7 +8,8 @@ const require = createRequire(import.meta.url);
|
|
|
8
8
|
const { version: packageVersion } = require("../package.json");
|
|
9
9
|
|
|
10
10
|
const defaultStorageDir = path.join(os.homedir(), ".kodevu");
|
|
11
|
-
const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot"];
|
|
11
|
+
const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot", "openai"];
|
|
12
|
+
const AUTO_SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot"];
|
|
12
13
|
|
|
13
14
|
const defaultConfig = {
|
|
14
15
|
reviewer: "auto",
|
|
@@ -21,7 +22,12 @@ const defaultConfig = {
|
|
|
21
22
|
maxRevisionsPerRun: 5,
|
|
22
23
|
outputFormats: ["markdown"],
|
|
23
24
|
rev: "",
|
|
24
|
-
last: 0
|
|
25
|
+
last: 0,
|
|
26
|
+
openaiApiKey: "",
|
|
27
|
+
openaiBaseUrl: "https://api.openai.com/v1",
|
|
28
|
+
openaiModel: "gpt-5-mini",
|
|
29
|
+
openaiOrganization: "",
|
|
30
|
+
openaiProject: ""
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
const ENV_MAP = {
|
|
@@ -31,7 +37,12 @@ const ENV_MAP = {
|
|
|
31
37
|
KODEVU_PROMPT: "prompt",
|
|
32
38
|
KODEVU_TIMEOUT: "commandTimeoutMs",
|
|
33
39
|
KODEVU_MAX_REVISIONS: "maxRevisionsPerRun",
|
|
34
|
-
KODEVU_FORMATS: "outputFormats"
|
|
40
|
+
KODEVU_FORMATS: "outputFormats",
|
|
41
|
+
KODEVU_OPENAI_API_KEY: "openaiApiKey",
|
|
42
|
+
KODEVU_OPENAI_BASE_URL: "openaiBaseUrl",
|
|
43
|
+
KODEVU_OPENAI_MODEL: "openaiModel",
|
|
44
|
+
KODEVU_OPENAI_ORG: "openaiOrganization",
|
|
45
|
+
KODEVU_OPENAI_PROJECT: "openaiProject"
|
|
35
46
|
};
|
|
36
47
|
|
|
37
48
|
function resolvePath(value) {
|
|
@@ -84,13 +95,13 @@ export function detectLanguage() {
|
|
|
84
95
|
|
|
85
96
|
async function resolveAutoReviewers(debug) {
|
|
86
97
|
const availableReviewers = [];
|
|
87
|
-
for (const reviewerName of
|
|
98
|
+
for (const reviewerName of AUTO_SUPPORTED_REVIEWERS) {
|
|
88
99
|
const commandPath = await findCommandOnPath(reviewerName, { debug });
|
|
89
100
|
if (commandPath) availableReviewers.push({ reviewerName, commandPath });
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
if (availableReviewers.length === 0) {
|
|
93
|
-
throw new Error(`No reviewer CLI found in PATH. Install one of: ${
|
|
104
|
+
throw new Error(`No reviewer CLI found in PATH. Install one of: ${AUTO_SUPPORTED_REVIEWERS.join(", ")}`);
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
// Shuffle for variety
|
|
@@ -114,7 +125,12 @@ export function parseCliArgs(argv) {
|
|
|
114
125
|
rev: "",
|
|
115
126
|
last: "",
|
|
116
127
|
outputDir: "",
|
|
117
|
-
outputFormats: ""
|
|
128
|
+
outputFormats: "",
|
|
129
|
+
openaiApiKey: "",
|
|
130
|
+
openaiBaseUrl: "",
|
|
131
|
+
openaiModel: "",
|
|
132
|
+
openaiOrganization: "",
|
|
133
|
+
openaiProject: ""
|
|
118
134
|
};
|
|
119
135
|
|
|
120
136
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -188,6 +204,41 @@ export function parseCliArgs(argv) {
|
|
|
188
204
|
continue;
|
|
189
205
|
}
|
|
190
206
|
|
|
207
|
+
if (value === "--openai-api-key") {
|
|
208
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
209
|
+
args.openaiApiKey = nextValue;
|
|
210
|
+
index += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (value === "--openai-base-url") {
|
|
215
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
216
|
+
args.openaiBaseUrl = nextValue;
|
|
217
|
+
index += 1;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (value === "--openai-model") {
|
|
222
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
223
|
+
args.openaiModel = nextValue;
|
|
224
|
+
index += 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (value === "--openai-org") {
|
|
229
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
230
|
+
args.openaiOrganization = nextValue;
|
|
231
|
+
index += 1;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (value === "--openai-project") {
|
|
236
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
237
|
+
args.openaiProject = nextValue;
|
|
238
|
+
index += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
191
242
|
if (!value.startsWith("-") && !args.target) {
|
|
192
243
|
args.target = value;
|
|
193
244
|
continue;
|
|
@@ -210,7 +261,21 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
210
261
|
}
|
|
211
262
|
|
|
212
263
|
// 2. Merge CLI Arguments
|
|
213
|
-
for (const key of [
|
|
264
|
+
for (const key of [
|
|
265
|
+
"target",
|
|
266
|
+
"reviewer",
|
|
267
|
+
"prompt",
|
|
268
|
+
"lang",
|
|
269
|
+
"rev",
|
|
270
|
+
"last",
|
|
271
|
+
"outputDir",
|
|
272
|
+
"outputFormats",
|
|
273
|
+
"openaiApiKey",
|
|
274
|
+
"openaiBaseUrl",
|
|
275
|
+
"openaiModel",
|
|
276
|
+
"openaiOrganization",
|
|
277
|
+
"openaiProject"
|
|
278
|
+
]) {
|
|
214
279
|
if (cliArgs[key] !== undefined && cliArgs[key] !== "") {
|
|
215
280
|
config[key] = cliArgs[key];
|
|
216
281
|
}
|
|
@@ -257,11 +322,20 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
257
322
|
config.commandTimeoutMs = Number(config.commandTimeoutMs);
|
|
258
323
|
config.last = Number(config.last);
|
|
259
324
|
config.outputFormats = normalizeOutputFormats(config.outputFormats);
|
|
325
|
+
config.openaiApiKey = String(config.openaiApiKey || "").trim();
|
|
326
|
+
config.openaiBaseUrl = String(config.openaiBaseUrl || defaultConfig.openaiBaseUrl).trim().replace(/\/+$/, "");
|
|
327
|
+
config.openaiModel = String(config.openaiModel || defaultConfig.openaiModel).trim();
|
|
328
|
+
config.openaiOrganization = String(config.openaiOrganization || "").trim();
|
|
329
|
+
config.openaiProject = String(config.openaiProject || "").trim();
|
|
260
330
|
|
|
261
331
|
if (!config.rev && (isNaN(config.last) || config.last === 0)) {
|
|
262
332
|
config.last = 1;
|
|
263
333
|
}
|
|
264
334
|
|
|
335
|
+
if (config.reviewer === "openai" && !config.openaiApiKey) {
|
|
336
|
+
throw new Error('Reviewer "openai" requires an API key. Set KODEVU_OPENAI_API_KEY or pass --openai-api-key.');
|
|
337
|
+
}
|
|
338
|
+
|
|
265
339
|
return config;
|
|
266
340
|
}
|
|
267
341
|
|
|
@@ -273,13 +347,18 @@ Usage:
|
|
|
273
347
|
|
|
274
348
|
Options:
|
|
275
349
|
--target, <path> Target repository path (default: current directory)
|
|
276
|
-
--reviewer, -r Reviewer (codex | gemini | copilot | auto, default: auto)
|
|
350
|
+
--reviewer, -r Reviewer (codex | gemini | copilot | openai | auto, default: auto)
|
|
277
351
|
--prompt, -p Additional instructions or @file.txt to read from file
|
|
278
352
|
--lang, -l Output language (e.g. zh, en, auto)
|
|
279
353
|
--rev, -v Review specific revision(s), hashes, branches or ranges (comma-separated)
|
|
280
354
|
--last, -n Review the latest N revisions; use negative (-N) to review only the Nth-from-last revision (default: 1)
|
|
281
355
|
--output, -o Output directory (default: ~/.kodevu)
|
|
282
356
|
--format, -f Output formats (markdown, json, comma-separated)
|
|
357
|
+
--openai-api-key API key used when reviewer=openai
|
|
358
|
+
--openai-base-url Base URL used when reviewer=openai (default: https://api.openai.com/v1)
|
|
359
|
+
--openai-model Model used when reviewer=openai (default: gpt-5-mini)
|
|
360
|
+
--openai-org Optional OpenAI organization ID
|
|
361
|
+
--openai-project Optional OpenAI project ID
|
|
283
362
|
--debug, -d Print extra debug information
|
|
284
363
|
--help, -h Show help
|
|
285
364
|
--version, -V Show version
|
|
@@ -290,6 +369,11 @@ Environment Variables:
|
|
|
290
369
|
KODEVU_OUTPUT_DIR Default output directory
|
|
291
370
|
KODEVU_PROMPT Default prompt text
|
|
292
371
|
KODEVU_TIMEOUT Reviewer timeout in ms
|
|
372
|
+
KODEVU_OPENAI_API_KEY API key for reviewer=openai
|
|
373
|
+
KODEVU_OPENAI_BASE_URL Base URL for reviewer=openai
|
|
374
|
+
KODEVU_OPENAI_MODEL Model for reviewer=openai
|
|
375
|
+
KODEVU_OPENAI_ORG Organization ID for reviewer=openai
|
|
376
|
+
KODEVU_OPENAI_PROJECT Project ID for reviewer=openai
|
|
293
377
|
`);
|
|
294
378
|
}
|
|
295
379
|
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,10 @@ try {
|
|
|
40
40
|
reviewer: config.reviewer,
|
|
41
41
|
reviewerCommandPath: config.reviewerCommandPath,
|
|
42
42
|
reviewerWasAutoSelected: config.reviewerWasAutoSelected,
|
|
43
|
+
openaiBaseUrl: config.openaiBaseUrl,
|
|
44
|
+
openaiModel: config.openaiModel,
|
|
45
|
+
openaiOrganization: config.openaiOrganization,
|
|
46
|
+
openaiProject: config.openaiProject,
|
|
43
47
|
target: config.target,
|
|
44
48
|
outputDir: config.outputDir,
|
|
45
49
|
lang: config.lang,
|
package/src/report-generator.js
CHANGED
|
@@ -52,7 +52,6 @@ export function shouldWriteFormat(config, format) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function buildReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage) {
|
|
55
|
-
const stderrText = reviewerResult.stderr?.trim();
|
|
56
55
|
const lang = config.resolvedLang || "en";
|
|
57
56
|
const lines = [
|
|
58
57
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
@@ -64,8 +63,6 @@ export function buildReport(config, backend, targetInfo, details, diffPayloads,
|
|
|
64
63
|
`- ${getPhrase("date", lang)}: \`${formatDate(details.date)}\``,
|
|
65
64
|
`- Generated At: \`${formatDate(new Date())}\``,
|
|
66
65
|
`- Reviewer: \`${reviewer.displayName}\``,
|
|
67
|
-
`- Reviewer Exit Code: \`${reviewerResult.code}\``,
|
|
68
|
-
`- Reviewer Timed Out: \`${reviewerResult.timedOut ? "yes" : "no"}\``,
|
|
69
66
|
"",
|
|
70
67
|
"## Token Usage",
|
|
71
68
|
"",
|
|
@@ -79,27 +76,12 @@ export function buildReport(config, backend, targetInfo, details, diffPayloads,
|
|
|
79
76
|
"",
|
|
80
77
|
details.message ? "```text\n" + details.message + "\n```" : "_Empty_",
|
|
81
78
|
"",
|
|
82
|
-
"## Review Context",
|
|
83
|
-
"",
|
|
84
|
-
"```text",
|
|
85
|
-
buildPrompt(config, backend, targetInfo, details, diffPayloads.review),
|
|
86
|
-
"```",
|
|
87
|
-
"",
|
|
88
|
-
"## Diff Handling",
|
|
89
|
-
"",
|
|
90
|
-
formatDiffHandling(diffPayloads.review, "Reviewer Input"),
|
|
91
|
-
formatDiffHandling(diffPayloads.report, "Report Diff"),
|
|
92
|
-
"",
|
|
93
79
|
"## Diff",
|
|
94
80
|
"",
|
|
95
81
|
"```diff",
|
|
96
82
|
diffPayloads.report.text.trim() || "(empty diff)",
|
|
97
83
|
"```",
|
|
98
84
|
"",
|
|
99
|
-
"## Reviewer Diagnostics",
|
|
100
|
-
"",
|
|
101
|
-
stderrText ? "```text\n" + stderrText + "\n```" : "_No stderr output._",
|
|
102
|
-
"",
|
|
103
85
|
`## ${reviewer.responseSectionTitle}`,
|
|
104
86
|
"",
|
|
105
87
|
reviewerResult.message?.trim() ? reviewerResult.message.trim() : reviewer.emptyResponseText
|
package/src/reviewers.js
CHANGED
|
@@ -6,6 +6,48 @@ import { prepareDiffPayloads } from "./diff-processor.js";
|
|
|
6
6
|
import { buildPrompt, getReviewWorkspaceRoot } from "./report-generator.js";
|
|
7
7
|
import { resolveTokenUsage } from "./token-usage.js";
|
|
8
8
|
|
|
9
|
+
function buildOpenAiRequestHeaders(config) {
|
|
10
|
+
const headers = {
|
|
11
|
+
"content-type": "application/json",
|
|
12
|
+
authorization: `Bearer ${config.openaiApiKey}`
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (config.openaiOrganization) {
|
|
16
|
+
headers["OpenAI-Organization"] = config.openaiOrganization;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (config.openaiProject) {
|
|
20
|
+
headers["OpenAI-Project"] = config.openaiProject;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractOpenAiMessageContent(content) {
|
|
27
|
+
if (typeof content === "string") {
|
|
28
|
+
return content;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!Array.isArray(content)) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return content
|
|
36
|
+
.map((item) => {
|
|
37
|
+
if (typeof item === "string") {
|
|
38
|
+
return item;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (item?.type === "text" && typeof item.text === "string") {
|
|
42
|
+
return item.text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return "";
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
9
51
|
export const REVIEWERS = {
|
|
10
52
|
codex: {
|
|
11
53
|
displayName: "Codex",
|
|
@@ -122,6 +164,78 @@ export const REVIEWERS = {
|
|
|
122
164
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
123
165
|
}
|
|
124
166
|
}
|
|
167
|
+
},
|
|
168
|
+
openai: {
|
|
169
|
+
displayName: "OpenAI API",
|
|
170
|
+
responseSectionTitle: "OpenAI Response",
|
|
171
|
+
emptyResponseText: "_No final response returned from the OpenAI API._",
|
|
172
|
+
async run(config, workingDir, promptText, diffText) {
|
|
173
|
+
const requestBody = {
|
|
174
|
+
model: config.openaiModel,
|
|
175
|
+
messages: [
|
|
176
|
+
{
|
|
177
|
+
role: "user",
|
|
178
|
+
content: [promptText, "Unified diff:", diffText].join("\n\n")
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${config.openaiBaseUrl}/chat/completions`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: buildOpenAiRequestHeaders(config),
|
|
187
|
+
body: JSON.stringify(requestBody),
|
|
188
|
+
signal: AbortSignal.timeout(config.commandTimeoutMs)
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const responseText = await response.text();
|
|
192
|
+
let payload;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
payload = responseText ? JSON.parse(responseText) : {};
|
|
196
|
+
} catch {
|
|
197
|
+
payload = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
const errorMessage = payload?.error?.message || responseText || `HTTP ${response.status}`;
|
|
202
|
+
return {
|
|
203
|
+
code: response.status,
|
|
204
|
+
timedOut: false,
|
|
205
|
+
stdout: "",
|
|
206
|
+
stderr: errorMessage,
|
|
207
|
+
message: ""
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const message = extractOpenAiMessageContent(payload?.choices?.[0]?.message?.content);
|
|
212
|
+
const usage = payload?.usage
|
|
213
|
+
? {
|
|
214
|
+
inputTokens: Number(payload.usage.prompt_tokens || 0),
|
|
215
|
+
outputTokens: Number(payload.usage.completion_tokens || 0),
|
|
216
|
+
totalTokens: Number(payload.usage.total_tokens || 0)
|
|
217
|
+
}
|
|
218
|
+
: null;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
code: 0,
|
|
222
|
+
timedOut: false,
|
|
223
|
+
stdout: responseText,
|
|
224
|
+
stderr: "",
|
|
225
|
+
message,
|
|
226
|
+
usage
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const timedOut = error?.name === "TimeoutError" || error?.name === "AbortError";
|
|
230
|
+
return {
|
|
231
|
+
code: 1,
|
|
232
|
+
timedOut,
|
|
233
|
+
stdout: "",
|
|
234
|
+
stderr: error?.message || String(error),
|
|
235
|
+
message: ""
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
125
239
|
}
|
|
126
240
|
};
|
|
127
241
|
|
|
@@ -133,6 +247,7 @@ export async function runReviewerPrompt(config, backend, targetInfo, details, di
|
|
|
133
247
|
const result = await reviewer.run(config, reviewWorkspaceRoot, promptText, diffPayloads.review.text);
|
|
134
248
|
const tokenUsage = resolveTokenUsage(
|
|
135
249
|
config.reviewer,
|
|
250
|
+
result.usage,
|
|
136
251
|
result.stderr,
|
|
137
252
|
promptText,
|
|
138
253
|
diffPayloads.review.text,
|
package/src/token-usage.js
CHANGED
|
@@ -32,7 +32,29 @@ export function parseTokenUsage(stderr) {
|
|
|
32
32
|
return { inputTokens, outputTokens, totalTokens };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function normalizeUsageObject(usage) {
|
|
36
|
+
if (!usage || typeof usage !== "object") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const inputTokens = Number(usage.inputTokens || 0);
|
|
41
|
+
const outputTokens = Number(usage.outputTokens || 0);
|
|
42
|
+
const totalTokens = Number(usage.totalTokens || (inputTokens + outputTokens));
|
|
43
|
+
|
|
44
|
+
if (totalTokens <= 0 && inputTokens <= 0 && outputTokens <= 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { inputTokens, outputTokens, totalTokens };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveTokenUsage(reviewerName, usage, stderr, promptText, diffText, responseText) {
|
|
52
|
+
const normalizedUsage = normalizeUsageObject(usage);
|
|
53
|
+
|
|
54
|
+
if (normalizedUsage) {
|
|
55
|
+
return { ...normalizedUsage, source: "reviewer" };
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
const parsed = parseTokenUsage(stderr);
|
|
37
59
|
|
|
38
60
|
if (parsed && parsed.totalTokens > 0) {
|