Changeset 0.21.6 (#92)
This commit is contained in:
@@ -25,6 +25,11 @@ type Config struct {
|
||||
// Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2"
|
||||
SeedUsers string
|
||||
|
||||
// Seed providers (dev/test only) — CSV: "provider:api_key[:name]"
|
||||
// Shortcuts: "openai:sk-xxx", "anthropic:sk-ant-xxx", "openrouter:sk-or-xxx"
|
||||
// Skips if provider with same name exists (idempotent on restart).
|
||||
SeedProviders string
|
||||
|
||||
// API key encryption (required for v0.9.4+)
|
||||
// Used to derive AES-256 key for global/team provider API keys.
|
||||
// Personal keys use per-user UEK (derived from password).
|
||||
@@ -76,6 +81,7 @@ func Load() *Config {
|
||||
AdminPassword: getEnv("SWITCHBOARD_ADMIN_PASSWORD", ""),
|
||||
AdminEmail: getEnv("SWITCHBOARD_ADMIN_EMAIL", ""),
|
||||
SeedUsers: getEnv("SEED_USERS", ""),
|
||||
SeedProviders: getEnv("SEED_PROVIDERS", ""),
|
||||
EncryptionKey: getEnv("ENCRYPTION_KEY", ""),
|
||||
|
||||
StorageBackend: getEnv("STORAGE_BACKEND", ""),
|
||||
|
||||
@@ -297,12 +297,12 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
||||
// Read back the row
|
||||
err = database.DB.QueryRow(`
|
||||
SELECT id, user_id, title, type, description, model, provider_config_id,
|
||||
system_prompt, is_archived, is_pinned, folder, project_id,
|
||||
system_prompt, is_archived, is_pinned, folder, project_id, workspace_id,
|
||||
tags, settings,
|
||||
created_at, updated_at
|
||||
FROM channels WHERE id = ?`, id).Scan(
|
||||
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
|
||||
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID,
|
||||
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
||||
scanTags(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -314,12 +314,12 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) {
|
||||
INSERT INTO channels (user_id, title, type, description, model, system_prompt, provider_config_id, folder, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, user_id, title, type, description, model, provider_config_id, system_prompt,
|
||||
is_archived, is_pinned, folder, project_id, tags, settings, created_at, updated_at
|
||||
is_archived, is_pinned, folder, project_id, workspace_id, tags, settings, created_at, updated_at
|
||||
`, userID, req.Title, channelType, req.Description, req.Model, req.SystemPrompt, req.APIConfigID,
|
||||
req.Folder, pq.Array(req.Tags),
|
||||
).Scan(
|
||||
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
|
||||
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID,
|
||||
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
|
||||
pq.Array(&tags), &ch.Settings, &ch.CreatedAt, &ch.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
141
server/handlers/seed_providers.go
Normal file
141
server/handlers/seed_providers.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/config"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// Known provider default endpoints for seeding.
|
||||
var seedDefaultEndpoints = map[string]string{
|
||||
"openai": "https://api.openai.com/v1",
|
||||
"anthropic": "https://api.anthropic.com",
|
||||
"openrouter": "https://openrouter.ai/api/v1",
|
||||
"venice": "https://api.venice.ai/api/v1",
|
||||
"mistral": "https://api.mistral.ai/v1",
|
||||
"groq": "https://api.groq.com/openai/v1",
|
||||
"together": "https://api.together.xyz/v1",
|
||||
"fireworks": "https://api.fireworks.ai/inference/v1",
|
||||
"deepseek": "https://api.deepseek.com/v1",
|
||||
"perplexity": "https://api.perplexity.ai",
|
||||
}
|
||||
|
||||
// SeedProviders creates global providers from the SEED_PROVIDERS env var.
|
||||
//
|
||||
// Format: "provider:api_key[:name],provider:api_key[:name]"
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// SEED_PROVIDERS="openai:sk-xxx,anthropic:sk-ant-xxx"
|
||||
// SEED_PROVIDERS="openai:sk-xxx:My OpenAI,openrouter:sk-or-xxx"
|
||||
//
|
||||
// Idempotent: skips if a global provider with the same name already exists.
|
||||
// Blocked in production.
|
||||
func SeedProviders(cfg *config.Config, stores store.Stores, resolver *crypto.KeyResolver) {
|
||||
if cfg.SeedProviders == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.Environment == "production" {
|
||||
log.Printf("⚠ SEED_PROVIDERS ignored in production environment")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
entries := strings.Split(cfg.SeedProviders, ",")
|
||||
log.Printf(" 🌱 SEED_PROVIDERS: %d entries to process", len(entries))
|
||||
|
||||
for _, entry := range entries {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(entry, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
log.Printf("⚠ Seed provider skipped (bad format, want provider:key[:name]): %q", entry)
|
||||
continue
|
||||
}
|
||||
|
||||
provider := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
apiKey := strings.TrimSpace(parts[1])
|
||||
|
||||
// Derive name: explicit or "OpenAI (seed)", "Anthropic (seed)", etc.
|
||||
name := ""
|
||||
if len(parts) >= 3 {
|
||||
name = strings.TrimSpace(parts[2])
|
||||
}
|
||||
if name == "" {
|
||||
// Capitalize provider name
|
||||
name = strings.ToUpper(provider[:1]) + provider[1:] + " (seed)"
|
||||
}
|
||||
|
||||
// Resolve endpoint
|
||||
endpoint, ok := seedDefaultEndpoints[provider]
|
||||
if !ok {
|
||||
// Unknown provider — treat as openai-compatible with custom endpoint
|
||||
log.Printf("⚠ Seed provider '%s': unknown provider, skipping (no default endpoint)", provider)
|
||||
continue
|
||||
}
|
||||
|
||||
if apiKey == "" {
|
||||
log.Printf("⚠ Seed provider '%s': empty API key, skipping", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if already exists (idempotent)
|
||||
existing, _ := stores.Providers.ListGlobal(ctx)
|
||||
found := false
|
||||
for _, p := range existing {
|
||||
if p.Name == name || (p.Provider == provider && p.Scope == models.ScopeGlobal) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
log.Printf(" 🌱 Seed provider '%s' already exists, skipping", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create provider config
|
||||
pcfg := &models.ProviderConfig{
|
||||
Name: name,
|
||||
Provider: provider,
|
||||
Endpoint: endpoint,
|
||||
Scope: models.ScopeGlobal,
|
||||
KeyScope: models.ScopeGlobal,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Encrypt API key
|
||||
if resolver != nil {
|
||||
enc, nonce, err := resolver.EncryptForScope(apiKey, "global", "")
|
||||
if err != nil {
|
||||
log.Printf("⚠ Seed provider '%s': encrypt failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
pcfg.APIKeyEnc = enc
|
||||
pcfg.KeyNonce = nonce
|
||||
} else {
|
||||
pcfg.APIKeyEnc = []byte(apiKey)
|
||||
}
|
||||
|
||||
if err := stores.Providers.Create(ctx, pcfg); err != nil {
|
||||
log.Printf("⚠ Seed provider '%s': create failed: %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Auto-fetch and enable models
|
||||
result, err := syncAndEnableProviderModels(ctx, stores, pcfg, apiKey)
|
||||
if err != nil {
|
||||
log.Printf(" 🌱 Seed provider '%s' created (model fetch failed: %v)", name, err)
|
||||
} else {
|
||||
log.Printf(" 🌱 Seed provider '%s' created (%d models fetched)", name, result.Total)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,9 @@ func main() {
|
||||
// Seed additional users from env (dev/test only, skipped in production)
|
||||
handlers.SeedUsers(cfg, stores)
|
||||
|
||||
// Seed providers from env (dev/test only, skipped in production)
|
||||
handlers.SeedProviders(cfg, stores, keyResolver)
|
||||
|
||||
// Seed builtin extensions from disk (idempotent, version-aware)
|
||||
handlers.SeedBuiltinExtensions(stores, "extensions/builtin")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user