Designing a General-Purpose LLM Node
The LLM node is the most critical building block in the XGen canvas. It needed to handle ChatOpenAI, ChatAnthropic, and internal models through a single unified interface. This post covers how we designed that integration.
Defining a Common Interface
We defined a base interface shared by all LLM nodes.
interface LLMNodeData {
provider: 'openai' | 'anthropic' | 'custom';
model: string;
temperature: number;
maxTokens: number;
systemPrompt: string;
topP?: number;
frequencyPenalty?: number;
presencePenalty?: number;
}
// 모델 목록 관리
const MODEL_OPTIONS: Record<string, string[]> = {
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview'],
anthropic: ['claude-sonnet-4-20250514', 'claude-3.5-sonnet', 'claude-3-haiku'],
custom: [], // 동적으로 로드
};
Building the ChatOpenAI Node UI
Each LLM node handles model selection, parameter tuning, and prompt input all within the node itself.
const ChatOpenAINode: React.FC<NodeProps<LLMNodeData>> = ({ data, id }) => {
const updateNodeData = useCanvasStore((s) => s.updateNodeData);
return (
<BaseNode title="ChatOpenAI" icon={<OpenAIIcon />} color="green">
<Handle type="target" position={Position.Left} id="input" />
<NodeSelect
label="Model"
value={data.model}
options={MODEL_OPTIONS.openai}
onChange={(v) => updateNodeData(id, { model: v })}
/>
<NodeSlider
label="Temperature"
value={data.temperature}
min={0} max={2} step={0.1}
onChange={(v) => updateNodeData(id, { temperature: v })}
/>
<NodeTextarea
label="System Prompt"
value={data.systemPrompt}
onChange={(v) => updateNodeData(id, { systemPrompt: v })}
/>
<Handle type="source" position={Position.Right} id="output" />
</BaseNode>
);
};
What's Different About the ChatAnthropic Node
Anthropic's parameter surface differs slightly from OpenAI's — most notably, it adds a top_k parameter and handles system prompts differently. We surfaced these differences naturally in the node UI rather than forcing a one-size-fits-all layout.
Prompt Template System
We added a feature for saving frequently used system prompts as templates and loading them on demand. Templates support variable substitution, so you can use placeholders like {{context}} or {{question}}.
interface PromptTemplate {
id: string;
name: string;
content: string;
variables: string[];
}
function resolveTemplate(template: string, vars: Record<string, string>) {
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] || '');
}
Key Takeaway
Each LLM provider returns responses in subtly different formats. If you want to present them uniformly on the frontend, a normalization layer in between is non-negotiable. Standardizing the response format at the backend API design stage pays off significantly later — it cuts a large amount of frontend work down the road.