co-maintainer 0.5.1-beta.2 → 0.5.1-beta.4
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/dist/package.json +1 -1
- package/dist/src/ai/provider.d.ts +1 -3
- package/dist/src/ai/provider.js +1 -10
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.js +23 -0
- package/dist/src/server/api/settings.js +2 -1
- package/dist/src/server/app.js +2 -2
- package/dist/src/server/pages/router.js +2 -1
- package/dist/src/server/pages/settings.js +14 -3
- package/dist/src/services/jobs.js +6 -2
- package/dist/src/services/remote_review.js +17 -10
- package/dist/src/services/review.js +2 -2
- package/dist/src/services/setup.js +4 -3
- package/dist/src/services/setup_checklist.js +2 -2
- package/package.json +1 -1
- package/dist/src/ai/hetzner.d.ts +0 -16
- package/dist/src/ai/hetzner.js +0 -69
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/dist/package.json
CHANGED
|
@@ -3,9 +3,7 @@ import type { AiProvider, AiRequest, AiResponse, Json, Options } from "../types.
|
|
|
3
3
|
* added or removed in a single place instead of the half dozen copies
|
|
4
4
|
* Hetzner's removal had to chase down. */
|
|
5
5
|
export declare const AI_PROVIDERS: readonly ["none", "openrouter", "openai", "anthropic", "opencode-zen", "opencode-go"];
|
|
6
|
-
|
|
7
|
-
* so a key meant for one provider is never sent to another. */
|
|
8
|
-
export declare function providerKeyEnv(ai: string, env: (name: string) => string | undefined): string | undefined;
|
|
6
|
+
export { providerKeyEnv } from "../config.ts";
|
|
9
7
|
/** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
|
|
10
8
|
export declare function rejectRetiredProvider(value: string | undefined): void;
|
|
11
9
|
export declare function createAiProvider(options: Options, model: string): AiProvider | undefined;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -14,16 +14,7 @@ export const AI_PROVIDERS = [
|
|
|
14
14
|
"opencode-zen",
|
|
15
15
|
"opencode-go",
|
|
16
16
|
];
|
|
17
|
-
|
|
18
|
-
* so a key meant for one provider is never sent to another. */
|
|
19
|
-
export function providerKeyEnv(ai, env) {
|
|
20
|
-
if (ai === "openrouter")
|
|
21
|
-
return env("OPENROUTER_API_KEY");
|
|
22
|
-
if (ai === "opencode-zen" || ai === "opencode-go") {
|
|
23
|
-
return env("OPENCODE_API_KEY");
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
17
|
+
export { providerKeyEnv } from "../config.js";
|
|
27
18
|
/** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
|
|
28
19
|
export function rejectRetiredProvider(value) {
|
|
29
20
|
if (value === "hetzner") {
|
package/dist/src/config.d.ts
CHANGED
|
@@ -97,6 +97,16 @@ export type ByokPolicy = "off" | "allow" | "require";
|
|
|
97
97
|
* overrides the saved config, so a hosted server cannot be talked out of its
|
|
98
98
|
* policy by a config edit. An unset or unreadable value falls back to the
|
|
99
99
|
* config, then to `off`. */
|
|
100
|
+
/** The provider's own key variable. It is read only when that provider runs,
|
|
101
|
+
* so a key meant for one provider is never sent to another. */
|
|
102
|
+
export declare function providerKeyEnv(ai: string, env?: (name: string) => string | undefined): string | undefined;
|
|
103
|
+
/** The AI key `serve` uses for its jobs and its setup checks, in the order the
|
|
104
|
+
* CLI resolves it: `CO_MAINTAINER_TOKEN`, then the provider's own variable,
|
|
105
|
+
* then the saved key. Without this a server started with the key in its
|
|
106
|
+
* environment reported AI as not configured and failed every job. */
|
|
107
|
+
export declare function resolvedAiToken(config: Pick<UserConfig, "ai" | "token">, ai?: string | undefined): string | undefined;
|
|
108
|
+
/** True when a provider is chosen and a key reaches it from anywhere. */
|
|
109
|
+
export declare function aiConfigured(config: Pick<UserConfig, "ai" | "token">): boolean;
|
|
100
110
|
export declare function remoteByokPolicy(config?: Pick<UserConfig, "remoteByokPolicy">): ByokPolicy;
|
|
101
111
|
/** Platform/env injection keeps the OS branches testable: the
|
|
102
112
|
* migration's `"windows"` vs `"win32"` trap is invisible otherwise. */
|
package/dist/src/config.js
CHANGED
|
@@ -3,6 +3,29 @@ import { chmod, currentPlatform, getEnv, isNotFound, mkdir, readTextFileSync, se
|
|
|
3
3
|
* overrides the saved config, so a hosted server cannot be talked out of its
|
|
4
4
|
* policy by a config edit. An unset or unreadable value falls back to the
|
|
5
5
|
* config, then to `off`. */
|
|
6
|
+
/** The provider's own key variable. It is read only when that provider runs,
|
|
7
|
+
* so a key meant for one provider is never sent to another. */
|
|
8
|
+
export function providerKeyEnv(ai, env = getEnv) {
|
|
9
|
+
if (ai === "openrouter")
|
|
10
|
+
return env("OPENROUTER_API_KEY");
|
|
11
|
+
if (ai === "opencode-zen" || ai === "opencode-go") {
|
|
12
|
+
return env("OPENCODE_API_KEY");
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
/** The AI key `serve` uses for its jobs and its setup checks, in the order the
|
|
17
|
+
* CLI resolves it: `CO_MAINTAINER_TOKEN`, then the provider's own variable,
|
|
18
|
+
* then the saved key. Without this a server started with the key in its
|
|
19
|
+
* environment reported AI as not configured and failed every job. */
|
|
20
|
+
export function resolvedAiToken(config, ai = config.ai) {
|
|
21
|
+
return (getEnv("CO_MAINTAINER_TOKEN") ??
|
|
22
|
+
(ai ? providerKeyEnv(ai) : undefined) ??
|
|
23
|
+
config.token);
|
|
24
|
+
}
|
|
25
|
+
/** True when a provider is chosen and a key reaches it from anywhere. */
|
|
26
|
+
export function aiConfigured(config) {
|
|
27
|
+
return Boolean(config.ai && config.ai !== "none" && resolvedAiToken(config));
|
|
28
|
+
}
|
|
6
29
|
export function remoteByokPolicy(config = readConfig()) {
|
|
7
30
|
// An env value that is not one of the three known policies is treated as
|
|
8
31
|
// unreadable and falls back to the config, so a typo cannot silently turn a
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { errorResponse } from "../errors.js";
|
|
2
2
|
import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
|
|
3
|
+
import { aiConfigured } from "../../config.js";
|
|
3
4
|
import { testAppAccess, testGithubAccess } from "../../services/credentials.js";
|
|
4
5
|
import { checkPassword, readSessionToken, revokeOtherSessions, } from "../auth.js";
|
|
5
6
|
import { passwordProblem } from "../../util/password.js";
|
|
@@ -231,7 +232,7 @@ export async function handleSettingsRoute(request, url, webhookUrl, passwords, i
|
|
|
231
232
|
privateKeyPem: resolveAppPrivateKey(config) ?? "",
|
|
232
233
|
});
|
|
233
234
|
return Response.json({
|
|
234
|
-
ai: { ok:
|
|
235
|
+
ai: { ok: aiConfigured(config) },
|
|
235
236
|
github: github.ok
|
|
236
237
|
? { ok: true, login: github.login }
|
|
237
238
|
: { ok: false, message: github.message },
|
package/dist/src/server/app.js
CHANGED
|
@@ -5,7 +5,7 @@ import { errorResponse } from "./errors.js";
|
|
|
5
5
|
import { hasCsrfHeader, login, logout, readSessionToken, sessionCookieHeader, verifySession, } from "./auth.js";
|
|
6
6
|
import { memoryPasswordStore } from "./auth.js";
|
|
7
7
|
import { clientAddress, forwardedHttps } from "./proxy_headers.js";
|
|
8
|
-
import { readConfig, resolveAppPrivateKey } from "../config.js";
|
|
8
|
+
import { aiConfigured, readConfig, resolveAppPrivateKey } from "../config.js";
|
|
9
9
|
import { listJobs } from "../store/jobs.js";
|
|
10
10
|
import { handleJobsRoute } from "./api/jobs.js";
|
|
11
11
|
import { handleReposRoute } from "./api/repos.js";
|
|
@@ -19,7 +19,7 @@ import { handleWebhookRequest } from "./webhook/index.js";
|
|
|
19
19
|
import { handlePageRequest } from "./pages/router.js";
|
|
20
20
|
function setupStatus() {
|
|
21
21
|
const config = readConfig();
|
|
22
|
-
const ai =
|
|
22
|
+
const ai = aiConfigured(config);
|
|
23
23
|
const github = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
|
|
24
24
|
const app = Boolean(config.githubAppId && resolveAppPrivateKey(config));
|
|
25
25
|
const missing = [
|
|
@@ -5,6 +5,7 @@ import { activityFeed, listReposForHome, prDetail, repoKnowledge, RepoNotFound,
|
|
|
5
5
|
import { escapeHtml, html } from "./layout.js";
|
|
6
6
|
import { formatCostTotal } from "./cost.js";
|
|
7
7
|
import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
|
|
8
|
+
import { aiConfigured } from "../../config.js";
|
|
8
9
|
import { appNameProblem, beginManifestState, buildAppManifest, consumeManifestState, convertManifest, } from "../../github/app_manifest.js";
|
|
9
10
|
import { renderHome, renderLogin } from "./home.js";
|
|
10
11
|
import { renderSetup } from "./setup.js";
|
|
@@ -113,7 +114,7 @@ export async function handlePageRequest(request, deps, ip) {
|
|
|
113
114
|
if (url.pathname === "/setup" && request.method === "GET") {
|
|
114
115
|
const config = readConfig();
|
|
115
116
|
return renderSetup(username, {
|
|
116
|
-
ai:
|
|
117
|
+
ai: aiConfigured(config),
|
|
117
118
|
github: Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat)),
|
|
118
119
|
app: Boolean(config.githubAppId && resolveAppPrivateKey(config)),
|
|
119
120
|
});
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { html, layout, skSlot, text } from "./layout.js";
|
|
2
|
-
import { resolveAppPrivateKey } from "../../config.js";
|
|
2
|
+
import { aiConfigured, resolveAppPrivateKey, resolvedAiToken, } from "../../config.js";
|
|
3
3
|
import { VERSION } from "../../version.js";
|
|
4
4
|
import { TEMPLATE_INFO } from "../../services/review_policy.js";
|
|
5
5
|
import { defaultAppName, manifestBlockedReason, } from "../../github/app_manifest.js";
|
|
6
6
|
const SOURCE_URL = "https://github.com/GroophyLifefor/co-maintainer";
|
|
7
|
+
/** Which key the jobs will use. The environment wins over the saved key, the
|
|
8
|
+
* same order `resolvedAiToken` follows, so the hint must check it first. */
|
|
9
|
+
function keyHint(config) {
|
|
10
|
+
const fromEnv = resolvedAiToken({ ai: config.ai });
|
|
11
|
+
if (fromEnv && config.token) {
|
|
12
|
+
return "Using the key from the server's environment, which overrides the saved key";
|
|
13
|
+
}
|
|
14
|
+
if (fromEnv)
|
|
15
|
+
return "Using the key from the server's environment";
|
|
16
|
+
return config.token ? "A key is saved" : "No key saved";
|
|
17
|
+
}
|
|
7
18
|
export function renderSettings(username, config, webhookUrl, baseUrl) {
|
|
8
|
-
const aiOk =
|
|
19
|
+
const aiOk = aiConfigured(config);
|
|
9
20
|
const ghOk = Boolean(config.auth === "gh" || (config.auth === "pat" && config.githubPat));
|
|
10
21
|
const appOk = Boolean(config.githubAppId && resolveAppPrivateKey(config));
|
|
11
22
|
let host = "localhost";
|
|
@@ -53,7 +64,7 @@ export function renderSettings(username, config, webhookUrl, baseUrl) {
|
|
|
53
64
|
</select></div>
|
|
54
65
|
<div class="field"><label>API key</label>
|
|
55
66
|
<input id="token" type="password" placeholder="Leave blank to keep the current key">
|
|
56
|
-
<div class="hint">${config
|
|
67
|
+
<div class="hint">${keyHint(config)}</div></div>
|
|
57
68
|
<div class="two" style="max-width:none">
|
|
58
69
|
<div class="field"><label>High model</label>
|
|
59
70
|
<input id="high" value="${text(config.highModel ?? "")}">
|
|
@@ -85,7 +85,9 @@ async function runJob(job) {
|
|
|
85
85
|
? cancelReason
|
|
86
86
|
? { cancel_reason: cancelReason }
|
|
87
87
|
: {}
|
|
88
|
-
:
|
|
88
|
+
: // The message only: `String(error)` would show the class name, and
|
|
89
|
+
// a remote client prints this text to its user as is.
|
|
90
|
+
{ error: message });
|
|
89
91
|
if (status === "failed")
|
|
90
92
|
log(job.id, "error", message);
|
|
91
93
|
log(job.id, "status", status);
|
|
@@ -210,7 +212,9 @@ export async function recoverOrphans() {
|
|
|
210
212
|
setJobStatus(job.id, outcome === "canceled" ? "canceled" : "done", outcome === "canceled" ? { cancel_reason: "server_restarted" } : {});
|
|
211
213
|
}
|
|
212
214
|
catch (error) {
|
|
213
|
-
setJobStatus(job.id, "failed", {
|
|
215
|
+
setJobStatus(job.id, "failed", {
|
|
216
|
+
error: error instanceof Error ? error.message : String(error),
|
|
217
|
+
});
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
220
|
else {
|
|
@@ -8,7 +8,7 @@ import { buildCarryPromptSection, classifyCarryItems, guideRebuiltSince, increme
|
|
|
8
8
|
import { remoteBridgeToolHandlers } from "../remote/tool_bridge.js";
|
|
9
9
|
import { revisionFromSubmitJson } from "../remote/revision_from_submit.js";
|
|
10
10
|
import { closeRemoteSession, openRemoteSession, setRemoteSyncResult, } from "../remote/server/sessions.js";
|
|
11
|
-
import { readConfig, remoteByokPolicy } from "../config.js";
|
|
11
|
+
import { readConfig, remoteByokPolicy, resolvedAiToken } from "../config.js";
|
|
12
12
|
import { dropByokKey, getByokKey, hasByokKey, registerByokKey, } from "../remote/server/byok_keys.js";
|
|
13
13
|
import { reviewBlockingFrom } from "../review/blocking.js";
|
|
14
14
|
import { loadGuides } from "../review/guides.js";
|
|
@@ -66,7 +66,7 @@ function reviewOptionsForRepo(repo, useCodegraph, aiToken) {
|
|
|
66
66
|
auth: config.auth ?? "gh",
|
|
67
67
|
githubPat: config.githubPat,
|
|
68
68
|
ai: config.ai ?? "openrouter",
|
|
69
|
-
aiToken: aiToken ?? config.
|
|
69
|
+
aiToken: aiToken ?? resolvedAiToken(config, config.ai ?? "openrouter"),
|
|
70
70
|
lowModel: config.lowModel,
|
|
71
71
|
highModel: config.highModel,
|
|
72
72
|
synthesisVersion: 16,
|
|
@@ -250,7 +250,12 @@ export function registerRemoteReviewHandler() {
|
|
|
250
250
|
.filter((name) => typeof name === "string" && name));
|
|
251
251
|
const useCodegraph = allowed.size > 0;
|
|
252
252
|
const options = reviewOptionsForRepo(job.repo, useCodegraph, byokKey);
|
|
253
|
-
|
|
253
|
+
// A BYOK client brings a key but not models, so a server with no
|
|
254
|
+
// models cannot run its review either.
|
|
255
|
+
if (!options.aiToken ||
|
|
256
|
+
options.ai === "none" ||
|
|
257
|
+
!options.lowModel ||
|
|
258
|
+
!options.highModel) {
|
|
254
259
|
throw new Error("server AI is not configured for remote review");
|
|
255
260
|
}
|
|
256
261
|
const billed = byokKey !== undefined ? "byok" : "server";
|
|
@@ -319,7 +324,12 @@ export function registerRemoteReviewHandler() {
|
|
|
319
324
|
codegraph: { state: result.codegraphState },
|
|
320
325
|
summary: summaryCounts(findings, blockingMode),
|
|
321
326
|
findings: findings.map((row) => toJsonFinding(row, blockingMode)),
|
|
322
|
-
usage: {
|
|
327
|
+
usage: {
|
|
328
|
+
tokensIn,
|
|
329
|
+
tokensOut,
|
|
330
|
+
...costUsage(costTally),
|
|
331
|
+
billedTo: billed,
|
|
332
|
+
},
|
|
323
333
|
remote: { jobId: job.id, reviewId, clientVersion: VERSION },
|
|
324
334
|
});
|
|
325
335
|
setReviewStatus(reviewId, "done", {
|
|
@@ -377,12 +387,9 @@ export function registerRemoteReviewHandler() {
|
|
|
377
387
|
dropByokKey(job.id);
|
|
378
388
|
if (!completed) {
|
|
379
389
|
// Reached on failure and on the aborted early return above.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
setReviewStatus(reviewId, "failed");
|
|
385
|
-
}
|
|
390
|
+
setReviewStatus(reviewId, signal.aborted ? "aborted" : "failed", {
|
|
391
|
+
billed_to: billedTo,
|
|
392
|
+
});
|
|
386
393
|
deleteRemoteReviewInput(job.id);
|
|
387
394
|
closeRemoteSession(job.id);
|
|
388
395
|
}
|
|
@@ -14,7 +14,7 @@ import { addResponseCost, costColumns, emptyTally, settle, } from "../util/cost.
|
|
|
14
14
|
import { loadGuides } from "../review/guides.js";
|
|
15
15
|
import { reviewBlockingFrom, isBlocking, DEFAULT_REVIEW_BLOCKING, } from "../review/blocking.js";
|
|
16
16
|
import { matchRepeat } from "../pr/rounds.js";
|
|
17
|
-
import { readConfig, resolveAppPrivateKey } from "../config.js";
|
|
17
|
+
import { readConfig, resolveAppPrivateKey, resolvedAiToken, } from "../config.js";
|
|
18
18
|
import { outsideCode, redact } from "../util/redact.js";
|
|
19
19
|
import { getRepo, setInstallationId } from "../store/repos.js";
|
|
20
20
|
import { getReviewByJobId, insertReview, latestPostedReview, setReviewScope, setReviewStatus, } from "../store/reviews.js";
|
|
@@ -143,7 +143,7 @@ export function reviewOptions(repo, prNumber) {
|
|
|
143
143
|
// With no provider saved the App review stays on OpenRouter, the same
|
|
144
144
|
// fallback the CLI review uses.
|
|
145
145
|
ai: config.ai && config.ai !== "none" ? config.ai : "openrouter",
|
|
146
|
-
aiToken: config.
|
|
146
|
+
aiToken: resolvedAiToken(config, config.ai && config.ai !== "none" ? config.ai : "openrouter"),
|
|
147
147
|
highModel: config.highModel,
|
|
148
148
|
lowModel: config.lowModel,
|
|
149
149
|
synthesisVersion: 16,
|
|
@@ -10,7 +10,7 @@ import { extractFacts } from "../knowledge/facts.js";
|
|
|
10
10
|
import { buildReviewDocuments, reviewSignalCount } from "../knowledge/guide.js";
|
|
11
11
|
import { assembleSkill, codebaseBody, factSectionHashes, } from "../knowledge/skill.js";
|
|
12
12
|
import { validateSkill } from "../knowledge/validate.js";
|
|
13
|
-
import { readConfig, reposDir, writeRepoConfig } from "../config.js";
|
|
13
|
+
import { readConfig, reposDir, resolvedAiToken, writeRepoConfig, } from "../config.js";
|
|
14
14
|
import { readState, writeState } from "../store/skill_state.js";
|
|
15
15
|
import { cacheSet } from "../store/cache_db.js";
|
|
16
16
|
import { log, timed, withLogSink } from "../util/log.js";
|
|
@@ -318,7 +318,8 @@ export function optionsFromConfig(repo, command) {
|
|
|
318
318
|
const ai = repoConfig.ai ?? config.ai ?? "none";
|
|
319
319
|
const lowModel = repoConfig.lowModel ?? config.lowModel;
|
|
320
320
|
const highModel = repoConfig.highModel ?? config.highModel;
|
|
321
|
-
|
|
321
|
+
const aiToken = resolvedAiToken(config, ai);
|
|
322
|
+
if (ai !== "none" && (!aiToken || !lowModel || !highModel)) {
|
|
322
323
|
throw new Error("AI is enabled but token/low-model/high-model are not fully configured. Run: " +
|
|
323
324
|
"co-maintainer set --token=... --low-model=... --high-model=...");
|
|
324
325
|
}
|
|
@@ -333,7 +334,7 @@ export function optionsFromConfig(repo, command) {
|
|
|
333
334
|
auth,
|
|
334
335
|
githubPat: config.githubPat,
|
|
335
336
|
ai,
|
|
336
|
-
aiToken
|
|
337
|
+
aiToken,
|
|
337
338
|
lowModel,
|
|
338
339
|
highModel,
|
|
339
340
|
synthesisVersion: 16,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Each item is derived from state that already exists, so the card reflects
|
|
4
4
|
* what serve can actually do rather than a parallel bookkeeping of it. The
|
|
5
5
|
* webhook item reuses the existing reachability check. */
|
|
6
|
-
import { readConfig, resolveAppPrivateKey } from "../config.js";
|
|
6
|
+
import { aiConfigured, readConfig, resolveAppPrivateKey } from "../config.js";
|
|
7
7
|
import { countReviews } from "../store/reviews.js";
|
|
8
8
|
import { listRemoteTokens } from "../store/remote_tokens.js";
|
|
9
9
|
import { listActiveRepos } from "../store/repos.js";
|
|
@@ -11,7 +11,7 @@ import { lastDelivery } from "../store/deliveries.js";
|
|
|
11
11
|
import { webhookReachabilityProblem } from "../util/webhook_reachability.js";
|
|
12
12
|
export function setupChecklist(webhookUrl) {
|
|
13
13
|
const config = readConfig();
|
|
14
|
-
const aiDone =
|
|
14
|
+
const aiDone = aiConfigured(config);
|
|
15
15
|
const appDone = Boolean(config.githubAppId && resolveAppPrivateKey(config));
|
|
16
16
|
const repoCount = listActiveRepos().length;
|
|
17
17
|
const reviewCount = countReviews();
|
package/package.json
CHANGED
package/dist/src/ai/hetzner.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { AiProvider, AiRequest, AiResponse } from "../types.ts";
|
|
2
|
-
declare const sleep: (milliseconds: number) => Promise<void>;
|
|
3
|
-
export declare class HetznerProvider implements AiProvider {
|
|
4
|
-
readonly supportsTools = false;
|
|
5
|
-
private readonly endpoint;
|
|
6
|
-
private readonly apiKey;
|
|
7
|
-
private readonly model;
|
|
8
|
-
private readonly sleeper;
|
|
9
|
-
private nextRequest;
|
|
10
|
-
private spacing;
|
|
11
|
-
constructor(apiKey: string, model: string, endpoint?: string, sleeper?: typeof sleep);
|
|
12
|
-
complete(request: AiRequest): Promise<AiResponse>;
|
|
13
|
-
private waitForQuota;
|
|
14
|
-
private jitter;
|
|
15
|
-
}
|
|
16
|
-
export {};
|
package/dist/src/ai/hetzner.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { chatBody, parseChatResponse } from "./provider.js";
|
|
2
|
-
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
3
|
-
export class HetznerProvider {
|
|
4
|
-
supportsTools = false;
|
|
5
|
-
endpoint;
|
|
6
|
-
apiKey;
|
|
7
|
-
model;
|
|
8
|
-
sleeper;
|
|
9
|
-
nextRequest = 0;
|
|
10
|
-
spacing = 8_000;
|
|
11
|
-
constructor(apiKey, model, endpoint = "https://inference.hetzner.com/api/v1/chat/completions", sleeper = sleep) {
|
|
12
|
-
if (!apiKey)
|
|
13
|
-
throw new Error("Hetzner requires HETZNER_API_KEY");
|
|
14
|
-
if (!model)
|
|
15
|
-
throw new Error("Hetzner requires HETZNER_MODEL");
|
|
16
|
-
this.apiKey = apiKey;
|
|
17
|
-
this.model = model;
|
|
18
|
-
this.sleeper = sleeper;
|
|
19
|
-
this.endpoint = endpoint;
|
|
20
|
-
}
|
|
21
|
-
async complete(request) {
|
|
22
|
-
let attempt = 0;
|
|
23
|
-
for (;;) {
|
|
24
|
-
attempt++;
|
|
25
|
-
await this.waitForQuota();
|
|
26
|
-
const response = await fetch(this.endpoint, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: {
|
|
29
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
30
|
-
"Content-Type": "application/json",
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify(chatBody(this.model, request)),
|
|
33
|
-
});
|
|
34
|
-
this.nextRequest = Date.now() + this.spacing;
|
|
35
|
-
if (response.ok) {
|
|
36
|
-
this.spacing = Math.max(8_000, this.spacing * 0.9);
|
|
37
|
-
return parseChatResponse((await response.json()), "hetzner", this.model);
|
|
38
|
-
}
|
|
39
|
-
if ([401, 403, 404].includes(response.status)) {
|
|
40
|
-
throw new Error(`Hetzner fatal ${response.status}: ${await response.text()}`);
|
|
41
|
-
}
|
|
42
|
-
if (response.status === 429) {
|
|
43
|
-
this.spacing = Math.min(180_000, this.spacing * 1.5);
|
|
44
|
-
const wait = this.jitter(this.spacing);
|
|
45
|
-
console.log(`[ai] hetzner 429 on attempt ${attempt}; retrying in ${Math.round(wait / 1000)}s`);
|
|
46
|
-
await this.sleeper(wait);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (response.status >= 500 || response.status === 408) {
|
|
50
|
-
const wait = this.jitter(Math.min(180_000, this.spacing * 1.5));
|
|
51
|
-
console.log(`[ai] hetzner ${response.status} on attempt ${attempt}; retrying in ${Math.round(wait / 1000)}s`);
|
|
52
|
-
await this.sleeper(wait);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`Hetzner ${response.status}: ${await response.text()}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async waitForQuota() {
|
|
59
|
-
const wait = this.nextRequest - Date.now();
|
|
60
|
-
if (wait > 0) {
|
|
61
|
-
const jittered = this.jitter(wait);
|
|
62
|
-
console.log(`[ai] hetzner quota spacing, waiting ${Math.round(jittered / 1000)}s`);
|
|
63
|
-
await this.sleeper(jittered);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
jitter(milliseconds) {
|
|
67
|
-
return Math.max(250, Math.round(milliseconds * (0.8 + Math.random() * 0.4)));
|
|
68
|
-
}
|
|
69
|
-
}
|