feat(recipe-studio): add support for inference_extra_body configuration with collapsible UI and enhanced validation logic

This commit is contained in:
Shine1i 2026-03-05 23:33:48 +01:00
commit cf8cb9109b
7 changed files with 94 additions and 36 deletions

View file

@ -100,36 +100,37 @@ function normalizeLintDiagnostic(diagnostic) {
if (!diagnostic || typeof diagnostic !== "object") {
return null;
}
const readString = (value) =>
typeof value === "string" ? value : null;
const readInt = (value) =>
Number.isInteger(value) ? value : null;
const asObject = (value) =>
value && typeof value === "object" ? value : null;
const message = String(diagnostic.message || "").trim();
if (!message) {
return null;
}
const severityRaw = String(diagnostic.severity || "").trim().toLowerCase();
const severity = severityRaw === "error" ? "error" : "warning";
const labels = Array.isArray(diagnostic.labels)
? diagnostic.labels.map((label) => {
const span = label && typeof label === "object" ? label.span : null;
const start =
span && typeof span === "object" && Number.isInteger(span.offset)
? span.offset
: null;
const length =
span && typeof span === "object" && Number.isInteger(span.length)
? span.length
: null;
return {
message:
label && typeof label === "object" && typeof label.label === "string"
? label.label
: null,
start,
end:
start !== null && length !== null
? start + length
: null,
};
})
: [];
const labels = [];
if (Array.isArray(diagnostic.labels)) {
for (const label of diagnostic.labels) {
const labelObj = asObject(label);
const span = asObject(labelObj?.span);
const start = readInt(span?.offset);
const length = readInt(span?.length);
labels.push({
message: readString(labelObj?.label),
start,
end: start !== null && length !== null ? start + length : null,
});
}
}
const code = typeof diagnostic.code === "string" ? diagnostic.code : null;
return {
message: code ? `${code}: ${message}` : message,

View file

@ -1,3 +1,8 @@
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Checkbox } from "@/components/ui/checkbox";
import {
Combobox,
@ -8,7 +13,8 @@ import {
ComboboxList,
} from "@/components/ui/combobox";
import { Input } from "@/components/ui/input";
import { type ReactElement, useRef } from "react";
import { Textarea } from "@/components/ui/textarea";
import { type ReactElement, useRef, useState } from "react";
import type { ModelConfig } from "../../types";
import { FieldLabel } from "../shared/field-label";
import { NameField } from "../shared/name-field";
@ -24,11 +30,13 @@ export function ModelConfigDialog({
providerOptions,
onUpdate,
}: ModelConfigDialogProps): ReactElement {
const [optionalOpen, setOptionalOpen] = useState(false);
const modelId = `${config.id}-model`;
const providerId = `${config.id}-provider`;
const tempId = `${config.id}-temperature`;
const topPId = `${config.id}-top-p`;
const maxTokensId = `${config.id}-max-tokens`;
const extraBodyId = `${config.id}-inference-extra-body`;
const providerAnchorRef = useRef<HTMLDivElement>(null);
const providerInputRef = useRef(config.provider);
const lastProviderRef = useRef(config.provider);
@ -145,15 +153,44 @@ export function ModelConfigDialog({
/>
</div>
</div>
<label className="flex items-center gap-2 text-xs font-semibold uppercase text-muted-foreground">
<Checkbox
checked={config.skip_health_check ?? false}
onCheckedChange={(value) =>
updateField("skip_health_check", Boolean(value))
}
/>
Skip health check
</label>
<Collapsible open={optionalOpen} onOpenChange={setOptionalOpen}>
<CollapsibleTrigger asChild={true}>
<button
type="button"
className="flex w-full items-center justify-between text-left text-xs text-muted-foreground"
>
<span className="font-semibold uppercase">Optional</span>
<span>{optionalOpen ? "Hide" : "Show"}</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 space-y-4">
<div className="grid gap-2">
<FieldLabel
label="Inference extra body (JSON)"
htmlFor={extraBodyId}
hint="Optional request fields merged into inference parameters."
/>
<Textarea
id={extraBodyId}
className="corner-squircle nodrag"
placeholder='{"top_k": 20, "min_p": 0.0}'
value={config.inference_extra_body ?? ""}
onChange={(event) =>
updateField("inference_extra_body", event.target.value)
}
/>
</div>
<label className="flex items-center gap-2 text-xs font-semibold uppercase text-muted-foreground">
<Checkbox
checked={config.skip_health_check ?? false}
onCheckedChange={(value) =>
updateField("skip_health_check", Boolean(value))
}
/>
Skip health check
</label>
</CollapsibleContent>
</Collapsible>
</div>
);
}

View file

@ -244,6 +244,8 @@ export type ModelConfig = {
// biome-ignore lint/style/useNamingConvention: api schema
inference_max_tokens?: string;
// biome-ignore lint/style/useNamingConvention: api schema
inference_extra_body?: string;
// biome-ignore lint/style/useNamingConvention: api schema
skip_health_check?: boolean;
};

View file

@ -260,6 +260,8 @@ export function makeModelConfig(
// biome-ignore lint/style/useNamingConvention: api schema
inference_top_p: "",
// biome-ignore lint/style/useNamingConvention: api schema
inference_extra_body: "",
// biome-ignore lint/style/useNamingConvention: api schema
skip_health_check: false,
};
}

View file

@ -56,6 +56,10 @@ export function parseModelConfig(
// biome-ignore lint/style/useNamingConvention: api schema
inference_max_tokens: readNumberString(inference.max_tokens),
// biome-ignore lint/style/useNamingConvention: api schema
inference_extra_body: isRecord(inference.extra_body)
? JSON.stringify(inference.extra_body, null, 2)
: "",
// biome-ignore lint/style/useNamingConvention: api schema
skip_health_check:
typeof model.skip_health_check === "boolean"
? model.skip_health_check

View file

@ -230,7 +230,7 @@ export function buildRecipePayload(
modelProviderConfigs.push(config);
continue;
}
modelConfigs.push(buildModelConfig(config));
modelConfigs.push(buildModelConfig(config, errors));
modelConfigConfigs.push(config);
}

View file

@ -31,11 +31,19 @@ export function buildModelProvider(
};
}
export function buildModelConfig(config: ModelConfig): Record<string, unknown> {
export function buildModelConfig(
config: ModelConfig,
errors: string[],
): Record<string, unknown> {
const inference: Record<string, unknown> = {};
const temp = config.inference_temperature?.trim();
const topP = config.inference_top_p?.trim();
const maxTokens = config.inference_max_tokens?.trim();
const extraBody = parseJsonObject(
config.inference_extra_body,
`Model ${config.name} inference extra_body`,
errors,
);
if (temp) {
const parsed = Number(temp);
@ -57,6 +65,10 @@ export function buildModelConfig(config: ModelConfig): Record<string, unknown> {
inference.max_tokens = parsed;
}
}
if (extraBody) {
// biome-ignore lint/style/useNamingConvention: api schema
inference.extra_body = extraBody;
}
return {
alias: config.name,