Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/pr-screenshots/admin-runtime-controls.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 50 additions & 1 deletion plugins/admin/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4348,6 +4348,15 @@ <h2>Default harness and model</h2>
<select id="base-harness" style="max-width: 360px"></select>
<label for="base-model">Model</label>
<select id="base-model" style="max-width: 360px"></select>
<label for="base-effort">Reasoning level</label>
<select id="base-effort" style="max-width: 360px"></select>
<label class="setting-toggle" style="margin-top: 10px">
<input type="checkbox" id="base-fast-mode" />
<span class="setting-switch" aria-hidden="true"></span>
<span class="setting-copy"
><strong>Fast mode</strong><small>Only available for supported models.</small></span
>
</label>
</div>
<div class="foot">
<button class="primary" data-save="runtime">Apply</button
Expand Down Expand Up @@ -6339,6 +6348,38 @@ <h2 id="governance-review-title">Confirm governance change</h2>
harness.appendChild(o);
});
harness.value = approvedHarnesses.includes(current.harnessId) ? current.harnessId : approvedHarnesses[0];
const effort = $("base-effort");
const thinkingLevelsByHarness = r.data.thinkingLevelsByHarness || {};
const syncEffort = () => {
const thinkingLevels = thinkingLevelsByHarness[harness.value] || ["auto"];
const prior = effort.value || current.effortLevel || "auto";
effort.textContent = "";
thinkingLevels.forEach((level) => {
const o = document.createElement("option");
o.value = level;
o.textContent =
{
auto: "Auto",
low: "Low",
medium: "Medium",
high: "High",
xhigh: "Extra high",
max: "Max",
ultracode: "Ultracode",
}[level] || level;
effort.appendChild(o);
});
effort.value = thinkingLevels.includes(prior) ? prior : "auto";
};
const fastMode = $("base-fast-mode");
fastMode.checked = current.fastMode === true;
const syncFastMode = () => {
const fastCapable =
(r.data.fastModeHarnessIds || []).includes(harness.value) &&
(r.data.fastModeModelIds || []).includes($("base-model").value);
fastMode.disabled = !fastCapable;
if (!fastCapable) fastMode.checked = false;
};
const syncModels = () => {
const selectedHarness = harness.value;
const compatible = modelsByHarness[selectedHarness] || opts;
Expand All @@ -6352,6 +6393,9 @@ <h2 id="governance-review-title">Confirm governance change</h2>
sel.appendChild(o);
});
sel.value = compatible.some((m) => m.id === prior) ? prior : (compatible[0] || {}).id || "";
sel.oninput = syncFastMode;
syncEffort();
syncFastMode();
};
harness.oninput = syncModels;
syncModels();
Expand Down Expand Up @@ -7937,7 +7981,12 @@ <h2 id="governance-review-title">Confirm governance change</h2>
"external-slack-participants": () => ({ on: $("external-slack-participants").checked }),
"org-ambient": () => ({ on: $("org-ambient").checked }),
"interactive-fast-mode": () => ({ on: $("interactive-fast-mode").checked }),
runtime: () => ({ harnessId: $("base-harness").value, modelId: $("base-model").value }),
runtime: () => ({
harnessId: $("base-harness").value,
modelId: $("base-model").value,
effortLevel: $("base-effort").value,
fastMode: $("base-fast-mode").checked,
}),
"approved-harnesses": () => ({
ids: Array.from(document.querySelectorAll("#approved-harnesses-list input[type=checkbox]:checked")).map(
(c) => c.value,
Expand Down
12 changes: 12 additions & 0 deletions plugins/admin/test/default-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ test("governance renders simple settings as compact rows with contextual actions
assert.match(html, /"turnWallClockSec" in r\.data/);
});

test("default runtime controls save reasoning level and fast mode", () => {
assert.match(html, /id="base-effort"/);
assert.match(html, /id="base-fast-mode"/);
assert.match(html, /thinkingLevelsByHarness/);
assert.match(html, /fastModeModelIds/);
assert.match(html, /fastModeHarnessIds/);
assert.match(
html,
/runtime: \(\) => \(\{[\s\S]*effortLevel: \$\("base-effort"\)\.value,[\s\S]*fastMode: \$\("base-fast-mode"\)\.checked/,
);
});

test("compact governance rows preserve policy detail and collapse before they overflow", () => {
assert.doesNotMatch(html, /#view-governance \.setting-row > \.head p[^}]*line-clamp/);
assert.doesNotMatch(html, /#view-governance \.setting-row > \.foot \.status[^}]*white-space:\s*nowrap/);
Expand Down
44 changes: 41 additions & 3 deletions src/api/routes/admin-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { parseCommandPolicy } from "../../policy/command-policy.ts";
import { parseScopeId, scopeId, type CommandPolicy, type Grant } from "../../types.ts";
import {
defaultModelForHarness,
FAST_MODE_MODEL_IDS,
harnessSupportsFastMode,
HARNESS_IDS,
isHarnessId,
modelSupportedByHarness,
modelServiceable,
modelProviderAvailabilityFor,
resolveModel,
SELECTABLE_BASE_MODELS,
thinkingLevelsForHarness,
ALL_PROVIDERS_AVAILABLE,
} from "../../model/pi-models.ts";
import { resolveRuntimeChoiceDurable } from "../../harness/harness-router.ts";
Expand Down Expand Up @@ -372,7 +375,19 @@ export const ADMIN_RESOURCES: readonly AdminResource[] = [
return { error: `model ${modelId} is not supported by ${runtime.harnessId}` };
const bad = unserviceable(runtime.harnessId);
if (bad) return bad;
await ctx.deps.config!.setRuntimeSelectionLatest(scope, { harnessId: runtime.harnessId, modelId });
await ctx.deps.config!.setRuntimeSelectionLatest(scope, {
harnessId: runtime.harnessId,
modelId,
...(runtime.effortLevel ? { effortLevel: runtime.effortLevel } : {}),
...(typeof runtime.fastMode === "boolean"
? {
fastMode:
runtime.fastMode &&
harnessSupportsFastMode(runtime.harnessId) &&
FAST_MODE_MODEL_IDS.includes(modelId),
}
: {}),
});
} else {
const harnessId = isHarnessId(ctx.deps.harnessId) ? ctx.deps.harnessId : "pi";
const effective = await resolveRuntimeChoiceDurable(ctx.deps.config!, scopeId("org", configOrgId()), scope, {
Expand All @@ -383,7 +398,19 @@ export const ADMIN_RESOURCES: readonly AdminResource[] = [
return { error: `model ${modelId} is not supported by ${effective.harnessId}` };
const bad = unserviceable(effective.harnessId);
if (bad) return bad;
await ctx.deps.config!.setRuntimeSelectionLatest(scope, { harnessId: effective.harnessId, modelId });
await ctx.deps.config!.setRuntimeSelectionLatest(scope, {
harnessId: effective.harnessId,
modelId,
...(effective.effortLevel ? { effortLevel: effective.effortLevel } : {}),
...(typeof effective.fastMode === "boolean"
? {
fastMode:
effective.fastMode &&
harnessSupportsFastMode(effective.harnessId) &&
FAST_MODE_MODEL_IDS.includes(modelId),
}
: {}),
});
}
return { ok: true };
},
Expand All @@ -402,17 +429,28 @@ export const ADMIN_RESOURCES: readonly AdminResource[] = [
}
const harnessId = (ctx.body as { harnessId?: unknown }).harnessId;
const modelId = (ctx.body as { modelId?: unknown }).modelId;
const effortLevel = (ctx.body as { effortLevel?: unknown }).effortLevel ?? "auto";
const fastMode = (ctx.body as { fastMode?: unknown }).fastMode ?? false;
if (!isHarnessId(harnessId)) return { error: `runtime requires harnessId (${HARNESS_IDS.join(" | ")})` };
const approved = (await ctx.deps.config!.getApprovedHarnessesDurable()) ?? [ctx.deps.harnessId ?? "pi"];
if (!approved.includes(harnessId)) return { error: `harness ${harnessId} is not approved` };
if (typeof modelId !== "string" || !modelSupportedByHarness(modelId, harnessId))
return { error: `model ${String(modelId)} is not supported by ${harnessId}` };
const thinkingLevels = thinkingLevelsForHarness(harnessId);
if (typeof effortLevel !== "string" || !thinkingLevels.includes(effortLevel))
return { error: `runtime requires effortLevel (${thinkingLevels.join(" | ")}) for ${harnessId}` };
if (typeof fastMode !== "boolean") return { error: "runtime requires fastMode (boolean)" };
const runtimeKeys = ctx.deps.providerKeys ?? ALL_PROVIDERS_AVAILABLE;
if (!modelServiceable(modelId, modelProviderAvailabilityFor(harnessId, runtimeKeys)))
return {
error: `model ${modelId} isn't serviceable on this deployment: its provider key is not configured for the ${harnessId} harness`,
};
await ctx.deps.config!.setRuntimeSelectionLatest(scope, { harnessId, modelId });
await ctx.deps.config!.setRuntimeSelectionLatest(scope, {
harnessId,
modelId,
effortLevel,
fastMode: fastMode && harnessSupportsFastMode(harnessId) && FAST_MODE_MODEL_IDS.includes(modelId),
});
return { ok: true };
},
},
Expand Down
8 changes: 8 additions & 0 deletions src/api/routes/admin/scope-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { parseScopeId } from "../../../types.ts";
import { encodeRef, serviceCredRef } from "../../../acl/resource-ref.ts";
import { computeRetention } from "../../../admin/retention.ts";
import {
FAST_MODE_MODEL_IDS,
harnessSupportsFastMode,
HARNESS_IDS,
SELECTABLE_BASE_MODELS,
defaultModelForHarness,
modelProviderAvailabilityFor,
modelServiceable,
ALL_PROVIDERS_AVAILABLE,
resolveModel,
thinkingLevelsForHarness,
} from "../../../model/pi-models.ts";
import {
builtInModelCatalog,
Expand Down Expand Up @@ -270,6 +273,11 @@ export async function getScopeConfig(ctx: ApiCtx): Promise<void> {
harnessDefault: deps.harnessId ?? "pi",
harnessOptions: HARNESS_IDS.filter((id) => id !== "mock"),
modelsByHarness: Object.fromEntries(HARNESS_IDS.map((id) => [id, modelsFor(id)])),
thinkingLevelsByHarness: Object.fromEntries(
HARNESS_IDS.filter((id) => id !== "mock").map((id) => [id, thinkingLevelsForHarness(id)]),
),
fastModeModelIds: FAST_MODE_MODEL_IDS,
fastModeHarnessIds: HARNESS_IDS.filter(harnessSupportsFastMode),
browseModelOptions: SELECTABLE_BASE_MODELS.filter((m) =>
modelServiceable(m.id, providersFor(deps.harnessId ?? "pi")),
),
Expand Down
65 changes: 53 additions & 12 deletions src/harness/harness-router.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import type { ScopedConfigStore } from "../resolution/config-store.ts";
import { defaultModelForHarness, isHarnessId, modelSupportedByHarness, type HarnessId } from "../model/pi-models.ts";
import {
defaultModelForHarness,
FAST_MODE_MODEL_IDS,
harnessSupportsFastMode,
isHarnessId,
modelSupportedByHarness,
thinkingLevelsForHarness,
type HarnessId,
} from "../model/pi-models.ts";
import type { ScopeId } from "../types.ts";
import type { Harness, HarnessTurnInput } from "./harness.ts";
import { NonRetryableTurnError } from "../core/turn-error.ts";

export interface RuntimeChoice {
harnessId: HarnessId;
modelId: string;
effortLevel?: string;
fastMode?: boolean;
}

function normalizeRuntimeChoice(choice: RuntimeChoice): RuntimeChoice {
return {
harnessId: choice.harnessId,
modelId: choice.modelId,
...(choice.effortLevel && thinkingLevelsForHarness(choice.harnessId).includes(choice.effortLevel)
? { effortLevel: choice.effortLevel }
: {}),
...(typeof choice.fastMode === "boolean"
? {
fastMode:
choice.fastMode &&
harnessSupportsFastMode(choice.harnessId) &&
FAST_MODE_MODEL_IDS.includes(choice.modelId),
}
: {}),
};
}

export function resolveRuntimeChoice(
Expand All @@ -19,9 +47,14 @@ export function resolveRuntimeChoice(
const approved = config.getApprovedHarnesses() ?? [fallback.harnessId];
const orgStored = config.getRuntimeSelection(orgScopeId);
const orgLegacy = config.getBaseModel(orgScopeId);
const configuredOrg =
const configuredOrg: RuntimeChoice =
orgStored && isHarnessId(orgStored.harnessId)
? { harnessId: orgStored.harnessId, modelId: orgStored.modelId }
? {
harnessId: orgStored.harnessId,
modelId: orgStored.modelId,
...(orgStored.effortLevel ? { effortLevel: orgStored.effortLevel } : {}),
...(typeof orgStored.fastMode === "boolean" ? { fastMode: orgStored.fastMode } : {}),
}
: { harnessId: fallback.harnessId, modelId: orgLegacy ?? fallback.modelId };
const firstApproved = approved.find(isHarnessId) ?? fallback.harnessId;
const safeFallback =
Expand All @@ -35,22 +68,24 @@ export function resolveRuntimeChoice(
: safeFallback;
const scopedStored = scope === orgScopeId ? null : config.getRuntimeSelection(scope);
const scopedLegacy = scope === orgScopeId ? null : config.getBaseModel(scope);
let inherited = org;
let inherited: RuntimeChoice = org;
if (scopedStored && isHarnessId(scopedStored.harnessId)) {
inherited = { harnessId: scopedStored.harnessId, modelId: scopedStored.modelId };
inherited = {
harnessId: scopedStored.harnessId,
modelId: scopedStored.modelId,
...(scopedStored.effortLevel ? { effortLevel: scopedStored.effortLevel } : {}),
...(typeof scopedStored.fastMode === "boolean" ? { fastMode: scopedStored.fastMode } : {}),
};
} else if (scopedLegacy) {
inherited = { harnessId: fallback.harnessId, modelId: scopedLegacy };
}
const choice =
requested?.harnessId || requested?.modelId
? { harnessId: requested.harnessId ?? inherited.harnessId, modelId: requested.modelId ?? inherited.modelId }
: inherited;
const choice = requested?.harnessId || requested?.modelId ? { ...inherited, ...requested } : inherited;
if (!approved.includes(choice.harnessId) || !modelSupportedByHarness(choice.modelId, choice.harnessId)) {
if (requested?.harnessId || requested?.modelId)
throw new NonRetryableTurnError(`runtime ${choice.harnessId}/${choice.modelId} is not approved`);
return org;
return normalizeRuntimeChoice(org);
}
return choice;
return normalizeRuntimeChoice(choice);
}

export async function resolveRuntimeChoiceDurable(
Expand Down Expand Up @@ -102,7 +137,13 @@ export function createHarnessRouter(
await adapter.turns.resetSession?.(input.session.id);
}
lastHarness.set(input.session.id, choice.harnessId);
return adapter.turns.runTurn({ ...input, harness: choice.harnessId, model: choice.modelId });
return adapter.turns.runTurn({
...input,
harness: choice.harnessId,
model: choice.modelId,
thinkingLevel: input.thinkingLevel ?? choice.effortLevel,
fastMode: input.fastMode ?? choice.fastMode,
});
},
async resetSession(sessionId) {
lastHarness.delete(sessionId);
Expand Down
12 changes: 7 additions & 5 deletions src/harness/pi-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,15 +1192,17 @@ function applyEffortAliases(model: unknown): void {
};
}

function applyTurnEffort(session: AgentSession, level?: string): void {
export function applyTurnEffort(session: AgentSession, level?: string): void {
if (!level || !TURN_EFFORT_LEVELS.has(level)) return;
if (level === "auto") return;
const effectiveLevel =
level === "auto" && session.state.model ? defaultInteractiveThinkingLevel(session.state.model) : level;
const normalizedLevel = effectiveLevel === "auto" ? "medium" : effectiveLevel;
applyEffortAliases(session.state.model);
try {
if (LEGACY_THINKING_LEVELS.has(level)) {
session.setThinkingLevel(level as LegacyThinkingLevel);
if (LEGACY_THINKING_LEVELS.has(normalizedLevel)) {
session.setThinkingLevel(normalizedLevel as LegacyThinkingLevel);
} else {
session.state.thinkingLevel = level as typeof session.state.thinkingLevel;
session.state.thinkingLevel = normalizedLevel as typeof session.state.thinkingLevel;
}
} catch (e) {
swallow("pi: set thinking level", e);
Expand Down
11 changes: 11 additions & 0 deletions src/model/pi-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export const THINKING_LEVELS = ["auto", "low", "medium", "high", "xhigh", "max",
export const HARNESS_IDS = ["pi", "opencode", "codex", "claude", "mock"] as const;
export type HarnessId = (typeof HARNESS_IDS)[number];

export function thinkingLevelsForHarness(harnessId: HarnessId): readonly string[] {
if (harnessId === "pi") return THINKING_LEVELS;
if (harnessId === "claude") return THINKING_LEVELS.filter((level) => level !== "ultracode");
if (harnessId === "codex") return THINKING_LEVELS.filter((level) => level !== "max" && level !== "ultracode");
return ["auto"];
}

export function harnessSupportsFastMode(harnessId: HarnessId): boolean {
return harnessId === "pi" || harnessId === "claude";
}

export const MODEL_PROVIDERS = ["anthropic", "openai", "openrouter"] as const;
export type ModelProvider = (typeof MODEL_PROVIDERS)[number];

Expand Down
Loading