feat(offload): reasoning offload for codegraph_explore (bring-your-own endpoint)
codegraph_explore can now hand the source it retrieved to a reasoning model you point at — any OpenAI-compatible endpoint (Cerebras, OpenAI, a local vLLM/Ollama) with your own key — and return that model's tight, cited answer instead of the raw source dump. The agent's main context gets the answer in far fewer tokens, at the cost of one network round-trip. Off by default. Configure with `codegraph offload set-endpoint <url> --model <m> --key-env <ENV>` (or the CODEGRAPH_OFFLOAD_* env vars); status/disable manage it. The API key is never written to disk — the config stores the NAME of an env var and the key is read from it at call time. Strictly degradable: any failure (no endpoint, network, timeout, empty answer) returns null and the call falls back to the local source, so the offload can never surface an error to the agent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f34f606342
commit
db4c9f3641
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Reasoning-offload configuration: the persistent, machine-level settings the
|
||||
* `codegraph offload` CLI writes, merged with `CODEGRAPH_OFFLOAD_*` env overrides.
|
||||
*
|
||||
* Stored in `~/.codegraph/config.json` under the `offload` key — the same global
|
||||
* home CodeGraph already uses for the daemon registry — because the reasoning
|
||||
* endpoint is a per-machine choice (the model you bring), not per-project state.
|
||||
* Every codegraph MCP server on the machine picks it up, so a user configures it
|
||||
* once. Env vars override the file (CI / ephemeral / advanced use).
|
||||
*
|
||||
* The API key is NEVER written to disk. The CLI stores the NAME of an env var
|
||||
* that holds it (`keyEnv`); at call time the key is read from that env var (or
|
||||
* directly from `CODEGRAPH_OFFLOAD_KEY`). So the config file carries no secret.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
export interface OffloadConfig {
|
||||
/** OpenAI-compatible base URL ending in `/v1` (e.g. https://api.cerebras.ai/v1). */
|
||||
url?: string;
|
||||
/** Model id to request (default `gpt-oss-120b`). */
|
||||
model?: string;
|
||||
/** Name of the env var holding the provider API key (the key itself is never persisted). */
|
||||
keyEnv?: string;
|
||||
/** reasoning_effort: low | medium | high (default `low`). */
|
||||
effort?: string;
|
||||
/** Output style: plain | report (default `plain`). */
|
||||
style?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedOffload {
|
||||
/** True when a reasoning endpoint is configured (by env or by file). */
|
||||
enabled: boolean;
|
||||
url?: string;
|
||||
model: string;
|
||||
/** Resolved API key (from `CODEGRAPH_OFFLOAD_KEY` or the configured `keyEnv`), if any. */
|
||||
apiKey?: string;
|
||||
/** Which env var the key came from (for `status` display) — never the key itself. */
|
||||
keySource?: string;
|
||||
effort: string;
|
||||
style: string;
|
||||
timeoutMs: number;
|
||||
maxTokens: number;
|
||||
strip: boolean;
|
||||
debug: boolean;
|
||||
/** Where the endpoint came from — drives `codegraph offload status`. */
|
||||
origin: 'env' | 'config' | 'none';
|
||||
}
|
||||
|
||||
function configDir(): string {
|
||||
return path.join(os.homedir(), '.codegraph');
|
||||
}
|
||||
function configPath(): string {
|
||||
return path.join(configDir(), 'config.json');
|
||||
}
|
||||
|
||||
function readUserConfig(): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(configPath(), 'utf8')) as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeUserConfig(cfg: Record<string, unknown>): void {
|
||||
fs.mkdirSync(configDir(), { recursive: true });
|
||||
fs.writeFileSync(configPath(), JSON.stringify(cfg, null, 2) + '\n');
|
||||
}
|
||||
|
||||
/** The persisted offload block (empty object if none). */
|
||||
export function readOffloadConfig(): OffloadConfig {
|
||||
const cfg = readUserConfig();
|
||||
const o = cfg.offload;
|
||||
return o && typeof o === 'object' ? (o as OffloadConfig) : {};
|
||||
}
|
||||
|
||||
/** Persist (or, with `null`, clear) the offload block, leaving other config keys intact. */
|
||||
export function writeOffloadConfig(offload: OffloadConfig | null): void {
|
||||
const cfg = readUserConfig();
|
||||
if (offload === null) delete cfg.offload;
|
||||
else cfg.offload = offload;
|
||||
writeUserConfig(cfg);
|
||||
}
|
||||
|
||||
const trimmed = (v: string | undefined): string | undefined => {
|
||||
const t = v?.trim();
|
||||
return t ? t : undefined;
|
||||
};
|
||||
|
||||
/** Merge the persisted config with `CODEGRAPH_OFFLOAD_*` env overrides (env wins). */
|
||||
export function resolveOffload(env: NodeJS.ProcessEnv = process.env): ResolvedOffload {
|
||||
const c = readOffloadConfig();
|
||||
const url = trimmed(env.CODEGRAPH_OFFLOAD_URL) ?? trimmed(c.url);
|
||||
|
||||
// Key: direct env var first, else the configured env-var name. Never from disk.
|
||||
let apiKey: string | undefined;
|
||||
let keySource: string | undefined;
|
||||
if (trimmed(env.CODEGRAPH_OFFLOAD_KEY)) {
|
||||
apiKey = trimmed(env.CODEGRAPH_OFFLOAD_KEY);
|
||||
keySource = 'CODEGRAPH_OFFLOAD_KEY';
|
||||
} else if (c.keyEnv && trimmed(env[c.keyEnv])) {
|
||||
apiKey = trimmed(env[c.keyEnv]);
|
||||
keySource = c.keyEnv;
|
||||
}
|
||||
|
||||
const origin: ResolvedOffload['origin'] = trimmed(env.CODEGRAPH_OFFLOAD_URL)
|
||||
? 'env'
|
||||
: trimmed(c.url)
|
||||
? 'config'
|
||||
: 'none';
|
||||
|
||||
return {
|
||||
enabled: !!url,
|
||||
url,
|
||||
model: trimmed(env.CODEGRAPH_OFFLOAD_MODEL) ?? trimmed(c.model) ?? 'gpt-oss-120b',
|
||||
apiKey,
|
||||
keySource,
|
||||
effort: trimmed(env.CODEGRAPH_OFFLOAD_EFFORT) ?? trimmed(c.effort) ?? 'low',
|
||||
style: trimmed(env.CODEGRAPH_OFFLOAD_STYLE) ?? trimmed(c.style) ?? 'plain',
|
||||
timeoutMs: Number(env.CODEGRAPH_OFFLOAD_TIMEOUT_MS) || 20000,
|
||||
maxTokens: Number(env.CODEGRAPH_OFFLOAD_MAXTOKENS) || 12000,
|
||||
strip: env.CODEGRAPH_OFFLOAD_STRIP === '1',
|
||||
debug: env.CODEGRAPH_OFFLOAD_DEBUG === '1',
|
||||
origin,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user