Implement backend configuration options and refactor related components

This commit is contained in:
2025-09-02 21:12:14 +02:00
parent d9542ba117
commit 4f6bb6292e
11 changed files with 358 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ import App from '@/App'
import { InstancesProvider } from '@/contexts/InstancesContext'
import { instancesApi } from '@/lib/api'
import type { Instance } from '@/types/instance'
import { BackendType } from '@/types/instance'
import { AuthProvider } from '@/contexts/AuthContext'
// Mock the API
@@ -46,8 +47,8 @@ function renderApp() {
describe('App Component - Critical Business Logic Only', () => {
const mockInstances: Instance[] = [
{ name: 'test-instance-1', status: 'stopped', options: { model: 'model1.gguf' } },
{ name: 'test-instance-2', status: 'running', options: { model: 'model2.gguf' } }
{ name: 'test-instance-1', status: 'stopped', options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'model1.gguf' } } },
{ name: 'test-instance-2', status: 'running', options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'model2.gguf' } } }
]
beforeEach(() => {
@@ -82,7 +83,7 @@ describe('App Component - Critical Business Logic Only', () => {
const newInstance: Instance = {
name: 'new-test-instance',
status: 'stopped',
options: { model: 'new-model.gguf' }
options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'new-model.gguf' } }
}
vi.mocked(instancesApi.create).mockResolvedValue(newInstance)
@@ -105,6 +106,7 @@ describe('App Component - Critical Business Logic Only', () => {
await waitFor(() => {
expect(instancesApi.create).toHaveBeenCalledWith('new-test-instance', {
auto_restart: true, // Default value
backend_type: BackendType.LLAMA_SERVER
})
})
@@ -119,7 +121,7 @@ describe('App Component - Critical Business Logic Only', () => {
const updatedInstance: Instance = {
name: 'test-instance-1',
status: 'stopped',
options: { model: 'updated-model.gguf' }
options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'updated-model.gguf' } }
}
vi.mocked(instancesApi.update).mockResolvedValue(updatedInstance)
@@ -138,7 +140,8 @@ describe('App Component - Critical Business Logic Only', () => {
// Verify correct API call with existing instance data
await waitFor(() => {
expect(instancesApi.update).toHaveBeenCalledWith('test-instance-1', {
model: "model1.gguf", // Pre-filled from existing instance
backend_type: BackendType.LLAMA_SERVER,
backend_options: { model: "model1.gguf" } // Pre-filled from existing instance
})
})
})

View File

@@ -0,0 +1,123 @@
import React from 'react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import type { BackendOptions } from '@/schemas/instanceOptions'
import { getBackendFieldType, basicBackendFieldsConfig } from '@/lib/zodFormUtils'
interface BackendFormFieldProps {
fieldKey: keyof BackendOptions
value: string | number | boolean | string[] | undefined
onChange: (key: string, value: string | number | boolean | string[] | undefined) => void
}
const BackendFormField: React.FC<BackendFormFieldProps> = ({ fieldKey, value, onChange }) => {
// Get configuration for basic fields, or use field name for advanced fields
const config = basicBackendFieldsConfig[fieldKey as string] || { label: fieldKey }
// Get type from Zod schema
const fieldType = getBackendFieldType(fieldKey)
const handleChange = (newValue: string | number | boolean | string[] | undefined) => {
onChange(fieldKey as string, newValue)
}
const renderField = () => {
switch (fieldType) {
case 'boolean':
return (
<div className="flex items-center space-x-2">
<Checkbox
id={fieldKey}
checked={typeof value === 'boolean' ? value : false}
onCheckedChange={(checked) => handleChange(checked)}
/>
<Label htmlFor={fieldKey} className="text-sm font-normal">
{config.label}
{config.description && (
<span className="text-muted-foreground ml-1">- {config.description}</span>
)}
</Label>
</div>
)
case 'number':
return (
<div className="grid gap-2">
<Label htmlFor={fieldKey}>
{config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={fieldKey}
type="number"
step="any" // This allows decimal numbers
value={typeof value === 'string' || typeof value === 'number' ? value : ''}
onChange={(e) => {
const numValue = e.target.value ? parseFloat(e.target.value) : undefined
// Only update if the parsed value is valid or the input is empty
if (e.target.value === '' || (numValue !== undefined && !isNaN(numValue))) {
handleChange(numValue)
}
}}
placeholder={config.placeholder}
/>
{config.description && (
<p className="text-sm text-muted-foreground">{config.description}</p>
)}
</div>
)
case 'array':
return (
<div className="grid gap-2">
<Label htmlFor={fieldKey}>
{config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={fieldKey}
type="text"
value={Array.isArray(value) ? value.join(', ') : ''}
onChange={(e) => {
const arrayValue = e.target.value
? e.target.value.split(',').map(s => s.trim()).filter(Boolean)
: undefined
handleChange(arrayValue)
}}
placeholder="item1, item2, item3"
/>
{config.description && (
<p className="text-sm text-muted-foreground">{config.description}</p>
)}
<p className="text-xs text-muted-foreground">Separate multiple values with commas</p>
</div>
)
case 'text':
default:
return (
<div className="grid gap-2">
<Label htmlFor={fieldKey}>
{config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<Input
id={fieldKey}
type="text"
value={typeof value === 'string' || typeof value === 'number' ? value : ''}
onChange={(e) => handleChange(e.target.value || undefined)}
placeholder={config.placeholder}
/>
{config.description && (
<p className="text-sm text-muted-foreground">{config.description}</p>
)}
</div>
)
}
}
return <div className="space-y-2">{renderField()}</div>
}
export default BackendFormField

View File

@@ -11,9 +11,11 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import type { CreateInstanceOptions, Instance } from "@/types/instance";
import { getBasicFields, getAdvancedFields } from "@/lib/zodFormUtils";
import { BackendType } from "@/types/instance";
import { getBasicFields, getAdvancedFields, getBasicBackendFields, getAdvancedBackendFields } from "@/lib/zodFormUtils";
import { ChevronDown, ChevronRight } from "lucide-react";
import ZodFormField from "@/components/ZodFormField";
import BackendFormField from "@/components/BackendFormField";
interface InstanceDialogProps {
open: boolean;
@@ -38,6 +40,8 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
// Get field lists dynamically from the type
const basicFields = getBasicFields();
const advancedFields = getAdvancedFields();
const basicBackendFields = getBasicBackendFields();
const advancedBackendFields = getAdvancedBackendFields();
// Reset form when dialog opens/closes or when instance changes
useEffect(() => {
@@ -51,6 +55,8 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
setInstanceName("");
setFormData({
auto_restart: true, // Default value
backend_type: BackendType.LLAMA_SERVER, // Default backend type
backend_options: {},
});
}
setShowAdvanced(false); // Always start with basic view
@@ -65,6 +71,16 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
}));
};
const handleBackendFieldChange = (key: string, value: any) => {
setFormData((prev) => ({
...prev,
backend_options: {
...prev.backend_options,
[key]: value,
},
}));
};
const handleNameChange = (name: string) => {
setInstanceName(name);
// Validate instance name
@@ -89,7 +105,24 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
// Clean up undefined values to avoid sending empty fields
const cleanOptions: CreateInstanceOptions = {};
Object.entries(formData).forEach(([key, value]) => {
if (value !== undefined && value !== "" && value !== null) {
if (key === 'backend_options' && value && typeof value === 'object') {
// Handle backend_options specially - clean nested object
const cleanBackendOptions: any = {};
Object.entries(value).forEach(([backendKey, backendValue]) => {
if (backendValue !== undefined && backendValue !== null && (typeof backendValue !== 'string' || backendValue.trim() !== "")) {
// Handle arrays - don't include empty arrays
if (Array.isArray(backendValue) && backendValue.length === 0) {
return;
}
cleanBackendOptions[backendKey] = backendValue;
}
});
// Only include backend_options if it has content
if (Object.keys(cleanBackendOptions).length > 0) {
(cleanOptions as any)[key] = cleanBackendOptions;
}
} else if (value !== undefined && value !== null && (typeof value !== 'string' || value.trim() !== "")) {
// Handle arrays - don't include empty arrays
if (Array.isArray(value) && value.length === 0) {
return;
@@ -196,8 +229,9 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
(fieldKey) =>
fieldKey !== "auto_restart" &&
fieldKey !== "max_restarts" &&
fieldKey !== "restart_delay"
) // Exclude auto_restart, max_restarts, and restart_delay as they're handled above
fieldKey !== "restart_delay" &&
fieldKey !== "backend_options" // backend_options is handled separately
)
.map((fieldKey) => (
<ZodFormField
key={fieldKey}
@@ -208,6 +242,21 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
))}
</div>
{/* Backend Configuration Section */}
<div className="space-y-4">
<h3 className="text-lg font-medium">Backend Configuration</h3>
{/* Basic backend fields */}
{basicBackendFields.map((fieldKey) => (
<BackendFormField
key={fieldKey}
fieldKey={fieldKey}
value={formData.backend_options?.[fieldKey]}
onChange={handleBackendFieldChange}
/>
))}
</div>
{/* Advanced Fields Toggle */}
<div className="border-t pt-4">
<Button
@@ -226,8 +275,8 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
{
advancedFields.filter(
(f) =>
!["max_restarts", "restart_delay"].includes(f as string)
).length
!["max_restarts", "restart_delay", "backend_options"].includes(f as string)
).length + advancedBackendFields.length
}{" "}
options)
</span>
@@ -237,24 +286,51 @@ const InstanceDialog: React.FC<InstanceDialogProps> = ({
{/* Advanced Fields - Automatically generated from type (excluding restart options) */}
{showAdvanced && (
<div className="space-y-4 pl-6 border-l-2 border-muted">
<div className="space-y-4">
{advancedFields
.filter(
(fieldKey) =>
!["max_restarts", "restart_delay"].includes(
fieldKey as string
)
) // Exclude restart options as they're handled above
.sort()
.map((fieldKey) => (
<ZodFormField
key={fieldKey}
fieldKey={fieldKey}
value={formData[fieldKey]}
onChange={handleFieldChange}
/>
))}
</div>
{/* Advanced instance fields */}
{advancedFields
.filter(
(fieldKey) =>
!["max_restarts", "restart_delay", "backend_options"].includes(
fieldKey as string
)
).length > 0 && (
<div className="space-y-4">
<h4 className="text-md font-medium">Advanced Instance Configuration</h4>
{advancedFields
.filter(
(fieldKey) =>
!["max_restarts", "restart_delay", "backend_options"].includes(
fieldKey as string
)
)
.sort()
.map((fieldKey) => (
<ZodFormField
key={fieldKey}
fieldKey={fieldKey}
value={fieldKey === 'backend_options' ? undefined : formData[fieldKey]}
onChange={handleFieldChange}
/>
))}
</div>
)}
{/* Advanced backend fields */}
{advancedBackendFields.length > 0 && (
<div className="space-y-4">
<h4 className="text-md font-medium">Advanced Backend Configuration</h4>
{advancedBackendFields
.sort()
.map((fieldKey) => (
<BackendFormField
key={fieldKey}
fieldKey={fieldKey}
value={formData.backend_options?.[fieldKey]}
onChange={handleBackendFieldChange}
/>
))}
</div>
)}
</div>
)}
</div>

View File

@@ -3,6 +3,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import type { CreateInstanceOptions } from '@/types/instance'
import { BackendType } from '@/types/instance'
import { getFieldType, basicFieldsConfig } from '@/lib/zodFormUtils'
interface ZodFormFieldProps {
@@ -23,6 +24,30 @@ const ZodFormField: React.FC<ZodFormFieldProps> = ({ fieldKey, value, onChange }
}
const renderField = () => {
// Special handling for backend_type field - render as dropdown
if (fieldKey === 'backend_type') {
return (
<div className="grid gap-2">
<Label htmlFor={fieldKey}>
{config.label}
{config.required && <span className="text-red-500 ml-1">*</span>}
</Label>
<select
id={fieldKey}
value={typeof value === 'string' ? value : BackendType.LLAMA_SERVER}
onChange={(e) => handleChange(e.target.value || undefined)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value={BackendType.LLAMA_SERVER}>Llama Server</option>
{/* Add more backend types here as they become available */}
</select>
{config.description && (
<p className="text-sm text-muted-foreground">{config.description}</p>
)}
</div>
)
}
switch (fieldType) {
case 'boolean':
return (

View File

@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import InstanceCard from '@/components/InstanceCard'
import type { Instance } from '@/types/instance'
import { BackendType } from '@/types/instance'
// Mock the health hook since we're not testing health logic here
vi.mock('@/hooks/useInstanceHealth', () => ({
@@ -18,13 +19,13 @@ describe('InstanceCard - Instance Actions and State', () => {
const stoppedInstance: Instance = {
name: 'test-instance',
status: 'stopped',
options: { model: 'test-model.gguf' }
options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'test-model.gguf' } }
}
const runningInstance: Instance = {
name: 'running-instance',
status: 'running',
options: { model: 'running-model.gguf' }
options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'running-model.gguf' } }
}
beforeEach(() => {

View File

@@ -5,6 +5,7 @@ import InstanceList from '@/components/InstanceList'
import { InstancesProvider } from '@/contexts/InstancesContext'
import { instancesApi } from '@/lib/api'
import type { Instance } from '@/types/instance'
import { BackendType } from '@/types/instance'
import { AuthProvider } from '@/contexts/AuthContext'
// Mock the API
@@ -44,9 +45,9 @@ describe('InstanceList - State Management and UI Logic', () => {
const mockEditInstance = vi.fn()
const mockInstances: Instance[] = [
{ name: 'instance-1', status: 'stopped', options: { model: 'model1.gguf' } },
{ name: 'instance-2', status: 'running', options: { model: 'model2.gguf' } },
{ name: 'instance-3', status: 'stopped', options: { model: 'model3.gguf' } }
{ name: 'instance-1', status: 'stopped', options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'model1.gguf' } } },
{ name: 'instance-2', status: 'running', options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'model2.gguf' } } },
{ name: 'instance-3', status: 'stopped', options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: 'model3.gguf' } } }
]
const DUMMY_API_KEY = 'test-api-key-123'

View File

@@ -3,6 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import InstanceDialog from '@/components/InstanceDialog'
import type { Instance } from '@/types/instance'
import { BackendType } from '@/types/instance'
describe('InstanceModal - Form Logic and Validation', () => {
const mockOnSave = vi.fn()
@@ -91,6 +92,7 @@ afterEach(() => {
expect(mockOnSave).toHaveBeenCalledWith('my-instance', {
auto_restart: true, // Default value
backend_type: BackendType.LLAMA_SERVER
})
})
@@ -136,8 +138,8 @@ afterEach(() => {
name: 'existing-instance',
status: 'stopped',
options: {
model: 'test-model.gguf',
gpu_layers: 10,
backend_type: BackendType.LLAMA_SERVER,
backend_options: { model: 'test-model.gguf', gpu_layers: 10 },
auto_restart: false
}
}
@@ -177,8 +179,8 @@ afterEach(() => {
await user.click(screen.getByTestId('dialog-save-button'))
expect(mockOnSave).toHaveBeenCalledWith('existing-instance', {
model: 'test-model.gguf',
gpu_layers: 10,
backend_type: BackendType.LLAMA_SERVER,
backend_options: { model: 'test-model.gguf', gpu_layers: 10 },
auto_restart: false
})
})
@@ -271,6 +273,7 @@ afterEach(() => {
expect(mockOnSave).toHaveBeenCalledWith('test-instance', {
auto_restart: true,
backend_type: BackendType.LLAMA_SERVER,
max_restarts: 5,
restart_delay: 10
})
@@ -321,6 +324,7 @@ afterEach(() => {
// Should only include non-empty values
expect(mockOnSave).toHaveBeenCalledWith('clean-instance', {
auto_restart: true, // Only this default value should be included
backend_type: BackendType.LLAMA_SERVER
})
})
@@ -345,7 +349,8 @@ afterEach(() => {
expect(mockOnSave).toHaveBeenCalledWith('numeric-test', {
auto_restart: true,
gpu_layers: 15, // Should be number, not string
backend_type: BackendType.LLAMA_SERVER,
backend_options: { gpu_layers: 15 }, // Should be number, not string
})
})
})

View File

@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
import { InstancesProvider, useInstances } from "@/contexts/InstancesContext";
import { instancesApi } from "@/lib/api";
import type { Instance } from "@/types/instance";
import { BackendType } from "@/types/instance";
import { AuthProvider } from "../AuthContext";
// Mock the API module
@@ -47,13 +48,13 @@ function TestComponent() {
{/* Action buttons for testing with specific instances */}
<button
onClick={() => createInstance("new-instance", { model: "test.gguf" })}
onClick={() => createInstance("new-instance", { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: "test.gguf" } })}
data-testid="create-instance"
>
Create Instance
</button>
<button
onClick={() => updateInstance("instance1", { model: "updated.gguf" })}
onClick={() => updateInstance("instance1", { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: "updated.gguf" } })}
data-testid="update-instance"
>
Update Instance
@@ -99,8 +100,8 @@ function renderWithProvider(children: ReactNode) {
describe("InstancesContext", () => {
const mockInstances: Instance[] = [
{ name: "instance1", status: "running", options: { model: "model1.gguf" } },
{ name: "instance2", status: "stopped", options: { model: "model2.gguf" } },
{ name: "instance1", status: "running", options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: "model1.gguf" } } },
{ name: "instance2", status: "stopped", options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: "model2.gguf" } } },
];
beforeEach(() => {
@@ -159,7 +160,7 @@ describe("InstancesContext", () => {
const newInstance: Instance = {
name: "new-instance",
status: "stopped",
options: { model: "test.gguf" },
options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: "test.gguf" } },
};
vi.mocked(instancesApi.create).mockResolvedValue(newInstance);
@@ -174,7 +175,8 @@ describe("InstancesContext", () => {
await waitFor(() => {
expect(instancesApi.create).toHaveBeenCalledWith("new-instance", {
model: "test.gguf",
backend_type: BackendType.LLAMA_SERVER,
backend_options: { model: "test.gguf" }
});
});
@@ -215,7 +217,7 @@ describe("InstancesContext", () => {
const updatedInstance: Instance = {
name: "instance1",
status: "running",
options: { model: "updated.gguf" },
options: { backend_type: BackendType.LLAMA_SERVER, backend_options: { model: "updated.gguf" } },
};
vi.mocked(instancesApi.update).mockResolvedValue(updatedInstance);
@@ -230,7 +232,8 @@ describe("InstancesContext", () => {
await waitFor(() => {
expect(instancesApi.update).toHaveBeenCalledWith("instance1", {
model: "updated.gguf",
backend_type: BackendType.LLAMA_SERVER,
backend_options: { model: "updated.gguf" }
});
});

View File

@@ -1,6 +1,6 @@
import { type CreateInstanceOptions, getAllFieldKeys } from '@/schemas/instanceOptions'
import { type CreateInstanceOptions, type BackendOptions, getAllFieldKeys, getAllBackendFieldKeys } from '@/schemas/instanceOptions'
// Only define the basic fields we want to show by default
// Instance-level basic fields (not backend-specific)
export const basicFieldsConfig: Record<string, {
label: string
description?: string
@@ -30,6 +30,19 @@ export const basicFieldsConfig: Record<string, {
label: 'On-Demand Start',
description: 'Start instance upon receiving OpenAI-compatible API request'
},
backend_type: {
label: 'Backend Type',
description: 'Type of backend to use for this instance'
}
}
// Backend-specific basic fields (these go in backend_options)
export const basicBackendFieldsConfig: Record<string, {
label: string
description?: string
placeholder?: string
required?: boolean
}> = {
model: {
label: 'Model Path',
placeholder: '/path/to/model.gguf',
@@ -56,6 +69,10 @@ export function isBasicField(key: keyof CreateInstanceOptions): boolean {
return key in basicFieldsConfig
}
export function isBasicBackendField(key: keyof BackendOptions): boolean {
return key in basicBackendFieldsConfig
}
export function getBasicFields(): (keyof CreateInstanceOptions)[] {
return Object.keys(basicFieldsConfig) as (keyof CreateInstanceOptions)[]
}
@@ -64,5 +81,13 @@ export function getAdvancedFields(): (keyof CreateInstanceOptions)[] {
return getAllFieldKeys().filter(key => !isBasicField(key))
}
export function getBasicBackendFields(): (keyof BackendOptions)[] {
return Object.keys(basicBackendFieldsConfig) as (keyof BackendOptions)[]
}
export function getAdvancedBackendFields(): (keyof BackendOptions)[] {
return getAllBackendFieldKeys().filter(key => !isBasicBackendField(key))
}
// Re-export the Zod-based functions
export { getFieldType } from '@/schemas/instanceOptions'
export { getFieldType, getBackendFieldType } from '@/schemas/instanceOptions'

View File

@@ -1,14 +1,7 @@
import { z } from 'zod'
// Define the Zod schema
export const CreateInstanceOptionsSchema = z.object({
// Restart options
auto_restart: z.boolean().optional(),
max_restarts: z.number().optional(),
restart_delay: z.number().optional(),
idle_timeout: z.number().optional(),
on_demand_start: z.boolean().optional(),
// Define the backend options schema (previously embedded in CreateInstanceOptionsSchema)
export const BackendOptionsSchema = z.object({
// Common params
verbose_prompt: z.boolean().optional(),
threads: z.number().optional(),
@@ -176,22 +169,57 @@ export const CreateInstanceOptionsSchema = z.object({
fim_qwen_14b_spec: z.boolean().optional(),
})
// Infer the TypeScript type from the schema
// Define the main create instance options schema
export const CreateInstanceOptionsSchema = z.object({
// Restart options
auto_restart: z.boolean().optional(),
max_restarts: z.number().optional(),
restart_delay: z.number().optional(),
idle_timeout: z.number().optional(),
on_demand_start: z.boolean().optional(),
// Backend configuration
backend_type: z.enum(['llama_server']).optional(),
backend_options: BackendOptionsSchema.optional(),
})
// Infer the TypeScript types from the schemas
export type BackendOptions = z.infer<typeof BackendOptionsSchema>
export type CreateInstanceOptions = z.infer<typeof CreateInstanceOptionsSchema>
// Helper to get all field keys
// Helper to get all field keys for CreateInstanceOptions
export function getAllFieldKeys(): (keyof CreateInstanceOptions)[] {
return Object.keys(CreateInstanceOptionsSchema.shape) as (keyof CreateInstanceOptions)[]
}
// Helper to get all backend option field keys
export function getAllBackendFieldKeys(): (keyof BackendOptions)[] {
return Object.keys(BackendOptionsSchema.shape) as (keyof BackendOptions)[]
}
// Get field type from Zod schema
export function getFieldType(key: keyof CreateInstanceOptions): 'text' | 'number' | 'boolean' | 'array' {
export function getFieldType(key: keyof CreateInstanceOptions): 'text' | 'number' | 'boolean' | 'array' | 'object' {
const fieldSchema = CreateInstanceOptionsSchema.shape[key]
if (!fieldSchema) return 'text'
// Handle ZodOptional wrapper
const innerSchema = fieldSchema instanceof z.ZodOptional ? fieldSchema.unwrap() : fieldSchema
if (innerSchema instanceof z.ZodBoolean) return 'boolean'
if (innerSchema instanceof z.ZodNumber) return 'number'
if (innerSchema instanceof z.ZodArray) return 'array'
if (innerSchema instanceof z.ZodObject) return 'object'
return 'text' // ZodString and others default to text
}
// Get field type for backend options
export function getBackendFieldType(key: keyof BackendOptions): 'text' | 'number' | 'boolean' | 'array' {
const fieldSchema = BackendOptionsSchema.shape[key]
if (!fieldSchema) return 'text'
// Handle ZodOptional wrapper
const innerSchema = fieldSchema instanceof z.ZodOptional ? fieldSchema.unwrap() : fieldSchema
if (innerSchema instanceof z.ZodBoolean) return 'boolean'
if (innerSchema instanceof z.ZodNumber) return 'number'
if (innerSchema instanceof z.ZodArray) return 'array'

View File

@@ -2,6 +2,12 @@ import type { CreateInstanceOptions } from '@/schemas/instanceOptions'
export { type CreateInstanceOptions } from '@/schemas/instanceOptions'
export const BackendType = {
LLAMA_SERVER: 'llama_server'
} as const
export type BackendTypeValue = typeof BackendType[keyof typeof BackendType]
export type InstanceStatus = 'running' | 'stopped' | 'failed'
export interface HealthStatus {