feat: add support for self hosted llm models

This commit is contained in:
Abhishek Kumar 2026-03-24 17:50:45 +05:30
parent 31e075d114
commit ac0731a374
17 changed files with 179 additions and 48 deletions

View file

@ -48,6 +48,12 @@ new api route in backend, and wish to use it in the UI, generate the client usin
npm run generate-client
```
## Conventions
### File Uploads
Always use a hidden `<input type="file">` with a visible `<Button>` that triggers it via `fileInputRef.current?.click()`. Never use a visible `<Input type="file">` — the native file input styling is inconsistent and confusing. Show the selected filename next to or below the button.
## Development
```bash

View file

@ -519,13 +519,17 @@ export default function RunsPage() {
variant="outline"
size="icon"
onClick={() => {
const filter = encodeURIComponent(
`metadata;stringObject;attributes;contains;conversation.id,metadata;stringObject;attributes;contains;${run.id}`,
);
window.open(
`${process.env.NEXT_PUBLIC_LANGFUSE_ENDPOINT}/project/${process.env.NEXT_PUBLIC_LANGFUSE_PROJECT_ID}/traces?search=&filter=${filter}&dateRange=All+time`,
'_blank',
);
if (run.gathered_context?.trace_url) {
window.open(String(run.gathered_context.trace_url), '_blank');
} else {
const filter = encodeURIComponent(
`metadata;stringObject;attributes;contains;conversation.id,metadata;stringObject;attributes;contains;${run.id}`,
);
window.open(
`${process.env.NEXT_PUBLIC_LANGFUSE_ENDPOINT}/project/${process.env.NEXT_PUBLIC_LANGFUSE_PROJECT_ID}/traces?search=&filter=${filter}&dateRange=All+time`,
'_blank',
);
}
}}
>
<Image

View file

@ -216,7 +216,7 @@ export const RecordingsDialog = ({
<Label className="text-xs text-muted-foreground">
Audio File
</Label>
<Input
<input
ref={fileInputRef}
type="file"
accept="audio/*"
@ -233,11 +233,24 @@ export const RecordingsDialog = ({
setError(null);
setSelectedFile(file);
}}
className="text-sm"
className="hidden"
/>
<p className="text-xs text-muted-foreground mt-1">
Max 5MB
</p>
<Button
type="button"
variant="outline"
size="sm"
className="w-full justify-start text-sm font-normal"
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-4 h-4 mr-2 shrink-0" />
{selectedFile ? (
<span className="truncate">
{selectedFile.name} ({(selectedFile.size / (1024 * 1024)).toFixed(1)}MB)
</span>
) : (
<span className="text-muted-foreground">Choose audio file (max 5MB)</span>
)}
</Button>
</div>
<div>
<Label className="text-xs text-muted-foreground">
@ -289,8 +302,8 @@ export const RecordingsDialog = ({
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">
{rec.recording_id}
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono truncate max-w-[300px]">
{(rec.metadata?.original_filename as string) || rec.recording_id}
</code>
</div>
<p className="text-sm text-muted-foreground mt-1 break-all line-clamp-2">

View file

@ -18,14 +18,12 @@ export const layoutNodes = (
// Separate nodes by type
const triggerNodes = nodes.filter(n => n.type === NodeType.TRIGGER);
const webhookNodes = nodes.filter(n => n.type === NodeType.WEBHOOK);
const globalNodes = nodes.filter(n => n.type === NodeType.GLOBAL_NODE || n.type === 'global');
const qaNodes = nodes.filter(n => n.type === NodeType.QA);
const globalNodes = nodes.filter(n => n.type === NodeType.GLOBAL_NODE);
const workflowNodes = nodes.filter(n =>
n.type === NodeType.START_CALL ||
n.type === NodeType.AGENT_NODE ||
n.type === NodeType.END_CALL ||
n.type === 'startCall' ||
n.type === 'agentNode' ||
n.type === 'endCall'
n.type === NodeType.END_CALL
);
// If no workflow nodes, just return original nodes
@ -161,12 +159,26 @@ export const layoutNodes = (
};
});
// Position QA nodes below webhook nodes on the right side
const qaStartY = webhookNodes.length > 0
? workflowCenterY - (webhookNodes.length * NODE_HEIGHT + (webhookNodes.length - 1) * VERTICAL_SPACING) / 2
+ webhookNodes.length * (NODE_HEIGHT + VERTICAL_SPACING) + VERTICAL_SPACING
: workflowCenterY;
const positionedQaNodes = qaNodes.map((node, index) => ({
...node,
position: {
x: webhookNodesX,
y: qaStartY + index * (NODE_HEIGHT + VERTICAL_SPACING)
}
}));
// Combine all positioned nodes
const allPositionedNodes = [
...positionedTriggerNodes,
...positionedGlobalNodes,
...positionedWorkflowNodes,
...positionedWebhookNodes
...positionedWebhookNodes,
...positionedQaNodes
];
// Create a map for quick lookup

View file

@ -236,11 +236,21 @@ export default function ServiceConfiguration() {
}
});
selectedProviders[service] = userConfig?.[service]?.provider as string;
// Fill in schema defaults for fields not present in userConfig
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
if (properties) {
Object.entries(properties).forEach(([field, schema]) => {
const key = `${service}_${field}`;
if (field !== "provider" && field !== "api_key" && schema.default !== undefined && !(key in defaultValues)) {
defaultValues[key] = schema.default;
}
});
}
} else {
const properties = response.data[service]?.[selectedProviders[service]]?.properties as Record<string, SchemaProperty>;
if (properties) {
Object.entries(properties).forEach(([field, schema]) => {
if (field !== "provider" && schema.default) {
if (field !== "provider" && schema.default !== undefined) {
defaultValues[`${service}_${field}`] = schema.default;
}
});

View file

@ -15,6 +15,7 @@ export interface MentionItem {
id: string;
name: string;
description: string;
filename: string;
}
interface MentionTextareaProps {
@ -46,6 +47,7 @@ export function MentionTextarea({
id: r.recording_id,
name: r.transcript,
description: r.transcript,
filename: (r.metadata?.original_filename as string) || r.recording_id,
})),
[recordings]
);
@ -195,7 +197,7 @@ export function MentionTextarea({
>
<div className="flex items-center gap-2">
<code className="text-xs bg-muted px-1 py-0.5 rounded font-mono">
{item.id}
{item.filename}
</code>
<span className="font-medium truncate">{item.name}</span>
</div>