docs(intake): make the drafting model an admin setting (#223)
Added mid-execution at Thom's request. Two consequences worth recording: it is a dropdown validated on the server rather than a free-text box, because a mistyped model name fails on every submission and surfaces only as drafts quietly not appearing; and the model list lives in one catalogue shared with the cost table, so the settings dropdown and the per-token rates cannot drift apart. Rates confirmed against the pricing page rather than recalled. Worth having checked: an increase to $3/$15 had been scheduled for tomorrow and was cancelled, with $2/$10 made permanent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -472,6 +472,30 @@ git commit -m "feat(intake): tell the model to describe rather than invent (#223
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Task 3b: The model catalogue, and choosing it from Admin settings
|
||||||
|
|
||||||
|
Added mid-execution, at Thom's request: the model must be settable from the Admin settings page rather than only by an environment variable and a redeploy.
|
||||||
|
|
||||||
|
Two things fall out of that. A **dropdown, not a free-text box** — a mistyped model name fails on every submission and surfaces only as drafts quietly not appearing, so the valid set is enforced on the server rather than merely offered by the UI. And **one catalogue, not two** — the settings dropdown needs the model list, `costMicros` needs each model's rates, and those must not be two lists that drift. So the catalogue is a module both import.
|
||||||
|
|
||||||
|
Rates confirmed against the pricing page on 2026-08-31, not recalled: Sonnet 5 $2/$10, Opus 5 $5/$25, Haiku 4.5 $1/$5 per million input/output tokens. Worth having checked — an increase to $3/$15 had been scheduled for 2026-09-01 and was cancelled, with $2/$10 made permanent.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `backend/src/intake/models.ts`, `backend/tests/unit/draftCost.test.ts`
|
||||||
|
- Modify: `backend/src/adminSettings.ts` (a `choice` type), `backend/src/routes/adminSettings.ts`, `frontend/src/admin/Settings.tsx`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `DRAFTING_MODELS`, `DEFAULT_DRAFTING_MODEL`, `isDraftingModel()`, `costMicros()`, and a `draftingModel` admin setting.
|
||||||
|
|
||||||
|
- [ ] **Step 1: The catalogue, test first.** `costMicros(model, input, output)` in whole micros. An unrecognised model must price above zero — a budget that reads as unspent however much was spent is the one failure a spend guard cannot have.
|
||||||
|
- [ ] **Step 2: Add a `choice` type to `adminSettings.ts`.** The module's own docstring says adding a setting means adding a row and nothing else; that holds for `hours` and `text`, and a third type is what makes it hold for a constrained one. Row: `{ key: 'drafting_model', name: 'draftingModel', type: 'choice', fallback: DEFAULT_DRAFTING_MODEL, options: [...] }`.
|
||||||
|
- [ ] **Step 3: Validate membership in the PUT route**, as a third loop beside the hours and text loops. A value outside the set is a 400, not a stored string that breaks drafting later.
|
||||||
|
- [ ] **Step 4: The dropdown in `Settings.tsx`**, showing each model's price so the person switching can see that Opus costs 2.5x Sonnet before they pick it.
|
||||||
|
- [ ] **Step 5:** `npm run lint && npm run build && npm run test:unit`, and the frontend's checks.
|
||||||
|
- [ ] **Step 6: Commit.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Task 4: One call, one draft
|
### Task 4: One call, one draft
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
@@ -582,8 +606,11 @@ import { buildSystemPrompt, buildUserContent } from './draftPrompt';
|
|||||||
*/
|
*/
|
||||||
const DEFAULT_MODEL = 'claude-sonnet-5';
|
const DEFAULT_MODEL = 'claude-sonnet-5';
|
||||||
|
|
||||||
export function draftingModel(): string {
|
// Read from Admin settings (Task 3b) rather than the environment, so the choice
|
||||||
return process.env.INTAKE_MODEL || DEFAULT_MODEL;
|
// can be changed without a redeploy. getSettings() supplies the fallback, so
|
||||||
|
// there is no default written twice here to disagree with the one there.
|
||||||
|
async function draftingModel(): Promise<string> {
|
||||||
|
return (await getSettings()).draftingModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -594,22 +621,8 @@ export function draftingModel(): string {
|
|||||||
* nothing would make a budget ceiling read as unspent however much was really
|
* nothing would make a budget ceiling read as unspent however much was really
|
||||||
* spent, which is the one failure a spend guard must not have.
|
* spent, which is the one failure a spend guard must not have.
|
||||||
*/
|
*/
|
||||||
// CONFIRM THESE against the current pricing page before committing. They are
|
// Rates and costMicros live in ./models, shared with the Admin settings
|
||||||
// the one set of numbers in this plan taken from memory rather than read out of
|
// dropdown so the list of models and their prices cannot drift apart.
|
||||||
// the repository, and the test above pins them exactly. If a rate differs,
|
|
||||||
// change the table and the test together — a cost column that is quietly wrong
|
|
||||||
// by half is worse than no cost column, because #227 sets a budget from it.
|
|
||||||
const RATES: Record<string, { input: number; output: number }> = {
|
|
||||||
'claude-sonnet-5': { input: 2, output: 10 },
|
|
||||||
'claude-opus-5': { input: 5, output: 25 },
|
|
||||||
'claude-haiku-4-5': { input: 1, output: 5 }
|
|
||||||
};
|
|
||||||
const FALLBACK_RATE = { input: 5, output: 25 };
|
|
||||||
|
|
||||||
export function costMicros(model: string, inputTokens: number, outputTokens: number): number {
|
|
||||||
const rate = RATES[model] ?? FALLBACK_RATE;
|
|
||||||
return Math.round(inputTokens * rate.input + outputTokens * rate.output);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftInput {
|
export interface DraftInput {
|
||||||
photos: { mediaType: string; base64: string }[];
|
photos: { mediaType: string; base64: string }[];
|
||||||
|
|||||||
Reference in New Issue
Block a user