Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: choose the review classifier's runtime on the settings page

The classifier settings page showed model and effort only, so the API-key
or ACP runtime could be switched only through a raw API call. It now has a
runtime select, and under ACP the provider's URL, model and prices. A PUT
carries the provider only on the ACP runtime, so the stored record stays
for a switch back. Closes #5249
This commit is contained in:
iris 2026-10-14 13:31:44 +02:00 • committed by mara
commit a04096ea19
4 changed files with 213 additions and 35 deletions

View file

@ -1,9 +1,11 @@
// <ClassifierSettings> — the review classifier's model, effort and how many
// requested reviews run at once, shown and saved here
// (`GET`/`PUT /api/review-classifier/settings`). A PUT replaces the stored
// settings whole, so the form sends all three fields. A refused save shows the
// route's own reason and leaves the form as typed. There is no API key field:
// admins set the key in the bao UI.
// <ClassifierSettings> — the review classifier's runtime, model, effort and
// how many requested reviews run at once, shown and saved here
// (`GET`/`PUT /api/review-classifier/settings`). A PUT changes only the
// fields it carries: the form sends the runtime, model, effort and limit, and
// the ACP provider only while the runtime is ACP, so the stored provider
// survives a switch back. A refused save shows the route's own reason and
// leaves the form as typed. There is no API key field: admins set each
// runtime's key in the bao UI.
import { useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
@ -20,12 +22,18 @@ import {
settingsProblem,
toSettings,
type ClassifierSettingsBody,
type Runtime,
type SettingsDraft,
} from "./SettingsTypes.js";
import "./ClassifierSettings.css";
const SETTINGS = "/api/review-classifier/settings";
const RUNTIME_OPTIONS = [
{ value: "api-key", label: "API key (claude)" },
{ value: "acp", label: "ACP (opencode)" },
];
const EFFORT_OPTIONS = [
{ value: "", label: "claude's default" },
...EFFORTS.map((e) => ({ value: e, label: e })),
@ -101,10 +109,12 @@ export function ClassifierSettings() {
return (
<Panel title="classifier settings" icon="⚙️">
<p>
The model and effort the review classifier runs with, and how many
requested reviews run at once. A change takes effect from the next run,
with no restart. Try-outs don't count toward the limit. The API key is
set in the bao UI, not here.
The runtime, model and effort the review classifier runs with, and how
many requested reviews run at once. The API key runtime runs claude on
the Anthropic key; the ACP runtime runs opencode on the provider given
here. A change takes effect from the next run, with no restart. Try-outs
don't count toward the limit. Each runtime's key is set in the bao UI,
not here.
</p>
{loadError ? (
<ApiErrorPanel
@ -115,13 +125,24 @@ export function ClassifierSettings() {
{draft ? (
<form class="ui-classifier-settings" onSubmit={(e) => void save(e)}>
<div class="ui-classifier-settings-fields">
<TextField
id="classifier-model"
label="model"
value={draft.model}
placeholder="haiku"
onInput={(model) => edit({ ...draft, model })}
<SelectField
id="classifier-runtime"
label="runtime"
value={draft.runtime}
options={RUNTIME_OPTIONS}
onChange={(runtime) =>
edit({ ...draft, runtime: runtime as Runtime })
}
/>
{draft.runtime === "api-key" ? (
<TextField
id="classifier-model"
label="model"
value={draft.model}
placeholder="haiku"
onInput={(model) => edit({ ...draft, model })}
/>
) : null}
<SelectField
id="classifier-effort"
label="effort"
@ -137,6 +158,50 @@ export function ClassifierSettings() {
onInput={(max) => edit({ ...draft, max })}
/>
</div>
{draft.runtime === "acp" ? (
<div class="ui-classifier-settings-fields">
<TextField
id="classifier-acp-url"
label="provider URL"
value={draft.acpUrl}
placeholder="https://llm.example/v1"
onInput={(acpUrl) => edit({ ...draft, acpUrl })}
/>
<TextField
id="classifier-acp-model"
label="provider model"
value={draft.acpModel}
placeholder="Qwen/Qwen3-35B"
onInput={(acpModel) => edit({ ...draft, acpModel })}
/>
<TextField
id="classifier-acp-price-input"
label="input price (USD per million tokens)"
value={draft.priceInput}
onInput={(priceInput) => edit({ ...draft, priceInput })}
/>
<TextField
id="classifier-acp-price-output"
label="output price (USD per million tokens)"
value={draft.priceOutput}
onInput={(priceOutput) => edit({ ...draft, priceOutput })}
/>
<TextField
id="classifier-acp-price-cache-read"
label="cache read price (optional)"
value={draft.priceCacheRead}
onInput={(priceCacheRead) => edit({ ...draft, priceCacheRead })}
/>
<TextField
id="classifier-acp-price-cache-write"
label="cache write price (optional)"
value={draft.priceCacheWrite}
onInput={(priceCacheWrite) =>
edit({ ...draft, priceCacheWrite })
}
/>
</div>
) : null}
{saveError ? (
<ApiErrorPanel
context="the settings were not saved"