feat: Model Provider and Model config
This commit is contained in:
parent
fbf5a30c77
commit
7350e52f2f
17 changed files with 825 additions and 57 deletions
|
|
@ -9,6 +9,8 @@ import {
|
|||
FunctionIcon,
|
||||
Parabola02Icon,
|
||||
PencilEdit02Icon,
|
||||
Plant01Icon,
|
||||
Shield02Icon,
|
||||
Tag01Icon,
|
||||
TagsIcon,
|
||||
UserAccountIcon,
|
||||
|
|
@ -18,10 +20,14 @@ import type { LlmType, NodeConfig, SamplerConfig, SamplerType } from "../types";
|
|||
import {
|
||||
makeExpressionConfig,
|
||||
makeLlmConfig,
|
||||
makeModelConfig,
|
||||
makeModelProviderConfig,
|
||||
makeSamplerConfig,
|
||||
} from "../utils";
|
||||
import { ExpressionDialog } from "../dialogs/expression/expression-dialog";
|
||||
import { LlmDialog } from "../dialogs/llm/llm-dialog";
|
||||
import { ModelConfigDialog } from "../dialogs/models/model-config-dialog";
|
||||
import { ModelProviderDialog } from "../dialogs/models/model-provider-dialog";
|
||||
import { CategoryDialog } from "../dialogs/samplers/category-dialog";
|
||||
import { DatetimeDialog } from "../dialogs/samplers/datetime-dialog";
|
||||
import { GaussianDialog } from "../dialogs/samplers/gaussian-dialog";
|
||||
|
|
@ -31,7 +37,12 @@ import { UniformDialog } from "../dialogs/samplers/uniform-dialog";
|
|||
import { UuidDialog } from "../dialogs/samplers/uuid-dialog";
|
||||
|
||||
export type BlockKind = "sampler" | "llm" | "expression";
|
||||
export type BlockType = SamplerType | LlmType | "expression";
|
||||
export type BlockType =
|
||||
| SamplerType
|
||||
| LlmType
|
||||
| "expression"
|
||||
| "model_provider"
|
||||
| "model_config";
|
||||
|
||||
type IconType = typeof CodeIcon;
|
||||
|
||||
|
|
@ -249,6 +260,36 @@ const BLOCK_DEFINITIONS: BlockDefinition[] = [
|
|||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
kind: "llm",
|
||||
type: "model_provider",
|
||||
title: "Model Provider",
|
||||
description: "Configure API endpoint + key.",
|
||||
icon: Shield02Icon,
|
||||
createConfig: (id, existing) => makeModelProviderConfig(id, existing),
|
||||
renderDialog: ({ config, onUpdate }) =>
|
||||
config.kind === "model_provider" ? (
|
||||
<ModelProviderDialog
|
||||
config={config}
|
||||
onUpdate={(patch) => onUpdate(config.id, patch)}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
kind: "llm",
|
||||
type: "model_config",
|
||||
title: "Model Config",
|
||||
description: "Alias + model + inference params.",
|
||||
icon: Plant01Icon,
|
||||
createConfig: (id, existing) => makeModelConfig(id, existing),
|
||||
renderDialog: ({ config, onUpdate }) =>
|
||||
config.kind === "model_config" ? (
|
||||
<ModelConfigDialog
|
||||
config={config}
|
||||
onUpdate={(patch) => onUpdate(config.id, patch)}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
kind: "expression",
|
||||
type: "expression",
|
||||
|
|
@ -297,6 +338,12 @@ export function getBlockDefinitionForConfig(
|
|||
if (config.kind === "llm") {
|
||||
return getBlockDefinition("llm", config.llm_type);
|
||||
}
|
||||
if (config.kind === "model_provider") {
|
||||
return getBlockDefinition("llm", "model_provider");
|
||||
}
|
||||
if (config.kind === "model_config") {
|
||||
return getBlockDefinition("llm", "model_config");
|
||||
}
|
||||
return getBlockDefinition("expression", "expression");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { importCanvasPayload } from "./utils/import";
|
|||
import { buildCanvasPayload } from "./utils/payload";
|
||||
|
||||
const NODE_TYPES: NodeTypes = { builder: CanvasNode };
|
||||
const EDGE_TYPES: EdgeTypes = { canvas: CanvasEdge };
|
||||
const EDGE_TYPES: EdgeTypes = { canvas: CanvasEdge, semantic: CanvasEdge };
|
||||
|
||||
type LayoutControlsProps = {
|
||||
direction: "LR" | "TB";
|
||||
|
|
@ -77,6 +77,8 @@ export function CanvasLabPage(): ReactElement {
|
|||
onConnect,
|
||||
addSamplerNode,
|
||||
addLlmNode,
|
||||
addModelProviderNode,
|
||||
addModelConfigNode,
|
||||
addExpressionNode,
|
||||
openConfig,
|
||||
updateConfig,
|
||||
|
|
@ -100,6 +102,8 @@ export function CanvasLabPage(): ReactElement {
|
|||
onConnect: state.onConnect,
|
||||
addSamplerNode: state.addSamplerNode,
|
||||
addLlmNode: state.addLlmNode,
|
||||
addModelProviderNode: state.addModelProviderNode,
|
||||
addModelConfigNode: state.addModelConfigNode,
|
||||
addExpressionNode: state.addExpressionNode,
|
||||
openConfig: state.openConfig,
|
||||
updateConfig: state.updateConfig,
|
||||
|
|
@ -306,6 +310,8 @@ export function CanvasLabPage(): ReactElement {
|
|||
onViewChange={setSheetView}
|
||||
onAddSampler={addSamplerNode}
|
||||
onAddLlm={addLlmNode}
|
||||
onAddModelProvider={addModelProviderNode}
|
||||
onAddModelConfig={addModelConfigNode}
|
||||
onAddExpression={addExpressionNode}
|
||||
/>
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ type BlockSheetProps = {
|
|||
onViewChange: (view: SheetView) => void;
|
||||
onAddSampler: (type: SamplerType) => void;
|
||||
onAddLlm: (type: LlmType) => void;
|
||||
onAddModelProvider: () => void;
|
||||
onAddModelConfig: () => void;
|
||||
onAddExpression: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -92,6 +94,8 @@ export function BlockSheet({
|
|||
onViewChange,
|
||||
onAddSampler,
|
||||
onAddLlm,
|
||||
onAddModelProvider,
|
||||
onAddModelConfig,
|
||||
onAddExpression,
|
||||
}: BlockSheetProps): ReactElement {
|
||||
const title = getSheetTitle(view);
|
||||
|
|
@ -157,7 +161,13 @@ export function BlockSheet({
|
|||
if (item.kind === "sampler") {
|
||||
onAddSampler(item.type as SamplerType);
|
||||
} else if (item.kind === "llm") {
|
||||
onAddLlm(item.type as LlmType);
|
||||
if (item.type === "model_provider") {
|
||||
onAddModelProvider();
|
||||
} else if (item.type === "model_config") {
|
||||
onAddModelConfig();
|
||||
} else {
|
||||
onAddLlm(item.type as LlmType);
|
||||
}
|
||||
} else {
|
||||
onAddExpression();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export const CanvasEdge = memo(function CanvasEdge({
|
|||
sourcePosition,
|
||||
targetPosition,
|
||||
style,
|
||||
type,
|
||||
}: EdgeProps): JSX.Element {
|
||||
const [path] = getSmoothStepPath({
|
||||
sourceX,
|
||||
|
|
@ -22,5 +23,10 @@ export const CanvasEdge = memo(function CanvasEdge({
|
|||
offset: 16,
|
||||
});
|
||||
|
||||
return <BaseEdge id={id} path={path} style={style} />;
|
||||
const nextStyle =
|
||||
type === "semantic"
|
||||
? { ...style, strokeDasharray: "4 4" }
|
||||
: style;
|
||||
|
||||
return <BaseEdge id={id} path={path} style={nextStyle} />;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
FunctionIcon,
|
||||
Parabola02Icon,
|
||||
PencilEdit02Icon,
|
||||
Plant01Icon,
|
||||
Shield02Icon,
|
||||
Tag01Icon,
|
||||
TagsIcon,
|
||||
UserAccountIcon,
|
||||
|
|
@ -36,6 +38,12 @@ const NODE_META = {
|
|||
expression: {
|
||||
tone: "bg-sky-50 text-sky-600 border-sky-100",
|
||||
},
|
||||
model_provider: {
|
||||
tone: "bg-amber-50 text-amber-600 border-amber-100",
|
||||
},
|
||||
model_config: {
|
||||
tone: "bg-indigo-50 text-indigo-600 border-indigo-100",
|
||||
},
|
||||
} as const;
|
||||
|
||||
const SAMPLER_ICONS: Record<SamplerType, IconType> = {
|
||||
|
|
@ -69,6 +77,12 @@ function resolveNodeIcon(
|
|||
if (kind === "expression") {
|
||||
return FunctionIcon;
|
||||
}
|
||||
if (kind === "model_provider") {
|
||||
return Shield02Icon;
|
||||
}
|
||||
if (kind === "model_config") {
|
||||
return Plant01Icon;
|
||||
}
|
||||
return DiceFaces03Icon;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { ReactElement } from "react";
|
||||
import type { ModelConfig } from "../../types";
|
||||
import { useCanvasLabStore } from "../../stores/canvas-lab";
|
||||
import { NameField } from "../shared/name-field";
|
||||
|
||||
type ModelConfigDialogProps = {
|
||||
config: ModelConfig;
|
||||
onUpdate: (patch: Partial<ModelConfig>) => void;
|
||||
};
|
||||
|
||||
export function ModelConfigDialog({
|
||||
config,
|
||||
onUpdate,
|
||||
}: ModelConfigDialogProps): ReactElement {
|
||||
const providerOptions = useCanvasLabStore((state) =>
|
||||
Object.values(state.configs)
|
||||
.filter((item) => item.kind === "model_provider")
|
||||
.map((item) => item.name),
|
||||
);
|
||||
const modelId = `${config.id}-model`;
|
||||
const providerId = `${config.id}-provider`;
|
||||
const providerListId = `${config.id}-provider-list`;
|
||||
const tempId = `${config.id}-temperature`;
|
||||
const topPId = `${config.id}-top-p`;
|
||||
const maxTokensId = `${config.id}-max-tokens`;
|
||||
const updateField = <K extends keyof ModelConfig>(
|
||||
key: K,
|
||||
value: ModelConfig[K],
|
||||
) => {
|
||||
onUpdate({ [key]: value } as Partial<ModelConfig>);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<NameField
|
||||
value={config.name}
|
||||
onChange={(value) => onUpdate({ name: value })}
|
||||
/>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={modelId}
|
||||
>
|
||||
Model
|
||||
</label>
|
||||
<Input
|
||||
id={modelId}
|
||||
className="nodrag"
|
||||
placeholder="gpt-4o-mini"
|
||||
value={config.model}
|
||||
onChange={(event) => updateField("model", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={providerId}
|
||||
>
|
||||
Provider name
|
||||
</label>
|
||||
<Input
|
||||
id={providerId}
|
||||
className="nodrag"
|
||||
value={config.provider}
|
||||
list={providerListId}
|
||||
onChange={(event) => updateField("provider", event.target.value)}
|
||||
/>
|
||||
<datalist id={providerListId}>
|
||||
{providerOptions.map((provider) => (
|
||||
<option key={provider} value={provider} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label className="text-xs font-semibold uppercase text-muted-foreground">
|
||||
Inference
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Input
|
||||
id={tempId}
|
||||
className="nodrag"
|
||||
placeholder="Temp"
|
||||
value={config.inference_temperature ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField("inference_temperature", event.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
id={topPId}
|
||||
className="nodrag"
|
||||
placeholder="Top_p"
|
||||
value={config.inference_top_p ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField("inference_top_p", event.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
id={maxTokensId}
|
||||
className="nodrag"
|
||||
placeholder="Max tokens"
|
||||
value={config.inference_max_tokens ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField("inference_max_tokens", event.target.value)
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { ReactElement } from "react";
|
||||
import type { ModelProviderConfig } from "../../types";
|
||||
import { NameField } from "../shared/name-field";
|
||||
|
||||
type ModelProviderDialogProps = {
|
||||
config: ModelProviderConfig;
|
||||
onUpdate: (patch: Partial<ModelProviderConfig>) => void;
|
||||
};
|
||||
|
||||
export function ModelProviderDialog({
|
||||
config,
|
||||
onUpdate,
|
||||
}: ModelProviderDialogProps): ReactElement {
|
||||
const endpointId = `${config.id}-endpoint`;
|
||||
const providerTypeId = `${config.id}-provider-type`;
|
||||
const apiKeyEnvId = `${config.id}-api-key-env`;
|
||||
const apiKeyId = `${config.id}-api-key`;
|
||||
const extraHeadersId = `${config.id}-extra-headers`;
|
||||
const extraBodyId = `${config.id}-extra-body`;
|
||||
const updateField = <K extends keyof ModelProviderConfig>(
|
||||
key: K,
|
||||
value: ModelProviderConfig[K],
|
||||
) => {
|
||||
onUpdate({ [key]: value } as Partial<ModelProviderConfig>);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<NameField
|
||||
value={config.name}
|
||||
onChange={(value) => onUpdate({ name: value })}
|
||||
/>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={providerTypeId}
|
||||
>
|
||||
Provider type
|
||||
</label>
|
||||
<Input
|
||||
id={providerTypeId}
|
||||
className="nodrag"
|
||||
placeholder="openai"
|
||||
value={config.provider_type}
|
||||
onChange={(event) =>
|
||||
updateField("provider_type", event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={endpointId}
|
||||
>
|
||||
Endpoint
|
||||
</label>
|
||||
<Input
|
||||
id={endpointId}
|
||||
className="nodrag"
|
||||
placeholder="https://..."
|
||||
value={config.endpoint}
|
||||
onChange={(event) => updateField("endpoint", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={apiKeyEnvId}
|
||||
>
|
||||
API key env (optional)
|
||||
</label>
|
||||
<Input
|
||||
id={apiKeyEnvId}
|
||||
className="nodrag"
|
||||
placeholder="OPENAI_API_KEY"
|
||||
value={config.api_key_env ?? ""}
|
||||
onChange={(event) => updateField("api_key_env", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={apiKeyId}
|
||||
>
|
||||
API key (optional)
|
||||
</label>
|
||||
<Input
|
||||
id={apiKeyId}
|
||||
className="nodrag"
|
||||
value={config.api_key ?? ""}
|
||||
onChange={(event) => updateField("api_key", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={extraHeadersId}
|
||||
>
|
||||
Extra headers (JSON)
|
||||
</label>
|
||||
<Textarea
|
||||
id={extraHeadersId}
|
||||
className="nodrag"
|
||||
placeholder='{"X-Header": "value"}'
|
||||
value={config.extra_headers ?? ""}
|
||||
onChange={(event) =>
|
||||
updateField("extra_headers", event.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label
|
||||
className="text-xs font-semibold uppercase text-muted-foreground"
|
||||
htmlFor={extraBodyId}
|
||||
>
|
||||
Extra body (JSON)
|
||||
</label>
|
||||
<Textarea
|
||||
id={extraBodyId}
|
||||
className="nodrag"
|
||||
placeholder='{"key": "value"}'
|
||||
value={config.extra_body ?? ""}
|
||||
onChange={(event) => updateField("extra_body", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -136,6 +136,14 @@ export function applyRenameToConfig(
|
|||
subcategory_parent: to,
|
||||
};
|
||||
}
|
||||
if (config.kind === "model_config" && config.provider === from) {
|
||||
const base = next === config ? config : next;
|
||||
next = { ...base, provider: to };
|
||||
}
|
||||
if (config.kind === "llm" && config.model_alias === from) {
|
||||
const base = next === config ? config : next;
|
||||
next = { ...base, model_alias: to };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +166,14 @@ export function applyRemovalToConfig(
|
|||
subcategory_mapping: {},
|
||||
};
|
||||
}
|
||||
if (config.kind === "model_config" && config.provider === ref) {
|
||||
const base = next === config ? config : next;
|
||||
next = { ...base, provider: "" };
|
||||
}
|
||||
if (config.kind === "llm" && config.model_alias === ref) {
|
||||
const base = next === config ? config : next;
|
||||
next = { ...base, model_alias: "" };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type {
|
|||
CanvasNode,
|
||||
LayoutDirection,
|
||||
LlmType,
|
||||
ModelConfig,
|
||||
NodeConfig,
|
||||
SamplerConfig,
|
||||
SamplerType,
|
||||
|
|
@ -51,6 +52,8 @@ type CanvasLabState = {
|
|||
applyLayout: () => void;
|
||||
addSamplerNode: (type: SamplerType) => void;
|
||||
addLlmNode: (type: LlmType) => void;
|
||||
addModelProviderNode: () => void;
|
||||
addModelConfigNode: () => void;
|
||||
addExpressionNode: () => void;
|
||||
updateConfig: (id: string, patch: Partial<NodeConfig>) => void;
|
||||
loadCanvas: (snapshot: CanvasSnapshot) => void;
|
||||
|
|
@ -119,6 +122,30 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
|
|||
return buildNodeUpdate(state, config, state.layoutDirection);
|
||||
});
|
||||
},
|
||||
addModelProviderNode: () => {
|
||||
set((state) => {
|
||||
const id = `n${state.nextId}`;
|
||||
const existing = Object.values(state.configs);
|
||||
const definition = getBlockDefinition("llm", "model_provider");
|
||||
if (!definition) {
|
||||
return state;
|
||||
}
|
||||
const config = definition.createConfig(id, existing);
|
||||
return buildNodeUpdate(state, config, state.layoutDirection);
|
||||
});
|
||||
},
|
||||
addModelConfigNode: () => {
|
||||
set((state) => {
|
||||
const id = `n${state.nextId}`;
|
||||
const existing = Object.values(state.configs);
|
||||
const definition = getBlockDefinition("llm", "model_config");
|
||||
if (!definition) {
|
||||
return state;
|
||||
}
|
||||
const config = definition.createConfig(id, existing);
|
||||
return buildNodeUpdate(state, config, state.layoutDirection);
|
||||
});
|
||||
},
|
||||
addExpressionNode: () => {
|
||||
set((state) => {
|
||||
const id = `n${state.nextId}`;
|
||||
|
|
@ -187,12 +214,43 @@ export const useCanvasLabStore = create<CanvasLabState>((set, get) => ({
|
|||
target: id,
|
||||
sourceHandle: null,
|
||||
targetHandle: null,
|
||||
type: "semantic",
|
||||
},
|
||||
edges,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const hasProviderPatch = Object.prototype.hasOwnProperty.call(
|
||||
patch,
|
||||
"provider",
|
||||
);
|
||||
if (current.kind === "model_config" && hasProviderPatch) {
|
||||
const nextProvider = (patch as Partial<ModelConfig>).provider ?? "";
|
||||
edges = edges.filter((edge) => {
|
||||
if (edge.target !== id) {
|
||||
return true;
|
||||
}
|
||||
const source = configs[edge.source];
|
||||
return !(source && source.kind === "model_provider");
|
||||
});
|
||||
if (nextProvider) {
|
||||
const providerId = findNodeIdByName(configs, nextProvider);
|
||||
if (providerId) {
|
||||
edges = addEdge(
|
||||
{
|
||||
source: providerId,
|
||||
target: id,
|
||||
sourceHandle: null,
|
||||
targetHandle: null,
|
||||
type: "semantic",
|
||||
},
|
||||
edges,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCategoryConfig(current)) {
|
||||
const nextCategory = isCategoryConfig(next) ? next : current;
|
||||
const oldValues = current.values ?? [];
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@ export type LayoutDirection = "LR" | "TB";
|
|||
export type CanvasNodeData = {
|
||||
title: string;
|
||||
name: string;
|
||||
kind: "sampler" | "llm" | "expression";
|
||||
kind: "sampler" | "llm" | "expression" | "model_provider" | "model_config";
|
||||
subtype: string;
|
||||
blockType: SamplerType | LlmType | "expression";
|
||||
blockType:
|
||||
| SamplerType
|
||||
| LlmType
|
||||
| "expression"
|
||||
| "model_provider"
|
||||
| "model_config";
|
||||
layoutDirection?: LayoutDirection;
|
||||
};
|
||||
|
||||
|
|
@ -94,6 +99,39 @@ export type LlmConfig = {
|
|||
scores?: Score[];
|
||||
};
|
||||
|
||||
export type ModelProviderConfig = {
|
||||
id: string;
|
||||
kind: "model_provider";
|
||||
name: string;
|
||||
endpoint: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
provider_type: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key_env?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_headers?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_body?: string;
|
||||
};
|
||||
|
||||
export type ModelConfig = {
|
||||
id: string;
|
||||
kind: "model_config";
|
||||
name: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_temperature?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_top_p?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_max_tokens?: string;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
skip_health_check?: boolean;
|
||||
};
|
||||
|
||||
export type ExpressionConfig = {
|
||||
id: string;
|
||||
kind: "expression";
|
||||
|
|
@ -102,4 +140,9 @@ export type ExpressionConfig = {
|
|||
dtype: ExpressionDtype;
|
||||
};
|
||||
|
||||
export type NodeConfig = SamplerConfig | LlmConfig | ExpressionConfig;
|
||||
export type NodeConfig =
|
||||
| SamplerConfig
|
||||
| LlmConfig
|
||||
| ExpressionConfig
|
||||
| ModelProviderConfig
|
||||
| ModelConfig;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,21 @@ function syncSubcategoryMapping(
|
|||
};
|
||||
}
|
||||
|
||||
function isSemanticEdge(source: NodeConfig, target: NodeConfig): boolean {
|
||||
if (source.kind === "model_provider" && target.kind === "model_config") {
|
||||
return true;
|
||||
}
|
||||
if (source.kind === "model_config" && target.kind === "llm") {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
source.kind === "sampler" &&
|
||||
source.sampler_type === "category" &&
|
||||
target.kind === "sampler" &&
|
||||
target.sampler_type === "subcategory"
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidCanvasConnection(
|
||||
connection: Connection,
|
||||
configs: Record<string, NodeConfig>,
|
||||
|
|
@ -73,8 +88,19 @@ export function applyCanvasConnection(
|
|||
if (!(source && target)) {
|
||||
return { edges };
|
||||
}
|
||||
const nextEdges = addEdge({ ...connection, type: "canvas" }, edges);
|
||||
if (isLlmConfig(target)) {
|
||||
const nextEdges = addEdge(
|
||||
{ ...connection, type: isSemanticEdge(source, target) ? "semantic" : "canvas" },
|
||||
edges,
|
||||
);
|
||||
if (source.kind === "model_provider" && target.kind === "model_config") {
|
||||
const next = { ...target, provider: source.name };
|
||||
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
|
||||
}
|
||||
if (source.kind === "model_config" && target.kind === "llm") {
|
||||
const next = { ...target, model_alias: source.name };
|
||||
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
|
||||
}
|
||||
if (isLlmConfig(target) && source.kind !== "model_provider" && source.kind !== "model_config") {
|
||||
const ref = `{{ ${source.name} }}`;
|
||||
const next = {
|
||||
...target,
|
||||
|
|
@ -82,7 +108,7 @@ export function applyCanvasConnection(
|
|||
};
|
||||
return { edges: nextEdges, configs: { ...configs, [target.id]: next } };
|
||||
}
|
||||
if (isExpressionConfig(target)) {
|
||||
if (isExpressionConfig(target) && source.kind !== "model_provider" && source.kind !== "model_config") {
|
||||
const ref = `{{ ${source.name} }}`;
|
||||
const next = {
|
||||
...target,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { extractRefs } from "./helpers";
|
|||
export function buildEdges(
|
||||
configs: NodeConfig[],
|
||||
nameToId: Map<string, string>,
|
||||
uiEdges: Array<{ from: string; to: string }> | null,
|
||||
uiEdges: Array<{ from: string; to: string; type?: string }> | null,
|
||||
): Edge[] {
|
||||
const edges: Edge[] = [];
|
||||
const seen = new Set<string>();
|
||||
const addEdgeByName = (from: string, to: string) => {
|
||||
const addEdgeByName = (from: string, to: string, type?: string) => {
|
||||
const sourceId = nameToId.get(from);
|
||||
const targetId = nameToId.get(to);
|
||||
if (!(sourceId && targetId)) {
|
||||
|
|
@ -24,13 +24,13 @@ export function buildEdges(
|
|||
id: `e-${key}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type: "canvas",
|
||||
type: type ?? "canvas",
|
||||
});
|
||||
};
|
||||
|
||||
if (uiEdges && uiEdges.length > 0) {
|
||||
for (const edge of uiEdges) {
|
||||
addEdgeByName(edge.from, edge.to);
|
||||
addEdgeByName(edge.from, edge.to, edge.type);
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
|
@ -54,7 +54,13 @@ export function buildEdges(
|
|||
config.sampler_type === "subcategory" &&
|
||||
config.subcategory_parent
|
||||
) {
|
||||
addEdgeByName(config.subcategory_parent, config.name);
|
||||
addEdgeByName(config.subcategory_parent, config.name, "semantic");
|
||||
}
|
||||
if (config.kind === "model_config" && config.provider) {
|
||||
addEdgeByName(config.provider, config.name, "semantic");
|
||||
}
|
||||
if (config.kind === "llm" && config.model_alias) {
|
||||
addEdgeByName(config.model_alias, config.name, "semantic");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import type { NodeConfig } from "../../types";
|
||||
import { buildEdges } from "./edges";
|
||||
import { isRecord, parseJson } from "./helpers";
|
||||
import { parseColumn } from "./parsers";
|
||||
import { isRecord, parseJson, readString } from "./helpers";
|
||||
import {
|
||||
parseColumn,
|
||||
parseModelConfig,
|
||||
parseModelProvider,
|
||||
} from "./parsers";
|
||||
import { buildNodes, parseUi } from "./ui";
|
||||
import type { ImportResult } from "./types";
|
||||
|
||||
|
|
@ -39,12 +43,61 @@ export function importCanvasPayload(input: string): ImportResult {
|
|||
const configs: NodeConfig[] = [];
|
||||
const nameToId = new Map<string, string>();
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
if (Array.isArray(recipe.model_providers)) {
|
||||
recipe.model_providers.forEach((provider, index) => {
|
||||
if (!isRecord(provider)) {
|
||||
errors.push(`Model provider ${index + 1}: invalid object.`);
|
||||
return;
|
||||
}
|
||||
const name = readString(provider.name);
|
||||
if (!name) {
|
||||
errors.push(`Model provider ${index + 1}: missing name.`);
|
||||
return;
|
||||
}
|
||||
const id = `n${nextId}`;
|
||||
nextId += 1;
|
||||
const config = parseModelProvider(provider, name, id);
|
||||
if (nameToId.has(config.name)) {
|
||||
errors.push(`Duplicate column name: ${config.name}.`);
|
||||
return;
|
||||
}
|
||||
nameToId.set(config.name, config.id);
|
||||
configs.push(config);
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(recipe.model_configs)) {
|
||||
recipe.model_configs.forEach((model, index) => {
|
||||
if (!isRecord(model)) {
|
||||
errors.push(`Model config ${index + 1}: invalid object.`);
|
||||
return;
|
||||
}
|
||||
const name = readString(model.alias) ?? readString(model.name);
|
||||
if (!name) {
|
||||
errors.push(`Model config ${index + 1}: missing alias.`);
|
||||
return;
|
||||
}
|
||||
const id = `n${nextId}`;
|
||||
nextId += 1;
|
||||
const config = parseModelConfig(model, name, id);
|
||||
if (nameToId.has(config.name)) {
|
||||
errors.push(`Duplicate column name: ${config.name}.`);
|
||||
return;
|
||||
}
|
||||
nameToId.set(config.name, config.id);
|
||||
configs.push(config);
|
||||
});
|
||||
}
|
||||
|
||||
recipe.columns.forEach((column, index) => {
|
||||
if (!isRecord(column)) {
|
||||
errors.push(`Column ${index + 1}: invalid object.`);
|
||||
return;
|
||||
}
|
||||
const id = `n${index + 1}`;
|
||||
const id = `n${nextId}`;
|
||||
nextId += 1;
|
||||
const config = parseColumn(column, id, errors);
|
||||
if (!config) {
|
||||
return;
|
||||
|
|
@ -76,7 +129,7 @@ export function importCanvasPayload(input: string): ImportResult {
|
|||
configs: Object.fromEntries(configs.map((config) => [config.id, config])),
|
||||
nodes,
|
||||
edges,
|
||||
nextId: configs.length + 1,
|
||||
nextId,
|
||||
nextY: maxY + 140,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type {
|
|||
ExpressionConfig,
|
||||
ExpressionDtype,
|
||||
LlmConfig,
|
||||
ModelConfig,
|
||||
ModelProviderConfig,
|
||||
NodeConfig,
|
||||
SamplerConfig,
|
||||
SamplerType,
|
||||
|
|
@ -234,6 +236,61 @@ function parseLlm(
|
|||
};
|
||||
}
|
||||
|
||||
export function parseModelProvider(
|
||||
provider: Record<string, unknown>,
|
||||
name: string,
|
||||
id: string,
|
||||
): ModelProviderConfig {
|
||||
return {
|
||||
id,
|
||||
kind: "model_provider",
|
||||
name,
|
||||
endpoint: readString(provider.endpoint) ?? "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
provider_type: readString(provider.provider_type) ?? "openai",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key_env: readString(provider.api_key_env) ?? "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key: readString(provider.api_key) ?? "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_headers: isRecord(provider.extra_headers)
|
||||
? JSON.stringify(provider.extra_headers, null, 2)
|
||||
: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_body: isRecord(provider.extra_body)
|
||||
? JSON.stringify(provider.extra_body, null, 2)
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function parseModelConfig(
|
||||
model: Record<string, unknown>,
|
||||
name: string,
|
||||
id: string,
|
||||
): ModelConfig {
|
||||
const inference = isRecord(model.inference_parameters)
|
||||
? (model.inference_parameters as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
id,
|
||||
kind: "model_config",
|
||||
name,
|
||||
model: readString(model.model) ?? "",
|
||||
provider: readString(model.provider) ?? "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_temperature: readNumberString(inference.temperature),
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_top_p: readNumberString(inference.top_p),
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_max_tokens: readNumberString(inference.max_tokens),
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
skip_health_check:
|
||||
typeof model.skip_health_check === "boolean"
|
||||
? model.skip_health_check
|
||||
: false,
|
||||
};
|
||||
}
|
||||
|
||||
function parseExpression(
|
||||
column: Record<string, unknown>,
|
||||
name: string,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ export function parseUi(
|
|||
ui: UiInput | null,
|
||||
): {
|
||||
positions: Map<string, { x: number; y: number }>;
|
||||
edges: Array<{ from: string; to: string }> | null;
|
||||
edges: Array<{ from: string; to: string; type?: string }> | null;
|
||||
} {
|
||||
const positions = new Map<string, { x: number; y: number }>();
|
||||
const edges: Array<{ from: string; to: string }> = [];
|
||||
const edges: Array<{ from: string; to: string; type?: string }> = [];
|
||||
if (ui && Array.isArray(ui.nodes)) {
|
||||
for (const node of ui.nodes) {
|
||||
if (isRecord(node)) {
|
||||
|
|
@ -33,7 +33,11 @@ export function parseUi(
|
|||
const from = readString(edge.from);
|
||||
const to = readString(edge.to);
|
||||
if (from && to) {
|
||||
edges.push({ from, to });
|
||||
edges.push({
|
||||
from,
|
||||
to,
|
||||
type: readString(edge.type) ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type {
|
|||
LayoutDirection,
|
||||
LlmConfig,
|
||||
LlmType,
|
||||
ModelConfig,
|
||||
ModelProviderConfig,
|
||||
NodeConfig,
|
||||
SamplerConfig,
|
||||
SamplerType,
|
||||
|
|
@ -220,6 +222,49 @@ export function makeLlmConfig(
|
|||
};
|
||||
}
|
||||
|
||||
export function makeModelProviderConfig(
|
||||
id: string,
|
||||
existing: NodeConfig[],
|
||||
): ModelProviderConfig {
|
||||
return {
|
||||
id,
|
||||
kind: "model_provider",
|
||||
name: nextName(existing, "provider"),
|
||||
endpoint: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
provider_type: "openai",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key_env: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_headers: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_body: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function makeModelConfig(
|
||||
id: string,
|
||||
existing: NodeConfig[],
|
||||
): ModelConfig {
|
||||
return {
|
||||
id,
|
||||
kind: "model_config",
|
||||
name: nextName(existing, "model"),
|
||||
model: "",
|
||||
provider: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_temperature: "0.7",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_max_tokens: "256",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_top_p: "",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
skip_health_check: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function makeExpressionConfig(
|
||||
id: string,
|
||||
existing: NodeConfig[],
|
||||
|
|
@ -269,6 +314,26 @@ export function nodeDataFromConfig(
|
|||
layoutDirection,
|
||||
};
|
||||
}
|
||||
if (config.kind === "model_provider") {
|
||||
return {
|
||||
title: "Model Provider",
|
||||
kind: "model_provider",
|
||||
subtype: config.provider_type || "Provider",
|
||||
blockType: "model_provider",
|
||||
name: config.name,
|
||||
layoutDirection,
|
||||
};
|
||||
}
|
||||
if (config.kind === "model_config") {
|
||||
return {
|
||||
title: "Model Config",
|
||||
kind: "model_config",
|
||||
subtype: config.model || "Model",
|
||||
blockType: "model_config",
|
||||
name: config.name,
|
||||
layoutDirection,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "LLM",
|
||||
kind: "llm",
|
||||
|
|
|
|||
|
|
@ -3,35 +3,13 @@ import type {
|
|||
CanvasNode,
|
||||
ExpressionConfig,
|
||||
LlmConfig,
|
||||
ModelConfig,
|
||||
ModelProviderConfig,
|
||||
NodeConfig,
|
||||
SamplerConfig,
|
||||
} from "../types";
|
||||
import { getConfigErrors } from "./index";
|
||||
|
||||
const DEFAULT_PROVIDER = {
|
||||
name: "openrouter",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
provider_type: "openai",
|
||||
endpoint: "https://openrouter.ai/api/v1",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key_env: "OPENROUTER_API_KEY",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_headers: {},
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_body: {},
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
provider: "openrouter",
|
||||
model: "allenai/olmo-3.1-32b-instruct",
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_parameters: {
|
||||
temperature: 0.7,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
max_tokens: 256,
|
||||
},
|
||||
};
|
||||
|
||||
type CanvasPayload = {
|
||||
recipe: {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
|
|
@ -49,7 +27,7 @@ type CanvasPayload = {
|
|||
};
|
||||
ui: {
|
||||
nodes: { id: string; x: number; y: number }[];
|
||||
edges: { from: string; to: string }[];
|
||||
edges: { from: string; to: string; type?: string }[];
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -82,6 +60,94 @@ function parseAgeRange(value?: string): [number, number] | null {
|
|||
return [min, max];
|
||||
}
|
||||
|
||||
function parseJsonObject(
|
||||
value: string | undefined,
|
||||
label: string,
|
||||
errors: string[],
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!value || !value.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
errors.push(`${label}: invalid JSON.`);
|
||||
return undefined;
|
||||
}
|
||||
errors.push(`${label}: must be a JSON object.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildModelProvider(
|
||||
config: ModelProviderConfig,
|
||||
errors: string[],
|
||||
): Record<string, unknown> {
|
||||
const extraHeaders = parseJsonObject(
|
||||
config.extra_headers,
|
||||
`Provider ${config.name} extra_headers`,
|
||||
errors,
|
||||
);
|
||||
const extraBody = parseJsonObject(
|
||||
config.extra_body,
|
||||
`Provider ${config.name} extra_body`,
|
||||
errors,
|
||||
);
|
||||
return {
|
||||
name: config.name,
|
||||
endpoint: config.endpoint,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
provider_type: config.provider_type,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key_env: config.api_key_env?.trim() || undefined,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
api_key: config.api_key?.trim() || undefined,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_headers: extraHeaders ?? {},
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
extra_body: extraBody ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
function buildModelConfig(config: ModelConfig): 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();
|
||||
if (temp) {
|
||||
const parsed = Number(temp);
|
||||
if (Number.isFinite(parsed)) {
|
||||
inference.temperature = parsed;
|
||||
}
|
||||
}
|
||||
if (topP) {
|
||||
const parsed = Number(topP);
|
||||
if (Number.isFinite(parsed)) {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference.top_p = parsed;
|
||||
}
|
||||
}
|
||||
if (maxTokens) {
|
||||
const parsed = Number(maxTokens);
|
||||
if (Number.isFinite(parsed)) {
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference.max_tokens = parsed;
|
||||
}
|
||||
}
|
||||
return {
|
||||
alias: config.name,
|
||||
model: config.model,
|
||||
provider: config.provider || undefined,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
inference_parameters:
|
||||
Object.keys(inference).length > 0 ? inference : undefined,
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
skip_health_check: config.skip_health_check || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function isValidSex(value?: string): value is "Male" | "Female" {
|
||||
if (!value) {
|
||||
return false;
|
||||
|
|
@ -282,6 +348,11 @@ export function buildCanvasPayload(
|
|||
const errors: string[] = [];
|
||||
const columns: Record<string, unknown>[] = [];
|
||||
const modelAliases = new Set<string>();
|
||||
const modelProviderNames = new Set<string>();
|
||||
const modelProviders: Record<string, unknown>[] = [];
|
||||
const modelConfigs: Record<string, unknown>[] = [];
|
||||
const modelProviderConfigs: ModelProviderConfig[] = [];
|
||||
const modelConfigConfigs: ModelConfig[] = [];
|
||||
const nameSet = new Set<string>();
|
||||
const nameToConfig = new Map<string, NodeConfig>();
|
||||
|
||||
|
|
@ -294,7 +365,7 @@ export function buildCanvasPayload(
|
|||
errors.push(`${config.name}: ${error}`);
|
||||
}
|
||||
if (nameSet.has(config.name)) {
|
||||
errors.push(`Duplicate column name: ${config.name}.`);
|
||||
errors.push(`Duplicate node name: ${config.name}.`);
|
||||
}
|
||||
nameSet.add(config.name);
|
||||
|
||||
|
|
@ -316,9 +387,16 @@ export function buildCanvasPayload(
|
|||
modelAliases.add(config.model_alias);
|
||||
}
|
||||
nameToConfig.set(config.name, config);
|
||||
} else {
|
||||
} else if (config.kind === "expression") {
|
||||
columns.push(buildExpressionColumn(config, errors));
|
||||
nameToConfig.set(config.name, config);
|
||||
} else if (config.kind === "model_provider") {
|
||||
modelProviderNames.add(config.name);
|
||||
modelProviders.push(buildModelProvider(config, errors));
|
||||
modelProviderConfigs.push(config);
|
||||
} else if (config.kind === "model_config") {
|
||||
modelConfigs.push(buildModelConfig(config));
|
||||
modelConfigConfigs.push(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -347,14 +425,41 @@ export function buildCanvasPayload(
|
|||
}
|
||||
}
|
||||
|
||||
const modelProviders = modelAliases.size > 0 ? [DEFAULT_PROVIDER] : [];
|
||||
const modelConfigs =
|
||||
modelAliases.size > 0
|
||||
? Array.from(modelAliases).map((alias) => ({
|
||||
alias,
|
||||
...DEFAULT_CONFIG,
|
||||
}))
|
||||
: [];
|
||||
for (const alias of modelAliases) {
|
||||
if (
|
||||
!modelConfigs.some(
|
||||
(config) => (config.alias as string | undefined) === alias,
|
||||
)
|
||||
) {
|
||||
errors.push(`LLM model_alias ${alias}: missing model config.`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const config of modelConfigConfigs) {
|
||||
const provider = config.provider.trim();
|
||||
const alias = config.name;
|
||||
if (modelAliases.has(alias) && !config.model.trim()) {
|
||||
errors.push(`Model config ${alias}: model is required.`);
|
||||
}
|
||||
if (provider && !modelProviderNames.has(provider)) {
|
||||
errors.push(`Model config ${alias}: provider ${provider} not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
const usedProviders = new Set(
|
||||
modelConfigConfigs.map((config) => config.provider.trim()).filter(Boolean),
|
||||
);
|
||||
for (const provider of modelProviderConfigs) {
|
||||
if (!usedProviders.has(provider.name)) {
|
||||
continue;
|
||||
}
|
||||
if (!provider.endpoint.trim()) {
|
||||
errors.push(`Model provider ${provider.name}: endpoint is required.`);
|
||||
}
|
||||
if (!provider.provider_type.trim()) {
|
||||
errors.push(`Model provider ${provider.name}: provider_type is required.`);
|
||||
}
|
||||
}
|
||||
|
||||
const uiNodes = nodes.flatMap((node) => {
|
||||
const config = configs[node.id];
|
||||
|
|
@ -380,6 +485,7 @@ export function buildCanvasPayload(
|
|||
{
|
||||
from: source.name,
|
||||
to: target.name,
|
||||
type: edge.type ?? "canvas",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue