co-maintainer 0.5.0 → 0.5.1-beta.2
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/opencode.d.ts +10 -0
- package/dist/src/ai/opencode.js +76 -0
- package/dist/src/ai/openrouter.d.ts +23 -7
- package/dist/src/ai/openrouter.js +39 -23
- package/dist/src/ai/pricing.js +1 -1
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +42 -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 +38 -26
- 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 +1 -9
- package/dist/src/cli/commands/registry.js +75 -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 +49 -39
- 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 +50 -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 +142 -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 +19 -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 +2 -0
- package/dist/src/util/run_summary.d.ts +7 -2
- package/dist/src/util/run_summary.js +28 -2
- package/dist/src/util/time.d.ts +1 -1
- package/dist/src/util/time.js +1 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Murat Kirazkaya
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Murat Kirazkaya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/co-maintainer) [](https://muratkirazkaya.com/blogs/co-maintainer-an-imitator-of-yours)
|
|
4
4
|
|
|
5
5
|
[Website](https://co-maintainer.com) ·
|
|
6
|
-
[Docs](https://co-maintainer.com/docs) ·
|
|
6
|
+
[Docs](https://co-maintainer.com/docs/getting-started.html) ·
|
|
7
7
|
[Cloud](https://cloud.co-maintainer.com) (hosted, free, early access, invite only for now)
|
|
8
8
|
|
|
9
9
|
An imitator of yours, A PR review tool like co-maintainer.
|
|
@@ -37,19 +37,21 @@ co-maintainer review owner/repo 123 --improve-matrix=2 --debug
|
|
|
37
37
|
|
|
38
38
|
co-maintainer review # local branch (staged, unstaged, untracked)
|
|
39
39
|
co-maintainer review --json # machine-readable stdout, logs on stderr
|
|
40
|
+
co-maintainer review --output=github # annotations and a job summary inside GitHub Actions
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
Settings live in `config.json`, so a flag you use every time can be saved once:
|
|
43
44
|
|
|
44
45
|
```sh
|
|
45
|
-
co-maintainer config set ai
|
|
46
|
+
co-maintainer config set ai openrouter # or openai, anthropic, opencode-zen, opencode-go
|
|
47
|
+
co-maintainer config set ai-key sk-or-... # your provider's key (OpenRouter, OpenAI, Anthropic or OpenCode)
|
|
46
48
|
co-maintainer config set high-model openai/gpt-5.6-luna
|
|
47
49
|
co-maintainer config set low-model openai/gpt-oss-120b
|
|
48
50
|
co-maintainer config list # every key, its value and its source
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
Documentation: [Getting started](https://co-maintainer.com/docs/getting-started.html)
|
|
52
|
-
(install to first review) and the [full docs site](https://co-maintainer.com/docs),
|
|
54
|
+
(install to first review) and the [full docs site](https://co-maintainer.com/docs/getting-started.html),
|
|
53
55
|
sources in [`docs/md/`](docs/md/).
|
|
54
56
|
|
|
55
57
|
Use `co-maintainer help`, `co-maintainer -h`, or `co-maintainer --help` for the
|
package/dist/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Anthropic via the Messages API.
|
|
2
|
+
*
|
|
3
|
+
* The raw fetch shape every other provider uses, against Anthropic's own
|
|
4
|
+
* contract (docs.anthropic.com/en/api/messages). The request/response shapes
|
|
5
|
+
* differ from the Chat-Completions style this codebase's `AiRequest`/
|
|
6
|
+
* `AiMessage` types follow the way OpenAI's Responses API does, so this file is
|
|
7
|
+
* the one place that translates:
|
|
8
|
+
*
|
|
9
|
+
* - the system message becomes the top-level `system` field, not a message
|
|
10
|
+
* - an assistant tool call is a `tool_use` content block, not a `tool_calls`
|
|
11
|
+
* field on the message
|
|
12
|
+
* - tool results are `tool_result` blocks inside a `user` message, referenced
|
|
13
|
+
* by `tool_use_id`
|
|
14
|
+
* - a function tool is `{name, description, input_schema}`, not the
|
|
15
|
+
* Chat-Completions `{type:"function", function:{...}}` nesting
|
|
16
|
+
* - structured output is `output_config.format`, reasoning effort is
|
|
17
|
+
* `output_config.effort`, and there is no `temperature`: Claude Opus 5 and
|
|
18
|
+
* Sonnet 5 reject sampling parameters with a 400
|
|
19
|
+
*
|
|
20
|
+
* `cache_control` is set on the system prompt, which every review sends as the
|
|
21
|
+
* same guide prefix. Anthropic caches the longest stable prefix, so the
|
|
22
|
+
* guides are billed at the cached rate from the second review on and the
|
|
23
|
+
* `cache_read_input_tokens` count shows up in the usage log.
|
|
24
|
+
*
|
|
25
|
+
* ponytail: this talks to Messages without streaming. The original design
|
|
26
|
+
* called for the SDK's `stream()` + `finalMessage()` helper for large `max_tokens` (a single
|
|
27
|
+
* review can ask for 96k). The SDK is not a dependency (kept fetch-only, the
|
|
28
|
+
* same as every other provider), and a hand-rolled SSE accumulation loop is a
|
|
29
|
+
* lot of code for a first cut. Anthropic's HTTP request timeout is the risk;
|
|
30
|
+
* if a real review ever hits it, add the streaming loop here. Upgrade path:
|
|
31
|
+
* `stream:true` + parse `content_block_delta` events into the same
|
|
32
|
+
* `parseMessagesBody` result shape.
|
|
33
|
+
*/
|
|
34
|
+
import type { AiProvider, AiRequest, AiResponse, Json } from "../types.ts";
|
|
35
|
+
import { CliError } from "../cli/error.ts";
|
|
36
|
+
/** Required on every Messages request and the only version the errors below
|
|
37
|
+
* are written against. */
|
|
38
|
+
export declare const ANTHROPIC_VERSION = "2023-06-01";
|
|
39
|
+
/** `CM_ANTHROPIC_URL` points at a local fake in tests. Unset means the real
|
|
40
|
+
* endpoint, so production behavior is unchanged. */
|
|
41
|
+
export declare function anthropicBase(): string;
|
|
42
|
+
/** A failed response as actionable text. Only the error's own
|
|
43
|
+
* `message` is surfaced: the raw body can carry request ids that mean nothing
|
|
44
|
+
* in a terminal. */
|
|
45
|
+
export declare function anthropicError(model: string, status: number, body: string): CliError;
|
|
46
|
+
export declare function parseMessagesBody(json: Json, model: string): AiResponse;
|
|
47
|
+
export declare class AnthropicProvider implements AiProvider {
|
|
48
|
+
readonly supportsTools = true;
|
|
49
|
+
private readonly endpoint;
|
|
50
|
+
private readonly apiKey;
|
|
51
|
+
private readonly model;
|
|
52
|
+
constructor(apiKey: string, model: string);
|
|
53
|
+
private post;
|
|
54
|
+
complete(request: AiRequest): Promise<AiResponse>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { getEnv } from "../util/runtime.js";
|
|
2
|
+
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
3
|
+
/** Required on every Messages request and the only version the errors below
|
|
4
|
+
* are written against. */
|
|
5
|
+
export const ANTHROPIC_VERSION = "2023-06-01";
|
|
6
|
+
/** `CM_ANTHROPIC_URL` points at a local fake in tests. Unset means the real
|
|
7
|
+
* endpoint, so production behavior is unchanged. */
|
|
8
|
+
export function anthropicBase() {
|
|
9
|
+
const endpoint = getEnv("CM_ANTHROPIC_URL") ?? "https://api.anthropic.com/v1";
|
|
10
|
+
return endpoint.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
/** A failed response as actionable text. Only the error's own
|
|
13
|
+
* `message` is surfaced: the raw body can carry request ids that mean nothing
|
|
14
|
+
* in a terminal. */
|
|
15
|
+
export function anthropicError(model, status, body) {
|
|
16
|
+
let detail = "";
|
|
17
|
+
let type = "";
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(body);
|
|
20
|
+
detail = String(parsed.error?.message ?? "");
|
|
21
|
+
type = String(parsed.error?.type ?? "");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
detail = body.trim().slice(0, 200);
|
|
25
|
+
}
|
|
26
|
+
if (status === 401 || status === 403) {
|
|
27
|
+
return new CliError("anthropic_unauthorized", "Anthropic rejected the API key.", "Set a new one with co-maintainer set --token=...", EXIT_USAGE);
|
|
28
|
+
}
|
|
29
|
+
if ((status === 400 || status === 404) &&
|
|
30
|
+
(type === "not_found_error" || /model/i.test(detail))) {
|
|
31
|
+
return 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);
|
|
32
|
+
}
|
|
33
|
+
return new CliError("anthropic_failed", `Anthropic failed with ${status}${detail ? `: ${detail}` : "."}`, undefined, EXIT_RUNTIME);
|
|
34
|
+
}
|
|
35
|
+
/** The Chat-Completions-shaped message list every caller already builds
|
|
36
|
+
* (`chatBody` does the same fallback for OpenRouter). */
|
|
37
|
+
function effectiveMessages(request) {
|
|
38
|
+
return (request.messages ?? [
|
|
39
|
+
...(request.system
|
|
40
|
+
? [{ role: "system", content: request.system }]
|
|
41
|
+
: []),
|
|
42
|
+
{ role: "user", content: request.prompt },
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
/** `{type:"function",function:{name,parameters}}` (what `mermaid.ts` and the
|
|
46
|
+
* codegraph tools build) to Anthropic's `input_schema` shape. */
|
|
47
|
+
function toAnthropicTool(tool) {
|
|
48
|
+
const fn = (tool.function ?? {});
|
|
49
|
+
return {
|
|
50
|
+
name: String(fn.name ?? ""),
|
|
51
|
+
description: fn.description ? String(fn.description) : undefined,
|
|
52
|
+
input_schema: fn.parameters ?? { type: "object" },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** A tool call and its result do not live inside a message the way Chat
|
|
56
|
+
* Completions nests them. Anthropic delivers `tool_use` blocks in the
|
|
57
|
+
* assistant turn, and the caller must send every result back in a single
|
|
58
|
+
* `user` message with one `tool_result` block each, in the same order. */
|
|
59
|
+
function toAnthropicMessages(messages) {
|
|
60
|
+
const system = [];
|
|
61
|
+
const out = [];
|
|
62
|
+
// Consecutive `tool` messages collapse into one `user` turn: Anthropic
|
|
63
|
+
// rejects two user messages in a row.
|
|
64
|
+
let pendingResults = [];
|
|
65
|
+
const flushResults = () => {
|
|
66
|
+
if (!pendingResults.length)
|
|
67
|
+
return;
|
|
68
|
+
out.push({ role: "user", content: pendingResults });
|
|
69
|
+
pendingResults = [];
|
|
70
|
+
};
|
|
71
|
+
for (const message of messages) {
|
|
72
|
+
if (message.role === "system") {
|
|
73
|
+
system.push(String(message.content ?? ""));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (message.role === "tool") {
|
|
77
|
+
pendingResults.push({
|
|
78
|
+
type: "tool_result",
|
|
79
|
+
tool_use_id: message.tool_call_id ?? "",
|
|
80
|
+
content: message.content ?? "",
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
flushResults();
|
|
85
|
+
if (message.tool_calls?.length) {
|
|
86
|
+
const content = [];
|
|
87
|
+
if (message.content)
|
|
88
|
+
content.push({ type: "text", text: message.content });
|
|
89
|
+
for (const call of message.tool_calls) {
|
|
90
|
+
let input = {};
|
|
91
|
+
try {
|
|
92
|
+
input = JSON.parse(call.function.arguments);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
input = {};
|
|
96
|
+
}
|
|
97
|
+
content.push({
|
|
98
|
+
type: "tool_use",
|
|
99
|
+
id: call.id,
|
|
100
|
+
name: call.function.name,
|
|
101
|
+
input,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
out.push({ role: "assistant", content });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out.push({ role: message.role, content: message.content ?? "" });
|
|
108
|
+
}
|
|
109
|
+
flushResults();
|
|
110
|
+
return {
|
|
111
|
+
system: system.length ? system.join("\n\n") : undefined,
|
|
112
|
+
messages: out,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** Haiku 4.5 does not support `output_config.effort`; sending it is a 400.
|
|
116
|
+
* Anthropic's effort accepts low/medium/high/xhigh/max, so the only level this
|
|
117
|
+
* has to drop is "none": it means "reasoning off" in `AiRequest` and is not an
|
|
118
|
+
* effort value Anthropic lists. */
|
|
119
|
+
function supportsEffort(model) {
|
|
120
|
+
return !/haiku/i.test(model);
|
|
121
|
+
}
|
|
122
|
+
function messagesBody(model, request) {
|
|
123
|
+
const { system, messages } = toAnthropicMessages(effectiveMessages(request));
|
|
124
|
+
const schema = request.responseFormat?.json_schema;
|
|
125
|
+
const effort = request.reasoningEffort && request.reasoningEffort !== "none"
|
|
126
|
+
? request.reasoningEffort
|
|
127
|
+
: undefined;
|
|
128
|
+
// `output_config` carries both the structured-output schema and the effort
|
|
129
|
+
// level. It is only sent when one of them applies, so a plain request stays
|
|
130
|
+
// on the request shape every model understands.
|
|
131
|
+
const outputConfig = {
|
|
132
|
+
...(schema?.schema
|
|
133
|
+
? { format: { type: "json_schema", schema: schema.schema } }
|
|
134
|
+
: {}),
|
|
135
|
+
...(effort && supportsEffort(model) ? { effort } : {}),
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
model,
|
|
139
|
+
max_tokens: request.maxTokens,
|
|
140
|
+
// `cache_control` marks the guide prefix every review repeats, so it is
|
|
141
|
+
// read from cache instead of re-billed.
|
|
142
|
+
...(system
|
|
143
|
+
? {
|
|
144
|
+
system: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: system,
|
|
148
|
+
cache_control: { type: "ephemeral" },
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
153
|
+
messages,
|
|
154
|
+
...(request.tools?.length
|
|
155
|
+
? {
|
|
156
|
+
tools: request.tools.map(toAnthropicTool),
|
|
157
|
+
tool_choice: { type: "auto" },
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
...(Object.keys(outputConfig).length
|
|
161
|
+
? { output_config: outputConfig }
|
|
162
|
+
: {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** `refusal` stop reason with the human-readable explanation when Anthropic
|
|
166
|
+
* supplies one. Kept here so both the non-streaming body and any future
|
|
167
|
+
* streaming loop share one message. */
|
|
168
|
+
function refusalError(details) {
|
|
169
|
+
const explanation = details && typeof details.explanation === "string"
|
|
170
|
+
? details.explanation
|
|
171
|
+
: "no reason given";
|
|
172
|
+
return new CliError("anthropic_refusal", `Anthropic refused to answer: ${explanation}`, undefined, EXIT_RUNTIME);
|
|
173
|
+
}
|
|
174
|
+
export function parseMessagesBody(json, model) {
|
|
175
|
+
const stopReason = String(json.stop_reason ?? "");
|
|
176
|
+
if (stopReason === "refusal") {
|
|
177
|
+
throw refusalError(json.stop_details);
|
|
178
|
+
}
|
|
179
|
+
const content = Array.isArray(json.content)
|
|
180
|
+
? json.content
|
|
181
|
+
: [];
|
|
182
|
+
const texts = [];
|
|
183
|
+
const toolCalls = [];
|
|
184
|
+
for (const block of content) {
|
|
185
|
+
if (block.type === "text" && block.text) {
|
|
186
|
+
texts.push(block.text);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
190
|
+
toolCalls.push({
|
|
191
|
+
id: block.id,
|
|
192
|
+
type: "function",
|
|
193
|
+
function: {
|
|
194
|
+
name: block.name,
|
|
195
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const usage = json.usage;
|
|
201
|
+
return {
|
|
202
|
+
text: texts.join(""),
|
|
203
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
204
|
+
tokensIn: Number(usage?.input_tokens ?? 0),
|
|
205
|
+
tokensOut: Number(usage?.output_tokens ?? 0),
|
|
206
|
+
// Anthropic reports only token counts, never a dollar cost, so this is
|
|
207
|
+
// always unknown (`costReasonText`'s "provider_did_not_report").
|
|
208
|
+
cost: undefined,
|
|
209
|
+
model,
|
|
210
|
+
provider: "anthropic",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export class AnthropicProvider {
|
|
214
|
+
supportsTools = true;
|
|
215
|
+
endpoint = `${anthropicBase()}/messages`;
|
|
216
|
+
apiKey;
|
|
217
|
+
model;
|
|
218
|
+
constructor(apiKey, model) {
|
|
219
|
+
if (!apiKey)
|
|
220
|
+
throw new Error("Anthropic requires an API key");
|
|
221
|
+
this.apiKey = apiKey;
|
|
222
|
+
this.model = model;
|
|
223
|
+
}
|
|
224
|
+
async post(body) {
|
|
225
|
+
try {
|
|
226
|
+
return await fetch(this.endpoint, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: {
|
|
229
|
+
"x-api-key": this.apiKey,
|
|
230
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
throw networkFailure(this.endpoint, error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async complete(request) {
|
|
241
|
+
let response = await this.post(messagesBody(this.model, request));
|
|
242
|
+
// A model may reject `output_config.format` next to tools. The schema is
|
|
243
|
+
// only a hint (the prompt also asks for a fenced JSON block), so one retry
|
|
244
|
+
// without it beats failing the review outright.
|
|
245
|
+
if (response.status === 400 && request.responseFormat) {
|
|
246
|
+
const detail = await response.text();
|
|
247
|
+
if (/output_config|json_schema|structured/i.test(detail)) {
|
|
248
|
+
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
249
|
+
response = await this.post(messagesBody(this.model, withoutSchema));
|
|
250
|
+
if (response.ok) {
|
|
251
|
+
return parseMessagesBody((await response.json()), this.model);
|
|
252
|
+
}
|
|
253
|
+
throw anthropicError(this.model, response.status, await response.text());
|
|
254
|
+
}
|
|
255
|
+
throw anthropicError(this.model, response.status, detail);
|
|
256
|
+
}
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
throw anthropicError(this.model, response.status, await response.text());
|
|
259
|
+
}
|
|
260
|
+
return parseMessagesBody((await response.json()), this.model);
|
|
261
|
+
}
|
|
262
|
+
}
|
package/dist/src/ai/batch.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ type UsageSink = (job: string, response: AiResponse) => Promise<void>;
|
|
|
3
3
|
/** Returns `null` when a response is usable, or a short reason when it is not.
|
|
4
4
|
* `synthesis.ts` supplies one that parses the model text, because a syntactically
|
|
5
5
|
* fine HTTP response can still be unparseable output, and caching that is what
|
|
6
|
-
* poisoned every later `sync
|
|
6
|
+
* poisoned every later `sync`. */
|
|
7
7
|
export type AiValidator = (request: AiRequest, response: AiResponse) => string | null;
|
|
8
8
|
/** A unit whose output never parsed, kept so the run can say what it dropped. */
|
|
9
9
|
export type SkippedUnit = {
|
|
@@ -28,7 +28,7 @@ export declare class AiBatch {
|
|
|
28
28
|
/** Runs one unit, caching its response only when the validator accepts it.
|
|
29
29
|
* A rejected response is retried once; a second rejection is recorded as
|
|
30
30
|
* skipped and never cached, so the next `sync` retries it rather than
|
|
31
|
-
* replaying the same unusable output
|
|
31
|
+
* replaying the same unusable output. */
|
|
32
32
|
private completeUnit;
|
|
33
33
|
/** Records a unit as skipped: no cache write, so it stays retried and its
|
|
34
34
|
* absence is never mistaken for a deliberate empty result. */
|
package/dist/src/ai/batch.js
CHANGED
|
@@ -91,7 +91,7 @@ export class AiBatch {
|
|
|
91
91
|
/** Runs one unit, caching its response only when the validator accepts it.
|
|
92
92
|
* A rejected response is retried once; a second rejection is recorded as
|
|
93
93
|
* skipped and never cached, so the next `sync` retries it rather than
|
|
94
|
-
* replaying the same unusable output
|
|
94
|
+
* replaying the same unusable output. */
|
|
95
95
|
async completeUnit(item, results, usage, startedAt) {
|
|
96
96
|
for (const attempt of [1, 2]) {
|
|
97
97
|
try {
|
|
@@ -28,6 +28,10 @@ export type Estimate = {
|
|
|
28
28
|
usd?: [number, number];
|
|
29
29
|
basis: "history" | "calibration";
|
|
30
30
|
};
|
|
31
|
+
/** The probe's cost line: a dollar range when one is known, otherwise why
|
|
32
|
+
* not, so a provider with no price list (OpenAI, Anthropic) gets its own
|
|
33
|
+
* reason instead of being told to switch to OpenRouter. */
|
|
34
|
+
export declare function estimateCostLine(estimate: Estimate, ai: string, openRouterPricesUnavailable: boolean): string;
|
|
31
35
|
/** The number of `extract_unit` jobs the recommended init would queue: one per
|
|
32
36
|
* pull request plus one for the codebase/documents, matching
|
|
33
37
|
* `extractAiFacts`. */
|
package/dist/src/ai/estimate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Cost and time estimate for the recommended `init
|
|
1
|
+
/** Cost and time estimate for the recommended `init`.
|
|
2
2
|
*
|
|
3
3
|
* Three sources, in order of trust:
|
|
4
4
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 3. Time and per-job tokens come from this repository's recorded jobs when
|
|
12
12
|
* there are enough, and from the calibration below otherwise.
|
|
13
13
|
*
|
|
14
|
-
* Calibration
|
|
14
|
+
* Calibration: cm-dx-lab, 4 pull requests and 8 files, 5
|
|
15
15
|
* extract and 9 synth jobs, 151 s, $0.0014 — about 151/14 ≈ 10.8 s per job.
|
|
16
16
|
* Token counts per job are near 2.4k in and 0.7k out. The text always says
|
|
17
17
|
* the numbers are an estimate. */
|
|
@@ -67,6 +67,21 @@ export async function readJobHistory(repo) {
|
|
|
67
67
|
function band(value) {
|
|
68
68
|
return [value * (1 - SPREAD), value * (1 + SPREAD)];
|
|
69
69
|
}
|
|
70
|
+
/** The probe's cost line: a dollar range when one is known, otherwise why
|
|
71
|
+
* not, so a provider with no price list (OpenAI, Anthropic) gets its own
|
|
72
|
+
* reason instead of being told to switch to OpenRouter. */
|
|
73
|
+
export function estimateCostLine(estimate, ai, openRouterPricesUnavailable) {
|
|
74
|
+
if (estimate.usd) {
|
|
75
|
+
return `$${estimate.usd[0].toFixed(4)}-$${estimate.usd[1].toFixed(4)}`;
|
|
76
|
+
}
|
|
77
|
+
if (ai !== "none" && ai !== "openrouter") {
|
|
78
|
+
return "estimate unavailable for this provider";
|
|
79
|
+
}
|
|
80
|
+
if (openRouterPricesUnavailable) {
|
|
81
|
+
return "unknown (could not read OpenRouter prices)";
|
|
82
|
+
}
|
|
83
|
+
return "set --ai=openrouter with both models to estimate dollars";
|
|
84
|
+
}
|
|
70
85
|
/** The number of `extract_unit` jobs the recommended init would queue: one per
|
|
71
86
|
* pull request plus one for the codebase/documents, matching
|
|
72
87
|
* `extractAiFacts`. */
|
|
@@ -40,18 +40,21 @@ export async function completeWithMermaidTools(provider, request, maxTools, extr
|
|
|
40
40
|
tokensOut: 0,
|
|
41
41
|
cost: undefined,
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
const calls = { known: 0, unknown: 0 };
|
|
44
44
|
const merge = (response) => {
|
|
45
45
|
totals.tokensIn += response.tokensIn;
|
|
46
46
|
totals.tokensOut += response.tokensOut;
|
|
47
47
|
if (response.cost === undefined)
|
|
48
|
-
|
|
49
|
-
else
|
|
48
|
+
calls.unknown++;
|
|
49
|
+
else {
|
|
50
|
+
calls.known++;
|
|
50
51
|
totals.cost = (totals.cost ?? 0) + response.cost;
|
|
52
|
+
}
|
|
51
53
|
return {
|
|
52
54
|
...response,
|
|
53
55
|
...totals,
|
|
54
|
-
cost:
|
|
56
|
+
cost: calls.unknown === 0 ? totals.cost : undefined,
|
|
57
|
+
costCalls: { ...calls },
|
|
55
58
|
};
|
|
56
59
|
};
|
|
57
60
|
for (let round = 0; round < maxToolRounds; round++) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** OpenAI via the Responses API.
|
|
2
|
+
*
|
|
3
|
+
* Chosen over Chat Completions on OpenAI's own advice: "Reasoning models work
|
|
4
|
+
* better with the Responses API" (platform.openai.com/docs/guides/reasoning).
|
|
5
|
+
* Responses has a different shape from the Chat-Completions style the rest of
|
|
6
|
+
* this codebase's `AiRequest`/`AiMessage` types follow (OpenRouter speaks Chat
|
|
7
|
+
* Completions directly), so this file is the one place that translates:
|
|
8
|
+
*
|
|
9
|
+
* - the system message becomes the top-level `instructions` field
|
|
10
|
+
* - a tool call is its own `function_call` item, not a field on a message
|
|
11
|
+
* - a tool's result is a `function_call_output` item, referenced by `call_id`
|
|
12
|
+
* - a function tool is `{type:"function", name, parameters}`, not nested
|
|
13
|
+
* under a `function` key
|
|
14
|
+
* - usage is `input_tokens`/`output_tokens`, not `prompt_tokens`/
|
|
15
|
+
* `completion_tokens`, and carries no dollar cost at all
|
|
16
|
+
*
|
|
17
|
+
* ponytail: a tool round replays only the function call and its result, not
|
|
18
|
+
* the `reasoning` items a reasoning model's own turn produced alongside them.
|
|
19
|
+
* OpenAI's function-calling guide says those "must also be passed back with
|
|
20
|
+
* tool call outputs" for a reasoning model, so a multi-round tool loop with
|
|
21
|
+
* one (`gpt-6-astra` and similar) may reason less well on later rounds than
|
|
22
|
+
* it could, or in the worst case have a round rejected for missing context.
|
|
23
|
+
* The full fix is `previous_response_id`, which needs a response id to
|
|
24
|
+
* survive between calls, and `AiProvider.complete` is stateless for every
|
|
25
|
+
* provider. Upgrade path: thread a response id through `AiResponse` if this
|
|
26
|
+
* turns out to matter in practice; codegraph's own tool loop caps rounds low
|
|
27
|
+
* (mermaid_loop.ts's `maxToolRounds`), which limits how much is at stake.
|
|
28
|
+
*/
|
|
29
|
+
import type { AiProvider, AiRequest, AiResponse } from "../types.ts";
|
|
30
|
+
import { CliError } from "../cli/error.ts";
|
|
31
|
+
/** `CM_OPENAI_URL` points at a local fake in tests. Unset means the real
|
|
32
|
+
* endpoint, so production behavior is unchanged. */
|
|
33
|
+
export declare function openAiBase(): string;
|
|
34
|
+
/** A failed response as actionable text. Only `error.message` is
|
|
35
|
+
* shown: the raw body can carry request ids that mean nothing in a terminal. */
|
|
36
|
+
export declare function openAiError(model: string, status: number, body: string): CliError;
|
|
37
|
+
export declare class OpenAiProvider implements AiProvider {
|
|
38
|
+
readonly supportsTools = true;
|
|
39
|
+
private readonly endpoint;
|
|
40
|
+
private readonly apiKey;
|
|
41
|
+
private readonly model;
|
|
42
|
+
constructor(apiKey: string, model: string);
|
|
43
|
+
private post;
|
|
44
|
+
complete(request: AiRequest): Promise<AiResponse>;
|
|
45
|
+
}
|