diff --git a/cmd/root.go b/cmd/root.go index ad1cf6e4..cc66ab1b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -689,6 +689,16 @@ func runNormalMode(ctx context.Context) error { } } + // When --provider-url is set but no explicit --model was provided, + // default to "custom/custom" so the user doesn't need to remember a + // provider/model pair for custom OpenAI-compatible endpoints. + // This intentionally overrides saved preferences and config-file + // defaults — if you're pointing at a custom URL you almost certainly + // don't want the default Anthropic model. + if viper.GetString("provider-url") != "" && !modelFlagChanged { + viper.Set("model", "custom/custom") + } + // Load MCP configuration. mcpConfig, err := config.LoadAndValidateConfig() if err != nil { diff --git a/internal/models/providers.go b/internal/models/providers.go index d3e88813..cda33598 100644 --- a/internal/models/providers.go +++ b/internal/models/providers.go @@ -253,6 +253,8 @@ func CreateProvider(ctx context.Context, config *ProviderConfig) (*ProviderResul return createBedrockProvider(ctx, config, modelName) case "vercel": return createVercelProvider(ctx, config, modelName) + case "custom": + return createCustomProvider(ctx, config, modelName) default: return autoRouteProvider(ctx, config, provider, modelName, registry) } @@ -779,6 +781,42 @@ func createVercelProvider(ctx context.Context, config *ProviderConfig, modelName return &ProviderResult{Model: model}, nil } +func createCustomProvider(ctx context.Context, config *ProviderConfig, modelName string) (*ProviderResult, error) { + if config.ProviderURL == "" { + return nil, fmt.Errorf("custom provider requires --provider-url") + } + + apiKey := config.ProviderAPIKey + if apiKey == "" { + apiKey = os.Getenv("CUSTOM_API_KEY") + } + if apiKey == "" { + // Many local/custom endpoints don't require a key; use a placeholder. + apiKey = "custom" + } + + var opts []openaicompat.Option + opts = append(opts, openaicompat.WithBaseURL(config.ProviderURL)) + opts = append(opts, openaicompat.WithAPIKey(apiKey)) + opts = append(opts, openaicompat.WithName("custom")) + + if config.TLSSkipVerify { + opts = append(opts, openaicompat.WithHTTPClient(createHTTPClientWithTLSConfig(true))) + } + + p, err := openaicompat.New(opts...) + if err != nil { + return nil, fmt.Errorf("failed to create custom provider: %w", err) + } + + model, err := p.LanguageModel(ctx, modelName) + if err != nil { + return nil, fmt.Errorf("failed to create custom model: %w", err) + } + + return &ProviderResult{Model: model}, nil +} + func createOllamaProvider(ctx context.Context, config *ProviderConfig, modelName string) (*ProviderResult, error) { baseURL := "http://localhost:11434" if host := os.Getenv("OLLAMA_HOST"); host != "" { diff --git a/internal/models/registry.go b/internal/models/registry.go index b3784b29..214cec7f 100644 --- a/internal/models/registry.go +++ b/internal/models/registry.go @@ -116,6 +116,31 @@ func buildFromModelsDB() map[string]ProviderInfo { } } + // Register the "custom" provider stub for --provider-url without --model. + // This allows users to point kit at any OpenAI-compatible endpoint without + // needing to specify a model from the database. + providers["custom"] = ProviderInfo{ + ID: "custom", + Name: "Custom", + Models: map[string]ModelInfo{ + "custom": { + ID: "custom", + Name: "Custom", + Attachment: false, + Reasoning: true, + Temperature: true, + Cost: Cost{ + Input: 0, + Output: 0, + }, + Limit: Limit{ + Context: 262_144, + Output: 65_536, + }, + }, + }, + } + return providers }