feat(admin): choose the drafting model from Settings (#223)
The model was going to be an environment variable, which meant a redeploy to change it. It is now an admin setting, so it can be changed from the Settings page like the cart expiry and the greeting. A dropdown validated on the server, not a free-text field. The API only rejects an unknown model at the point of use, so a typo would be stored happily and then fail on every submission, surfacing as drafts quietly not appearing rather than as an error anybody could act on. The PUT refuses anything outside the offered set, and getSettings falls back rather than handing on a value that is no longer offered — drafting with the default beats drafting with a model the API will refuse. One catalogue rather than two lists. The dropdown needs the models, costMicros needs their rates, and the price shown beside a model in Admin has to be the price it is actually billed at, which it cannot be if the two are maintained separately. Rates were confirmed against the pricing page rather than recalled: Sonnet 5 $2/$10, Opus 5 $5/$25, Haiku 4.5 $1/$5 per million tokens. The unknown-model fallback is deliberately the most expensive rate and never zero, because a budget that reads as unspent however much was spent is the one failure a spend guard cannot have. Adding a third setting type pushed getSettings past the cognitive complexity limit, so the per-type resolution moved out into one small function each — the same shape the definitions block above it already argues for. The exhaustive assertion in the GET test gained the new field rather than being loosened. It exists to catch a setting silently vanishing from the response, and that is worth more than not having to touch it here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@ import message from 'antd/es/message';
|
||||
import Card from 'antd/es/card';
|
||||
import Space from 'antd/es/space';
|
||||
import Input from 'antd/es/input';
|
||||
import { fetchAdminSettings, updateAdminSettings } from './adminSettingsApi';
|
||||
import Select from 'antd/es/select';
|
||||
import { fetchAdminSettings, updateAdminSettings, DRAFTING_MODELS } from './adminSettingsApi';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -115,6 +116,32 @@ export default function Settings() {
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Title level={4}>Item drafting</Title>
|
||||
{/* A dropdown rather than a text field, and validated again on the
|
||||
server. A mistyped model name is accepted by the API only at the
|
||||
point of use, so it would fail on every submission and show up
|
||||
merely as drafts not appearing. */}
|
||||
<Text type="secondary">
|
||||
Which model writes the name, description and suggested price for a submitted item. The price is per
|
||||
million tokens; a typical item costs a few pence. Drafts are always reviewed before anything is
|
||||
published.
|
||||
</Text>
|
||||
<Form.Item
|
||||
name="draftingModel"
|
||||
label="Drafting model"
|
||||
rules={[{ required: true }]}
|
||||
style={{ marginTop: 16, marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
options={DRAFTING_MODELS.map(m => ({
|
||||
value: m.id,
|
||||
label: `${m.label} — ${m.price}`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
<Button type="primary" onClick={handleSave} loading={loading || saving}>Save</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
|
||||
@@ -4,8 +4,22 @@ export interface AdminSettings {
|
||||
passwordResetHours: number;
|
||||
greetingFormat: string;
|
||||
greetingFallback: string;
|
||||
draftingModel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The models the drafting worker will accept, with what each costs per million
|
||||
* input/output tokens. Mirrors the backend catalogue in src/intake/models.ts,
|
||||
* which is the authority — the server refuses anything outside its own list, so
|
||||
* a stale entry here fails loudly on save rather than silently drafting with
|
||||
* the wrong model.
|
||||
*/
|
||||
export const DRAFTING_MODELS = [
|
||||
{ id: 'claude-sonnet-5', label: 'Claude Sonnet 5', price: '$2 / $10 per Mtok' },
|
||||
{ id: 'claude-opus-5', label: 'Claude Opus 5', price: '$5 / $25 per Mtok' },
|
||||
{ id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', price: '$1 / $5 per Mtok' }
|
||||
];
|
||||
|
||||
export async function fetchAdminSettings(): Promise<AdminSettings> {
|
||||
const res = await fetch('/api/admin/settings');
|
||||
if (!res.ok) throw new Error('Could not load settings');
|
||||
|
||||
Reference in New Issue
Block a user