feat: drop auxiliary model from thread selection and registry defaults
ThreadModelSelection is now "inherit" or { primary, reasoning_effort };
the BFF validator tolerates and drops legacy auxiliary keys, the runs
route no longer snapshots an auxiliary ref, and the Registry Editor's
default-auxiliary field is removed, matching the backend's single-role
snapshot resolution.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,10 @@ EVOSCIENTIST_BACKEND_URL=http://127.0.0.1:6174
|
||||
# Deployment workspace root. This is not a browser-selectable directory and is
|
||||
# shared with the trusted EvoScientist backend. Defaults to ~/.evoscientist/workspace.
|
||||
EVOSCIENTIST_WORKSPACE_DIR=
|
||||
# Deployment id, matching the backend's EVOSCIENTIST_DEPLOYMENT_ID. Only needed
|
||||
# when the backend was started manually (no deploy sidecar); otherwise the
|
||||
# sidecar value wins. Defaults to a uuid derived from the workspace root.
|
||||
EVOSCIENTIST_DEPLOYMENT_ID=
|
||||
# Optional for local processes running as the same OS user: both sides share
|
||||
# ~/.config/evoscientist/provider-admin-token automatically. Set this explicitly
|
||||
# when the WebUI and backend run on different hosts, users, or containers.
|
||||
|
||||
@@ -224,8 +224,10 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
||||
model_selection_revision: selection.revision,
|
||||
primary:
|
||||
selectionRef === "inherit" ? null : selectionRef.primary,
|
||||
auxiliary:
|
||||
selectionRef === "inherit" ? null : selectionRef.auxiliary,
|
||||
reasoning_effort:
|
||||
selectionRef === "inherit"
|
||||
? null
|
||||
: (selectionRef.reasoning_effort ?? null),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -101,7 +101,7 @@ import { WorkspaceFileDialog } from "@/app/components/WorkspaceFileDialog";
|
||||
import { MemoryFileDialog } from "@/app/components/MemoryFileDialog";
|
||||
import { FILE_LINK_EVENT, type FileLinkEventDetail } from "@/lib/fileLink";
|
||||
import { notifyWorkspaceChanged } from "@/lib/workspaceEvents";
|
||||
import type { ModelRef } from "@/lib/modelRegistry";
|
||||
import type { ModelRef, ReasoningEffort } from "@/lib/modelRegistry";
|
||||
import {
|
||||
modelRefLabel,
|
||||
useAvailableModels,
|
||||
@@ -446,6 +446,7 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
|
||||
}, [modelPickerOpen]);
|
||||
const {
|
||||
models: selectableModels,
|
||||
defaults: registryDefaults,
|
||||
loading: modelRegistryLoading,
|
||||
error: modelRegistryError,
|
||||
} = useAvailableModels();
|
||||
@@ -709,7 +710,8 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
|
||||
// 1. Explicit per-thread selection (ModelRef picker) — the next run
|
||||
// freezes exactly this into its runtime snapshot.
|
||||
// 2. `inherit` — the registry default resolved server-side at snapshot
|
||||
// creation; the pill shows the last observed model as a hint.
|
||||
// creation; the pill shows the last observed model as a hint, or the
|
||||
// registry default when no turn has run yet.
|
||||
// Token/context usage is intentionally NOT shown — the backend doesn't
|
||||
// persist usage_metadata, so it isn't reliably available here.
|
||||
const currentModel = useMemo(() => {
|
||||
@@ -734,8 +736,22 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
|
||||
);
|
||||
if (info) return info;
|
||||
}
|
||||
// Fresh thread on `inherit`: show the registry default so the picker
|
||||
// entry point exists before the first turn (the run would use it).
|
||||
const fallbackRef = registryDefaults?.primary ?? null;
|
||||
if (fallbackRef) {
|
||||
const match = selectableModels.find(
|
||||
(m) =>
|
||||
m.model_ref.provider_id === fallbackRef.provider_id &&
|
||||
m.model_ref.model_key === fallbackRef.model_key
|
||||
);
|
||||
return {
|
||||
name: match?.name ?? fallbackRef.model_key,
|
||||
provider: match?.provider_name || fallbackRef.provider_id,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [messages, modelSelection, selectableModels]);
|
||||
}, [messages, modelSelection, selectableModels, registryDefaults]);
|
||||
|
||||
// Bind captured sub-agent activity (keyed by subgraph namespace) to each task
|
||||
// tool call → its live steps. B': match a finished sub-agent to a task by its
|
||||
@@ -1626,7 +1642,6 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
|
||||
try {
|
||||
await setModelSelection({
|
||||
primary: ref,
|
||||
auxiliary: null,
|
||||
});
|
||||
toast.success(`Model set to ${modelKey}.`);
|
||||
setModelPickerOpen(false);
|
||||
@@ -2215,6 +2230,36 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{currentModel && (
|
||||
<ReasoningEffortSlider
|
||||
disabled={modelSelection === "inherit"}
|
||||
value={
|
||||
modelSelection === "inherit"
|
||||
? null
|
||||
: (modelSelection.reasoning_effort ?? null)
|
||||
}
|
||||
onCommit={async (effort) => {
|
||||
if (modelSelection === "inherit") return;
|
||||
try {
|
||||
await setModelSelection({
|
||||
primary: modelSelection.primary,
|
||||
reasoning_effort: effort,
|
||||
});
|
||||
toast.success(
|
||||
effort === null
|
||||
? "Reasoning effort back to the registry default."
|
||||
: `Reasoning effort set to ${effort}.`
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? `Couldn't update reasoning effort: ${err.message}`
|
||||
: "Couldn't update reasoning effort — try again."
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{threadId && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -2433,3 +2478,80 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
|
||||
);
|
||||
|
||||
ChatInterface.displayName = "ChatInterface";
|
||||
|
||||
const EFFORT_LEVELS: ReadonlyArray<ReasoningEffort> = [
|
||||
"auto",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
];
|
||||
|
||||
function ReasoningEffortSlider({
|
||||
disabled,
|
||||
value,
|
||||
onCommit,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
value: ReasoningEffort | null;
|
||||
onCommit: (effort: ReasoningEffort | null) => void | Promise<void>;
|
||||
}) {
|
||||
const committedIndex =
|
||||
value && value !== "auto" ? EFFORT_LEVELS.indexOf(value) : 0;
|
||||
const [draft, setDraft] = useState<number | null>(null);
|
||||
const shownIndex = draft ?? committedIndex;
|
||||
const shown = EFFORT_LEVELS[shownIndex];
|
||||
|
||||
const commit = (index: number) => {
|
||||
setDraft(null);
|
||||
if (index !== committedIndex) {
|
||||
void onCommit(index === 0 ? null : EFFORT_LEVELS[index]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("flex items-center gap-1.5", disabled && "opacity-50")}
|
||||
title={
|
||||
disabled
|
||||
? "Pick a model to override the registry-configured effort"
|
||||
: "Reasoning effort for the primary model"
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
aria-label="Reasoning effort"
|
||||
aria-valuetext={shown}
|
||||
disabled={disabled}
|
||||
min={0}
|
||||
max={EFFORT_LEVELS.length - 1}
|
||||
step={1}
|
||||
value={shownIndex}
|
||||
onChange={(event) => setDraft(Number(event.target.value))}
|
||||
onPointerUp={(event) =>
|
||||
commit(Number((event.target as HTMLInputElement).value))
|
||||
}
|
||||
onKeyUp={(event) => {
|
||||
if (
|
||||
event.key.startsWith("Arrow") ||
|
||||
event.key === "Home" ||
|
||||
event.key === "End"
|
||||
) {
|
||||
commit(Number((event.target as HTMLInputElement).value));
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => commit(Number(event.target.value))}
|
||||
className="w-20 cursor-pointer accent-[var(--brand)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"w-11 text-xs capitalize",
|
||||
shownIndex === 0
|
||||
? "text-muted-foreground"
|
||||
: "font-medium text-foreground"
|
||||
)}
|
||||
>
|
||||
{shown}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1001,9 +1001,6 @@ export function RegistryEditor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAffected(next.defaults.auxiliary)) {
|
||||
next.defaults.auxiliary = null;
|
||||
}
|
||||
if (isAffected(next.defaults.primary)) {
|
||||
const replacement = enabledModelRefs(next)[0]?.ref ?? null;
|
||||
if (!replacement) {
|
||||
@@ -1204,30 +1201,6 @@ export function RegistryEditor() {
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Default auxiliary model">
|
||||
<select
|
||||
className={inputClass}
|
||||
value={
|
||||
draft.defaults.auxiliary ? modelRefKey(draft.defaults.auxiliary) : ""
|
||||
}
|
||||
onChange={(event) =>
|
||||
setDraft({
|
||||
...draft,
|
||||
defaults: {
|
||||
...draft.defaults,
|
||||
auxiliary: refFromKey(event.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">— none (uses primary) —</option>
|
||||
{defaultOptions.map((option) => (
|
||||
<option key={modelRefKey(option.ref)} value={modelRefKey(option.ref)}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[14rem_1fr]">
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ModelRef, SelectableModel } from "@/lib/modelRegistry";
|
||||
import type {
|
||||
ModelRef,
|
||||
RegistryDefaults,
|
||||
SelectableModel,
|
||||
} from "@/lib/modelRegistry";
|
||||
|
||||
const EMPTY: SelectableModel[] = [];
|
||||
|
||||
let cachedModels: Promise<SelectableModel[]> | null = null;
|
||||
interface AvailableModelsResult {
|
||||
models: SelectableModel[];
|
||||
defaults: RegistryDefaults | null;
|
||||
}
|
||||
|
||||
let cachedModels: Promise<AvailableModelsResult> | null = null;
|
||||
const MODELS_CHANGED_EVENT = "evoscientist-models-changed";
|
||||
|
||||
export function invalidateAvailableModels(): void {
|
||||
@@ -56,20 +65,32 @@ function parseSelectableModel(raw: unknown): SelectableModel | null {
|
||||
};
|
||||
}
|
||||
|
||||
function fetchSelectableModels(): Promise<SelectableModel[]> {
|
||||
function parseDefaults(raw: unknown): RegistryDefaults | null {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const d = raw as { primary?: unknown };
|
||||
return {
|
||||
primary: isModelRef(d.primary) ? d.primary : null,
|
||||
};
|
||||
}
|
||||
|
||||
function fetchSelectableModels(): Promise<AvailableModelsResult> {
|
||||
if (cachedModels) return cachedModels;
|
||||
|
||||
const pending = fetch("/api/models")
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const body = (await response.json()) as { models?: unknown };
|
||||
if (!Array.isArray(body.models)) return EMPTY;
|
||||
const body = (await response.json()) as {
|
||||
models?: unknown;
|
||||
defaults?: unknown;
|
||||
};
|
||||
const models: SelectableModel[] = [];
|
||||
for (const raw of body.models) {
|
||||
const parsed = parseSelectableModel(raw);
|
||||
if (parsed) models.push(parsed);
|
||||
if (Array.isArray(body.models)) {
|
||||
for (const raw of body.models) {
|
||||
const parsed = parseSelectableModel(raw);
|
||||
if (parsed) models.push(parsed);
|
||||
}
|
||||
}
|
||||
return models;
|
||||
return { models, defaults: parseDefaults(body.defaults) };
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
cachedModels = null;
|
||||
@@ -86,10 +107,12 @@ function fetchSelectableModels(): Promise<SelectableModel[]> {
|
||||
* mount retries. There is no hard-coded fallback list. */
|
||||
export function useAvailableModels(): {
|
||||
models: SelectableModel[];
|
||||
defaults: RegistryDefaults | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
} {
|
||||
const [models, setModels] = useState<SelectableModel[]>(EMPTY);
|
||||
const [defaults, setDefaults] = useState<RegistryDefaults | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshRevision, setRefreshRevision] = useState(0);
|
||||
@@ -107,13 +130,15 @@ export function useAvailableModels(): {
|
||||
fetchSelectableModels()
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
setModels(result);
|
||||
setModels(result.models);
|
||||
setDefaults(result.defaults);
|
||||
setError(null);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (cancelled) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to load models.");
|
||||
setModels(EMPTY);
|
||||
setDefaults(null);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -124,7 +149,7 @@ export function useAvailableModels(): {
|
||||
};
|
||||
}, [refreshRevision]);
|
||||
|
||||
return { models, loading, error };
|
||||
return { models, defaults, loading, error };
|
||||
}
|
||||
|
||||
export function modelRefLabel(ref: ModelRef): string {
|
||||
|
||||
@@ -170,18 +170,20 @@ function readSelectionFromMetadata(metadata: Record<string, unknown> | undefined
|
||||
return { selection: "inherit", revision };
|
||||
}
|
||||
if (raw && typeof raw === "object") {
|
||||
const candidate = raw as { primary?: unknown; auxiliary?: unknown };
|
||||
if (
|
||||
isModelRefShape(candidate.primary) &&
|
||||
(candidate.auxiliary === null || candidate.auxiliary === undefined ||
|
||||
isModelRefShape(candidate.auxiliary))
|
||||
) {
|
||||
// A legacy `auxiliary` key from pre-6.1 metadata is tolerated and ignored.
|
||||
const candidate = raw as {
|
||||
primary?: unknown;
|
||||
reasoning_effort?: unknown;
|
||||
};
|
||||
if (isModelRefShape(candidate.primary)) {
|
||||
const effort = candidate.reasoning_effort;
|
||||
return {
|
||||
selection: {
|
||||
primary: candidate.primary,
|
||||
auxiliary: isModelRefShape(candidate.auxiliary)
|
||||
? candidate.auxiliary
|
||||
: null,
|
||||
reasoning_effort:
|
||||
effort === "low" || effort === "medium" || effort === "high"
|
||||
? effort
|
||||
: null,
|
||||
},
|
||||
revision,
|
||||
};
|
||||
@@ -619,6 +621,21 @@ export function useChat({
|
||||
createdThreadIdRef.current = createdThreadId;
|
||||
threadIdRef.current = createdThreadId;
|
||||
setFetchedThreadId(createdThreadId);
|
||||
// Flush a staged pre-thread pick before returning: the caller creates
|
||||
// the first run immediately after, and the run snapshot freezes
|
||||
// whatever selection the thread metadata holds at that moment.
|
||||
const pending = pendingSelectionRef.current;
|
||||
if (pending) {
|
||||
pendingSelectionRef.current = null;
|
||||
createdThreadIdRef.current = null;
|
||||
try {
|
||||
await setThreadModelSelection(createdThreadId, pending, 0);
|
||||
setModelSelectionState(pending);
|
||||
setSelectionRevision(1);
|
||||
} catch {
|
||||
// The thread-id effect resyncs from metadata.
|
||||
}
|
||||
}
|
||||
await setThreadId(createdThreadId);
|
||||
return createdThreadId;
|
||||
})();
|
||||
|
||||
@@ -72,7 +72,6 @@ export interface ProviderConfig {
|
||||
|
||||
export interface RegistryDefaults {
|
||||
primary: ModelRef | null;
|
||||
auxiliary: ModelRef | null;
|
||||
}
|
||||
|
||||
export interface RegistryV4 {
|
||||
@@ -190,12 +189,17 @@ export interface SelectableModel {
|
||||
|
||||
export interface GetSelectableModelsResponse {
|
||||
models: SelectableModel[];
|
||||
defaults?: RegistryDefaults;
|
||||
}
|
||||
|
||||
/** ThreadModelSelection (design doc 7.2). */
|
||||
/** ThreadModelSelection (design doc 7.2). The optional reasoning_effort is a
|
||||
* per-thread override frozen into the run snapshot for the primary role. */
|
||||
export type ThreadModelSelection =
|
||||
| "inherit"
|
||||
| { primary: ModelRef; auxiliary: ModelRef | null };
|
||||
| {
|
||||
primary: ModelRef;
|
||||
reasoning_effort?: ReasoningEffort | null;
|
||||
};
|
||||
|
||||
export interface RegistryApiError {
|
||||
code?: string;
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("modelsNeedingReverify", () => {
|
||||
version: 4,
|
||||
revision: 1,
|
||||
state: "active",
|
||||
defaults: { primary: null, auxiliary: null },
|
||||
defaults: { primary: null },
|
||||
providers: [provider],
|
||||
} as unknown as RegistryV4;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,8 @@ export async function getActiveDeployment(): Promise<ActiveDeployment> {
|
||||
const deploymentId =
|
||||
typeof sidecarDeploymentId === "string" && sidecarDeploymentId
|
||||
? sidecarDeploymentId
|
||||
: fallbackDeploymentId(workspaceRoot);
|
||||
: process.env.EVOSCIENTIST_DEPLOYMENT_ID?.trim() ||
|
||||
fallbackDeploymentId(workspaceRoot);
|
||||
const apiUrl =
|
||||
typeof sidecar?.api_url === "string" && sidecar.api_url
|
||||
? sidecar.api_url
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseThreadModelSelection } from "@/lib/server/threadModelSelection";
|
||||
|
||||
const REF = { provider_id: "glm", model_key: "glm-5.2" };
|
||||
|
||||
describe("parseThreadModelSelection", () => {
|
||||
it("passes through inherit", () => {
|
||||
expect(parseThreadModelSelection("inherit")).toBe("inherit");
|
||||
});
|
||||
|
||||
it("keeps an explicit reasoning effort", () => {
|
||||
expect(
|
||||
parseThreadModelSelection({
|
||||
primary: REF,
|
||||
reasoning_effort: "high",
|
||||
})
|
||||
).toEqual({ primary: REF, reasoning_effort: "high" });
|
||||
});
|
||||
|
||||
it("normalizes auto and missing effort to null", () => {
|
||||
expect(
|
||||
parseThreadModelSelection({
|
||||
primary: REF,
|
||||
reasoning_effort: "auto",
|
||||
})
|
||||
).toEqual({ primary: REF, reasoning_effort: null });
|
||||
expect(parseThreadModelSelection({ primary: REF })).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("tolerates and drops a legacy auxiliary key", () => {
|
||||
expect(
|
||||
parseThreadModelSelection({
|
||||
primary: REF,
|
||||
auxiliary: { provider_id: "ollama", model_key: "qwen3" },
|
||||
reasoning_effort: "low",
|
||||
})
|
||||
).toEqual({ primary: REF, reasoning_effort: "low" });
|
||||
});
|
||||
|
||||
it("rejects an unknown effort value", () => {
|
||||
expect(() =>
|
||||
parseThreadModelSelection({
|
||||
primary: REF,
|
||||
reasoning_effort: "extreme",
|
||||
})
|
||||
).toThrowError(/model_selection must be/);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { ModelRef, ThreadModelSelection } from "@/lib/modelRegistry";
|
||||
import type {
|
||||
ModelRef,
|
||||
ReasoningEffort,
|
||||
ThreadModelSelection,
|
||||
} from "@/lib/modelRegistry";
|
||||
|
||||
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
||||
const EFFORTS = new Set<ReasoningEffort>(["auto", "low", "medium", "high"]);
|
||||
|
||||
export class ThreadModelSelectionError extends Error {
|
||||
readonly code: string;
|
||||
@@ -24,28 +29,32 @@ function isModelRef(value: unknown): value is ModelRef {
|
||||
);
|
||||
}
|
||||
|
||||
/** Validates a browser-supplied `model_selection` payload (design doc 7.2). */
|
||||
/** Validates a browser-supplied `model_selection` payload (design doc 7.2).
|
||||
* `reasoning_effort` is optional; "auto" is normalized away so toggling the
|
||||
* picker back to Auto keeps the selection (and its snapshot hash) stable. A
|
||||
* legacy `auxiliary` key from pre-6.1 metadata is tolerated and dropped. */
|
||||
export function parseThreadModelSelection(value: unknown): ThreadModelSelection {
|
||||
if (value === "inherit") return "inherit";
|
||||
if (
|
||||
isPlainObject(value) &&
|
||||
isModelRef(value.primary) &&
|
||||
(value.auxiliary === null || isModelRef(value.auxiliary))
|
||||
(value.reasoning_effort === undefined ||
|
||||
value.reasoning_effort === null ||
|
||||
(typeof value.reasoning_effort === "string" &&
|
||||
EFFORTS.has(value.reasoning_effort as ReasoningEffort)))
|
||||
) {
|
||||
const effort = value.reasoning_effort;
|
||||
return {
|
||||
primary: { provider_id: value.primary.provider_id, model_key: value.primary.model_key },
|
||||
auxiliary:
|
||||
value.auxiliary === null
|
||||
? null
|
||||
: {
|
||||
provider_id: value.auxiliary.provider_id,
|
||||
model_key: value.auxiliary.model_key,
|
||||
},
|
||||
reasoning_effort:
|
||||
typeof effort === "string" && effort !== "auto"
|
||||
? (effort as ReasoningEffort)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
throw new ThreadModelSelectionError(
|
||||
"INVALID_REQUEST",
|
||||
"model_selection must be \"inherit\" or { primary: ModelRef, auxiliary: ModelRef | null }."
|
||||
"model_selection must be \"inherit\" or { primary: ModelRef, reasoning_effort?: \"low\" | \"medium\" | \"high\" | null }."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user