co-maintainer 0.5.0 → 0.5.1-beta.1
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/LICENSE +21 -21
- package/README.md +5 -3
- package/dist/package.json +1 -1
- package/dist/src/ai/anthropic.d.ts +55 -0
- package/dist/src/ai/anthropic.js +262 -0
- package/dist/src/ai/batch.d.ts +2 -2
- package/dist/src/ai/batch.js +1 -1
- package/dist/src/ai/estimate.d.ts +4 -0
- package/dist/src/ai/estimate.js +17 -2
- package/dist/src/ai/mermaid_loop.js +7 -4
- package/dist/src/ai/openai.d.ts +45 -0
- package/dist/src/ai/openai.js +206 -0
- package/dist/src/ai/openrouter.d.ts +1 -1
- package/dist/src/ai/openrouter.js +3 -3
- package/dist/src/ai/pricing.js +1 -1
- package/dist/src/ai/provider.d.ts +6 -0
- package/dist/src/ai/provider.js +26 -3
- package/dist/src/ai/verify.d.ts +9 -1
- package/dist/src/ai/verify.js +81 -3
- package/dist/src/cli/args.d.ts +1 -2
- package/dist/src/cli/args.js +24 -20
- package/dist/src/cli/commands/config.d.ts +1 -1
- package/dist/src/cli/commands/config.js +10 -4
- package/dist/src/cli/commands/probe.js +3 -11
- package/dist/src/cli/commands/registry.d.ts +4 -4
- package/dist/src/cli/commands/registry.js +84 -10
- package/dist/src/cli/commands/review.js +18 -13
- package/dist/src/cli/commands/rollback.d.ts +1 -0
- package/dist/src/cli/commands/rollback.js +42 -0
- package/dist/src/cli/commands/serve.js +2 -3
- package/dist/src/cli/commands/set.js +71 -7
- package/dist/src/cli/commands/view.js +10 -9
- package/dist/src/cli/error.d.ts +4 -4
- package/dist/src/cli/error.js +4 -4
- package/dist/src/cli/main.d.ts +4 -4
- package/dist/src/cli/main.js +13 -8
- package/dist/src/cli/prompt.js +1 -1
- package/dist/src/cli/review_args.d.ts +9 -3
- package/dist/src/cli/review_args.js +28 -5
- package/dist/src/cli/review_github.d.ts +31 -0
- package/dist/src/cli/review_github.js +128 -0
- package/dist/src/cli/review_result.d.ts +27 -16
- package/dist/src/cli/review_result.js +7 -7
- package/dist/src/config.d.ts +28 -6
- package/dist/src/config.js +15 -1
- package/dist/src/git/default_branch.d.ts +1 -1
- package/dist/src/git/default_branch.js +1 -1
- package/dist/src/github/app.js +2 -2
- package/dist/src/github/app_manifest.d.ts +1 -7
- package/dist/src/github/app_manifest.js +2 -7
- package/dist/src/github/client.d.ts +4 -1
- package/dist/src/github/client.js +14 -2
- package/dist/src/github/gh.js +10 -7
- package/dist/src/github/pat.js +4 -4
- package/dist/src/github/permissions.d.ts +36 -0
- package/dist/src/github/permissions.js +334 -0
- package/dist/src/knowledge/facts.js +1 -1
- package/dist/src/knowledge/guide.d.ts +3 -3
- package/dist/src/knowledge/guide.js +5 -5
- package/dist/src/knowledge/probe.js +1 -2
- package/dist/src/knowledge/sections.d.ts +3 -3
- package/dist/src/knowledge/sections.js +3 -3
- package/dist/src/knowledge/skill.d.ts +1 -1
- package/dist/src/knowledge/skill.js +4 -4
- package/dist/src/knowledge/synthesis.d.ts +1 -1
- package/dist/src/knowledge/synthesis.js +5 -5
- package/dist/src/knowledge/types.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.js +1 -1
- package/dist/src/local/codegraph_prepare.js +1 -1
- package/dist/src/local/git_ops.d.ts +1 -1
- package/dist/src/local/git_ops.js +1 -1
- package/dist/src/local/git_parse.d.ts +2 -2
- package/dist/src/local/git_parse.js +2 -2
- package/dist/src/local/git_revision.d.ts +1 -1
- package/dist/src/local/git_revision.js +1 -1
- package/dist/src/local/git_untracked.d.ts +1 -1
- package/dist/src/local/git_untracked.js +1 -1
- package/dist/src/local/review_local.js +17 -14
- package/dist/src/local/review_lock.d.ts +1 -1
- package/dist/src/local/review_lock.js +1 -1
- package/dist/src/pr/checkout.js +2 -2
- package/dist/src/pr/findings.js +1 -1
- package/dist/src/pr/findings_json.d.ts +2 -2
- package/dist/src/pr/findings_json.js +1 -1
- package/dist/src/pr/github_revision.js +1 -1
- package/dist/src/pr/review_copy.d.ts +2 -2
- package/dist/src/pr/review_copy.js +2 -2
- package/dist/src/pr/reviewer.d.ts +6 -0
- package/dist/src/pr/reviewer.js +34 -28
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +84 -23
- package/dist/src/remote/fixtures.js +1 -1
- package/dist/src/remote/http.d.ts +10 -1
- package/dist/src/remote/http.js +15 -3
- package/dist/src/remote/schema.d.ts +3 -3
- package/dist/src/remote/schema.js +3 -3
- package/dist/src/remote/server/body.d.ts +1 -1
- package/dist/src/remote/server/body.js +1 -1
- package/dist/src/remote/server/byok_keys.d.ts +15 -0
- package/dist/src/remote/server/byok_keys.js +40 -0
- package/dist/src/remote/server/guides.js +2 -2
- package/dist/src/remote/server/routes.js +8 -1
- package/dist/src/remote/validate.js +15 -1
- package/dist/src/review/blocking.d.ts +2 -2
- package/dist/src/review/blocking.js +2 -2
- package/dist/src/review/carry_over.d.ts +2 -2
- package/dist/src/review/carry_over.js +2 -2
- package/dist/src/review/guides.js +1 -1
- package/dist/src/server/api/remote_tokens.js +2 -0
- package/dist/src/server/api/repos.js +51 -0
- package/dist/src/server/api/settings.js +10 -0
- package/dist/src/server/errors.d.ts +1 -1
- package/dist/src/server/http.js +1 -1
- package/dist/src/server/pages/activity.d.ts +2 -0
- package/dist/src/server/pages/activity.js +18 -4
- package/dist/src/server/pages/add_repo.js +61 -1
- package/dist/src/server/pages/analytics.js +18 -8
- package/dist/src/server/pages/cost.d.ts +21 -0
- package/dist/src/server/pages/cost.js +72 -0
- package/dist/src/server/pages/pr.js +5 -4
- package/dist/src/server/pages/repo.js +3 -2
- package/dist/src/server/pages/repo_prs.js +3 -2
- package/dist/src/server/pages/repo_remote.js +3 -2
- package/dist/src/server/pages/repo_settings.js +326 -12
- package/dist/src/server/pages/router.js +3 -2
- package/dist/src/server/pages/settings.js +48 -6
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/credentials.js +8 -11
- package/dist/src/services/dashboard.d.ts +22 -9
- package/dist/src/services/dashboard.js +16 -2
- package/dist/src/services/probe.js +1 -1
- package/dist/src/services/remote_review.js +141 -35
- package/dist/src/services/remote_review_abort.d.ts +5 -1
- package/dist/src/services/remote_review_abort.js +9 -1
- package/dist/src/services/review.d.ts +1 -1
- package/dist/src/services/review.js +17 -5
- package/dist/src/services/review_policy.d.ts +116 -0
- package/dist/src/services/review_policy.js +489 -0
- package/dist/src/services/setup.d.ts +4 -3
- package/dist/src/services/setup.js +18 -16
- package/dist/src/services/setup_checklist.js +2 -2
- package/dist/src/services/webhook.d.ts +23 -0
- package/dist/src/services/webhook.js +141 -34
- package/dist/src/store/app_db.d.ts +5 -0
- package/dist/src/store/app_db.js +69 -13
- package/dist/src/store/backup.d.ts +31 -0
- package/dist/src/store/backup.js +158 -0
- package/dist/src/store/migrations.d.ts +1 -1
- package/dist/src/store/migrations.js +21 -1
- package/dist/src/store/remote_review_inputs.d.ts +3 -0
- package/dist/src/store/remote_review_inputs.js +3 -3
- package/dist/src/store/remote_tokens.d.ts +1 -1
- package/dist/src/store/remote_tokens.js +1 -1
- package/dist/src/store/repos.d.ts +2 -2
- package/dist/src/store/repos.js +1 -1
- package/dist/src/store/reviews.d.ts +21 -13
- package/dist/src/store/reviews.js +60 -15
- package/dist/src/store/rows.d.ts +10 -0
- package/dist/src/tools/codegraph.d.ts +4 -4
- package/dist/src/tools/codegraph.js +5 -5
- package/dist/src/tools/codegraph_exec.d.ts +1 -1
- package/dist/src/tools/codegraph_exec.js +1 -1
- package/dist/src/types.d.ts +12 -6
- package/dist/src/util/cost.d.ts +52 -0
- package/dist/src/util/cost.js +70 -0
- package/dist/src/util/log.d.ts +1 -1
- package/dist/src/util/log.js +2 -2
- package/dist/src/util/redact.js +1 -0
- package/dist/src/util/run_summary.d.ts +7 -2
- package/dist/src/util/run_summary.js +12 -2
- package/dist/src/util/time.d.ts +1 -1
- package/dist/src/util/time.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { getEnv } from "../util/runtime.js";
|
|
2
|
+
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
3
|
+
/** `CM_OPENAI_URL` points at a local fake in tests. Unset means the real
|
|
4
|
+
* endpoint, so production behavior is unchanged. */
|
|
5
|
+
export function openAiBase() {
|
|
6
|
+
const endpoint = getEnv("CM_OPENAI_URL") ?? "https://api.openai.com/v1/responses";
|
|
7
|
+
return endpoint.replace(/\/responses\/?$/, "");
|
|
8
|
+
}
|
|
9
|
+
/** A failed response as actionable text. Only `error.message` is
|
|
10
|
+
* shown: the raw body can carry request ids that mean nothing in a terminal. */
|
|
11
|
+
export function openAiError(model, status, body) {
|
|
12
|
+
let detail = "";
|
|
13
|
+
let code = "";
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(body);
|
|
16
|
+
detail = String(parsed.error?.message ?? "");
|
|
17
|
+
code = String(parsed.error?.code ?? "");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
detail = body.trim().slice(0, 200);
|
|
21
|
+
}
|
|
22
|
+
if (status === 401) {
|
|
23
|
+
return new CliError("openai_unauthorized", "OpenAI rejected the API key.", "Set a new one with co-maintainer set --token=...", EXIT_USAGE);
|
|
24
|
+
}
|
|
25
|
+
if (status === 400 && (code === "model_not_found" || /model/i.test(detail))) {
|
|
26
|
+
return new CliError("openai_unknown_model", `OpenAI does not know the model ${model}.`, "Pick one at https://platform.openai.com/docs/models and set it with co-maintainer set --high-model=...", EXIT_USAGE);
|
|
27
|
+
}
|
|
28
|
+
return new CliError("openai_failed", `OpenAI failed with ${status}${detail ? `: ${detail}` : "."}`, undefined, EXIT_RUNTIME);
|
|
29
|
+
}
|
|
30
|
+
/** The Chat-Completions-shaped message list every caller already builds
|
|
31
|
+
* (`chatBody` does the same fallback for OpenRouter). */
|
|
32
|
+
function effectiveMessages(request) {
|
|
33
|
+
return (request.messages ?? [
|
|
34
|
+
...(request.system
|
|
35
|
+
? [{ role: "system", content: request.system }]
|
|
36
|
+
: []),
|
|
37
|
+
{ role: "user", content: request.prompt },
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
/** `{type:"function",function:{name,parameters}}` (what `mermaid.ts` and the
|
|
41
|
+
* codegraph tools build) to Responses' flat shape. */
|
|
42
|
+
function toResponsesTool(tool) {
|
|
43
|
+
const fn = (tool.function ?? {});
|
|
44
|
+
return {
|
|
45
|
+
type: "function",
|
|
46
|
+
name: String(fn.name ?? ""),
|
|
47
|
+
description: fn.description ? String(fn.description) : undefined,
|
|
48
|
+
parameters: fn.parameters,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** A tool call and its result do not live inside a message the way Chat
|
|
52
|
+
* Completions nests them; each is its own item in `input`. */
|
|
53
|
+
function toResponsesInput(messages) {
|
|
54
|
+
const input = [];
|
|
55
|
+
let instructions;
|
|
56
|
+
for (const message of messages) {
|
|
57
|
+
if (message.role === "system") {
|
|
58
|
+
instructions = instructions
|
|
59
|
+
? `${instructions}\n\n${message.content ?? ""}`
|
|
60
|
+
: String(message.content ?? "");
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (message.role === "tool") {
|
|
64
|
+
input.push({
|
|
65
|
+
type: "function_call_output",
|
|
66
|
+
call_id: message.tool_call_id,
|
|
67
|
+
output: message.content ?? "",
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (message.tool_calls?.length) {
|
|
72
|
+
if (message.content) {
|
|
73
|
+
input.push({ role: "assistant", content: message.content });
|
|
74
|
+
}
|
|
75
|
+
for (const call of message.tool_calls) {
|
|
76
|
+
input.push({
|
|
77
|
+
type: "function_call",
|
|
78
|
+
call_id: call.id,
|
|
79
|
+
name: call.function.name,
|
|
80
|
+
arguments: call.function.arguments,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
input.push({ role: message.role, content: message.content ?? "" });
|
|
86
|
+
}
|
|
87
|
+
return { instructions, input };
|
|
88
|
+
}
|
|
89
|
+
function responsesBody(model, request) {
|
|
90
|
+
const { instructions, input } = toResponsesInput(effectiveMessages(request));
|
|
91
|
+
const schema = request.responseFormat?.json_schema;
|
|
92
|
+
return {
|
|
93
|
+
model,
|
|
94
|
+
...(instructions ? { instructions } : {}),
|
|
95
|
+
input,
|
|
96
|
+
max_output_tokens: request.maxTokens,
|
|
97
|
+
...(request.reasoningEffort
|
|
98
|
+
? { reasoning: { effort: request.reasoningEffort } }
|
|
99
|
+
: {}),
|
|
100
|
+
...(request.tools?.length
|
|
101
|
+
? {
|
|
102
|
+
tools: request.tools.map(toResponsesTool),
|
|
103
|
+
tool_choice: "auto",
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(schema
|
|
107
|
+
? {
|
|
108
|
+
text: {
|
|
109
|
+
format: {
|
|
110
|
+
type: "json_schema",
|
|
111
|
+
name: schema.name,
|
|
112
|
+
schema: schema.schema,
|
|
113
|
+
strict: schema.strict ?? false,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function parseResponsesBody(json, model) {
|
|
121
|
+
const output = Array.isArray(json.output)
|
|
122
|
+
? json.output
|
|
123
|
+
: [];
|
|
124
|
+
const texts = [];
|
|
125
|
+
const toolCalls = [];
|
|
126
|
+
for (const item of output) {
|
|
127
|
+
if (item.type === "function_call" && item.call_id && item.name) {
|
|
128
|
+
toolCalls.push({
|
|
129
|
+
id: item.call_id,
|
|
130
|
+
type: "function",
|
|
131
|
+
function: { name: item.name, arguments: item.arguments ?? "{}" },
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (item.type !== "message")
|
|
136
|
+
continue;
|
|
137
|
+
for (const part of item.content ?? []) {
|
|
138
|
+
if (part.type === "refusal") {
|
|
139
|
+
throw new CliError("openai_refusal", `OpenAI refused to answer: ${part.refusal ?? "no reason given"}`, undefined, EXIT_RUNTIME);
|
|
140
|
+
}
|
|
141
|
+
if (part.type === "output_text" && part.text)
|
|
142
|
+
texts.push(part.text);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const usage = json.usage;
|
|
146
|
+
return {
|
|
147
|
+
text: texts.join(""),
|
|
148
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
149
|
+
tokensIn: Number(usage?.input_tokens ?? 0),
|
|
150
|
+
tokensOut: Number(usage?.output_tokens ?? 0),
|
|
151
|
+
// OpenAI's Responses API reports no dollar cost, so this is always
|
|
152
|
+
// unknown (`costReasonText`'s "provider_did_not_report").
|
|
153
|
+
cost: undefined,
|
|
154
|
+
model,
|
|
155
|
+
provider: "openai",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export class OpenAiProvider {
|
|
159
|
+
supportsTools = true;
|
|
160
|
+
endpoint = `${openAiBase()}/responses`;
|
|
161
|
+
apiKey;
|
|
162
|
+
model;
|
|
163
|
+
constructor(apiKey, model) {
|
|
164
|
+
if (!apiKey)
|
|
165
|
+
throw new Error("OpenAI requires an API key");
|
|
166
|
+
this.apiKey = apiKey;
|
|
167
|
+
this.model = model;
|
|
168
|
+
}
|
|
169
|
+
async post(body) {
|
|
170
|
+
try {
|
|
171
|
+
return await fetch(this.endpoint, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify(body),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw networkFailure(this.endpoint, error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async complete(request) {
|
|
185
|
+
let response = await this.post(responsesBody(this.model, request));
|
|
186
|
+
// A model may reject `text.format` next to tools. The schema is only a
|
|
187
|
+
// hint (the prompt also asks for a fenced JSON block), so one retry
|
|
188
|
+
// without it beats failing the review outright.
|
|
189
|
+
if (response.status === 400 && request.responseFormat) {
|
|
190
|
+
const detail = await response.text();
|
|
191
|
+
if (/text\.format|json_schema|structured/i.test(detail)) {
|
|
192
|
+
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
193
|
+
response = await this.post(responsesBody(this.model, withoutSchema));
|
|
194
|
+
if (response.ok) {
|
|
195
|
+
return parseResponsesBody((await response.json()), this.model);
|
|
196
|
+
}
|
|
197
|
+
throw openAiError(this.model, response.status, await response.text());
|
|
198
|
+
}
|
|
199
|
+
throw openAiError(this.model, response.status, detail);
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw openAiError(this.model, response.status, await response.text());
|
|
203
|
+
}
|
|
204
|
+
return parseResponsesBody((await response.json()), this.model);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AiProvider, AiRequest, AiResponse } from "../types.ts";
|
|
2
2
|
import { CliError } from "../cli/error.ts";
|
|
3
|
-
/** Provider failures as actionable text
|
|
3
|
+
/** Provider failures as actionable text. A 401 and a 400 for
|
|
4
4
|
* an unknown model are preconditions the user can fix, so they are usage
|
|
5
5
|
* errors; anything else is a runtime failure. Only the provider's own
|
|
6
6
|
* `error.message` is surfaced: the raw body can carry account metadata
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chatBody, parseChatResponse } from "./provider.js";
|
|
2
2
|
import { getEnv } from "../util/runtime.js";
|
|
3
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
4
|
-
/** Provider failures as actionable text
|
|
4
|
+
/** Provider failures as actionable text. A 401 and a 400 for
|
|
5
5
|
* an unknown model are preconditions the user can fix, so they are usage
|
|
6
6
|
* errors; anything else is a runtime failure. Only the provider's own
|
|
7
7
|
* `error.message` is surfaced: the raw body can carry account metadata
|
|
@@ -54,7 +54,7 @@ export class OpenRouterProvider {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
catch (error) {
|
|
57
|
-
// `fetch failed` alone names neither the host nor the cause
|
|
57
|
+
// `fetch failed` alone names neither the host nor the cause.
|
|
58
58
|
throw networkFailure(this.endpoint, error);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -62,7 +62,7 @@ export class OpenRouterProvider {
|
|
|
62
62
|
let response = await this.post(chatBody(this.model, request));
|
|
63
63
|
// A provider may refuse `response_format` next to `tools`. The schema is
|
|
64
64
|
// only a hint — the prompt also asks for a fenced JSON block — so one
|
|
65
|
-
// retry without it is better than failing the review
|
|
65
|
+
// retry without it is better than failing the review.
|
|
66
66
|
if (response.status === 400 && request.responseFormat) {
|
|
67
67
|
const detail = await response.text();
|
|
68
68
|
if (/response_format|json_schema|structured/i.test(detail)) {
|
package/dist/src/ai/pricing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** OpenRouter model prices for the probe estimate
|
|
1
|
+
/** OpenRouter model prices for the probe estimate.
|
|
2
2
|
*
|
|
3
3
|
* `/api/v1/models` is public: no key needed. The list is large and changes
|
|
4
4
|
* slowly, so it is cached for 24 hours in cache.db. A network failure is not
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AiProvider, AiRequest, AiResponse, Json, Options } from "../types.ts";
|
|
2
|
+
/** The one list of accepted `--ai` values, so a new or retired provider is
|
|
3
|
+
* added or removed in a single place instead of the half dozen copies
|
|
4
|
+
* Hetzner's removal had to chase down. */
|
|
5
|
+
export declare const AI_PROVIDERS: readonly ["none", "openrouter", "openai", "anthropic"];
|
|
6
|
+
/** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
|
|
7
|
+
export declare function rejectRetiredProvider(value: string | undefined): void;
|
|
2
8
|
export declare function createAiProvider(options: Options, model: string): AiProvider | undefined;
|
|
3
9
|
export declare function parseChatResponse(json: Json, provider: AiResponse["provider"], model: string): AiResponse;
|
|
4
10
|
export declare function chatBody(model: string, request: AiRequest): Json;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import { HetznerProvider } from "./hetzner.js";
|
|
2
1
|
import { OpenRouterProvider } from "./openrouter.js";
|
|
2
|
+
import { OpenAiProvider } from "./openai.js";
|
|
3
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
4
|
+
import { die } from "../cli/error.js";
|
|
5
|
+
/** The one list of accepted `--ai` values, so a new or retired provider is
|
|
6
|
+
* added or removed in a single place instead of the half dozen copies
|
|
7
|
+
* Hetzner's removal had to chase down. */
|
|
8
|
+
export const AI_PROVIDERS = [
|
|
9
|
+
"none",
|
|
10
|
+
"openrouter",
|
|
11
|
+
"openai",
|
|
12
|
+
"anthropic",
|
|
13
|
+
];
|
|
14
|
+
/** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
|
|
15
|
+
export function rejectRetiredProvider(value) {
|
|
16
|
+
if (value === "hetzner") {
|
|
17
|
+
die("Hetzner is no longer supported. Use openrouter or none.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
export function createAiProvider(options, model) {
|
|
4
21
|
if (options.ai === "none")
|
|
5
22
|
return undefined;
|
|
6
23
|
if (options.ai === "openrouter") {
|
|
7
24
|
return new OpenRouterProvider(options.aiToken ?? "", model);
|
|
8
25
|
}
|
|
9
|
-
|
|
26
|
+
if (options.ai === "openai") {
|
|
27
|
+
return new OpenAiProvider(options.aiToken ?? "", model);
|
|
28
|
+
}
|
|
29
|
+
if (options.ai === "anthropic") {
|
|
30
|
+
return new AnthropicProvider(options.aiToken ?? "", model);
|
|
31
|
+
}
|
|
32
|
+
die(`${options.ai} support is not finished yet. Use openrouter or none.`);
|
|
10
33
|
}
|
|
11
34
|
export function parseChatResponse(json, provider, model) {
|
|
12
35
|
const choice = Array.isArray(json.choices)
|
|
@@ -59,7 +82,7 @@ export function chatBody(model, request) {
|
|
|
59
82
|
...(request.tools?.length
|
|
60
83
|
? { tools: request.tools, tool_choice: "auto" }
|
|
61
84
|
: {}),
|
|
62
|
-
// A review asks for structured output
|
|
85
|
+
// A review asks for structured output. The schema is sent even
|
|
63
86
|
// when tools are present, because most providers accept the pair; one that
|
|
64
87
|
// rejects it gets a single retry without the schema (see
|
|
65
88
|
// `OpenRouterProvider.complete`), and the prompt also asks for a fenced
|
package/dist/src/ai/verify.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Pre-flight checks for `config set
|
|
1
|
+
/** Pre-flight checks for `config set`.
|
|
2
2
|
*
|
|
3
3
|
* A wrong API key or an unknown model used to be accepted by `set` and only
|
|
4
4
|
* surfaced during a review, after the pull request had been fetched and
|
|
@@ -23,3 +23,11 @@ export type VerifyResult = {
|
|
|
23
23
|
* lists it. The first failure decides: a bad key is reported before a model
|
|
24
24
|
* question, since the model check would fail on the key too. */
|
|
25
25
|
export declare function verifyOpenRouter(apiKey: string, model?: string): Promise<VerifyResult>;
|
|
26
|
+
/** Same shape as {@link verifyOpenRouter}: `/models` both authenticates the
|
|
27
|
+
* key and, when `model` is given, confirms OpenAI actually has it. */
|
|
28
|
+
export declare function verifyOpenAi(apiKey: string, model?: string): Promise<VerifyResult>;
|
|
29
|
+
/** Anthropic authenticates the key and lists models at `/models`, but with its
|
|
30
|
+
* own headers (`x-api-key` + `anthropic-version`) instead of a Bearer token.
|
|
31
|
+
* Without a model the call only proves the key; with one it also confirms
|
|
32
|
+
* Anthropic serves it. */
|
|
33
|
+
export declare function verifyAnthropic(apiKey: string, model?: string): Promise<VerifyResult>;
|
package/dist/src/ai/verify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Pre-flight checks for `config set
|
|
1
|
+
/** Pre-flight checks for `config set`.
|
|
2
2
|
*
|
|
3
3
|
* A wrong API key or an unknown model used to be accepted by `set` and only
|
|
4
4
|
* surfaced during a review, after the pull request had been fetched and
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* user's mistake, so it warns and saves instead of refusing. */
|
|
8
8
|
import { CliError, EXIT_USAGE } from "../cli/error.js";
|
|
9
9
|
import { getEnv } from "../util/runtime.js";
|
|
10
|
+
import { openAiBase } from "./openai.js";
|
|
11
|
+
import { anthropicBase, ANTHROPIC_VERSION } from "./anthropic.js";
|
|
10
12
|
/** The chat endpoint the provider uses, with `/chat/completions` removed.
|
|
11
13
|
* `CM_OPENROUTER_URL` points at a local fake in tests, so the base follows it
|
|
12
14
|
* and the verification hits the same server the review would. */
|
|
@@ -15,11 +17,11 @@ export function openRouterBase() {
|
|
|
15
17
|
"https://openrouter.ai/api/v1/chat/completions";
|
|
16
18
|
return endpoint.replace(/\/chat\/completions\/?$/, "");
|
|
17
19
|
}
|
|
18
|
-
async function getJson(url, apiKey) {
|
|
20
|
+
async function getJson(url, apiKey, headers) {
|
|
19
21
|
let response;
|
|
20
22
|
try {
|
|
21
23
|
response = await fetch(url, {
|
|
22
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
24
|
+
headers: headers ?? { Authorization: `Bearer ${apiKey}` },
|
|
23
25
|
});
|
|
24
26
|
}
|
|
25
27
|
catch (error) {
|
|
@@ -76,3 +78,79 @@ export async function verifyOpenRouter(apiKey, model) {
|
|
|
76
78
|
}
|
|
77
79
|
return { status: "ok" };
|
|
78
80
|
}
|
|
81
|
+
/** Same shape as {@link verifyOpenRouter}: `/models` both authenticates the
|
|
82
|
+
* key and, when `model` is given, confirms OpenAI actually has it. */
|
|
83
|
+
export async function verifyOpenAi(apiKey, model) {
|
|
84
|
+
const base = openAiBase();
|
|
85
|
+
const models = await getJson(`${base}/models`, apiKey);
|
|
86
|
+
if ("unreachable" in models) {
|
|
87
|
+
return { status: "unreachable", reason: models.unreachable };
|
|
88
|
+
}
|
|
89
|
+
if (models.status === 401) {
|
|
90
|
+
return {
|
|
91
|
+
status: "rejected",
|
|
92
|
+
error: new CliError("openai_unauthorized", "OpenAI rejected the API key.", "Check the key at https://platform.openai.com/api-keys and pass it again.", EXIT_USAGE),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (models.status >= 400) {
|
|
96
|
+
return {
|
|
97
|
+
status: "unreachable",
|
|
98
|
+
reason: `OpenAI returned ${models.status}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!model)
|
|
102
|
+
return { status: "ok" };
|
|
103
|
+
const rows = models.body?.data;
|
|
104
|
+
if (!Array.isArray(rows)) {
|
|
105
|
+
return { status: "unreachable", reason: "OpenAI returned no model list" };
|
|
106
|
+
}
|
|
107
|
+
if (!rows.some((row) => row?.id === model)) {
|
|
108
|
+
return {
|
|
109
|
+
status: "rejected",
|
|
110
|
+
error: new CliError("openai_unknown_model", `OpenAI does not know the model ${model}.`, "Pick one at https://platform.openai.com/docs/models and set it with co-maintainer set --high-model=...", EXIT_USAGE),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { status: "ok" };
|
|
114
|
+
}
|
|
115
|
+
/** Anthropic authenticates the key and lists models at `/models`, but with its
|
|
116
|
+
* own headers (`x-api-key` + `anthropic-version`) instead of a Bearer token.
|
|
117
|
+
* Without a model the call only proves the key; with one it also confirms
|
|
118
|
+
* Anthropic serves it. */
|
|
119
|
+
export async function verifyAnthropic(apiKey, model) {
|
|
120
|
+
const base = anthropicBase();
|
|
121
|
+
const models = await getJson(`${base}/models`, apiKey, {
|
|
122
|
+
"x-api-key": apiKey,
|
|
123
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
124
|
+
});
|
|
125
|
+
if ("unreachable" in models) {
|
|
126
|
+
return { status: "unreachable", reason: models.unreachable };
|
|
127
|
+
}
|
|
128
|
+
if (models.status === 401 || models.status === 403) {
|
|
129
|
+
return {
|
|
130
|
+
status: "rejected",
|
|
131
|
+
error: new CliError("anthropic_unauthorized", "Anthropic rejected the API key.", "Check the key at https://console.anthropic.com/settings/keys and pass it again.", EXIT_USAGE),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (models.status >= 400) {
|
|
135
|
+
return {
|
|
136
|
+
status: "unreachable",
|
|
137
|
+
reason: `Anthropic returned ${models.status}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!model)
|
|
141
|
+
return { status: "ok" };
|
|
142
|
+
const rows = models.body?.data;
|
|
143
|
+
if (!Array.isArray(rows)) {
|
|
144
|
+
return {
|
|
145
|
+
status: "unreachable",
|
|
146
|
+
reason: "Anthropic returned no model list",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!rows.some((row) => row?.id === model)) {
|
|
150
|
+
return {
|
|
151
|
+
status: "rejected",
|
|
152
|
+
error: new CliError("anthropic_unknown_model", `Anthropic does not know the model ${model}.`, "Pick one at https://docs.anthropic.com/en/docs/about-claude/models and set it with co-maintainer set --high-model=...", EXIT_USAGE),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { status: "ok" };
|
|
156
|
+
}
|
package/dist/src/cli/args.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Options } from "../types.ts";
|
|
2
|
-
|
|
3
|
-
/** Plan §8.7 — `--json` must not prompt. */
|
|
2
|
+
/** `--json` must not prompt. */
|
|
4
3
|
export declare function setCliInteractive(value: boolean): void;
|
|
5
4
|
export declare function parseArgs(args: string[]): Promise<Options>;
|
package/dist/src/cli/args.js
CHANGED
|
@@ -2,6 +2,7 @@ import { prepareConfig } from "../config.js";
|
|
|
2
2
|
import { getEnv } from "../util/runtime.js";
|
|
3
3
|
import { askLine } from "./prompt.js";
|
|
4
4
|
import { die } from "./error.js";
|
|
5
|
+
import { AI_PROVIDERS, rejectRetiredProvider } from "../ai/provider.js";
|
|
5
6
|
import { detectRemoteRepo } from "../local/git_ops.js";
|
|
6
7
|
import { reviewBlockingFrom } from "../review/blocking.js";
|
|
7
8
|
import { renderCommandHelp, renderGlobalHelp, unknownCommandMessage, unknownOptionMessage, } from "./commands/registry.js";
|
|
@@ -13,10 +14,8 @@ function numberOption(value, name) {
|
|
|
13
14
|
return number;
|
|
14
15
|
}
|
|
15
16
|
const commands = ["probe", "init", "sync", "remake", "review"];
|
|
16
|
-
export const defaultLowModel = "openai/gpt-oss-120b";
|
|
17
|
-
const defaultHighModel = "openai/gpt-5.6-luna";
|
|
18
17
|
let cliInteractive = true;
|
|
19
|
-
/**
|
|
18
|
+
/** `--json` must not prompt. */
|
|
20
19
|
export function setCliInteractive(value) {
|
|
21
20
|
cliInteractive = value;
|
|
22
21
|
}
|
|
@@ -24,10 +23,10 @@ async function ask(label, fallback, required = false) {
|
|
|
24
23
|
if (!cliInteractive) {
|
|
25
24
|
if (fallback !== undefined && fallback !== "")
|
|
26
25
|
return fallback;
|
|
27
|
-
die(`Missing ${label}
|
|
26
|
+
die(`Missing ${label}. Pass it as a CLI option when running without an interactive terminal`);
|
|
28
27
|
}
|
|
29
28
|
if (process.stdin.isTTY !== true) {
|
|
30
|
-
die(`Missing ${label}
|
|
29
|
+
die(`Missing ${label}. Pass it as a CLI option when running without an interactive terminal`);
|
|
31
30
|
}
|
|
32
31
|
const value = await askLine(label, fallback);
|
|
33
32
|
if (required && !value)
|
|
@@ -57,7 +56,7 @@ export async function parseArgs(args) {
|
|
|
57
56
|
process.exit(0);
|
|
58
57
|
}
|
|
59
58
|
// `probe` (and only probe, for now) may omit the repo and let the current
|
|
60
|
-
// directory's git remote name it
|
|
59
|
+
// directory's git remote name it.
|
|
61
60
|
let repoName = repo;
|
|
62
61
|
if ((!repoName || repoName.startsWith("-")) && command === "probe") {
|
|
63
62
|
repoName = await detectRemoteRepo(process.cwd());
|
|
@@ -168,9 +167,10 @@ export async function parseArgs(args) {
|
|
|
168
167
|
}
|
|
169
168
|
const explicitAi = rest.some((arg) => arg.startsWith("--ai="));
|
|
170
169
|
const configuredAiRaw = env("CO_MAINTAINER_AI") ?? repoConfig.ai ?? config.ai;
|
|
170
|
+
rejectRetiredProvider(configuredAiRaw);
|
|
171
171
|
if (configuredAiRaw &&
|
|
172
|
-
!
|
|
173
|
-
die(
|
|
172
|
+
!AI_PROVIDERS.includes(configuredAiRaw)) {
|
|
173
|
+
die(`CO_MAINTAINER_AI/config.ai must be one of: ${AI_PROVIDERS.join(", ")}`);
|
|
174
174
|
}
|
|
175
175
|
const configuredAi = configuredAiRaw;
|
|
176
176
|
const configuredAuthRaw = env("CO_MAINTAINER_AUTH") ?? repoConfig.auth ?? config.auth;
|
|
@@ -187,46 +187,50 @@ export async function parseArgs(args) {
|
|
|
187
187
|
die("--auth=pat requires a GitHub token. Pass --github-pat=..., set GITHUB_TOKEN/GH_TOKEN, " +
|
|
188
188
|
"or run: co-maintainer set --github-pat=...");
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
rejectRetiredProvider(text("ai"));
|
|
191
|
+
let ai = choice("ai", AI_PROVIDERS, configuredAi ?? "none");
|
|
191
192
|
if (command === "review") {
|
|
192
|
-
if (explicitAi &&
|
|
193
|
-
choice("ai", ["none", "openrouter", "hetzner"], "none") !== "openrouter") {
|
|
193
|
+
if (explicitAi && choice("ai", AI_PROVIDERS, "none") !== "openrouter") {
|
|
194
194
|
die("review supports OpenRouter only");
|
|
195
195
|
}
|
|
196
196
|
ai = "openrouter";
|
|
197
197
|
}
|
|
198
198
|
else if (!explicitAi && !configuredAi && command !== "probe") {
|
|
199
|
-
const selected = await ask(
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
const selected = await ask(`AI provider (${AI_PROVIDERS.join("|")})`, "openrouter");
|
|
200
|
+
rejectRetiredProvider(selected);
|
|
201
|
+
if (!AI_PROVIDERS.includes(selected)) {
|
|
202
|
+
die(`AI provider must be one of: ${AI_PROVIDERS.join(", ")}`);
|
|
202
203
|
}
|
|
203
204
|
ai = selected;
|
|
204
205
|
}
|
|
205
206
|
let aiToken = text("token") ??
|
|
206
207
|
env("CO_MAINTAINER_TOKEN") ??
|
|
207
208
|
(ai === "openrouter" ? env("OPENROUTER_API_KEY") : undefined) ??
|
|
208
|
-
(ai === "hetzner" ? env("HETZNER_API_KEY") : undefined) ??
|
|
209
209
|
config.token;
|
|
210
210
|
let lowModel = text("low-model") ??
|
|
211
211
|
env("OPENROUTER_LOW_MODEL") ??
|
|
212
|
-
env("HETZNER_LOW_MODEL") ??
|
|
213
212
|
env("LOW_MODEL") ??
|
|
214
213
|
repoConfig.lowModel ??
|
|
215
214
|
config.lowModel;
|
|
216
215
|
let highModel = text("high-model") ??
|
|
217
216
|
env("OPENROUTER_HIGH_MODEL") ??
|
|
218
|
-
env("HETZNER_HIGH_MODEL") ??
|
|
219
217
|
env("HIGH_MODEL") ??
|
|
220
218
|
repoConfig.highModel ??
|
|
221
219
|
config.highModel;
|
|
222
220
|
if (command === "review") {
|
|
223
221
|
aiToken ??= await ask("openrouter API key", undefined, true);
|
|
224
|
-
|
|
222
|
+
// Review summarizes large files with the low model, so it needs one just
|
|
223
|
+
// as init and sync do. Without a default to fall back on, ask here too.
|
|
224
|
+
lowModel ??= await ask("low model", undefined, true);
|
|
225
|
+
highModel ??= await ask("high model", undefined, true);
|
|
225
226
|
}
|
|
226
227
|
else if (ai !== "none" && command !== "probe") {
|
|
228
|
+
// No house pick for any provider: models go stale and a shipped build
|
|
229
|
+
// cannot fetch a fresher one, so the model to use is the user's call,
|
|
230
|
+
// set once in config or passed every time (Murat, 2026-09-28).
|
|
227
231
|
aiToken ??= await ask(`${ai} API key`, undefined, true);
|
|
228
|
-
lowModel ??= await ask("low model",
|
|
229
|
-
highModel ??= await ask("high model",
|
|
232
|
+
lowModel ??= await ask("low model", undefined, true);
|
|
233
|
+
highModel ??= await ask("high model", undefined, true);
|
|
230
234
|
}
|
|
231
235
|
if (command !== "review" &&
|
|
232
236
|
ai !== "none" &&
|
|
@@ -37,7 +37,7 @@ export declare function resolveField(name: string): string;
|
|
|
37
37
|
* `defaults` are nested maps, not settings, so they are not listed.
|
|
38
38
|
* `satisfies` makes this the type's own vocabulary: a name that is not a
|
|
39
39
|
* `UserConfig` key is a compile error rather than a silently undefined read. */
|
|
40
|
-
export declare const KNOWN_FIELDS: readonly ["auth", "ai", "lowModel", "highModel", "token", "githubPat", "githubAppId", "githubAppPrivateKey", "githubAppPrivateKeyPath", "githubWebhookSecret", "webhookUrl", "githubOAuthClientId", "githubOAuthClientSecret", "githubOAuthAllowedUser", "passwordAuthDisabled", "githubAuthEnabled", "dashboardPasswordHash", "maxConcurrentJobs", "reviewBlocking", "remoteHost", "remoteToken", "remoteNoticeShownFor", "remoteSyncTimeoutSeconds", "maxConcurrentRemoteReviewsPerToken", "remoteToolOutputMaxChars"];
|
|
40
|
+
export declare const KNOWN_FIELDS: readonly ["auth", "ai", "lowModel", "highModel", "token", "githubPat", "githubAppId", "githubAppPrivateKey", "githubAppPrivateKeyPath", "githubWebhookSecret", "webhookUrl", "githubOAuthClientId", "githubOAuthClientSecret", "githubOAuthAllowedUser", "passwordAuthDisabled", "githubAuthEnabled", "dashboardPasswordHash", "maxConcurrentJobs", "reviewBlocking", "remoteHost", "remoteToken", "remoteNoticeShownFor", "remoteSyncTimeoutSeconds", "maxConcurrentRemoteReviewsPerToken", "remoteToolOutputMaxChars", "remoteByok", "remoteByokDefault", "reviewPolicy"];
|
|
41
41
|
/** `co-maintainer config <sub> ...`. `set` is handled by runSet, which this
|
|
42
42
|
* command delegates to so there is one implementation of the write rules. */
|
|
43
43
|
export declare function runConfig(args: string[]): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The `config` command: read and edit the user config without opening the
|
|
2
|
-
* file
|
|
2
|
+
* file. `set` is `config set` under its own name, so both share
|
|
3
3
|
* the mapping and masking here.
|
|
4
4
|
*
|
|
5
5
|
* The key list is derived from `UserConfig` at runtime where possible: a
|
|
@@ -18,6 +18,7 @@ export const SECRET_FIELDS = new Set([
|
|
|
18
18
|
"githubOAuthClientSecret",
|
|
19
19
|
"remoteToken",
|
|
20
20
|
"dashboardPasswordHash",
|
|
21
|
+
"remoteByok",
|
|
21
22
|
]);
|
|
22
23
|
/** `lowModel` -> `low-model`. The inverse of {@link configKey}. */
|
|
23
24
|
export function flagName(field) {
|
|
@@ -38,13 +39,15 @@ export function maskSecret(value) {
|
|
|
38
39
|
/** The env var that supplies each field, mirroring args.ts. Only the fields
|
|
39
40
|
* that actually read env are listed. */
|
|
40
41
|
export const ENV_BY_FIELD = {
|
|
41
|
-
token: ["CO_MAINTAINER_TOKEN", "OPENROUTER_API_KEY"
|
|
42
|
+
token: ["CO_MAINTAINER_TOKEN", "OPENROUTER_API_KEY"],
|
|
42
43
|
githubPat: ["GITHUB_TOKEN", "GH_TOKEN"],
|
|
43
44
|
ai: ["CO_MAINTAINER_AI"],
|
|
44
45
|
auth: ["CO_MAINTAINER_AUTH"],
|
|
45
|
-
lowModel: ["OPENROUTER_LOW_MODEL", "
|
|
46
|
-
highModel: ["OPENROUTER_HIGH_MODEL", "
|
|
46
|
+
lowModel: ["OPENROUTER_LOW_MODEL", "LOW_MODEL"],
|
|
47
|
+
highModel: ["OPENROUTER_HIGH_MODEL", "HIGH_MODEL"],
|
|
47
48
|
webhookUrl: ["CM_WEBHOOK_URL"],
|
|
49
|
+
remoteHost: ["CM_REMOTE_HOST"],
|
|
50
|
+
remoteToken: ["CM_REMOTE_TOKEN"],
|
|
48
51
|
reviewBlocking: ["CO_MAINTAINER_REVIEW_BLOCKING"],
|
|
49
52
|
};
|
|
50
53
|
/** Renders one field for people. A secret is masked; the password hash, which
|
|
@@ -151,6 +154,9 @@ export const KNOWN_FIELDS = [
|
|
|
151
154
|
"remoteSyncTimeoutSeconds",
|
|
152
155
|
"maxConcurrentRemoteReviewsPerToken",
|
|
153
156
|
"remoteToolOutputMaxChars",
|
|
157
|
+
"remoteByok",
|
|
158
|
+
"remoteByokDefault",
|
|
159
|
+
"reviewPolicy",
|
|
154
160
|
];
|
|
155
161
|
/** `co-maintainer config <sub> ...`. `set` is handled by runSet, which this
|
|
156
162
|
* command delegates to so there is one implementation of the write rules. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clientFor, runInitOrRemake } from "../../services/setup.js";
|
|
2
2
|
import { probePlan } from "../../services/probe.js";
|
|
3
3
|
import { log, startHeartbeat } from "../../util/log.js";
|
|
4
|
-
import { estimateInit, readJobHistory } from "../../ai/estimate.js";
|
|
4
|
+
import { estimateCostLine, estimateInit, readJobHistory, } from "../../ai/estimate.js";
|
|
5
5
|
import { loadPrices } from "../../ai/pricing.js";
|
|
6
6
|
import { cacheSet } from "../../store/cache_db.js";
|
|
7
7
|
export async function runProbe(options) {
|
|
@@ -25,7 +25,7 @@ export async function runProbe(options) {
|
|
|
25
25
|
for (const reason of analysis.reasons)
|
|
26
26
|
console.log(` - ${reason}`);
|
|
27
27
|
// The estimate is separate from the recommendation: the flags say what to
|
|
28
|
-
// read, this says what that costs in time and dollars
|
|
28
|
+
// read, this says what that costs in time and dollars. Prices are
|
|
29
29
|
// only fetched when an OpenRouter model would actually be billed, so a
|
|
30
30
|
// plain `--ai=none` probe makes no network call.
|
|
31
31
|
const needsPrices = options.ai === "openrouter" &&
|
|
@@ -50,15 +50,7 @@ export async function runProbe(options) {
|
|
|
50
50
|
console.log(` AI jobs: ${estimate.extract} extract + ${estimate.synth} synth`);
|
|
51
51
|
console.log(` tokens: ${range(estimate.tokensIn)} in · ${range(estimate.tokensOut)} out`);
|
|
52
52
|
console.log(` time: ${secondsRange(estimate.seconds)}s`);
|
|
53
|
-
|
|
54
|
-
console.log(` cost: $${estimate.usd[0].toFixed(4)}-$${estimate.usd[1].toFixed(4)}`);
|
|
55
|
-
}
|
|
56
|
-
else if (needsPrices && prices.source === "unavailable") {
|
|
57
|
-
console.log(" cost: unknown (could not read OpenRouter prices)");
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
console.log(" cost: set --ai=openrouter with both models to estimate dollars");
|
|
61
|
-
}
|
|
53
|
+
console.log(` cost: ${estimateCostLine(estimate, options.ai, needsPrices && prices.source === "unavailable")}`);
|
|
62
54
|
console.log(` basis: ${estimate.basis === "history"
|
|
63
55
|
? "this repository's recorded jobs"
|
|
64
56
|
: "the cm-dx-lab calibration"}; an estimate, not a bill`);
|