feat: complete TASK-WP3-01 model configuration
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
.admin-models-page {
|
||||
min-height: 100vh;
|
||||
color: #111111;
|
||||
background: #f6f6f4;
|
||||
}
|
||||
|
||||
.admin-models-page > main {
|
||||
width: min(1440px, calc(100% - 64px));
|
||||
margin: 0 auto;
|
||||
padding: 36px 0 80px;
|
||||
}
|
||||
|
||||
.admin-models-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid #999993;
|
||||
}
|
||||
|
||||
.admin-models-heading p {
|
||||
margin: 0 0 4px;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-models-heading h1 {
|
||||
margin: 0;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.admin-models-heading > strong {
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.admin-models-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin: 22px 0;
|
||||
border-block: 1px solid #8c8c86;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.admin-models-summary > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
padding: 18px;
|
||||
border-right: 1px solid #c1c1ba;
|
||||
color: #65655f;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-models-summary > span:last-child { border-right: 0; }
|
||||
.admin-models-summary strong { overflow-wrap: anywhere; color: #111111; font-size: 15px; }
|
||||
|
||||
.admin-models-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #8c8c86;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap table {
|
||||
width: 100%;
|
||||
min-width: 1220px;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap th,
|
||||
.admin-models-table-wrap td {
|
||||
padding: 14px 12px;
|
||||
border-right: 1px solid #d0d0ca;
|
||||
border-bottom: 1px solid #d0d0ca;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap thead th {
|
||||
background: #e7e7e2;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap th:first-child { width: 210px; }
|
||||
.admin-models-table-wrap th:nth-child(2),
|
||||
.admin-models-table-wrap th:nth-child(3) { width: 62px; text-align: center; }
|
||||
.admin-models-table-wrap th:nth-child(4) { width: 112px; }
|
||||
.admin-models-table-wrap th:nth-child(5) { width: 96px; }
|
||||
.admin-models-table-wrap th:nth-child(6) { width: 170px; }
|
||||
.admin-models-table-wrap th:nth-child(7) { width: 90px; }
|
||||
.admin-models-table-wrap th:nth-child(8) { width: 180px; }
|
||||
.admin-models-table-wrap th:nth-child(9) { width: 84px; }
|
||||
|
||||
.admin-models-table-wrap tbody th strong,
|
||||
.admin-models-table-wrap tbody th code,
|
||||
.admin-models-table-wrap td small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap tbody th code,
|
||||
.admin-models-table-wrap td small {
|
||||
margin-top: 5px;
|
||||
color: #65655f;
|
||||
font-size: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap td:nth-child(2),
|
||||
.admin-models-table-wrap td:nth-child(3) { text-align: center; }
|
||||
|
||||
.admin-models-table-wrap input[type="number"] {
|
||||
width: 76px;
|
||||
min-height: 38px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #777770;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.admin-models-table-wrap input[type="checkbox"],
|
||||
.admin-models-table-wrap input[type="radio"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.model-state { display: inline-block; padding: 4px 6px; border: 1px solid #7d7d77; font-family: Consolas, monospace; }
|
||||
.model-state.is-available,
|
||||
.model-state.is-verified { border-color: #287b45; color: #1f6639; background: #edf8f0; }
|
||||
.model-state.is-unavailable,
|
||||
.model-state.is-unverified,
|
||||
.model-state.is-blocked { border-color: #a66c18; color: #7c4a06; background: #fff7df; }
|
||||
|
||||
.admin-models-actions {
|
||||
display: flex;
|
||||
min-height: 74px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #999993;
|
||||
}
|
||||
|
||||
.admin-models-actions > div:last-child { display: flex; gap: 8px; }
|
||||
.admin-models-actions button,
|
||||
.admin-models-alert button {
|
||||
min-height: 42px;
|
||||
padding: 9px 14px;
|
||||
border: 1px solid #111111;
|
||||
border-radius: 0;
|
||||
background: #f2f500;
|
||||
font-weight: 800;
|
||||
}
|
||||
.admin-models-actions button:disabled { color: #777770; background: #dfdfda; cursor: not-allowed; }
|
||||
.admin-models-validation,
|
||||
.admin-models-notice { margin: 0; font-weight: 700; }
|
||||
.admin-models-validation { color: #a52e24; }
|
||||
.admin-models-notice { color: #1f6639; }
|
||||
|
||||
.admin-models-alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
border-left: 5px solid #d14a3b;
|
||||
background: #fff1ef;
|
||||
}
|
||||
|
||||
.admin-models-loading { display: grid; gap: 10px; margin-top: 24px; }
|
||||
.admin-models-loading span { display: block; height: 62px; background: #dfdfda; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.admin-models-page > main { width: 100%; padding-right: 16px; padding-left: 16px; }
|
||||
.admin-models-summary { grid-template-columns: 1fr; }
|
||||
.admin-models-summary > span { border-right: 0; border-bottom: 1px solid #c1c1ba; }
|
||||
.admin-models-actions { align-items: stretch; flex-direction: column; }
|
||||
.admin-models-actions > div:last-child { display: grid; }
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import "./admin-models.css";
|
||||
|
||||
interface AdminSession { csrf_token: string }
|
||||
interface RuntimeAvailability {
|
||||
available_for_new_jobs: boolean;
|
||||
checked_at: string;
|
||||
reason: string;
|
||||
}
|
||||
interface ModelConfig {
|
||||
config_version: number;
|
||||
contract_evidence_ref: string | null;
|
||||
contract_validation_status: "blocked" | "unverified" | "verified";
|
||||
credit_cost: number;
|
||||
display_name: string;
|
||||
enabled: boolean;
|
||||
error_mapping_profile: Record<string, string>;
|
||||
gateway_account_ref: string;
|
||||
is_default: boolean;
|
||||
model_id: string;
|
||||
prompt_max_length: number;
|
||||
recommendation_priority: number;
|
||||
reference_limits: { max_file_bytes: number; max_files: number; max_total_bytes: number };
|
||||
route_profile: Record<string, unknown>;
|
||||
runtime_availability: RuntimeAvailability;
|
||||
safety_source: string;
|
||||
supported_ratios: string[];
|
||||
}
|
||||
interface ModelConfiguration {
|
||||
config_set_version: number;
|
||||
configured_default_model_id: string;
|
||||
models: ModelConfig[];
|
||||
recommended_model_id: string | null;
|
||||
}
|
||||
|
||||
async function responseJson<T>(url: string, init?: RequestInit) {
|
||||
const response = await fetch(url, { credentials: "same-origin", ...init });
|
||||
const body = await response.json() as T;
|
||||
return { body, response };
|
||||
}
|
||||
|
||||
function idempotencyKey() {
|
||||
return crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", "");
|
||||
}
|
||||
|
||||
function editableModel(model: ModelConfig) {
|
||||
const { config_version: _configVersion, contract_evidence_ref: _evidence, contract_validation_status: _status, runtime_availability: _runtime, ...editable } = model;
|
||||
return editable;
|
||||
}
|
||||
|
||||
function runtimeLabel(runtime: RuntimeAvailability) {
|
||||
return runtime.available_for_new_jobs ? "可用于新任务" : `不可用 · ${runtime.reason}`;
|
||||
}
|
||||
|
||||
export function AdminModelsPage() {
|
||||
const [session, setSession] = useState<AdminSession>();
|
||||
const [configuration, setConfiguration] = useState<ModelConfiguration>();
|
||||
const [draft, setDraft] = useState<ModelConfig[]>();
|
||||
const [loadingFailed, setLoadingFailed] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
const [conflicted, setConflicted] = useState(false);
|
||||
const priorityRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
||||
const defaultRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
||||
|
||||
async function load() {
|
||||
setLoadingFailed(false);
|
||||
setConflicted(false);
|
||||
try {
|
||||
const [sessionResult, modelResult] = await Promise.all([
|
||||
responseJson<AdminSession>("/api/v1/admin-auth/session"),
|
||||
responseJson<ModelConfiguration>("/api/v1/models"),
|
||||
]);
|
||||
if (!sessionResult.response.ok || !modelResult.response.ok) throw new Error("load_failed");
|
||||
setSession(sessionResult.body);
|
||||
setConfiguration(modelResult.body);
|
||||
setDraft(structuredClone(modelResult.body.models));
|
||||
setNotice("");
|
||||
} catch {
|
||||
setLoadingFailed(true);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { void load(); }, []);
|
||||
|
||||
const validation = useMemo(() => {
|
||||
if (!draft) return { valid: false, message: "" };
|
||||
const invalidPriority = draft.find((model) => !Number.isSafeInteger(model.recommendation_priority) || model.recommendation_priority <= 0);
|
||||
if (invalidPriority) return { field: invalidPriority.model_id, kind: "priority" as const, message: "推荐优先级必须是正整数。", valid: false };
|
||||
const duplicatePriority = draft.find((model, index) => draft.findIndex((item) => item.recommendation_priority === model.recommendation_priority) !== index);
|
||||
if (duplicatePriority) return { field: duplicatePriority.model_id, kind: "priority" as const, message: "推荐优先级不能重复。", valid: false };
|
||||
const defaults = draft.filter((model) => model.enabled && model.is_default);
|
||||
if (defaults.length !== 1) return { field: draft.find((model) => model.is_default)?.model_id ?? draft[0]?.model_id, kind: "default" as const, message: "必须指定一个已启用的默认模型。", valid: false };
|
||||
return { message: "", valid: true };
|
||||
}, [draft]);
|
||||
|
||||
function updateModel(modelId: string, change: Partial<ModelConfig>) {
|
||||
setDraft((current) => current?.map((model) => model.model_id === modelId ? { ...model, ...change } : model));
|
||||
setNotice("");
|
||||
}
|
||||
|
||||
function chooseDefault(modelId: string) {
|
||||
setDraft((current) => current?.map((model) => ({ ...model, is_default: model.model_id === modelId })));
|
||||
setNotice("");
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!configuration || !draft || !session || !validation.valid || saving || conflicted) return;
|
||||
setSaving(true);
|
||||
setNotice("");
|
||||
try {
|
||||
const { body, response } = await responseJson<ModelConfiguration & { error?: { code?: string } }>("/api/v1/admin/models/configuration", {
|
||||
body: JSON.stringify({ expected_config_set_version: configuration.config_set_version, models: draft.map(editableModel) }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Idempotency-Key": idempotencyKey(),
|
||||
"X-CSRF-Token": session.csrf_token,
|
||||
},
|
||||
method: "PUT",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const code = body.error?.code;
|
||||
if (code === "MODEL_CONFIG_VERSION_CONFLICT") {
|
||||
setConflicted(true);
|
||||
setNotice("配置已被其他管理员更新,请刷新最新配置后重新编辑。");
|
||||
} else if (code === "MODEL_RECOMMENDATION_PRIORITY_INVALID" || code === "MODEL_RECOMMENDATION_PRIORITY_CONFLICT") {
|
||||
const modelId = validation.field ?? draft[0]?.model_id;
|
||||
if (modelId) priorityRefs.current[modelId]?.focus();
|
||||
setNotice("请修正推荐优先级后再保存。");
|
||||
} else if (code === "MODEL_DEFAULT_REPLACEMENT_REQUIRED" || code === "MODEL_DEFAULT_REPLACEMENT_INVALID") {
|
||||
const modelId = validation.field ?? draft[0]?.model_id;
|
||||
if (modelId) defaultRefs.current[modelId]?.focus();
|
||||
setNotice("请指定一个已启用的替代默认模型。");
|
||||
} else setNotice("操作未完成。");
|
||||
return;
|
||||
}
|
||||
setConfiguration(body);
|
||||
setDraft(structuredClone(body.models));
|
||||
setNotice("模型配置已原子保存并记录审计。");
|
||||
} catch {
|
||||
setNotice("操作未完成。");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-models-page">
|
||||
<header className="admin-product-header">
|
||||
<a href="/admin">DADA ADMIN</a>
|
||||
<nav aria-label="后台导航"><a href="/admin/users">用户</a><a aria-current="page" href="/admin/models">模型</a><a href="/admin/audit">审计</a></nav>
|
||||
</header>
|
||||
<main>
|
||||
<header className="admin-models-heading">
|
||||
<div><p>MODEL OPERATIONS</p><h1>模型配置</h1></div>
|
||||
{configuration ? <strong>配置集合 v{configuration.config_set_version}</strong> : null}
|
||||
</header>
|
||||
{!configuration && !loadingFailed ? <div aria-label="模型配置加载中" className="admin-models-loading"><span /><span /><span /></div> : null}
|
||||
{loadingFailed ? <p className="admin-models-alert" role="alert">模型配置暂时无法读取。<button onClick={() => void load()} type="button">重试</button></p> : null}
|
||||
{configuration && draft ? (
|
||||
<>
|
||||
<div className="admin-models-summary" aria-label="模型配置摘要">
|
||||
<span>配置默认 <strong>{configuration.configured_default_model_id}</strong></span>
|
||||
<span>当前推荐 <strong>{configuration.recommended_model_id ?? "无"}</strong></span>
|
||||
<span>运行时可用 <strong>{configuration.models.filter((model) => model.runtime_availability.available_for_new_jobs).length} / 3</strong></span>
|
||||
</div>
|
||||
<div className="admin-models-table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>模型</th><th>启用</th><th>默认</th><th>推荐优先级</th><th>契约</th><th>运行时可用</th><th>当前推荐</th><th>能力</th><th>配置版本</th></tr></thead>
|
||||
<tbody>
|
||||
{draft.map((model) => (
|
||||
<tr key={model.model_id}>
|
||||
<th scope="row"><strong>{model.display_name}</strong><code>{model.model_id}</code></th>
|
||||
<td><input aria-label={`启用 ${model.display_name}`} checked={model.enabled} onChange={(event) => updateModel(model.model_id, { enabled: event.target.checked })} type="checkbox" /></td>
|
||||
<td><input aria-label={`设为默认 ${model.display_name}`} checked={model.is_default} disabled={!model.enabled} name="configured-default" onChange={() => chooseDefault(model.model_id)} ref={(node) => { defaultRefs.current[model.model_id] = node; }} type="radio" /></td>
|
||||
<td><input aria-label={`${model.display_name} 推荐优先级`} inputMode="numeric" min="1" onChange={(event) => updateModel(model.model_id, { recommendation_priority: Number(event.target.value) })} ref={(node) => { priorityRefs.current[model.model_id] = node; }} type="number" value={model.recommendation_priority} /></td>
|
||||
<td><span className={`model-state is-${model.contract_validation_status}`}>{model.contract_validation_status}</span></td>
|
||||
<td><span className={`model-state ${model.runtime_availability.available_for_new_jobs ? "is-available" : "is-unavailable"}`}>{runtimeLabel(model.runtime_availability)}</span><small>{new Date(model.runtime_availability.checked_at).toLocaleString("zh-CN")}</small></td>
|
||||
<td>{configuration.recommended_model_id === model.model_id ? "是" : "否"}</td>
|
||||
<td><span>{model.credit_cost} 点</span><small>{model.supported_ratios.join(" / ")} · 最多 {model.reference_limits.max_files} 张参考图</small></td>
|
||||
<td>v{model.config_version}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<footer className="admin-models-actions">
|
||||
<div aria-live="polite">{validation.message ? <p className="admin-models-validation">{validation.message}</p> : null}{notice ? <p className="admin-models-notice" role={conflicted ? "alert" : "status"}>{notice}</p> : null}</div>
|
||||
<div>{conflicted ? <button onClick={() => void load()} type="button">刷新最新配置</button> : null}<button disabled={!validation.valid || saving || conflicted} onClick={() => void save()} type="button">{saving ? "保存中" : "保存完整配置集合"}</button></div>
|
||||
</footer>
|
||||
</>
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export function AdminUsersPage() {
|
||||
<div className="admin-users-page">
|
||||
<header className="admin-product-header">
|
||||
<a href="/admin">DADA ADMIN</a>
|
||||
<nav aria-label="后台导航"><a aria-current="page" href="/admin/users">用户</a><a href="/admin/audit">审计</a></nav>
|
||||
<nav aria-label="后台导航"><a aria-current="page" href="/admin/users">用户</a><a href="/admin/models">模型</a><a href="/admin/audit">审计</a></nav>
|
||||
</header>
|
||||
<main>
|
||||
<header className="admin-users-heading">
|
||||
|
||||
@@ -190,6 +190,66 @@ export async function getGeneration(options: ClientOptions = {}): Promise<Genera
|
||||
return response.json() as Promise<GenerationTaskResponse>;
|
||||
}
|
||||
|
||||
export async function getModel(options: ClientOptions = {}): Promise<{
|
||||
"config_version": number;
|
||||
"contract_evidence_ref": string | null;
|
||||
"contract_validation_status": ModelContractValidationStatus;
|
||||
"credit_cost": number;
|
||||
"display_name": string;
|
||||
"enabled": boolean;
|
||||
"error_mapping_profile": Record<string, never>;
|
||||
"gateway_account_ref": string;
|
||||
"is_default": boolean;
|
||||
"model_id": ModelId;
|
||||
"prompt_max_length": number;
|
||||
"recommendation_priority": number;
|
||||
"reference_limits": ModelReferenceLimits;
|
||||
"route_profile": Record<string, never>;
|
||||
"runtime_availability": ModelRuntimeAvailability;
|
||||
"safety_source": string;
|
||||
"supported_ratios": Array<string>;
|
||||
}> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/models/{model_id}`, { method: "GET", headers: options.headers ?? {} });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<{
|
||||
"config_version": number;
|
||||
"contract_evidence_ref": string | null;
|
||||
"contract_validation_status": ModelContractValidationStatus;
|
||||
"credit_cost": number;
|
||||
"display_name": string;
|
||||
"enabled": boolean;
|
||||
"error_mapping_profile": Record<string, never>;
|
||||
"gateway_account_ref": string;
|
||||
"is_default": boolean;
|
||||
"model_id": ModelId;
|
||||
"prompt_max_length": number;
|
||||
"recommendation_priority": number;
|
||||
"reference_limits": ModelReferenceLimits;
|
||||
"route_profile": Record<string, never>;
|
||||
"runtime_availability": ModelRuntimeAvailability;
|
||||
"safety_source": string;
|
||||
"supported_ratios": Array<string>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function getModels(options: ClientOptions = {}): Promise<{
|
||||
"config_set_version": number;
|
||||
"configured_default_model_id": ModelId;
|
||||
"models": Array<ModelConfig>;
|
||||
"recommended_model_id": ModelId | null;
|
||||
}> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/models`, { method: "GET", headers: options.headers ?? {} });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<{
|
||||
"config_set_version": number;
|
||||
"configured_default_model_id": ModelId;
|
||||
"models": Array<ModelConfig>;
|
||||
"recommended_model_id": ModelId | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function getMyCreditLedger(options: ClientOptions = {}): Promise<CreditLedgerResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/me/credit-ledger`, { method: "GET", headers: options.headers ?? {} });
|
||||
@@ -248,6 +308,28 @@ export async function renameProject(body: ProjectRenameRequest, options: ClientO
|
||||
return response.json() as Promise<ProjectRenameResponse>;
|
||||
}
|
||||
|
||||
export async function replaceModelConfiguration(body: {
|
||||
"expected_config_set_version": number;
|
||||
"models": Array<ModelConfigCandidate>;
|
||||
}, options: ClientOptions = {}): Promise<{
|
||||
"config_set_version": number;
|
||||
"configured_default_model_id": ModelId;
|
||||
"models": Array<ModelConfig>;
|
||||
"recommended_model_id": ModelId | null;
|
||||
}> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set("Content-Type", "application/json");
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/admin/models/configuration`, { body: JSON.stringify(body), method: "PUT", headers });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json() as Promise<{
|
||||
"config_set_version": number;
|
||||
"configured_default_model_id": ModelId;
|
||||
"models": Array<ModelConfig>;
|
||||
"recommended_model_id": ModelId | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function restoreProject(options: ClientOptions = {}): Promise<ProjectRestoreResponse> {
|
||||
const request = options.fetch ?? globalThis.fetch;
|
||||
const response = await request(`${options.baseUrl ?? ""}/api/v1/projects/{projectId}/restore`, { method: "POST", headers: options.headers ?? {} });
|
||||
|
||||
@@ -273,6 +273,7 @@ export type CsrfHeaders = {
|
||||
|
||||
export type ErrorDetails = {
|
||||
"capacity_status"?: "normal" | "warning" | "critical" | "full" | "unavailable";
|
||||
"conflict_model_ids"?: Array<string>;
|
||||
"current_task_ref"?: string;
|
||||
"field_errors"?: Array<{
|
||||
"field": string;
|
||||
@@ -293,6 +294,7 @@ export type ErrorEnvelope = {
|
||||
"correlation_id": string;
|
||||
"details": {
|
||||
"capacity_status"?: "normal" | "warning" | "critical" | "full" | "unavailable";
|
||||
"conflict_model_ids"?: Array<string>;
|
||||
"current_task_ref"?: string;
|
||||
"field_errors"?: Array<{
|
||||
"field": string;
|
||||
@@ -448,6 +450,42 @@ export type LogoutResponse = {
|
||||
"status": "logged_out";
|
||||
};
|
||||
|
||||
export type ModelConfig = {
|
||||
"config_version": number;
|
||||
"contract_evidence_ref": string | null;
|
||||
"contract_validation_status": ModelContractValidationStatus;
|
||||
"credit_cost": number;
|
||||
"display_name": string;
|
||||
"enabled": boolean;
|
||||
"error_mapping_profile": Record<string, never>;
|
||||
"gateway_account_ref": string;
|
||||
"is_default": boolean;
|
||||
"model_id": ModelId;
|
||||
"prompt_max_length": number;
|
||||
"recommendation_priority": number;
|
||||
"reference_limits": ModelReferenceLimits;
|
||||
"route_profile": Record<string, never>;
|
||||
"runtime_availability": ModelRuntimeAvailability;
|
||||
"safety_source": string;
|
||||
"supported_ratios": Array<string>;
|
||||
};
|
||||
|
||||
export type ModelConfigCandidate = {
|
||||
"credit_cost": number;
|
||||
"display_name": string;
|
||||
"enabled": boolean;
|
||||
"error_mapping_profile": Record<string, never>;
|
||||
"gateway_account_ref": string;
|
||||
"is_default": boolean;
|
||||
"model_id": ModelId;
|
||||
"prompt_max_length": number;
|
||||
"recommendation_priority": number;
|
||||
"reference_limits": ModelReferenceLimits;
|
||||
"route_profile": Record<string, never>;
|
||||
"safety_source": string;
|
||||
"supported_ratios": Array<string>;
|
||||
};
|
||||
|
||||
export type ModelConfigSseEvent = {
|
||||
"config_set_version": number;
|
||||
"entity_ref": string;
|
||||
@@ -456,6 +494,45 @@ export type ModelConfigSseEvent = {
|
||||
"occurred_at": string;
|
||||
};
|
||||
|
||||
export type ModelConfigUpdateHeaders = {
|
||||
"idempotency-key": string;
|
||||
"x-csrf-token": string;
|
||||
};
|
||||
|
||||
export type ModelConfigUpdateRequest = {
|
||||
"expected_config_set_version": number;
|
||||
"models": Array<ModelConfigCandidate>;
|
||||
};
|
||||
|
||||
export type ModelConfigurationResponse = {
|
||||
"config_set_version": number;
|
||||
"configured_default_model_id": ModelId;
|
||||
"models": Array<ModelConfig>;
|
||||
"recommended_model_id": ModelId | null;
|
||||
};
|
||||
|
||||
export type ModelContractValidationStatus = "blocked" | "unverified" | "verified";
|
||||
|
||||
export type ModelId = string;
|
||||
|
||||
export type ModelParams = {
|
||||
"model_id": ModelId;
|
||||
};
|
||||
|
||||
export type ModelReferenceLimits = {
|
||||
"max_file_bytes": number;
|
||||
"max_files": number;
|
||||
"max_total_bytes": number;
|
||||
};
|
||||
|
||||
export type ModelRuntimeAvailability = {
|
||||
"available_for_new_jobs": boolean;
|
||||
"checked_at": string;
|
||||
"reason": ModelRuntimeReason;
|
||||
};
|
||||
|
||||
export type ModelRuntimeReason = "available" | "configured_disabled" | "contract_unverified" | "contract_blocked" | "gateway_balance_insufficient" | "gateway_paused" | "worker_degraded";
|
||||
|
||||
export type ModelRuntimeSseEvent = {
|
||||
"entity_ref": string;
|
||||
"event_id": number;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AdminAuthPage } from "./admin-auth.js";
|
||||
import { UserAuthPage } from "./user-auth.js";
|
||||
import { AccountSettingsPage } from "./account-settings.js";
|
||||
import { AdminUsersPage } from "./admin-users.js";
|
||||
import { AdminModelsPage } from "./admin-models.js";
|
||||
import { CreditsPage } from "./credits-page.js";
|
||||
import { ProjectDetailPage, ProjectsPage, WorkspacePage } from "./project-pages.js";
|
||||
|
||||
@@ -31,6 +32,7 @@ function renderAuthenticationEntry() {
|
||||
else if (window.location.pathname === "/app/projects") authenticationPage = <ProjectsPage key={authRevision} />;
|
||||
else if (window.location.pathname === "/app") authenticationPage = <WorkspacePage key={authRevision} />;
|
||||
else if (window.location.pathname === "/admin/users") authenticationPage = <AdminUsersPage key={authRevision} />;
|
||||
else if (window.location.pathname === "/admin/models") authenticationPage = <AdminModelsPage key={authRevision} />;
|
||||
else if (window.location.pathname.startsWith("/admin")) authenticationPage = <AdminAuthPage key={authRevision} />;
|
||||
else authenticationPage = <UserAuthPage key={authRevision} />;
|
||||
appRoot.render(
|
||||
|
||||
Reference in New Issue
Block a user