Changeset 0.29.1 (#196)

This commit is contained in:
2026-03-17 19:32:20 +00:00
parent 5d637d3a90
commit d4de84f3f1
24 changed files with 3117 additions and 28 deletions

132
server/providers/custom.go Normal file
View File

@@ -0,0 +1,132 @@
// Package providers — custom.go
//
// v0.29.1 CS4: Config-file provider types. Registers OpenAI-compatible
// provider types from a JSON file at startup. No code deploy needed to
// add Ollama, LiteLLM, vLLM, or any other OpenAI-compatible endpoint.
//
// File format (array of objects):
//
// [
// {
// "id": "ollama",
// "name": "Ollama",
// "description": "Local Ollama instance",
// "default_endpoint": "http://localhost:11434/v1",
// "api": "openai"
// },
// {
// "id": "litellm",
// "name": "LiteLLM Proxy",
// "description": "LiteLLM unified proxy",
// "default_endpoint": "http://litellm:4000/v1",
// "api": "openai"
// }
// ]
//
// Fields:
// - id: Unique provider type ID (used in provider_configs.provider column)
// - name: Human-readable label for admin UI
// - description: Help text shown in provider creation form
// - default_endpoint: Pre-filled endpoint URL for new configs
// - api: Wire protocol — "openai" (required, only supported value for now)
//
// The api field determines which Provider implementation handles
// requests. Currently only "openai" is supported (covers Ollama,
// LiteLLM, vLLM, LocalAI, and any other OpenAI-compatible API).
// Future: "anthropic" for Anthropic-compatible proxies.
//
// Built-in provider IDs (openai, anthropic, venice, openrouter) cannot
// be overridden by config-file entries — they are silently skipped.
package providers
import (
"encoding/json"
"fmt"
"log"
"os"
)
// CustomProviderType represents one entry in the provider types JSON file.
type CustomProviderType struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
DefaultEndpoint string `json:"default_endpoint"`
API string `json:"api"` // "openai" (required)
}
// builtinIDs is the set of provider IDs registered by Init().
// Config-file entries with these IDs are silently skipped.
var builtinIDs = map[string]bool{
"openai": true,
"anthropic": true,
"venice": true,
"openrouter": true,
}
// LoadCustomTypes reads a JSON file of provider type definitions and
// registers each as a new provider type. Only OpenAI-compatible APIs
// are currently supported (api="openai").
//
// Returns the number of providers registered. Errors are non-fatal —
// individual invalid entries are logged and skipped.
//
// Call after Init() so that built-in types are already registered
// and can be detected for skip logic.
func LoadCustomTypes(path string) (int, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, fmt.Errorf("reading provider types file: %w", err)
}
var entries []CustomProviderType
if err := json.Unmarshal(data, &entries); err != nil {
return 0, fmt.Errorf("parsing provider types file: %w", err)
}
registered := 0
for _, entry := range entries {
// Validate required fields
if entry.ID == "" {
log.Printf("⚠️ custom provider: skipping entry with empty id")
continue
}
if entry.Name == "" {
log.Printf("⚠️ custom provider %q: skipping — name is required", entry.ID)
continue
}
if entry.API == "" {
log.Printf("⚠️ custom provider %q: skipping — api is required", entry.ID)
continue
}
// Skip built-in provider IDs
if builtinIDs[entry.ID] {
log.Printf("⚠️ custom provider %q: skipping — conflicts with built-in provider", entry.ID)
continue
}
// Resolve API implementation
var impl Provider
switch entry.API {
case "openai":
impl = &OpenAIProvider{}
default:
log.Printf("⚠️ custom provider %q: unsupported api %q (only \"openai\" supported)", entry.ID, entry.API)
continue
}
RegisterType(ProviderTypeMeta{
ID: entry.ID,
Name: entry.Name,
Description: entry.Description,
DefaultEndpoint: entry.DefaultEndpoint,
ProfileSchema: GetProfileSchema(entry.API), // inherit schema from API type
}, impl)
registered++
log.Printf(" 📦 Custom provider type: %s (%s) → %s", entry.ID, entry.Name, entry.DefaultEndpoint)
}
return registered, nil
}