CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

feat(ai): internal repairs run on the owner subscription — agent queue, not API credits #5442

Merged⚡ AI-generatedXSccantynz wants to mergefeat/internal-repairs-on-subscriptionmainopened 24d ago
3 changed files+117−2
Modifiedsrc/lib/ai-client.ts+32−2View fileUnifiedSplit
7777 light: "ai_model_light",
7878} as const;
7979
80type ModelOverrides = { internal?: string; standard?: string; light?: string };
80/**
81 * How INTERNAL platform AI work is executed. Owner decision 2026-08-08:
82 * internal repairs run on the owner's Claude subscription, not API credits.
83 *
84 * "agent" (default) — repairs are QUEUED as `ai:repair` issues that a
85 * Claude Code agent on the owner's subscription drains
86 * (implements the fix, opens the PR). No API spend.
87 * "api" — repairs call the Anthropic API directly with the platform
88 * key (requires API credit balance).
89 */
90export const AI_INTERNAL_MODE_FLAG = "ai_internal_mode";
91export type InternalAiMode = "agent" | "api";
92
93type ModelOverrides = {
94 internal?: string;
95 standard?: string;
96 light?: string;
97 internalMode?: InternalAiMode;
98};
8199let _overrides: ModelOverrides = {};
82100let _overridesFetchedAt = 0;
83101let _overridesRefreshing = false;
94112 // Dynamic import avoids a static cycle (admin.ts → db → … → ai callers).
95113 void import("./admin")
96114 .then(async ({ getFlag }) => {
97 const [internal, standard, light] = await Promise.all([
115 const [internal, standard, light, mode] = await Promise.all([
98116 getFlag(AI_MODEL_FLAG_KEYS.internal),
99117 getFlag(AI_MODEL_FLAG_KEYS.standard),
100118 getFlag(AI_MODEL_FLAG_KEYS.light),
119 getFlag(AI_INTERNAL_MODE_FLAG),
101120 ]);
102121 // Only accept catalog models — a typo'd flag must never send every
103122 // AI call to a 404ing model id.
105124 internal: internal && KNOWN_MODEL_IDS.has(internal) ? internal : undefined,
106125 standard: standard && KNOWN_MODEL_IDS.has(standard) ? standard : undefined,
107126 light: light && KNOWN_MODEL_IDS.has(light) ? light : undefined,
127 internalMode: mode === "api" || mode === "agent" ? mode : undefined,
108128 };
109129 _overridesFetchedAt = Date.now();
110130 })
134154 return _overrides.internal ?? MODEL_OPUS;
135155}
136156
157/**
158 * Execution mode for internal AI work. Default "agent": queue for the
159 * owner's subscription-backed Claude Code agent instead of spending API
160 * credits. See AI_INTERNAL_MODE_FLAG.
161 */
162export function internalAiMode(): InternalAiMode {
163 maybeRefreshOverrides();
164 return _overrides.internalMode ?? "agent";
165}
166
137167/**
138168 * Task → model routing.
139169 *
Modifiedsrc/routes/admin.tsx+39−0View fileUnifiedSplit
4747 setFlag,
4848} from "../lib/admin";
4949import {
50 AI_INTERNAL_MODE_FLAG,
5051 AI_MODEL_FLAG_KEYS,
5152 AVAILABLE_MODELS,
5253 MODEL_SONNET,
41064107 user on <a href="/billing/usage">/billing/usage</a>. Changes apply
41074108 within 60 seconds, no deploy.
41084109 </div>
4110 {(() => {
4111 const mode = existingMap.get(AI_INTERNAL_MODE_FLAG) ?? "";
4112 return (
4113 <div class="adm-flags-field">
4114 <div class="adm-flags-field-head">
4115 <label for={`flag-${AI_INTERNAL_MODE_FLAG}`} class="adm-flags-key">
4116 Internal execution mode
4117 </label>
4118 {mode && <span class="adm-flags-mono">overridden</span>}
4119 </div>
4120 <select
4121 id={`flag-${AI_INTERNAL_MODE_FLAG}`}
4122 name={AI_INTERNAL_MODE_FLAG}
4123 class="adm-flags-input"
4124 aria-label="Internal execution mode"
4125 >
4126 <option value="" selected={!mode}>
4127 Default (agent — owner's Claude subscription, no API spend)
4128 </option>
4129 <option value="agent" selected={mode === "agent"}>
4130 Agent queue — repairs become ai:repair issues, drained by Claude Code
4131 </option>
4132 <option value="api" selected={mode === "api"}>
4133 Direct API — requires Anthropic API credit balance
4134 </option>
4135 </select>
4136 <div class="adm-flags-hint">
4137 "Agent" queues internal repairs for a Claude Code agent on the
4138 owner's subscription; "API" calls Anthropic directly with the
4139 platform key.
4140 </div>
4141 </div>
4142 );
4143 })()}
41094144 {(
41104145 [
41114146 ["internal", AI_MODEL_FLAG_KEYS.internal, "claude-opus-5", "Internal operations (platform self-repair)"],
42084243 await setFlag(flagKey, v, user.id);
42094244 }
42104245 }
4246 const mode = String(body[AI_INTERNAL_MODE_FLAG] ?? "");
4247 if (mode === "" || mode === "agent" || mode === "api") {
4248 await setFlag(AI_INTERNAL_MODE_FLAG, mode, user.id);
4249 }
42114250 await audit({ userId: user.id, action: "admin.flags.ai_models.save" });
42124251 return c.redirect("/admin/flags");
42134252});
Modifiedsrc/routes/health.tsx+46−0View fileUnifiedSplit
2525import { loadRepoByPath } from "../lib/namespace";
2626import { resolveRepoAccess, satisfiesAccess } from "../middleware/repo-access";
2727import { createSpecPR } from "../lib/spec-to-pr";
28import { internalAiMode } from "../lib/ai-client";
29import { db } from "../db";
30import { issues, issueLabels } from "../db/schema";
31import { ensureLabel } from "../lib/ensure-label";
2832
2933const health = new Hono<AuthEnv>();
3034
378382 .filter(Boolean)
379383 .join("\n");
380384
385 // Owner decision 2026-08-08: internal repairs run on the owner's Claude
386 // subscription, not API credits. In "agent" mode (the default) the repair
387 // is queued as an `ai:repair` issue that a subscription-backed Claude
388 // Code agent drains — it implements the fix and opens the PR. "api" mode
389 // (admin-switchable) calls the Anthropic API directly.
390 if (internalAiMode() === "agent") {
391 try {
392 const [created] = await db
393 .insert(issues)
394 .values({
395 repositoryId: repoRow.id,
396 authorId: user.id,
397 title: `Health repair: ${action}`.slice(0, 255),
398 body: [
399 spec,
400 ``,
401 `---`,
402 `_Queued from the health page for the internal repair agent._`,
403 `_Agent: implement this on a branch and open a PR; close this issue from the PR._`,
404 ].join("\n"),
405 state: "open",
406 })
407 .returning({ id: issues.id, number: issues.number });
408 if (!created) return fail("Could not queue the repair.");
409 const labelId = await ensureLabel({
410 repositoryId: repoRow.id,
411 name: "ai:repair",
412 color: "#1f5f57",
413 description: "Queued for the internal repair agent",
414 });
415 if (labelId) {
416 await db
417 .insert(issueLabels)
418 .values({ issueId: created.id, labelId })
419 .onConflictDoNothing();
420 }
421 return c.redirect(`/${owner}/${repo}/issues/${created.number}`);
422 } catch {
423 return fail("Could not queue the repair.");
424 }
425 }
426
381427 const result = await createSpecPR({
382428 repoId: repoRow.id,
383429 spec,
384430
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts