finetune-studio / src /types.ts
qgallouedec's picture
qgallouedec HF Staff
Fix dataset search and the runs list; add model logos; reclaim vertical space
0a5812a
Raw
History Blame Contribute Delete
8.13 kB
/**
* The contract.
*
* Almost all of this comes straight from the Hub, which is CORS-open. The app has
* two endpoints of its own: `POST /api/train` to start a run, and
* `POST /api/preflight` to measure a dataset — the one read that needs the model's
* real tokenizer and chat template. See `src/hub.ts` for where each shape is
* filled in.
*
* A rule that shapes this file: the user has never trained a model and never
* will read a docs page. Nothing here is measured in tokens, steps or anything
* else they'd have to look up — budgets are minutes and conversation counts, and
* the only structural concept that ever surfaces is "this conversation is too
* long and loses its ending", and only when it actually happens.
*/
/* -------------------------------------------------------------------------- */
/* Account */
/* -------------------------------------------------------------------------- */
export interface HfUser {
/** Hub username, and the namespace the trained model lands in. */
name: string;
/**
* Whether the account has a way to pay. Jobs are pay-as-you-go on the user's
* own credit, so without this nothing can run — and it's worth knowing that
* before they've picked anything, not after.
*/
canPay: boolean;
}
/* -------------------------------------------------------------------------- */
/* Recipes */
/* -------------------------------------------------------------------------- */
/** One of three curated models. Hyperparameters are never exposed. */
export interface Model {
/** The Hub id, which is also what `POST /api/train` takes. */
id: string;
/** "Qwen3 4B" */
label: string;
/** One short line on what picking this one gets you. */
note: string;
/** Exactly one model is flagged; it's preselected. */
recommended: boolean;
}
export type PlanId = "quick" | "balanced" | "full";
/**
* How much to train on. The only dial the user touches.
*
* Just a label here: the budget itself is a token count that lives in
* PLAN_TOKENS in server/app.py, because turning it into a number of
* conversations needs the dataset measured with a real tokenizer.
*/
export interface Plan {
id: PlanId;
/** "Quick" */
label: string;
}
/* -------------------------------------------------------------------------- */
/* Datasets */
/* -------------------------------------------------------------------------- */
/**
* Where a dataset came from. Used only for ordering and the example badge —
* there are no tabs, everything lives in one list.
*/
export type DatasetSource = "example" | "yours" | "hub";
export interface DatasetSummary {
/** "qgallouedec/my-chats" */
id: string;
private: boolean;
/** Gated datasets need terms accepted before the job can read them. */
gated: boolean;
rows: number;
description: string;
source: DatasetSource;
/**
* What the dataset's own card claims about having a `messages` column.
*
* "chat" — it declares one, so it can be trained on
* "other" — it declares its columns and `messages` isn't among them, which
* is the one reliable *negative*: it really can't be trained on
* "unknown" — the card says nothing about its columns
*
* "unknown" used to be lumped in with "other" and hidden, which was wrong and
* badly so: of 30 datasets under one real account, 28 declare nothing at all, so
* searching your own name returned "nothing usable matched". A card is a weak
* signal — plenty of perfectly good datasets never write one — and preflight,
* which reads actual rows through the viewer, is the real authority. So unknown
* datasets are listed and selectable, and the Check step decides.
*/
format: "chat" | "other" | "unknown";
}
/* -------------------------------------------------------------------------- */
/* Preflight */
/* -------------------------------------------------------------------------- */
/**
* `tool` appears in tool-calling datasets: it's the turn carrying what a tool
* returned, which the model reads but is not trained to produce.
*/
export type Role = "system" | "user" | "assistant" | "tool";
/**
* One call the assistant is being taught to make. `arguments` arrives with values
* already flattened to strings — the server does that so this side only lays them
* out, and so a nested object reads as compact JSON rather than "[object Object]".
*/
export interface ToolCall {
name: string;
arguments: Record<string, string>;
}
export interface Message {
role: Role;
content: string;
/** Set on an assistant turn that calls tools instead of writing a reply. */
toolCalls?: ToolCall[];
/** On a `tool` turn, which tool produced it. */
name?: string;
}
/**
* One conversation, shown to the user as a conversation.
*
* If the length limit falls inside it, `cutFromMessage` marks the first turn
* that gets lost so the preview can fade out at exactly that point. Both cut
* fields are null whenever the conversation fits, which is the common case —
* the idea of a limit never appears unless it actually bites.
*/
export interface PreviewSample {
messages: Message[];
cutFromMessage: number | null;
/** If the cut lands mid-message, how many characters of it survive. */
cutAtChar: number | null;
}
export type IssueLevel = "error" | "warning" | "info";
/** A preflight finding, phrased for someone who has never trained a model. */
export interface Issue {
level: IssueLevel;
title: string;
detail: string;
}
/** What one plan works out to for this dataset. */
export interface PlanEstimate {
planId: PlanId;
conversations: number;
estMinutes: number;
/** What it costs on the user's own account, at the flavor's hourly rate. */
estCostUsd: number;
}
export interface PreflightReport {
datasetId: string;
totalRows: number;
/** Conversations that lose their ending at the shared length limit. */
truncatedCount: number;
/** Same, as a share of the sample, 0..1. */
truncatedPct: number;
issues: Issue[];
samples: PreviewSample[];
perPlan: Record<PlanId, PlanEstimate>;
recommendedPlan: PlanId;
}
/* -------------------------------------------------------------------------- */
/* Runs */
/* -------------------------------------------------------------------------- */
/**
* The stages a run moves through. The first three come from the Hub's own job
* stage; the rest are the recipe announcing itself on stdout, which the log
* stream carries back. See `[fts] stage` in server/recipe/train.py.
*/
export type RunStatus =
| "queued"
| "starting"
| "training"
| "sampling"
| "pushing"
| "done"
| "error";
export interface LogLine {
text: string;
level: "info" | "warn" | "error";
}
/** Base vs tuned on the same prompt, taken from conversations held back from training. */
export interface SampleComparison {
prompt: string;
before: string;
after: string;
}
export interface Run {
/** The HF Jobs job id — the id every Hub jobs endpoint takes. */
id: string;
status: RunStatus;
modelId: string;
planId: PlanId;
datasetId: string;
/** Destination repo in the user's namespace. */
outputRepo: string;
createdAt: string;
/** Overall completion, 0..1. The user never sees a step count. */
progress: number;
/** Technical detail, available behind a disclosure but never surfaced by default. */
logs: LogLine[];
/** Seconds remaining, or null when we can't say. */
etaSeconds: number | null;
/** Populated once status reaches "done" and the samples file has been read. */
samples?: SampleComparison[];
error?: string;
}
/** Anything that hasn't finished — what the header lists. */
export function isActive(run: Run) {
return run.status !== "done" && run.status !== "error";
}