// 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 }