feat(webui): accept temperature/top_p in thread model selection
This commit is contained in:
@@ -174,6 +174,8 @@ function readSelectionFromMetadata(metadata: Record<string, unknown> | undefined
|
||||
const candidate = raw as {
|
||||
primary?: unknown;
|
||||
reasoning_effort?: unknown;
|
||||
temperature?: unknown;
|
||||
top_p?: unknown;
|
||||
};
|
||||
if (isModelRefShape(candidate.primary)) {
|
||||
const effort = candidate.reasoning_effort;
|
||||
@@ -184,6 +186,16 @@ function readSelectionFromMetadata(metadata: Record<string, unknown> | undefined
|
||||
effort === "low" || effort === "medium" || effort === "high"
|
||||
? effort
|
||||
: null,
|
||||
temperature:
|
||||
typeof candidate.temperature === "number" &&
|
||||
Number.isFinite(candidate.temperature)
|
||||
? candidate.temperature
|
||||
: null,
|
||||
top_p:
|
||||
typeof candidate.top_p === "number" &&
|
||||
Number.isFinite(candidate.top_p)
|
||||
? candidate.top_p
|
||||
: null,
|
||||
},
|
||||
revision,
|
||||
};
|
||||
|
||||
@@ -192,13 +192,16 @@ export interface GetSelectableModelsResponse {
|
||||
defaults?: RegistryDefaults;
|
||||
}
|
||||
|
||||
/** ThreadModelSelection (design doc 7.2). The optional reasoning_effort is a
|
||||
* per-thread override frozen into the run snapshot for the primary role. */
|
||||
/** ThreadModelSelection (design doc 7.2). The optional reasoning_effort,
|
||||
* temperature, and top_p are per-thread overrides frozen into the run
|
||||
* snapshot for the primary role; null means the registry default. */
|
||||
export type ThreadModelSelection =
|
||||
| "inherit"
|
||||
| {
|
||||
primary: ModelRef;
|
||||
reasoning_effort?: ReasoningEffort | null;
|
||||
temperature?: number | null;
|
||||
top_p?: number | null;
|
||||
};
|
||||
|
||||
export interface RegistryApiError {
|
||||
|
||||
@@ -14,7 +14,12 @@ describe("parseThreadModelSelection", () => {
|
||||
primary: REF,
|
||||
reasoning_effort: "high",
|
||||
})
|
||||
).toEqual({ primary: REF, reasoning_effort: "high" });
|
||||
).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: "high",
|
||||
temperature: null,
|
||||
top_p: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes auto and missing effort to null", () => {
|
||||
@@ -23,10 +28,17 @@ describe("parseThreadModelSelection", () => {
|
||||
primary: REF,
|
||||
reasoning_effort: "auto",
|
||||
})
|
||||
).toEqual({ primary: REF, reasoning_effort: null });
|
||||
).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: null,
|
||||
temperature: null,
|
||||
top_p: null,
|
||||
});
|
||||
expect(parseThreadModelSelection({ primary: REF })).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: null,
|
||||
temperature: null,
|
||||
top_p: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +49,47 @@ describe("parseThreadModelSelection", () => {
|
||||
auxiliary: { provider_id: "ollama", model_key: "qwen3" },
|
||||
reasoning_effort: "low",
|
||||
})
|
||||
).toEqual({ primary: REF, reasoning_effort: "low" });
|
||||
).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: "low",
|
||||
temperature: null,
|
||||
top_p: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps explicit temperature/top_p overrides", () => {
|
||||
expect(
|
||||
parseThreadModelSelection({ primary: REF, temperature: 1.2, top_p: 0.5 })
|
||||
).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: null,
|
||||
temperature: 1.2,
|
||||
top_p: 0.5,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes missing generation overrides to null", () => {
|
||||
expect(parseThreadModelSelection({ primary: REF })).toEqual({
|
||||
primary: REF,
|
||||
reasoning_effort: null,
|
||||
temperature: null,
|
||||
top_p: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects out-of-range or non-finite generation overrides", () => {
|
||||
for (const bad of [
|
||||
{ primary: REF, temperature: -0.1 },
|
||||
{ primary: REF, temperature: 2.1 },
|
||||
{ primary: REF, temperature: Number.NaN },
|
||||
{ primary: REF, top_p: 0 },
|
||||
{ primary: REF, top_p: 1.01 },
|
||||
{ primary: REF, top_p: "high" },
|
||||
]) {
|
||||
expect(() => parseThreadModelSelection(bad)).toThrowError(
|
||||
/model_selection must be/
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an unknown effort value", () => {
|
||||
|
||||
@@ -29,10 +29,25 @@ function isModelRef(value: unknown): value is ModelRef {
|
||||
);
|
||||
}
|
||||
|
||||
function isTemperature(value: unknown): boolean {
|
||||
return (
|
||||
typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 2
|
||||
);
|
||||
}
|
||||
|
||||
function isTopP(value: unknown): boolean {
|
||||
return (
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 1
|
||||
);
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
* picker back to Auto keeps the selection (and its snapshot hash) stable.
|
||||
* `temperature`/`top_p` are optional per-thread overrides, shape-checked
|
||||
* here (0–2 / (0,1]); model-level contracts are enforced server-side when
|
||||
* the run snapshot is created. 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 (
|
||||
@@ -41,7 +56,11 @@ export function parseThreadModelSelection(value: unknown): ThreadModelSelection
|
||||
(value.reasoning_effort === undefined ||
|
||||
value.reasoning_effort === null ||
|
||||
(typeof value.reasoning_effort === "string" &&
|
||||
EFFORTS.has(value.reasoning_effort as ReasoningEffort)))
|
||||
EFFORTS.has(value.reasoning_effort as ReasoningEffort))) &&
|
||||
(value.temperature === undefined ||
|
||||
value.temperature === null ||
|
||||
isTemperature(value.temperature)) &&
|
||||
(value.top_p === undefined || value.top_p === null || isTopP(value.top_p))
|
||||
) {
|
||||
const effort = value.reasoning_effort;
|
||||
return {
|
||||
@@ -50,11 +69,14 @@ export function parseThreadModelSelection(value: unknown): ThreadModelSelection
|
||||
typeof effort === "string" && effort !== "auto"
|
||||
? (effort as ReasoningEffort)
|
||||
: null,
|
||||
temperature:
|
||||
typeof value.temperature === "number" ? value.temperature : null,
|
||||
top_p: typeof value.top_p === "number" ? value.top_p : null,
|
||||
};
|
||||
}
|
||||
throw new ThreadModelSelectionError(
|
||||
"INVALID_REQUEST",
|
||||
"model_selection must be \"inherit\" or { primary: ModelRef, reasoning_effort?: \"low\" | \"medium\" | \"high\" | null }."
|
||||
"model_selection must be \"inherit\" or { primary: ModelRef, reasoning_effort?: \"low\" | \"medium\" | \"high\" | null, temperature?: number | null, top_p?: number | null }."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user