mirror of
https://github.com/lordmathis/llamactl.git
synced 2025-12-22 17:14:22 +00:00
Compare commits
13 Commits
cd1bd64889
...
v0.13.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c776785f30 | |||
| 1cfbd42eda | |||
| 8fee27054d | |||
| fd33837026 | |||
| 3c4ebf7403 | |||
| b7a0f7e3d8 | |||
| d5b68a900f | |||
| 00cd8c8877 | |||
| 4b1b12a7a8 | |||
| 0ce9016488 | |||
| 1acbcafe1c | |||
| 00a502a268 | |||
| 54fe0f7421 |
@@ -183,7 +183,7 @@ data_dir: ~/.local/share/llamactl # Main data directory (database, instances, l
|
|||||||
|
|
||||||
instances:
|
instances:
|
||||||
port_range: [8000, 9000] # Port range for instances
|
port_range: [8000, 9000] # Port range for instances
|
||||||
configs_dir: ~/.local/share/llamactl/instances # Instance configs directory (platform dependent)
|
configs_dir: ~/.local/share/llamactl/instances # Instance configs directory (platform dependent) [deprecated]
|
||||||
logs_dir: ~/.local/share/llamactl/logs # Logs directory (platform dependent)
|
logs_dir: ~/.local/share/llamactl/logs # Logs directory (platform dependent)
|
||||||
auto_create_dirs: true # Auto-create data/config/logs dirs if missing
|
auto_create_dirs: true # Auto-create data/config/logs dirs if missing
|
||||||
max_instances: -1 # Max instances (-1 = unlimited)
|
max_instances: -1 # Max instances (-1 = unlimited)
|
||||||
@@ -203,8 +203,7 @@ database:
|
|||||||
connection_max_lifetime: 5m # Connection max lifetime
|
connection_max_lifetime: 5m # Connection max lifetime
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
require_inference_auth: true # Require auth for inference endpoints
|
require_inference_auth: true # Require auth for inference endpoints, API keys are created in web UI
|
||||||
inference_keys: [] # Keys for inference endpoints
|
|
||||||
require_management_auth: true # Require auth for management endpoints
|
require_management_auth: true # Require auth for management endpoints
|
||||||
management_keys: [] # Keys for management endpoints
|
management_keys: [] # Keys for management endpoints
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
// migrateFromJSON migrates instances from JSON files to SQLite database
|
// migrateFromJSON migrates instances from JSON files to SQLite database
|
||||||
// This is a one-time migration that runs on first startup with existing JSON files.
|
// This is a one-time migration that runs on first startup with existing JSON files.
|
||||||
|
// Migrated files are moved to a migrated subdirectory to avoid re-importing.
|
||||||
func migrateFromJSON(cfg *config.AppConfig, db database.InstanceStore) error {
|
func migrateFromJSON(cfg *config.AppConfig, db database.InstanceStore) error {
|
||||||
instancesDir := cfg.Instances.InstancesDir
|
instancesDir := cfg.Instances.InstancesDir
|
||||||
if instancesDir == "" {
|
if instancesDir == "" {
|
||||||
@@ -24,16 +25,6 @@ func migrateFromJSON(cfg *config.AppConfig, db database.InstanceStore) error {
|
|||||||
return nil // No instances directory, nothing to migrate
|
return nil // No instances directory, nothing to migrate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if database is empty (no instances)
|
|
||||||
existing, err := db.LoadAll()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check existing instances: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(existing) > 0 {
|
|
||||||
return nil // Database already has instances, skip migration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all JSON files
|
// Find all JSON files
|
||||||
files, err := filepath.Glob(filepath.Join(instancesDir, "*.json"))
|
files, err := filepath.Glob(filepath.Join(instancesDir, "*.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -46,6 +37,12 @@ func migrateFromJSON(cfg *config.AppConfig, db database.InstanceStore) error {
|
|||||||
|
|
||||||
log.Printf("Migrating %d instances from JSON to SQLite...", len(files))
|
log.Printf("Migrating %d instances from JSON to SQLite...", len(files))
|
||||||
|
|
||||||
|
// Create migrated directory
|
||||||
|
migratedDir := filepath.Join(instancesDir, "migrated")
|
||||||
|
if err := os.MkdirAll(migratedDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create migrated directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate each JSON file
|
// Migrate each JSON file
|
||||||
var migrated int
|
var migrated int
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -53,6 +50,14 @@ func migrateFromJSON(cfg *config.AppConfig, db database.InstanceStore) error {
|
|||||||
log.Printf("Failed to migrate %s: %v", file, err)
|
log.Printf("Failed to migrate %s: %v", file, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move the file to the migrated directory
|
||||||
|
destPath := filepath.Join(migratedDir, filepath.Base(file))
|
||||||
|
if err := os.Rename(file, destPath); err != nil {
|
||||||
|
log.Printf("Warning: Failed to move %s to migrated directory: %v", file, err)
|
||||||
|
// Don't fail the migration if we can't move the file
|
||||||
|
}
|
||||||
|
|
||||||
migrated++
|
migrated++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ database:
|
|||||||
|
|
||||||
auth:
|
auth:
|
||||||
require_inference_auth: true # Require auth for inference endpoints
|
require_inference_auth: true # Require auth for inference endpoints
|
||||||
inference_keys: [] # Keys for inference endpoints
|
|
||||||
require_management_auth: true # Require auth for management endpoints
|
require_management_auth: true # Require auth for management endpoints
|
||||||
management_keys: [] # Keys for management endpoints
|
management_keys: [] # Keys for management endpoints
|
||||||
|
|
||||||
@@ -266,17 +265,33 @@ database:
|
|||||||
|
|
||||||
### Authentication Configuration
|
### Authentication Configuration
|
||||||
|
|
||||||
|
llamactl supports two types of authentication:
|
||||||
|
|
||||||
|
- **Management API Keys**: For accessing the web UI and management API (creating/managing instances). These can be configured in the config file or via environment variables.
|
||||||
|
- **Inference API Keys**: For accessing the OpenAI-compatible inference endpoints. These are managed via the web UI (Settings → API Keys) and stored in the database.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
auth:
|
auth:
|
||||||
require_inference_auth: true # Require API key for OpenAI endpoints (default: true)
|
require_inference_auth: true # Require API key for OpenAI endpoints (default: true)
|
||||||
inference_keys: [] # List of valid inference API keys
|
|
||||||
require_management_auth: true # Require API key for management endpoints (default: true)
|
require_management_auth: true # Require API key for management endpoints (default: true)
|
||||||
management_keys: [] # List of valid management API keys
|
management_keys: [] # List of valid management API keys
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Managing Inference API Keys:**
|
||||||
|
|
||||||
|
Inference API keys are managed through the web UI or management API and stored in the database. To create and manage inference keys:
|
||||||
|
|
||||||
|
1. Open the web UI and log in with a management API key
|
||||||
|
2. Navigate to **Settings → API Keys**
|
||||||
|
3. Click **Create API Key**
|
||||||
|
4. Configure the key:
|
||||||
|
- **Name**: A descriptive name for the key
|
||||||
|
- **Expiration**: Optional expiration date
|
||||||
|
- **Permissions**: Grant access to all instances or specific instances only
|
||||||
|
5. Copy the generated key - it won't be shown again
|
||||||
|
|
||||||
**Environment Variables:**
|
**Environment Variables:**
|
||||||
- `LLAMACTL_REQUIRE_INFERENCE_AUTH` - Require auth for OpenAI endpoints (true/false)
|
- `LLAMACTL_REQUIRE_INFERENCE_AUTH` - Require auth for OpenAI endpoints (true/false)
|
||||||
- `LLAMACTL_INFERENCE_KEYS` - Comma-separated inference API keys
|
|
||||||
- `LLAMACTL_REQUIRE_MANAGEMENT_AUTH` - Require auth for management endpoints (true/false)
|
- `LLAMACTL_REQUIRE_MANAGEMENT_AUTH` - Require auth for management endpoints (true/false)
|
||||||
- `LLAMACTL_MANAGEMENT_KEYS` - Comma-separated management API keys
|
- `LLAMACTL_MANAGEMENT_KEYS` - Comma-separated management API keys
|
||||||
|
|
||||||
|
|||||||
37
docs/docs.go
37
docs/docs.go
@@ -2063,20 +2063,19 @@ const docTemplate = `{
|
|||||||
"server.CreateKeyRequest": {
|
"server.CreateKeyRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"expiresAt": {
|
"expires_at": {
|
||||||
"type": "integer",
|
"type": "integer"
|
||||||
"format": "int64"
|
|
||||||
},
|
},
|
||||||
"instancePermissions": {
|
"instance_ids": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/server.InstancePermission"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"permissionMode": {
|
"permission_mode": {
|
||||||
"$ref": "#/definitions/auth.PermissionMode"
|
"$ref": "#/definitions/auth.PermissionMode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2087,9 +2086,6 @@ const docTemplate = `{
|
|||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -2116,29 +2112,9 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server.InstancePermission": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"can_infer": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"can_view_logs": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"instance_id": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"server.KeyPermissionResponse": {
|
"server.KeyPermissionResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"can_infer": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"can_view_logs": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"instance_id": {
|
"instance_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -2153,9 +2129,6 @@ const docTemplate = `{
|
|||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ Before you start, let's clarify a few key terms:
|
|||||||
|
|
||||||
Llamactl uses two types of API keys:
|
Llamactl uses two types of API keys:
|
||||||
|
|
||||||
- **Management API Key**: Used to authenticate with the Llamactl management API (creating, starting, stopping instances).
|
- **Management API Key**: Used to authenticate with the Llamactl management API and web UI. If not configured, one is auto-generated at startup and printed to the terminal.
|
||||||
- **Inference API Key**: Used to authenticate requests to the OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/completions`, etc.).
|
- **Inference API Key**: Used to authenticate requests to the OpenAI-compatible endpoints (`/v1/chat/completions`, `/v1/completions`, etc.). These are created and managed via the web UI.
|
||||||
|
|
||||||
By default, authentication is required. If you don't configure these keys in your configuration file, llamactl will auto-generate them and print them to the terminal on startup. You can also configure custom keys or disable authentication entirely in the [Configuration](configuration.md) guide.
|
By default, authentication is required for both management and inference endpoints. You can configure custom management keys or disable authentication in the [Configuration](configuration.md) guide.
|
||||||
|
|
||||||
## Start Llamactl
|
## Start Llamactl
|
||||||
|
|
||||||
@@ -38,24 +38,17 @@ llamactl
|
|||||||
|
|
||||||
sk-management-...
|
sk-management-...
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
⚠️ INFERENCE AUTHENTICATION REQUIRED
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
🔑 Generated Inference API Key:
|
|
||||||
|
|
||||||
sk-inference-...
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
⚠️ IMPORTANT
|
⚠️ IMPORTANT
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
• These keys are auto-generated and will change on restart
|
• This key is auto-generated and will change on restart
|
||||||
• For production, add explicit keys to your configuration
|
• For production, add explicit management_keys to your configuration
|
||||||
• Copy these keys before they disappear from the terminal
|
• Copy this key before it disappears from the terminal
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
Llamactl server listening on 0.0.0.0:8080
|
Llamactl server listening on 0.0.0.0:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy the **Management** and **Inference** API Keys from the terminal - you'll need them to access the web UI and make inference requests.
|
Copy the **Management API Key** from the terminal - you'll need it to access the web UI.
|
||||||
|
|
||||||
By default, Llamactl will start on `http://localhost:8080`.
|
By default, Llamactl will start on `http://localhost:8080`.
|
||||||
|
|
||||||
@@ -82,7 +75,7 @@ You should see the Llamactl web interface.
|
|||||||
- **Additional Options**: Backend-specific parameters
|
- **Additional Options**: Backend-specific parameters
|
||||||
|
|
||||||
!!! tip "Auto-Assignment"
|
!!! tip "Auto-Assignment"
|
||||||
Llamactl automatically assigns ports from the configured port range (default: 8000-9000) and generates API keys if authentication is enabled. You typically don't need to manually specify these values.
|
Llamactl automatically assigns ports from the configured port range (default: 8000-9000) and manages API keys if authentication is enabled. You typically don't need to manually specify these values.
|
||||||
|
|
||||||
!!! note "Remote Node Deployment"
|
!!! note "Remote Node Deployment"
|
||||||
If you have configured remote nodes in your configuration file, you can select which node to deploy the instance to. This allows you to distribute instances across multiple machines. See the [Configuration](configuration.md#remote-node-configuration) guide for details on setting up remote nodes.
|
If you have configured remote nodes in your configuration file, you can select which node to deploy the instance to. This allows you to distribute instances across multiple machines. See the [Configuration](configuration.md#remote-node-configuration) guide for details on setting up remote nodes.
|
||||||
@@ -98,6 +91,24 @@ Once created, you can:
|
|||||||
- **View logs** by clicking the logs button
|
- **View logs** by clicking the logs button
|
||||||
- **Stop** the instance when needed
|
- **Stop** the instance when needed
|
||||||
|
|
||||||
|
## Create an Inference API Key
|
||||||
|
|
||||||
|
To make inference requests to your instances, you'll need an inference API key:
|
||||||
|
|
||||||
|
1. In the web UI, click the **Settings** icon (gear icon in the top-right)
|
||||||
|
2. Navigate to the **API Keys** tab
|
||||||
|
3. Click **Create API Key**
|
||||||
|
4. Configure your key:
|
||||||
|
- **Name**: Give it a descriptive name (e.g., "Production Key", "Development Key")
|
||||||
|
- **Expiration**: Optionally set an expiration date for the key
|
||||||
|
- **Permissions**: Choose whether the key can access all instances or only specific ones
|
||||||
|
5. Click **Create**
|
||||||
|
6. **Copy the generated key** - it will only be shown once!
|
||||||
|
|
||||||
|
The key will look like: `llamactl-...`
|
||||||
|
|
||||||
|
You can create multiple inference keys with different permissions for different use cases (e.g., one for development, one for production, or keys limited to specific instances).
|
||||||
|
|
||||||
## Example Configurations
|
## Example Configurations
|
||||||
|
|
||||||
Here are basic example configurations for each backend:
|
Here are basic example configurations for each backend:
|
||||||
@@ -246,7 +257,7 @@ print(response.choices[0].message.content)
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! note "API Key"
|
!!! note "API Key"
|
||||||
If you disabled authentication in your config, you can use any value for `api_key` (e.g., `"not-needed"`). Otherwise, use the inference API key shown in the terminal output on startup.
|
If you disabled authentication in your config, you can use any value for `api_key` (e.g., `"not-needed"`). Otherwise, use the inference API key you created via the web UI (Settings → API Keys).
|
||||||
|
|
||||||
### List Available Models
|
### List Available Models
|
||||||
|
|
||||||
|
|||||||
@@ -2056,20 +2056,19 @@
|
|||||||
"server.CreateKeyRequest": {
|
"server.CreateKeyRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"expiresAt": {
|
"expires_at": {
|
||||||
"type": "integer",
|
"type": "integer"
|
||||||
"format": "int64"
|
|
||||||
},
|
},
|
||||||
"instancePermissions": {
|
"instance_ids": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/server.InstancePermission"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"permissionMode": {
|
"permission_mode": {
|
||||||
"$ref": "#/definitions/auth.PermissionMode"
|
"$ref": "#/definitions/auth.PermissionMode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2080,9 +2079,6 @@
|
|||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -2109,29 +2105,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"server.InstancePermission": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"can_infer": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"can_view_logs": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"instance_id": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"server.KeyPermissionResponse": {
|
"server.KeyPermissionResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"can_infer": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"can_view_logs": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"instance_id": {
|
"instance_id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@@ -2146,9 +2122,6 @@
|
|||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -232,24 +232,21 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
server.CreateKeyRequest:
|
server.CreateKeyRequest:
|
||||||
properties:
|
properties:
|
||||||
expiresAt:
|
expires_at:
|
||||||
format: int64
|
|
||||||
type: integer
|
type: integer
|
||||||
instancePermissions:
|
instance_ids:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/server.InstancePermission'
|
type: integer
|
||||||
type: array
|
type: array
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
permissionMode:
|
permission_mode:
|
||||||
$ref: '#/definitions/auth.PermissionMode'
|
$ref: '#/definitions/auth.PermissionMode'
|
||||||
type: object
|
type: object
|
||||||
server.CreateKeyResponse:
|
server.CreateKeyResponse:
|
||||||
properties:
|
properties:
|
||||||
created_at:
|
created_at:
|
||||||
type: integer
|
type: integer
|
||||||
enabled:
|
|
||||||
type: boolean
|
|
||||||
expires_at:
|
expires_at:
|
||||||
type: integer
|
type: integer
|
||||||
id:
|
id:
|
||||||
@@ -267,21 +264,8 @@ definitions:
|
|||||||
user_id:
|
user_id:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
server.InstancePermission:
|
|
||||||
properties:
|
|
||||||
can_infer:
|
|
||||||
type: boolean
|
|
||||||
can_view_logs:
|
|
||||||
type: boolean
|
|
||||||
instance_id:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
server.KeyPermissionResponse:
|
server.KeyPermissionResponse:
|
||||||
properties:
|
properties:
|
||||||
can_infer:
|
|
||||||
type: boolean
|
|
||||||
can_view_logs:
|
|
||||||
type: boolean
|
|
||||||
instance_id:
|
instance_id:
|
||||||
type: integer
|
type: integer
|
||||||
instance_name:
|
instance_name:
|
||||||
@@ -291,8 +275,6 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
created_at:
|
created_at:
|
||||||
type: integer
|
type: integer
|
||||||
enabled:
|
|
||||||
type: boolean
|
|
||||||
expires_at:
|
expires_at:
|
||||||
type: integer
|
type: integer
|
||||||
id:
|
id:
|
||||||
|
|||||||
@@ -115,15 +115,15 @@ vllm serve microsoft/DialoGPT-medium --port 8081
|
|||||||
require_inference_auth: false
|
require_inference_auth: false
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Configure API keys:**
|
2. **Configure management API keys:**
|
||||||
```yaml
|
```yaml
|
||||||
auth:
|
auth:
|
||||||
management_keys:
|
management_keys:
|
||||||
- "your-management-key"
|
- "your-management-key"
|
||||||
inference_keys:
|
|
||||||
- "your-inference-key"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For inference API keys, create them via the web UI (Settings → API Keys) after logging in with your management key.
|
||||||
|
|
||||||
3. **Use correct Authorization header:**
|
3. **Use correct Authorization header:**
|
||||||
```bash
|
```bash
|
||||||
curl -H "Authorization: Bearer your-api-key" \
|
curl -H "Authorization: Bearer your-api-key" \
|
||||||
|
|||||||
136
test_client.py
Normal file
136
test_client.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple Python script to interact with local LLM server's OpenAI-compatible API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Local LLM server configuration
|
||||||
|
BASE_URL = "http://localhost:8080"
|
||||||
|
API_KEY = None
|
||||||
|
MODEL_NAME = None
|
||||||
|
|
||||||
|
def get_models():
|
||||||
|
"""Fetch available models from /v1/models endpoint"""
|
||||||
|
headers = {}
|
||||||
|
if API_KEY:
|
||||||
|
headers["Authorization"] = f"Bearer {API_KEY}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/v1/models", headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["data"]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching models: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def send_message(message):
|
||||||
|
"""
|
||||||
|
Send a message to local LLM server API
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): The message to send
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The AI response or error message
|
||||||
|
"""
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
if API_KEY:
|
||||||
|
headers["Authorization"] = f"Bearer {API_KEY}"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"model": MODEL_NAME,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": message
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 1000,
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(f"{BASE_URL}/v1/chat/completions", headers=headers, json=data, timeout=60)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
def interactive_mode():
|
||||||
|
"""Run in interactive mode for continuous conversation"""
|
||||||
|
global BASE_URL, API_KEY, MODEL_NAME
|
||||||
|
|
||||||
|
# Get base URL
|
||||||
|
url_input = input(f"Base URL [{BASE_URL}]: ").strip()
|
||||||
|
if url_input:
|
||||||
|
BASE_URL = url_input
|
||||||
|
|
||||||
|
# Get API key (optional)
|
||||||
|
key_input = input("API key (optional): ").strip()
|
||||||
|
if key_input:
|
||||||
|
API_KEY = key_input
|
||||||
|
|
||||||
|
# Fetch and select model
|
||||||
|
models = get_models()
|
||||||
|
if not models:
|
||||||
|
print("No models available. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\nAvailable models:")
|
||||||
|
for i, m in enumerate(models, 1):
|
||||||
|
print(f"{i}. {m['id']}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
selection = int(input("\nSelect model: "))
|
||||||
|
if 1 <= selection <= len(models):
|
||||||
|
MODEL_NAME = models[selection - 1]["id"]
|
||||||
|
break
|
||||||
|
print(f"Please enter a number between 1 and {len(models)}")
|
||||||
|
except ValueError:
|
||||||
|
print("Please enter a valid number")
|
||||||
|
|
||||||
|
print(f"\nUsing model: {MODEL_NAME}")
|
||||||
|
print("Type 'quit' or 'exit' to stop")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("\nYou: ").strip()
|
||||||
|
|
||||||
|
if user_input.lower() in ['quit', 'exit', 'q']:
|
||||||
|
print("Goodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("AI: ", end="", flush=True)
|
||||||
|
response = send_message(user_input)
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
print("\nGoodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
# Single message mode
|
||||||
|
message = " ".join(sys.argv[1:])
|
||||||
|
response = send_message(message)
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
# Interactive mode
|
||||||
|
interactive_mode()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
197
webui/package-lock.json
generated
197
webui/package-lock.json
generated
@@ -9,11 +9,11 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -31,7 +31,6 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/eslint__js": "^9.14.0",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.4",
|
"@types/react": "^19.2.4",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -1250,21 +1249,21 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/primitive": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-checkbox": {
|
"node_modules/@radix-ui/react-checkbox": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
|
||||||
"integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
|
"integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.2",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.2",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-presence": "1.1.4",
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
"@radix-ui/react-use-previous": "1.1.1",
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
@@ -1311,6 +1310,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -1342,20 +1359,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dialog": {
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
"version": "1.1.14",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||||
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
|
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.2",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.2",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
"@radix-ui/react-focus-guards": "1.1.2",
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
"@radix-ui/react-focus-scope": "1.1.7",
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
"@radix-ui/react-id": "1.1.1",
|
"@radix-ui/react-id": "1.1.1",
|
||||||
"@radix-ui/react-portal": "1.1.9",
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
"@radix-ui/react-presence": "1.1.4",
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-slot": "1.2.3",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
@@ -1377,6 +1394,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-direction": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
@@ -1393,12 +1428,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.10",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.2",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
@@ -1420,9 +1455,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-focus-guards": {
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||||
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
|
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1478,12 +1513,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-label": {
|
"node_modules/@radix-ui/react-label": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
|
||||||
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
|
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-primitive": "2.1.3"
|
"@radix-ui/react-primitive": "2.1.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
@@ -1525,9 +1583,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-presence": {
|
"node_modules/@radix-ui/react-presence": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
@@ -1571,6 +1629,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-radio-group": {
|
"node_modules/@radix-ui/react-radio-group": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
|
||||||
@@ -1603,36 +1679,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": {
|
|
||||||
"version": "1.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
|
||||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-roving-focus": {
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||||
@@ -1664,16 +1710,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.2"
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
@@ -2557,17 +2597,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/eslint__js": {
|
|
||||||
"version": "9.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-9.14.0.tgz",
|
|
||||||
"integrity": "sha512-s0jepCjOJWB/GKcuba4jISaVpBudw3ClXJ3fUK4tugChUMQsp6kSwuA8Dcx6wFd/JsJqcY8n4rEpa5RTHs5ypA==",
|
|
||||||
"deprecated": "This is a stub types definition. @eslint/js provides its own type definitions, so you do not need this installed.",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint/js": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -40,7 +40,6 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/eslint__js": "^9.14.0",
|
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.4",
|
"@types/react": "^19.2.4",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import userEvent from '@testing-library/user-event'
|
|||||||
import InstanceList from '@/components/InstanceList'
|
import InstanceList from '@/components/InstanceList'
|
||||||
import { InstancesProvider } from '@/contexts/InstancesContext'
|
import { InstancesProvider } from '@/contexts/InstancesContext'
|
||||||
import { instancesApi } from '@/lib/api'
|
import { instancesApi } from '@/lib/api'
|
||||||
import type { Instance } from '@/types/instance'
|
import { BackendType, type Instance } from '@/types/instance'
|
||||||
import { BackendType } from '@/types/instance'
|
|
||||||
import { AuthProvider } from '@/contexts/AuthContext'
|
import { AuthProvider } from '@/contexts/AuthContext'
|
||||||
|
|
||||||
// Mock the API
|
// Mock the API
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function CreateApiKeyDialog({ open, onOpenChange, onKeyCreated }: CreateApiKeyDi
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create API Key</DialogTitle>
|
<DialogTitle>Create API Key</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-4">
|
||||||
{error && (
|
{error && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>{error}</AlertDescription>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({
|
|||||||
// Reset to single empty row if value is explicitly undefined/null
|
// Reset to single empty row if value is explicitly undefined/null
|
||||||
setPairs([{ key: '', value: '' }])
|
setPairs([{ key: '', value: '' }])
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
// Update parent component when pairs change
|
// Update parent component when pairs change
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import NumberInput from '@/components/form/NumberInput'
|
|||||||
|
|
||||||
interface AutoRestartConfigurationProps {
|
interface AutoRestartConfigurationProps {
|
||||||
formData: CreateInstanceOptions
|
formData: CreateInstanceOptions
|
||||||
onChange: (key: keyof CreateInstanceOptions, value: any) => void
|
onChange: <K extends keyof CreateInstanceOptions>(key: K, value: CreateInstanceOptions[K]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutoRestartConfiguration: React.FC<AutoRestartConfigurationProps> = ({
|
const AutoRestartConfiguration: React.FC<AutoRestartConfigurationProps> = ({
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import type { CreateInstanceOptions } from '@/types/instance'
|
|||||||
import { getBasicBackendFields, getAdvancedBackendFields } from '@/lib/zodFormUtils'
|
import { getBasicBackendFields, getAdvancedBackendFields } from '@/lib/zodFormUtils'
|
||||||
import BackendFormField from '@/components/BackendFormField'
|
import BackendFormField from '@/components/BackendFormField'
|
||||||
|
|
||||||
|
type BackendFieldValue = string | number | boolean | string[] | Record<string, string> | undefined
|
||||||
|
|
||||||
interface BackendConfigurationProps {
|
interface BackendConfigurationProps {
|
||||||
formData: CreateInstanceOptions
|
formData: CreateInstanceOptions
|
||||||
onBackendFieldChange: (key: string, value: any) => void
|
onBackendFieldChange: (key: string, value: BackendFieldValue) => void
|
||||||
showAdvanced?: boolean
|
showAdvanced?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ const BackendConfiguration: React.FC<BackendConfigurationProps> = ({
|
|||||||
<BackendFormField
|
<BackendFormField
|
||||||
key={fieldKey}
|
key={fieldKey}
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
value={(formData.backend_options as any)?.[fieldKey]}
|
value={(formData.backend_options as Record<string, BackendFieldValue> | undefined)?.[fieldKey]}
|
||||||
onChange={onBackendFieldChange}
|
onChange={onBackendFieldChange}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -41,7 +43,7 @@ const BackendConfiguration: React.FC<BackendConfigurationProps> = ({
|
|||||||
<BackendFormField
|
<BackendFormField
|
||||||
key={fieldKey}
|
key={fieldKey}
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
value={(formData.backend_options as any)?.[fieldKey]}
|
value={(formData.backend_options as Record<string, BackendFieldValue> | undefined)?.[fieldKey]}
|
||||||
onChange={onBackendFieldChange}
|
onChange={onBackendFieldChange}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -53,7 +55,7 @@ const BackendConfiguration: React.FC<BackendConfigurationProps> = ({
|
|||||||
<BackendFormField
|
<BackendFormField
|
||||||
key="extra_args"
|
key="extra_args"
|
||||||
fieldKey="extra_args"
|
fieldKey="extra_args"
|
||||||
value={(formData.backend_options as any)?.extra_args}
|
value={(formData.backend_options as Record<string, BackendFieldValue> | undefined)?.extra_args}
|
||||||
onChange={onBackendFieldChange}
|
onChange={onBackendFieldChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Trash2, Copy, Check, X, ChevronDown, ChevronRight } from "lucide-react";
|
import { Trash2, Copy, Check, X, ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { apiKeysApi } from "@/lib/api";
|
import { apiKeysApi } from "@/lib/api";
|
||||||
import { ApiKey, KeyPermissionResponse, PermissionMode } from "@/types/apiKey";
|
import { type ApiKey, type KeyPermissionResponse, PermissionMode } from "@/types/apiKey";
|
||||||
import CreateApiKeyDialog from "@/components/apikeys/CreateApiKeyDialog";
|
import CreateApiKeyDialog from "@/components/apikeys/CreateApiKeyDialog";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ function ApiKeysSection() {
|
|||||||
const [loadingPermissions, setLoadingPermissions] = useState<Record<number, boolean>>({});
|
const [loadingPermissions, setLoadingPermissions] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchKeys();
|
void fetchKeys();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchKeys = async () => {
|
const fetchKeys = async () => {
|
||||||
@@ -52,7 +52,7 @@ function ApiKeysSection() {
|
|||||||
|
|
||||||
const handleKeyCreated = (plainTextKey: string) => {
|
const handleKeyCreated = (plainTextKey: string) => {
|
||||||
setNewKeyPlainText(plainTextKey);
|
setNewKeyPlainText(plainTextKey);
|
||||||
fetchKeys();
|
void fetchKeys();
|
||||||
setCreateDialogOpen(false);
|
setCreateDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ function ApiKeysSection() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await apiKeysApi.delete(id);
|
await apiKeysApi.delete(id);
|
||||||
fetchKeys();
|
void fetchKeys();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err instanceof Error ? err.message : "Failed to delete API key");
|
alert(err instanceof Error ? err.message : "Failed to delete API key");
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ function ApiKeysSection() {
|
|||||||
} else {
|
} else {
|
||||||
setExpandedRowId(key.id);
|
setExpandedRowId(key.id);
|
||||||
if (key.permission_mode === PermissionMode.PerInstance) {
|
if (key.permission_mode === PermissionMode.PerInstance) {
|
||||||
fetchPermissions(key.id);
|
void fetchPermissions(key.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -136,7 +136,7 @@ function ApiKeysSection() {
|
|||||||
<code className="flex-1 p-3 bg-white dark:bg-gray-900 border border-green-300 dark:border-green-800 rounded font-mono text-sm break-all">
|
<code className="flex-1 p-3 bg-white dark:bg-gray-900 border border-green-300 dark:border-green-800 rounded font-mono text-sm break-all">
|
||||||
{newKeyPlainText}
|
{newKeyPlainText}
|
||||||
</code>
|
</code>
|
||||||
<Button onClick={handleCopyKey} variant="outline" size="sm">
|
<Button onClick={() => void handleCopyKey()} variant="outline" size="sm">
|
||||||
{copiedKey ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
{copiedKey ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +216,7 @@ function ApiKeysSection() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteKey(key.id, key.name);
|
void handleDeleteKey(key.id, key.name);
|
||||||
}}
|
}}
|
||||||
title="Delete key"
|
title="Delete key"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import ApiKeysSection from "./ApiKeysSection";
|
import ApiKeysSection from "./ApiKeysSection";
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
@@ -12,6 +12,9 @@ function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|||||||
<DialogContent className="sm:max-w-5xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Settings</DialogTitle>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Manage your application settings and API keys.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<ApiKeysSection />
|
<ApiKeysSection />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border p-4",
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-background text-foreground",
|
default: "bg-card text-card-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -19,41 +19,48 @@ const alertVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
function Alert({
|
||||||
HTMLDivElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
variant,
|
||||||
>(({ className, variant, ...props }, ref) => (
|
...props
|
||||||
<div
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
ref={ref}
|
return (
|
||||||
role="alert"
|
<div
|
||||||
className={cn(alertVariants({ variant }), className)}
|
data-slot="alert"
|
||||||
{...props}
|
role="alert"
|
||||||
/>
|
className={cn(alertVariants({ variant }), className)}
|
||||||
))
|
{...props}
|
||||||
Alert.displayName = "Alert"
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
HTMLParagraphElement,
|
return (
|
||||||
React.HTMLAttributes<HTMLHeadingElement>
|
<div
|
||||||
>(({ className, ...props }, ref) => (
|
data-slot="alert-title"
|
||||||
<h5
|
className={cn(
|
||||||
ref={ref}
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
className
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
))
|
/>
|
||||||
AlertTitle.displayName = "AlertTitle"
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
function AlertDescription({
|
||||||
HTMLParagraphElement,
|
className,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
...props
|
||||||
>(({ className, ...props }, ref) => (
|
}: React.ComponentProps<"div">) {
|
||||||
<div
|
return (
|
||||||
ref={ref}
|
<div
|
||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
data-slot="alert-description"
|
||||||
{...props}
|
className={cn(
|
||||||
/>
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
))
|
className
|
||||||
AlertDescription.displayName = "AlertDescription"
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
@@ -26,6 +25,8 @@ const buttonVariants = cva(
|
|||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
icon: "size-9",
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function Checkbox({
|
|||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
data-slot="checkbox-indicator"
|
data-slot="checkbox-indicator"
|
||||||
className="flex items-center justify-center text-current transition-none"
|
className="grid place-content-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<CheckIcon className="size-3.5" />
|
<CheckIcon className="size-3.5" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -1,42 +1,43 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
import { Circle } from "lucide-react"
|
import { CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const RadioGroup = React.forwardRef<
|
function RadioGroup({
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Root
|
<RadioGroupPrimitive.Root
|
||||||
className={cn("grid gap-2", className)}
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const RadioGroupItem = React.forwardRef<
|
function RadioGroupItem({
|
||||||
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
className,
|
||||||
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
...props
|
||||||
>(({ className, ...props }, ref) => {
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
return (
|
return (
|
||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
ref={ref}
|
data-slot="radio-group-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
<RadioGroupPrimitive.Indicator
|
||||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
</RadioGroupPrimitive.Indicator>
|
</RadioGroupPrimitive.Indicator>
|
||||||
</RadioGroupPrimitive.Item>
|
</RadioGroupPrimitive.Item>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
export { RadioGroup, RadioGroupItem }
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { render, screen, waitFor } from "@testing-library/react";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { InstancesProvider, useInstances } from "@/contexts/InstancesContext";
|
import { InstancesProvider, useInstances } from "@/contexts/InstancesContext";
|
||||||
import { instancesApi } from "@/lib/api";
|
import { instancesApi } from "@/lib/api";
|
||||||
import type { Instance } from "@/types/instance";
|
import { BackendType, type Instance } from "@/types/instance";
|
||||||
import { BackendType } from "@/types/instance";
|
|
||||||
import { AuthProvider } from "../AuthContext";
|
import { AuthProvider } from "../AuthContext";
|
||||||
|
|
||||||
// Mock the API module
|
// Mock the API module
|
||||||
@@ -71,37 +70,37 @@ function TestComponent() {
|
|||||||
|
|
||||||
{/* Action buttons for testing with specific instances */}
|
{/* Action buttons for testing with specific instances */}
|
||||||
<button
|
<button
|
||||||
onClick={() => createInstance("new-instance", { backend_type: BackendType.LLAMA_CPP, backend_options: { model: "test.gguf" } })}
|
onClick={() => void createInstance("new-instance", { backend_type: BackendType.LLAMA_CPP, backend_options: { model: "test.gguf" } })}
|
||||||
data-testid="create-instance"
|
data-testid="create-instance"
|
||||||
>
|
>
|
||||||
Create Instance
|
Create Instance
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateInstance("instance1", { backend_type: BackendType.LLAMA_CPP, backend_options: { model: "updated.gguf" } })}
|
onClick={() => void updateInstance("instance1", { backend_type: BackendType.LLAMA_CPP, backend_options: { model: "updated.gguf" } })}
|
||||||
data-testid="update-instance"
|
data-testid="update-instance"
|
||||||
>
|
>
|
||||||
Update Instance
|
Update Instance
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => startInstance("instance2")}
|
onClick={() => void startInstance("instance2")}
|
||||||
data-testid="start-instance"
|
data-testid="start-instance"
|
||||||
>
|
>
|
||||||
Start Instance2
|
Start Instance2
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => stopInstance("instance1")}
|
onClick={() => void stopInstance("instance1")}
|
||||||
data-testid="stop-instance"
|
data-testid="stop-instance"
|
||||||
>
|
>
|
||||||
Stop Instance1
|
Stop Instance1
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => restartInstance("instance1")}
|
onClick={() => void restartInstance("instance1")}
|
||||||
data-testid="restart-instance"
|
data-testid="restart-instance"
|
||||||
>
|
>
|
||||||
Restart Instance1
|
Restart Instance1
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteInstance("instance2")}
|
onClick={() => void deleteInstance("instance2")}
|
||||||
data-testid="delete-instance"
|
data-testid="delete-instance"
|
||||||
>
|
>
|
||||||
Delete Instance2
|
Delete Instance2
|
||||||
|
|||||||
@@ -156,11 +156,14 @@ class HealthService {
|
|||||||
this.callbacks.set(instanceName, new Set())
|
this.callbacks.set(instanceName, new Set())
|
||||||
}
|
}
|
||||||
|
|
||||||
this.callbacks.get(instanceName)!.add(callback)
|
const callbacks = this.callbacks.get(instanceName)
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.add(callback)
|
||||||
|
|
||||||
// Start health checking if this is the first subscriber
|
// Start health checking if this is the first subscriber
|
||||||
if (this.callbacks.get(instanceName)!.size === 1) {
|
if (callbacks.size === 1) {
|
||||||
this.startHealthCheck(instanceName)
|
this.startHealthCheck(instanceName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return unsubscribe function
|
// Return unsubscribe function
|
||||||
@@ -214,22 +217,24 @@ class HealthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start new interval with appropriate timing
|
// Start new interval with appropriate timing
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(() => {
|
||||||
try {
|
void (async () => {
|
||||||
const health = await this.performHealthCheck(instanceName)
|
try {
|
||||||
this.notifyCallbacks(instanceName, health)
|
const health = await this.performHealthCheck(instanceName)
|
||||||
|
this.notifyCallbacks(instanceName, health)
|
||||||
|
|
||||||
// Check if state changed and adjust interval
|
// Check if state changed and adjust interval
|
||||||
const previousState = this.lastHealthState.get(instanceName)
|
const previousState = this.lastHealthState.get(instanceName)
|
||||||
this.lastHealthState.set(instanceName, health.state)
|
this.lastHealthState.set(instanceName, health.state)
|
||||||
|
|
||||||
if (previousState !== health.state) {
|
if (previousState !== health.state) {
|
||||||
this.adjustPollingInterval(instanceName, health.state)
|
this.adjustPollingInterval(instanceName, health.state)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Health check failed for ${instanceName}:`, error)
|
||||||
|
// Continue polling even on error
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})()
|
||||||
console.error(`Health check failed for ${instanceName}:`, error)
|
|
||||||
// Continue polling even on error
|
|
||||||
}
|
|
||||||
}, pollInterval)
|
}, pollInterval)
|
||||||
|
|
||||||
this.intervals.set(instanceName, interval)
|
this.intervals.set(instanceName, interval)
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import './index.css'
|
|||||||
import { AuthProvider } from './contexts/AuthContext'
|
import { AuthProvider } from './contexts/AuthContext'
|
||||||
import { ConfigProvider } from './contexts/ConfigContext'
|
import { ConfigProvider } from './contexts/ConfigContext'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
const rootElement = document.getElementById('root')
|
||||||
|
if (!rootElement) throw new Error('Failed to find the root element')
|
||||||
|
|
||||||
|
ReactDOM.createRoot(rootElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
|
|||||||
@@ -1,10 +1,44 @@
|
|||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import { afterEach, vi } from 'vitest'
|
import { afterEach, beforeEach } from 'vitest'
|
||||||
|
|
||||||
// Mock fetch globally since your app uses fetch
|
// Create a working localStorage implementation for tests
|
||||||
global.fetch = vi.fn()
|
// This ensures localStorage works in both CLI and VSCode test runner
|
||||||
|
class LocalStorageMock implements Storage {
|
||||||
|
private store: Map<string, string> = new Map()
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.store.size
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.store.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(key: string): string | null {
|
||||||
|
return this.store.get(key) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
key(index: number): string | null {
|
||||||
|
return Array.from(this.store.keys())[index] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key: string): void {
|
||||||
|
this.store.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key: string, value: string): void {
|
||||||
|
this.store.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace global localStorage
|
||||||
|
global.localStorage = new LocalStorageMock()
|
||||||
|
|
||||||
|
// Clean up before each test
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
// Clean up after each test
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks()
|
localStorage.clear()
|
||||||
})
|
})
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user