-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathopenCodeModels.ts
More file actions
109 lines (101 loc) · 4.58 KB
/
Copy pathopenCodeModels.ts
File metadata and controls
109 lines (101 loc) · 4.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// OpenCode-as-source-of-truth for BYOK detection.
//
// `opencode models` returns the `provider/model` specifiers that OpenCode
// can actually route given the current env (workflow env block + GH Actions
// secrets) and `auth.json` (Codex / future managed credentials). This is
// authoritative — strictly more accurate than the static
// `provider.envVars + provider.managedCredentials` catalog in `models.ts`
// for the "do we have BYOK auth?" gate. The catalog can (and will) miss
// new auth shapes; OpenCode itself can't.
//
// Two captures per run:
// 1. `captureBaselineModels` — called BEFORE Pullfrog-stored credentials
// (dbSecrets + Codex auth.json) land in the env. The set OpenCode can
// serve from the runner's pre-existing environment alone.
// 2. `captureAuthorizedModels` — called AFTER dbSecrets merge + Codex
// auth.json materialization. The authoritative set for BYOK
// decisions (validateAgentApiKey).
//
// The set difference (`authorized - baseline`) is the contribution of
// Pullfrog-stored auth to this run — logged once for operator visibility
// and reserved for a future server-side "OSS proxy opt-out" detection.
//
// Memoized at module scope so the two consumers
// (`validateAgentApiKey` + `autoSelectModel`) share one shell-out.
import { spawnSync } from "node:child_process";
import { log } from "./cli.ts";
let baseline: Set<string> | undefined;
let authorized: Set<string> | undefined;
let failure: string | undefined;
// the CLI paints its errors; these get re-rendered into a PR comment where raw
// SGR escapes read as literal `[91m` noise.
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching SGR escapes is the point
const ANSI_PATTERN = /\[[0-9;]*m/g;
function readModels(cliPath: string): Set<string> {
// spawnSync, not execFileSync: we want opencode's stderr as a value rather
// than a throw, and `stdio` keeps it out of the job log (execFileSync leaks
// it to the parent, so a config error printed once per capture).
const result = spawnSync(cliPath, ["models"], {
encoding: "utf-8",
timeout: 30_000,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
if (result.status !== 0) {
// a timeout or spawn failure kills the child before it writes stderr, so
// `result.error` carries the only reason there is. without folding it in,
// `failure` stays undefined and the empty set reaches validateAgentApiKey
// as a bare "you have no key" verdict for what is really a runner problem.
const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
failure = stderr || result.error?.message;
log.debug(`» \`opencode models\` failed (${result.status}): ${failure}`);
return new Set();
}
failure = undefined;
return new Set(
result.stdout
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
);
}
/**
* Why the last `opencode models` came back empty, in opencode's own words.
*
* An unloadable repo config (`opencode.json`, `.opencode/**`) is the common cause
* and is fatal — opencode's schema is strict, so every later invocation fails the
* same way. Without this the empty set reaches `validateAgentApiKey` and the run
* dies telling the user to go add provider secrets, which is a false lead.
*/
export function getModelsFailure(): string | undefined {
return failure;
}
/** Snapshot the set of models OpenCode can serve from the current env, BEFORE
* Pullfrog-stored credentials are merged in. Call once early in `main.ts`. */
export function captureBaselineModels(cliPath: string): void {
baseline = readModels(cliPath);
log.debug(`» opencode baseline: ${baseline.size} models`);
}
/** Snapshot the set of models OpenCode can serve AFTER dbSecrets +
* Codex auth.json are in place. Logs the diff against the baseline as
* `» BYOK auth enabled N model(s): …`. */
export function captureAuthorizedModels(cliPath: string): void {
authorized = readModels(cliPath);
const base = baseline;
if (base) {
const diff = [...authorized].filter((m) => !base.has(m));
if (diff.length > 0) {
log.info(`» BYOK auth enabled ${diff.length} model(s): ${diff.join(", ")}`);
}
}
log.debug(`» opencode authorized: ${authorized.size} models`);
}
/** Authorized set captured after Pullfrog-stored auth is applied. Throws if
* called before `captureAuthorizedModels` — the call sites (api-key
* validation, auto-select) all run strictly after capture. */
export function getAuthorizedModels(): Set<string> {
if (!authorized) {
throw new Error("getAuthorizedModels called before captureAuthorizedModels");
}
return authorized;
}