From 2917cd234ce139938019b76aa35ca359ea0a59cb Mon Sep 17 00:00:00 2001 From: canvrno <46584286+canvrno@users.noreply.github.com> Date: Mon, 6 Oct 2025 23:10:07 +0000 Subject: [PATCH] Provider scripts (#6668) * Provider auth scripts * changeset * Updated default providers for script --- .changeset/slow-things-enter.md | 5 + cli/pkg/generated/providers.go | 1371 +++++++++++++++++++++++++++++++ package.json | 1 + scripts/api-secrets-parser.mjs | 374 +++++++++ scripts/cli-providers.mjs | 1057 ++++++++++++++++++++++++ 5 files changed, 2808 insertions(+) create mode 100644 .changeset/slow-things-enter.md create mode 100644 cli/pkg/generated/providers.go create mode 100644 scripts/api-secrets-parser.mjs create mode 100644 scripts/cli-providers.mjs diff --git a/.changeset/slow-things-enter.md b/.changeset/slow-things-enter.md new file mode 100644 index 0000000000..2eb484585e --- /dev/null +++ b/.changeset/slow-things-enter.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Added scripts to generate providers.go diff --git a/cli/pkg/generated/providers.go b/cli/pkg/generated/providers.go new file mode 100644 index 0000000000..10746c59d0 --- /dev/null +++ b/cli/pkg/generated/providers.go @@ -0,0 +1,1371 @@ +// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY +// Generated by scripts/generate-provider-definitions.mjs +// Source: src/shared/api.ts +// +// ============================================================================ +// DATA CONTRACT & DOCUMENTATION +// ============================================================================ +// +// This file provides structured provider metadata extracted from TypeScript source. +// It serves as the bridge between the VSCode extension's TypeScript API definitions +// and the CLI's Go-based setup wizard. +// +// CORE STRUCTURES +// =============== +// +// ConfigField: Individual configuration fields with type, category, and validation metadata +// - Name: Field name as it appears in ApiHandlerOptions (e.g., "cerebrasApiKey") +// - Type: TypeScript type (e.g., "string", "number") +// - Comment: Inline comment from TypeScript source +// - Category: Provider categorization (e.g., "cerebras", "general") +// - Required: Whether this field MUST be collected for any provider +// - FieldType: UI field type hint ("password", "url", "string", "select") +// - Placeholder: Suggested placeholder text for UI input +// +// ModelInfo: Model capabilities, pricing, and limits +// - MaxTokens: Maximum output tokens +// - ContextWindow: Total context window size +// - SupportsImages: Whether model accepts image inputs +// - SupportsPromptCache: Whether model supports prompt caching +// - InputPrice: Cost per 1M input tokens (USD) +// - OutputPrice: Cost per 1M output tokens (USD) +// - CacheWritesPrice: Cost per 1M cached tokens written (USD) +// - CacheReadsPrice: Cost per 1M cached tokens read (USD) +// - Description: Human-readable model description +// +// ProviderDefinition: Complete provider metadata including required/optional fields +// - ID: Provider identifier (e.g., "cerebras", "anthropic") +// - Name: Human-readable display name (e.g., "Cerebras", "Anthropic (Claude)") +// - RequiredFields: Fields that MUST be collected (filtered by category + overrides) +// - OptionalFields: Fields that MAY be collected (filtered by category + overrides) +// - Models: Map of model IDs to ModelInfo +// - DefaultModelID: Recommended default model from TypeScript source +// - HasDynamicModels: Whether provider supports runtime model discovery +// - SetupInstructions: User-facing setup guidance +// +// FIELD FILTERING LOGIC +// ===================== +// +// Fields are categorized during parsing based on provider-specific prefixes in field names: +// - "cerebrasApiKey" → category="cerebras" +// - "awsAccessKey" → category="aws" (used by bedrock) +// - "requestTimeoutMs" → category="general" (applies to all providers) +// +// The getFieldsByProvider() function filters fields using this priority: +// 1. Check field_overrides.go via GetFieldOverride() for manual corrections +// 2. Match field.Category against provider ID (primary filtering) +// 3. Apply hardcoded switch cases for complex provider relationships +// 4. Include universal fields (requestTimeoutMs, ulid, clineAccountId) for all providers +// +// Required vs Optional: +// - Fields are marked as required if they appear in the providerRequiredFields map +// in the generator script (scripts/generate-provider-definitions.mjs) +// - getFieldsByProvider() respects the required parameter to separate required/optional +// +// MODEL SELECTION +// =============== +// +// DefaultModelID extraction priority: +// 1. Exact match from TypeScript constant (e.g., cerebrasDefaultModelId = "llama-3.3-70b") +// 2. Pattern matching on model IDs ("latest", "default", "sonnet", "gpt-4", etc.) +// 3. First model in the models map +// +// Models map contains full capability and pricing data extracted from TypeScript model +// definitions (e.g., cerebrasModels, anthropicModels). +// +// HasDynamicModels indicates providers that support runtime model discovery via API +// (e.g., OpenRouter, Ollama, LM Studio). For these providers, the models map may be +// incomplete or a representative sample. +// +// USAGE EXAMPLE +// ============= +// +// def, err := GetProviderDefinition("cerebras") +// if err != nil { +// return err +// } +// +// // Collect required fields from user +// for _, field := range def.RequiredFields { +// value := promptUser(field.Name, field.Placeholder, field.FieldType == "password") +// config[field.Name] = value +// } +// +// // Use default model or let user choose +// if def.DefaultModelID != "" { +// config["modelId"] = def.DefaultModelID +// } +// +// EXTENDING & OVERRIDING +// ====================== +// +// DO NOT modify this generated file directly. Changes will be lost on regeneration. +// +// To fix incorrect field categorization: +// - Edit cli/pkg/generated/field_overrides.go +// - Add entries to GetFieldOverride() function +// - Example: Force "awsSessionToken" to be relevant for "bedrock" +// +// To change required fields: +// - Edit providerRequiredFields map in scripts/generate-provider-definitions.mjs +// - Rerun: npm run generate-provider-definitions +// +// To add new providers: +// - Add to ApiProvider type in src/shared/api.ts +// - Add fields to ApiHandlerOptions with provider-specific prefixes +// - Optionally add model definitions (e.g., export const newProviderModels = {...}) +// - Rerun generator +// +// To fix default model extraction: +// - Ensure TypeScript source has: export const DefaultModelId = "model-id" +// - Or update extractDefaultModelIds() patterns in generator script +// +// For upstream changes: +// - Submit pull request to src/shared/api.ts in the main repository +// +// ============================================================================ + +package generated + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Provider constants +const ( + ANTHROPIC = "anthropic" + OPENROUTER = "openrouter" + BEDROCK = "bedrock" + OPENAI = "openai" + OLLAMA = "ollama" + GEMINI = "gemini" + OPENAI_NATIVE = "openai-native" + XAI = "xai" +) + +// AllProviders returns a slice of enabled provider IDs for the CLI build. +// This is a filtered subset of all providers available in the VSCode extension. +// To modify which providers are included, edit ENABLED_PROVIDERS in scripts/cli-providers.mjs +var AllProviders = []string{ + "anthropic", + "openrouter", + "bedrock", + "openai", + "ollama", + "gemini", + "openai-native", + "xai", +} + +// ConfigField represents a configuration field requirement +type ConfigField struct { + Name string `json:"name"` + Type string `json:"type"` + Comment string `json:"comment"` + Category string `json:"category"` + Required bool `json:"required"` + FieldType string `json:"fieldType"` + Placeholder string `json:"placeholder"` +} + +// ModelInfo represents model capabilities and pricing +type ModelInfo struct { + MaxTokens int `json:"maxTokens,omitempty"` + ContextWindow int `json:"contextWindow,omitempty"` + SupportsImages bool `json:"supportsImages"` + SupportsPromptCache bool `json:"supportsPromptCache"` + InputPrice float64 `json:"inputPrice,omitempty"` + OutputPrice float64 `json:"outputPrice,omitempty"` + CacheWritesPrice float64 `json:"cacheWritesPrice,omitempty"` + CacheReadsPrice float64 `json:"cacheReadsPrice,omitempty"` + Description string `json:"description,omitempty"` +} + +// ProviderDefinition represents a provider's metadata and requirements +type ProviderDefinition struct { + ID string `json:"id"` + Name string `json:"name"` + RequiredFields []ConfigField `json:"requiredFields"` + OptionalFields []ConfigField `json:"optionalFields"` + Models map[string]ModelInfo `json:"models"` + DefaultModelID string `json:"defaultModelId"` + HasDynamicModels bool `json:"hasDynamicModels"` + SetupInstructions string `json:"setupInstructions"` +} + +// Raw configuration fields data (parsed from TypeScript) +var rawConfigFields = ` [ + { + "name": "apiKey", + "type": "string", + "comment": "anthropic", + "category": "anthropic", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "awsAccessKey", + "type": "string", + "comment": "", + "category": "bedrock", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "awsSecretKey", + "type": "string", + "comment": "", + "category": "bedrock", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "openRouterApiKey", + "type": "string", + "comment": "", + "category": "openrouter", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "awsSessionToken", + "type": "string", + "comment": "", + "category": "bedrock", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "awsBedrockApiKey", + "type": "string", + "comment": "", + "category": "bedrock", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "openAiApiKey", + "type": "string", + "comment": "", + "category": "openai", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "geminiApiKey", + "type": "string", + "comment": "", + "category": "gemini", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "openAiNativeApiKey", + "type": "string", + "comment": "", + "category": "openai-native", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "ollamaApiKey", + "type": "string", + "comment": "", + "category": "ollama", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "authNonce", + "type": "string", + "comment": "", + "category": "general", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "xaiApiKey", + "type": "string", + "comment": "", + "category": "xai", + "required": true, + "fieldType": "password", + "placeholder": "Enter your API key" + }, + { + "name": "ulid", + "type": "string", + "comment": "Used to identify the task in API requests", + "category": "general", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "openAiHeaders", + "type": "Record", + "comment": "Custom headers for OpenAI requests", + "category": "openai", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "anthropicBaseUrl", + "type": "string", + "comment": "", + "category": "anthropic", + "required": false, + "fieldType": "url", + "placeholder": "https://api.example.com" + }, + { + "name": "openRouterProviderSorting", + "type": "string", + "comment": "", + "category": "openrouter", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "openAiBaseUrl", + "type": "string", + "comment": "", + "category": "openai", + "required": false, + "fieldType": "url", + "placeholder": "https://api.example.com" + }, + { + "name": "ollamaBaseUrl", + "type": "string", + "comment": "", + "category": "ollama", + "required": false, + "fieldType": "url", + "placeholder": "https://api.example.com" + }, + { + "name": "ollamaApiOptionsCtxNum", + "type": "string", + "comment": "", + "category": "ollama", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "geminiBaseUrl", + "type": "string", + "comment": "", + "category": "gemini", + "required": false, + "fieldType": "url", + "placeholder": "https://api.example.com" + }, + { + "name": "azureApiVersion", + "type": "string", + "comment": "", + "category": "general", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "requestTimeoutMs", + "type": "number", + "comment": "", + "category": "general", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "sapAiResourceGroup", + "type": "string", + "comment": "", + "category": "general", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "onRetryAttempt", + "type": "(attempt: number, maxRetries: number, delay: number, error: any) => void", + "comment": "", + "category": "general", + "required": false, + "fieldType": "string", + "placeholder": "" + }, + { + "name": "ocaBaseUrl", + "type": "string", + "comment": "", + "category": "general", + "required": false, + "fieldType": "url", + "placeholder": "https://api.example.com" + } + ]` + +// Raw model definitions data (parsed from TypeScript) +var rawModelDefinitions = ` { + "anthropic": { + "claude-sonnet-4-5-20250929": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-sonnet-4-5-20250929:1m": { + "maxTokens": 8192, + "contextWindow": 1000000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-sonnet-4-20250514": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-sonnet-4-20250514:1m": { + "maxTokens": 8192, + "contextWindow": 1000000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-opus-4-1-20250805": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 15, + "outputPrice": 75, + "cacheWritesPrice": 18, + "cacheReadsPrice": 1, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-opus-4-20250514": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 15, + "outputPrice": 75, + "cacheWritesPrice": 18, + "cacheReadsPrice": 1, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-3-7-sonnet-20250219": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-3-5-sonnet-20241022": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-3-5-haiku-20241022": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 0, + "outputPrice": 4, + "cacheWritesPrice": 1, + "cacheReadsPrice": 0, + "supportsImages": false, + "supportsPromptCache": true + }, + "claude-3-opus-20240229": { + "maxTokens": 4096, + "contextWindow": 200000, + "inputPrice": 15, + "outputPrice": 75, + "cacheWritesPrice": 18, + "cacheReadsPrice": 1, + "supportsImages": true, + "supportsPromptCache": true + }, + "claude-3-haiku-20240307": { + "maxTokens": 4096, + "contextWindow": 200000, + "inputPrice": 0, + "outputPrice": 1, + "cacheWritesPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + } + }, + "bedrock": { + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-sonnet-4-5-20250929-v1:0:1m": { + "maxTokens": 8192, + "contextWindow": 1000000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-sonnet-4-20250514-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-sonnet-4-20250514-v1:0:1m": { + "maxTokens": 8192, + "contextWindow": 1000000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-opus-4-20250514-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 15, + "outputPrice": 75, + "cacheWritesPrice": 18, + "cacheReadsPrice": 1, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-opus-4-1-20250805-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 15, + "outputPrice": 75, + "cacheWritesPrice": 18, + "cacheReadsPrice": 1, + "supportsImages": true, + "supportsPromptCache": true + }, + "amazon.nova-premier-v1:0": { + "maxTokens": 10000, + "contextWindow": 1000000, + "inputPrice": 2, + "outputPrice": 12, + "supportsImages": true, + "supportsPromptCache": false + }, + "amazon.nova-pro-v1:0": { + "maxTokens": 5000, + "contextWindow": 300000, + "inputPrice": 0, + "outputPrice": 3, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "amazon.nova-lite-v1:0": { + "maxTokens": 5000, + "contextWindow": 300000, + "inputPrice": 0, + "outputPrice": 0, + "cacheWritesPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "amazon.nova-micro-v1:0": { + "maxTokens": 5000, + "contextWindow": 128000, + "inputPrice": 0, + "outputPrice": 0, + "cacheWritesPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": false, + "supportsPromptCache": true + }, + "anthropic.claude-3-7-sonnet-20250219-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-3-5-sonnet-20241022-v2:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "cacheWritesPrice": 3, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-3-5-haiku-20241022-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 0, + "outputPrice": 4, + "cacheWritesPrice": 1, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "anthropic.claude-3-5-sonnet-20240620-v1:0": { + "maxTokens": 8192, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "supportsImages": true, + "supportsPromptCache": false + }, + "anthropic.claude-3-opus-20240229-v1:0": { + "maxTokens": 4096, + "contextWindow": 200000, + "inputPrice": 15, + "outputPrice": 75, + "supportsImages": true, + "supportsPromptCache": false + }, + "anthropic.claude-3-sonnet-20240229-v1:0": { + "maxTokens": 4096, + "contextWindow": 200000, + "inputPrice": 3, + "outputPrice": 15, + "supportsImages": true, + "supportsPromptCache": false + }, + "anthropic.claude-3-haiku-20240307-v1:0": { + "maxTokens": 4096, + "contextWindow": 200000, + "inputPrice": 0, + "outputPrice": 1, + "supportsImages": true, + "supportsPromptCache": false + }, + "deepseek.r1-v1:0": { + "maxTokens": 8000, + "contextWindow": 64000, + "inputPrice": 1, + "outputPrice": 5, + "supportsImages": false, + "supportsPromptCache": false + }, + "openai.gpt-oss-120b-1:0": { + "maxTokens": 8192, + "contextWindow": 128000, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": false, + "supportsPromptCache": false, + "description": "A state-of-the-art 120B open-weight Mixture-of-Experts language model optimized for strong reasoning, tool use, and efficient deployment on large GPUs" + }, + "openai.gpt-oss-20b-1:0": { + "maxTokens": 8192, + "contextWindow": 128000, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": false, + "supportsPromptCache": false, + "description": "A compact 20B open-weight Mixture-of-Experts language model designed for strong reasoning and tool use, ideal for edge devices and local inference." + } + }, + "gemini": { + "gemini-2.5-pro": { + "maxTokens": 65536, + "contextWindow": 1048576, + "inputPrice": 2, + "outputPrice": 15, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gemini-2.5-flash-lite-preview-06-17": { + "maxTokens": 64000, + "contextWindow": 1000000, + "inputPrice": 0, + "outputPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true, + "description": "Preview version - may not be available in all regions" + }, + "gemini-2.5-flash": { + "maxTokens": 65536, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 2, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gemini-2.0-flash-001": { + "maxTokens": 8192, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "cacheWritesPrice": 1, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gemini-2.0-flash-lite-preview-02-05": { + "maxTokens": 8192, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-2.0-pro-exp-02-05": { + "maxTokens": 8192, + "contextWindow": 2097152, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-2.0-flash-thinking-exp-01-21": { + "maxTokens": 65536, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-2.0-flash-thinking-exp-1219": { + "maxTokens": 8192, + "contextWindow": 32767, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-2.0-flash-exp": { + "maxTokens": 8192, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-1.5-flash-002": { + "maxTokens": 8192, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "cacheWritesPrice": 1, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gemini-1.5-flash-exp-0827": { + "maxTokens": 8192, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-1.5-flash-8b-exp-0827": { + "maxTokens": 8192, + "contextWindow": 1048576, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-1.5-pro-002": { + "maxTokens": 8192, + "contextWindow": 2097152, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-1.5-pro-exp-0827": { + "maxTokens": 8192, + "contextWindow": 2097152, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + }, + "gemini-exp-1206": { + "maxTokens": 8192, + "contextWindow": 2097152, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": true, + "supportsPromptCache": false + } + }, + "openai-native": { + "gpt-5-2025-08-07": { + "maxTokens": 8192, + "contextWindow": 272000, + "inputPrice": 1, + "outputPrice": 10, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-5-mini-2025-08-07": { + "maxTokens": 8192, + "contextWindow": 272000, + "inputPrice": 0, + "outputPrice": 2, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-5-nano-2025-08-07": { + "maxTokens": 8192, + "contextWindow": 272000, + "inputPrice": 0, + "outputPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-5-chat-latest": { + "maxTokens": 8192, + "contextWindow": 400000, + "inputPrice": 1, + "outputPrice": 10, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "o4-mini": { + "maxTokens": 100000, + "contextWindow": 200000, + "inputPrice": 1, + "outputPrice": 4, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-4.1": { + "maxTokens": 32768, + "contextWindow": 1047576, + "inputPrice": 2, + "outputPrice": 8, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-4.1-mini": { + "maxTokens": 32768, + "contextWindow": 1047576, + "inputPrice": 0, + "outputPrice": 1, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-4.1-nano": { + "maxTokens": 32768, + "contextWindow": 1047576, + "inputPrice": 0, + "outputPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "o3-mini": { + "maxTokens": 100000, + "contextWindow": 200000, + "inputPrice": 1, + "outputPrice": 4, + "cacheReadsPrice": 0, + "supportsImages": false, + "supportsPromptCache": true + }, + "o1-preview": { + "maxTokens": 32768, + "contextWindow": 128000, + "inputPrice": 15, + "outputPrice": 60, + "cacheReadsPrice": 7, + "supportsImages": true, + "supportsPromptCache": true + }, + "o1-mini": { + "maxTokens": 65536, + "contextWindow": 128000, + "inputPrice": 1, + "outputPrice": 4, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-4o": { + "maxTokens": 4096, + "contextWindow": 128000, + "inputPrice": 2, + "outputPrice": 10, + "cacheReadsPrice": 1, + "supportsImages": true, + "supportsPromptCache": true + }, + "gpt-4o-mini": { + "maxTokens": 16384, + "contextWindow": 128000, + "inputPrice": 0, + "outputPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "chatgpt-4o-latest": { + "maxTokens": 16384, + "contextWindow": 128000, + "inputPrice": 5, + "outputPrice": 15, + "supportsImages": true, + "supportsPromptCache": false + } + }, + "xai": { + "grok-4-fast-reasoning": { + "maxTokens": 30000, + "contextWindow": 2000000, + "inputPrice": 0, + "outputPrice": 0, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": false, + "description": "xAI's Grok 4 Fast (free) multimodal model with 2M context." + }, + "grok-4": { + "maxTokens": 8192, + "contextWindow": 262144, + "inputPrice": 3, + "outputPrice": 15, + "cacheReadsPrice": 0, + "supportsImages": true, + "supportsPromptCache": true + }, + "grok-3-beta": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 3, + "outputPrice": 15, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 beta model with 131K context window" + }, + "grok-3-fast-beta": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 5, + "outputPrice": 25, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 fast beta model with 131K context window" + }, + "grok-3-mini-beta": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 mini beta model with 131K context window" + }, + "grok-3-mini-fast-beta": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 0, + "outputPrice": 4, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 mini fast beta model with 131K context window" + }, + "grok-3": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 3, + "outputPrice": 15, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 model with 131K context window" + }, + "grok-3-fast": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 5, + "outputPrice": 25, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 fast model with 131K context window" + }, + "grok-3-mini": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 0, + "outputPrice": 0, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 mini model with 131K context window" + }, + "grok-3-mini-fast": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 0, + "outputPrice": 4, + "supportsImages": false, + "supportsPromptCache": true, + "description": "X AI's Grok-3 mini fast model with 131K context window" + }, + "grok-2-latest": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 2, + "outputPrice": 10, + "supportsImages": false, + "supportsPromptCache": false, + "description": "X AI's Grok-2 model - latest version with 131K context window" + }, + "grok-2": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 2, + "outputPrice": 10, + "supportsImages": false, + "supportsPromptCache": false, + "description": "X AI's Grok-2 model with 131K context window" + }, + "grok-2-1212": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 2, + "outputPrice": 10, + "supportsImages": false, + "supportsPromptCache": false, + "description": "X AI's Grok-2 model (version 1212) with 131K context window" + }, + "grok-2-vision-latest": { + "maxTokens": 8192, + "contextWindow": 32768, + "inputPrice": 2, + "outputPrice": 10, + "supportsImages": true, + "supportsPromptCache": false, + "description": "X AI's Grok-2 Vision model - latest version with image support and 32K context window" + }, + "grok-2-vision": { + "maxTokens": 8192, + "contextWindow": 32768, + "inputPrice": 2, + "outputPrice": 10, + "supportsImages": true, + "supportsPromptCache": false, + "description": "X AI's Grok-2 Vision model with image support and 32K context window" + }, + "grok-2-vision-1212": { + "maxTokens": 8192, + "contextWindow": 32768, + "inputPrice": 2, + "outputPrice": 10, + "supportsImages": true, + "supportsPromptCache": false, + "description": "X AI's Grok-2 Vision model (version 1212) with image support and 32K context window" + }, + "grok-vision-beta": { + "maxTokens": 8192, + "contextWindow": 8192, + "inputPrice": 5, + "outputPrice": 15, + "supportsImages": true, + "supportsPromptCache": false, + "description": "X AI's Grok Vision Beta model with image support and 8K context window" + }, + "grok-beta": { + "maxTokens": 8192, + "contextWindow": 131072, + "inputPrice": 5, + "outputPrice": 15, + "supportsImages": false, + "supportsPromptCache": false, + "description": "X AI's Grok Beta model (legacy) with 131K context window" + } + } + }` + +// GetConfigFields returns all configuration fields +func GetConfigFields() ([]ConfigField, error) { + var fields []ConfigField + if err := json.Unmarshal([]byte(rawConfigFields), &fields); err != nil { + return nil, fmt.Errorf("failed to parse config fields: %w", err) + } + return fields, nil +} + +// GetModelDefinitions returns all model definitions +func GetModelDefinitions() (map[string]map[string]ModelInfo, error) { + var models map[string]map[string]ModelInfo + if err := json.Unmarshal([]byte(rawModelDefinitions), &models); err != nil { + return nil, fmt.Errorf("failed to parse model definitions: %w", err) + } + return models, nil +} + +// GetProviderDefinition returns the definition for a specific provider +func GetProviderDefinition(providerID string) (*ProviderDefinition, error) { + definitions, err := GetProviderDefinitions() + if err != nil { + return nil, err + } + + def, exists := definitions[providerID] + if !exists { + return nil, fmt.Errorf("provider %s not found", providerID) + } + + return &def, nil +} + +// GetProviderDefinitions returns all provider definitions +func GetProviderDefinitions() (map[string]ProviderDefinition, error) { + configFields, err := GetConfigFields() + if err != nil { + return nil, err + } + + modelDefinitions, err := GetModelDefinitions() + if err != nil { + return nil, err + } + + definitions := make(map[string]ProviderDefinition) + + // Anthropic (Claude) + definitions["anthropic"] = ProviderDefinition{ + ID: "anthropic", + Name: "Anthropic (Claude)", + RequiredFields: getFieldsByProvider("anthropic", configFields, true), + OptionalFields: getFieldsByProvider("anthropic", configFields, false), + Models: modelDefinitions["anthropic"], + DefaultModelID: "claude-sonnet-4-5-20250929", + HasDynamicModels: false, + SetupInstructions: `Get your API key from https://console.anthropic.com/`, + } + + // OpenRouter + definitions["openrouter"] = ProviderDefinition{ + ID: "openrouter", + Name: "OpenRouter", + RequiredFields: getFieldsByProvider("openrouter", configFields, true), + OptionalFields: getFieldsByProvider("openrouter", configFields, false), + Models: modelDefinitions["openrouter"], + DefaultModelID: "", + HasDynamicModels: true, + SetupInstructions: `Get your API key from https://openrouter.ai/keys`, + } + + // AWS Bedrock + definitions["bedrock"] = ProviderDefinition{ + ID: "bedrock", + Name: "AWS Bedrock", + RequiredFields: getFieldsByProvider("bedrock", configFields, true), + OptionalFields: getFieldsByProvider("bedrock", configFields, false), + Models: modelDefinitions["bedrock"], + DefaultModelID: "anthropic.claude-sonnet-4-20250514-v1", + HasDynamicModels: false, + SetupInstructions: `Configure AWS credentials with Bedrock access permissions`, + } + + // OpenAI Compatible + definitions["openai"] = ProviderDefinition{ + ID: "openai", + Name: "OpenAI Compatible", + RequiredFields: getFieldsByProvider("openai", configFields, true), + OptionalFields: getFieldsByProvider("openai", configFields, false), + Models: modelDefinitions["openai"], + DefaultModelID: "", + HasDynamicModels: true, + SetupInstructions: `Get your API key from https://platform.openai.com/api-keys`, + } + + // Ollama + definitions["ollama"] = ProviderDefinition{ + ID: "ollama", + Name: "Ollama", + RequiredFields: getFieldsByProvider("ollama", configFields, true), + OptionalFields: getFieldsByProvider("ollama", configFields, false), + Models: modelDefinitions["ollama"], + DefaultModelID: "", + HasDynamicModels: true, + SetupInstructions: `Install Ollama locally and ensure it's running on the specified port`, + } + + // Google Gemini + definitions["gemini"] = ProviderDefinition{ + ID: "gemini", + Name: "Google Gemini", + RequiredFields: getFieldsByProvider("gemini", configFields, true), + OptionalFields: getFieldsByProvider("gemini", configFields, false), + Models: modelDefinitions["gemini"], + DefaultModelID: "gemini-2.5-pro", + HasDynamicModels: false, + SetupInstructions: `Get your API key from https://makersuite.google.com/app/apikey`, + } + + // OpenAI + definitions["openai-native"] = ProviderDefinition{ + ID: "openai-native", + Name: "OpenAI", + RequiredFields: getFieldsByProvider("openai-native", configFields, true), + OptionalFields: getFieldsByProvider("openai-native", configFields, false), + Models: modelDefinitions["openai-native"], + DefaultModelID: "gpt-5-chat-latest", + HasDynamicModels: true, + SetupInstructions: `Get your API key from your API provider`, + } + + // X AI (Grok) + definitions["xai"] = ProviderDefinition{ + ID: "xai", + Name: "X AI (Grok)", + RequiredFields: getFieldsByProvider("xai", configFields, true), + OptionalFields: getFieldsByProvider("xai", configFields, false), + Models: modelDefinitions["xai"], + DefaultModelID: "grok-4", + HasDynamicModels: false, + SetupInstructions: `Get your API key from https://console.x.ai/`, + } + + return definitions, nil +} + +// IsValidProvider checks if a provider ID is valid +func IsValidProvider(providerID string) bool { + for _, p := range AllProviders { + if p == providerID { + return true + } + } + return false +} + +// GetProviderDisplayName returns a human-readable name for a provider +func GetProviderDisplayName(providerID string) string { + displayNames := map[string]string{ + "anthropic": "Anthropic (Claude)", + "openrouter": "OpenRouter", + "bedrock": "AWS Bedrock", + "openai": "OpenAI Compatible", + "ollama": "Ollama", + "gemini": "Google Gemini", + "openai-native": "OpenAI", + "xai": "X AI (Grok)", + } + + if name, exists := displayNames[providerID]; exists { + return name + } + return providerID +} + +// getFieldsByProvider filters configuration fields by provider and requirement +// Uses category field as primary filter with override support +func getFieldsByProvider(providerID string, allFields []ConfigField, required bool) []ConfigField { + var fields []ConfigField + + for _, field := range allFields { + fieldName := strings.ToLower(field.Name) + fieldCategory := strings.ToLower(field.Category) + providerName := strings.ToLower(providerID) + + isRelevant := false + + // Priority 1: Check manual overrides FIRST (from GetFieldOverride in this package) + if override, hasOverride := GetFieldOverride(providerID, field.Name); hasOverride { + isRelevant = override + } else if fieldCategory == providerName { + // Priority 2: Direct category match (primary filtering mechanism) + isRelevant = true + } else if fieldCategory == "aws" && providerID == "bedrock" { + // Priority 3: Handle provider-specific category relationships + // AWS fields are used by Bedrock provider + isRelevant = true + } else if fieldCategory == "openai" && providerID == "openai-native" { + // OpenAI fields used by openai-native + isRelevant = true + } else if fieldCategory == "general" { + // Priority 4: Universal fields that apply to all providers + // Note: ulid is excluded as it's auto-generated and users should not set it + universalFields := []string{"requesttimeoutms", "clineaccountid"} + for _, universal := range universalFields { + if fieldName == universal { + isRelevant = true + break + } + } + } + + if isRelevant && field.Required == required { + fields = append(fields, field) + } + } + + return fields +} diff --git a/package.json b/package.json index 068f5a07be..cb7f5af5d3 100644 --- a/package.json +++ b/package.json @@ -304,6 +304,7 @@ "package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production", "protos": "node scripts/build-proto.mjs", "protos-go": "node scripts/build-go-proto.mjs", + "cli-providers": "node scripts/cli-providers.mjs", "postprotos": "biome format src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --no-errors-on-unmatched", "clean:build": "rimraf dist dist-standalone webview-ui/build src/generated out/", "clean:deps": "rimraf node_modules webview-ui/node_modules", diff --git a/scripts/api-secrets-parser.mjs b/scripts/api-secrets-parser.mjs new file mode 100644 index 0000000000..c9c16b0187 --- /dev/null +++ b/scripts/api-secrets-parser.mjs @@ -0,0 +1,374 @@ +/** + * API Secrets Parser Module + * + * Parses the ApiHandlerSecrets TypeScript interface from src/shared/api.ts + * to automatically discover API key fields for all providers. + * + * This eliminates the need for manual maintenance of provider-to-API-key mappings. + */ + +/** + * Parses the ApiHandlerSecrets interface from api.ts content + * + * @param {string} content - Content of api.ts file + * @returns {Object} Parsed API key fields with metadata + * @returns {Object.fields} - Map of field names to their metadata + * @returns {Object.fieldNames} - Array of all field names + */ +export function parseApiHandlerSecrets(content) { + // Find the ApiHandlerSecrets interface definition + const interfaceMatch = content.match(/export interface ApiHandlerSecrets \{([\s\S]*?)\}/m) + + if (!interfaceMatch) { + throw new Error("Could not find ApiHandlerSecrets interface definition") + } + + const interfaceContent = interfaceMatch[1] + const fields = {} + const fieldNames = [] + + // Match field definitions like: fieldName?: string // comment + const fieldMatches = interfaceContent.matchAll(/^\s*([a-zA-Z][a-zA-Z0-9_]*)\?\s*:\s*([^/\n]+)(?:\/\/\s*(.*))?$/gm) + + for (const match of fieldMatches) { + const [, name, type, comment] = match + + fields[name] = { + name, + type: type.trim(), + comment: comment?.trim() || "", + isSecret: true, // All fields in ApiHandlerSecrets are secrets + } + + fieldNames.push(name) + } + + return { fields, fieldNames } +} + +/** + * Maps provider IDs to their required API key fields + * + * @param {Array} providerIds - List of provider IDs from ApiProvider type + * @param {Object} apiSecretsFields - Parsed fields from ApiHandlerSecrets + * @returns {Object} Map of provider ID to array of API key field names + * + * Example output: + * { + * "anthropic": ["apiKey"], + * "bedrock": ["awsAccessKey", "awsSecretKey"], + * "cerebras": ["cerebrasApiKey"], + * ... + * } + */ +export function mapProviderToApiKeys(providerIds, apiSecretsFields) { + const providerApiKeyMap = {} + + // Track which fields have been assigned to prevent duplicates + const assignedFields = new Set() + + // First pass: Map provider-specific API key fields + for (const providerId of providerIds) { + const apiKeyFields = [] + + for (const fieldName of apiSecretsFields.fieldNames) { + if (assignedFields.has(fieldName)) { + continue + } + + const providerFromField = extractProviderFromFieldName(fieldName) + + if (providerFromField === providerId) { + apiKeyFields.push(fieldName) + assignedFields.add(fieldName) + } + } + + if (apiKeyFields.length > 0) { + providerApiKeyMap[providerId] = apiKeyFields + } + } + + // Second pass: Handle special cases and multi-key providers + applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) + + return providerApiKeyMap +} + +/** + * Determines the provider ID from an API key field name + * Uses pattern matching on common naming conventions + * + * @param {string} fieldName - API key field name (e.g., "cerebrasApiKey") + * @returns {string|null} Provider ID or null if not a provider-specific key + */ +export function extractProviderFromFieldName(fieldName) { + // Normalize field name to lowercase for matching + const lowerFieldName = fieldName.toLowerCase() + + // SPECIAL CASES FIRST (before pattern matching) + + // Special case: "apiKey" alone maps to "anthropic" (primary provider) + if (fieldName === "apiKey") { + return "anthropic" + } + + // Special case: clineAccountId maps to "cline" + if (lowerFieldName === "clineaccountid") { + return "cline" + } + + // Special case: authNonce is not provider-specific + if (lowerFieldName === "authnonce") { + return null + } + + // Special case: Vertex fields (not in ApiHandlerSecrets but in ApiHandlerOptions) + if (lowerFieldName === "vertexprojectid" || lowerFieldName === "vertexregion") { + return "vertex" + } + + // Pattern 1: AWS-specific fields (check before generic pattern to avoid false positives) + if (lowerFieldName.startsWith("aws")) { + // awsAccessKey, awsSecretKey, awsSessionToken, awsRegion -> bedrock + if ( + lowerFieldName.includes("accesskey") || + lowerFieldName.includes("secretkey") || + lowerFieldName.includes("sessiontoken") || + lowerFieldName.includes("region") + ) { + return "bedrock" + } + // awsBedrockApiKey is explicitly bedrock + if (lowerFieldName.includes("bedrock")) { + return "bedrock" + } + } + + // Pattern 2: Vertex-specific fields + if (lowerFieldName.startsWith("vertex")) { + return "vertex" + } + + // Pattern 3: SAP AI Core fields + if (lowerFieldName.startsWith("sapaicore") || lowerFieldName.startsWith("sapai")) { + return "sapaicore" + } + + // Pattern 4: Provider name in the middle (e.g., openAiNativeApiKey) - check before generic pattern + const providerPatterns = [ + { pattern: "openainative", providerId: "openai-native" }, + { pattern: "openrouter", providerId: "openrouter" }, + { pattern: "openai", providerId: "openai" }, + { pattern: "gemini", providerId: "gemini" }, + { pattern: "deepseek", providerId: "deepseek" }, + { pattern: "ollama", providerId: "ollama" }, + { pattern: "lmstudio", providerId: "lmstudio" }, + { pattern: "litellm", providerId: "litellm" }, + { pattern: "qwen", providerId: "qwen" }, + { pattern: "doubao", providerId: "doubao" }, + { pattern: "mistral", providerId: "mistral" }, + { pattern: "fireworks", providerId: "fireworks" }, + { pattern: "asksage", providerId: "asksage" }, + { pattern: "xai", providerId: "xai" }, + { pattern: "moonshot", providerId: "moonshot" }, + { pattern: "sambanova", providerId: "sambanova" }, + { pattern: "cerebras", providerId: "cerebras" }, + { pattern: "groq", providerId: "groq" }, + { pattern: "huggingface", providerId: "huggingface" }, + { pattern: "huawei", providerId: "huawei-cloud-maas" }, + { pattern: "baseten", providerId: "baseten" }, + { pattern: "vercel", providerId: "vercel-ai-gateway" }, + { pattern: "zai", providerId: "zai" }, + { pattern: "requesty", providerId: "requesty" }, + { pattern: "together", providerId: "together" }, + { pattern: "dify", providerId: "dify" }, + ] + + for (const { pattern, providerId } of providerPatterns) { + if (lowerFieldName.includes(pattern)) { + return providerId + } + } + + // Pattern 5: ApiKey format (most common) - checked LAST to avoid false positives + if (lowerFieldName.endsWith("apikey")) { + // Extract from ORIGINAL fieldName to preserve camelCase for normalization + const providerPart = fieldName.slice(0, -6) // Remove "ApiKey" + return normalizeProviderName(providerPart) + } + + return null +} + +/** + * Normalizes provider name extracted from field name to match provider ID format + * + * @param {string} providerPart - Provider part extracted from field name + * @returns {string} Normalized provider ID + */ +function normalizeProviderName(providerPart) { + // Handle camelCase to kebab-case conversion + const normalized = providerPart + .replace(/([A-Z])/g, "-$1") + .toLowerCase() + .replace(/^-/, "") + + // Handle special cases + const specialCases = { + "open-router": "openrouter", + "open-ai-native": "openai-native", + "open-ai": "openai", + "lite-llm": "litellm", + "deep-seek": "deepseek", + "ask-sage": "asksage", + "hugging-face": "huggingface", + "huawei-cloud-maas": "huawei-cloud-maas", + "sap-ai-core": "sapaicore", + "vercel-ai-gateway": "vercel-ai-gateway", + } + + return specialCases[normalized] || normalized +} + +/** + * Applies special case mappings for complex provider relationships + * + * @param {Object} providerApiKeyMap - Current map being built + * @param {Object} apiSecretsFields - Parsed API secrets fields + * @param {Set} assignedFields - Set of already assigned field names + */ +function applySpecialCaseMappings(providerApiKeyMap, apiSecretsFields, assignedFields) { + // Special case 1: Bedrock needs AWS fields (if not already assigned) + const awsFields = ["awsAccessKey", "awsSecretKey", "awsRegion"] + const bedrockFields = providerApiKeyMap["bedrock"] || [] + + for (const field of awsFields) { + if (apiSecretsFields.fieldNames.includes(field) && !bedrockFields.includes(field)) { + bedrockFields.push(field) + assignedFields.add(field) + } + } + + // Optional: awsSessionToken for temporary credentials + if (apiSecretsFields.fieldNames.includes("awsSessionToken") && !bedrockFields.includes("awsSessionToken")) { + bedrockFields.push("awsSessionToken") + assignedFields.add("awsSessionToken") + } + + if (bedrockFields.length > 0) { + providerApiKeyMap["bedrock"] = bedrockFields + } + + // Special case 2: Vertex needs project ID and region + if (providerApiKeyMap["vertex"]) { + // Vertex typically uses application default credentials, + // but requires project ID and region configuration + // These are already captured if they exist in ApiHandlerSecrets + } + + // Special case 3: SAP AI Core multi-key authentication + if (providerApiKeyMap["sapaicore"]) { + const sapFields = providerApiKeyMap["sapaicore"] + const requiredSapFields = ["sapAiCoreClientId", "sapAiCoreClientSecret"] + + for (const field of requiredSapFields) { + if (apiSecretsFields.fieldNames.includes(field) && !sapFields.includes(field)) { + sapFields.push(field) + assignedFields.add(field) + } + } + } +} + +/** + * Generates display name for an API key field + * Converts camelCase to Title Case with proper spacing + * + * @param {string} fieldName - API key field name + * @returns {string} Human-readable display name + */ +export function generateApiKeyDisplayName(fieldName) { + // Special cases for known abbreviations + const specialCases = { + apiKey: "API Key", + awsAccessKey: "AWS Access Key", + awsSecretKey: "AWS Secret Key", + awsSessionToken: "AWS Session Token", + awsRegion: "AWS Region", + awsBedrockApiKey: "AWS Bedrock API Key", + openRouterApiKey: "OpenRouter API Key", + openAiApiKey: "OpenAI API Key", + openAiNativeApiKey: "OpenAI Native API Key", + geminiApiKey: "Gemini API Key", + ollamaApiKey: "Ollama API Key", + deepSeekApiKey: "DeepSeek API Key", + liteLlmApiKey: "LiteLLM API Key", + qwenApiKey: "Qwen API Key", + doubaoApiKey: "Doubao API Key", + mistralApiKey: "Mistral API Key", + fireworksApiKey: "Fireworks API Key", + asksageApiKey: "AskSage API Key", + xaiApiKey: "X AI API Key", + moonshotApiKey: "Moonshot API Key", + sambanovaApiKey: "SambaNova API Key", + cerebrasApiKey: "Cerebras API Key", + groqApiKey: "Groq API Key", + huggingFaceApiKey: "Hugging Face API Key", + nebiusApiKey: "Nebius API Key", + basetenApiKey: "Baseten API Key", + vercelAiGatewayApiKey: "Vercel AI Gateway API Key", + zaiApiKey: "Z AI API Key", + requestyApiKey: "Requesty API Key", + togetherApiKey: "Together AI API Key", + difyApiKey: "Dify API Key", + clineAccountId: "Cline Account ID", + vertexProjectId: "Vertex Project ID", + vertexRegion: "Vertex Region", + sapAiCoreClientId: "SAP AI Core Client ID", + sapAiCoreClientSecret: "SAP AI Core Client Secret", + huaweiCloudMaasApiKey: "Huawei Cloud MaaS API Key", + } + + if (specialCases[fieldName]) { + return specialCases[fieldName] + } + + // Generic conversion: camelCase -> Title Case + return fieldName + .replace(/([A-Z])/g, " $1") + .replace(/^./, (str) => str.toUpperCase()) + .trim() +} + +/** + * Validates that all providers have at least one API key field mapped + * + * @param {Array} providerIds - All provider IDs + * @param {Object} providerApiKeyMap - Generated mapping + * @returns {Object} Validation result with warnings for unmapped providers + */ +export function validateApiKeyMappings(providerIds, providerApiKeyMap) { + const unmappedProviders = [] + const warnings = [] + + for (const providerId of providerIds) { + if (!providerApiKeyMap[providerId] || providerApiKeyMap[providerId].length === 0) { + // Some providers don't require API keys - they use alternative authentication: + const noKeyProviders = ["vscode-lm", "ollama", "lmstudio", "claude-code", "oca", "vertex", "qwen-code"] + + if (!noKeyProviders.includes(providerId)) { + unmappedProviders.push(providerId) + warnings.push(`WARNING: Provider "${providerId}" has no API key fields mapped`) + } + } + } + + return { + valid: unmappedProviders.length === 0, + unmappedProviders, + warnings, + totalProviders: providerIds.length, + mappedProviders: Object.keys(providerApiKeyMap).length, + } +} diff --git a/scripts/cli-providers.mjs b/scripts/cli-providers.mjs new file mode 100644 index 0000000000..ab41b2b0c4 --- /dev/null +++ b/scripts/cli-providers.mjs @@ -0,0 +1,1057 @@ +#!/usr/bin/env node + +/** + * CLI Provider Definition Generator + * ================================== + * + * This script generates Go code for the CLI version of Cline by extracting provider + * metadata from the TypeScript source (src/shared/api.ts) and converting it to Go + * structs. It serves as the bridge between the VSCode extension's TypeScript API + * definitions and the CLI's Go-based setup wizard. + * + * Purpose: + * -------- + * - Extract provider configurations, API key requirements, and model definitions + * - Filter to only include whitelisted providers (ENABLED_PROVIDERS constant) + * - Generate type-safe Go code with embedded JSON data + * - Keep the CLI binary lean by excluding unused providers + * + * What it generates: + * ------------------ + * - cli/pkg/generated/providers.go - Go structs and constants for provider metadata + * - Includes: Provider constants, config fields, model definitions, helper functions + * + * How it works: + * ------------- + * 1. Parses TypeScript API definitions from src/shared/api.ts + * 2. Extracts provider IDs, configuration fields, and model information + * 3. Filters config fields and models to only include ENABLED_PROVIDERS + * 4. Generates Go code with JSON-embedded data for runtime access + * 5. Includes comprehensive documentation in the generated file + * + * Data Filtering: + * --------------- + * - Provider list: Filtered to ENABLED_PROVIDERS (currently 9 of 36 providers) + * - Config fields: Only includes fields where category matches a whitelisted provider + * - Model definitions: Only includes model maps for whitelisted providers + * - Result: Non-whitelisted provider data never makes it into the CLI binary + * + * Usage: + * ------ + * npm run cli-providers + * + * To modify which providers are included: + * 1. Edit the ENABLED_PROVIDERS array below + * 2. Run: npm run cli-providers + * 3. Verify the output in cli/pkg/generated/providers.go + * + * Dependencies: + * ------------- + * - api-secrets-parser.mjs - Helper module for parsing API key fields + * - src/shared/api.ts - Source of truth for provider definitions + * + * Output: + * ------- + * The generated Go file includes: + * - Type definitions (ConfigField, ModelInfo, ProviderDefinition) + * - Provider constants and AllProviders array + * - Embedded JSON data for config fields and model definitions + * - Helper functions for querying provider metadata + * - Comprehensive documentation for developers + */ + +import chalk from "chalk" +import * as fs from "fs/promises" +import * as path from "path" +import { fileURLToPath } from "url" + +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)) +const ROOT_DIR = path.resolve(SCRIPT_DIR, "..") +const API_DEFINITIONS_FILE = path.resolve(ROOT_DIR, "src", "shared", "api.ts") +const GO_OUTPUT_FILE = path.resolve(ROOT_DIR, "cli", "pkg", "generated", "providers.go") + +/** + * ENABLED_PROVIDERS - Controls which providers are included in the CLI build + * + * This list determines which providers from src/shared/api.ts will be included + * in the generated Go code for the CLI version. This allows us to keep the CLI + * lean by only including the most commonly used providers. + * + * To add or remove providers: + * 1. Add/remove the provider ID from this array (must match ApiProvider values) + * 2. Run: npm run cli-providers (or node scripts/cli-providers.mjs) + * 3. Verify the output in cli/pkg/generated/providers.go + * + * Provider IDs must match exactly as defined in the ApiProvider type in api.ts + */ +const ENABLED_PROVIDERS = [ + "anthropic", // Anthropic Claude models + "openai", // OpenAI-compatible providers + "openai-native", // OpenAI official API + "openrouter", // OpenRouter meta-provider + "xai", // X AI (Grok) + "bedrock", // AWS Bedrock + "gemini", // Google Gemini + "ollama", // Ollama local models +] + +/** + * Extract default model IDs from TypeScript source + * Uses multiple regex patterns to catch different variable declaration styles + */ +function extractDefaultModelIds(content) { + const defaultIds = {} + + // Multiple regex patterns to handle different TypeScript patterns + const patterns = [ + // Pattern 1: With type annotation - export const anthropicDefaultModelId: AnthropicModelId = "model-id" + /export const (\w+)DefaultModelId\s*:\s*\w+\s*=\s*"([^"]+)"/g, + // Pattern 2: Without type annotation - export const anthropicDefaultModelId = "model-id" + /export const (\w+)DefaultModelId\s*=\s*"([^"]+)"/g, + // Pattern 3: Without export - const anthropicDefaultModelId = "model-id" + /const (\w+)DefaultModelId\s*=\s*"([^"]+)"/g, + ] + + for (const regex of patterns) { + // Reset regex state for each pattern + regex.lastIndex = 0 + let match + + while ((match = regex.exec(content)) !== null) { + const [, providerPrefix, modelId] = match + // Map prefix to provider ID (e.g., "anthropic" -> "anthropic", "openAiNative" -> "openai-native") + const providerId = providerPrefix + .replace(/([A-Z])/g, "-$1") + .toLowerCase() + .replace(/^-/, "") + + // Don't overwrite if already found (first match wins) + if (!defaultIds[providerId]) { + // Clean up model ID - remove any suffix like ":1m" + const cleanModelId = modelId.split(":")[0] + defaultIds[providerId] = cleanModelId + } + } + } + + return defaultIds +} + +/** + * Parse TypeScript API definitions and extract provider information + */ +async function parseApiDefinitions() { + console.log(chalk.cyan("Reading TypeScript API definitions...")) + + const content = await fs.readFile(API_DEFINITIONS_FILE, "utf-8") + + // Extract ApiProvider type definition + const providerTypeMatch = content.match(/export type ApiProvider =\s*([\s\S]*?)(?=\n\nexport|\n\ninterface|\ninterface)/m) + if (!providerTypeMatch) { + throw new Error("Could not find ApiProvider type definition") + } + + // Parse provider IDs from the union type + const providerTypeContent = providerTypeMatch[1] + const providerIds = [] + const providerMatches = providerTypeContent.matchAll(/\|\s*"([^"]+)"/g) + for (const match of providerMatches) { + providerIds.push(match[1]) + } + + // Also get the first provider (without |) + const firstProviderMatch = providerTypeContent.match(/"([^"]+)"/) + if (firstProviderMatch && !providerIds.includes(firstProviderMatch[1])) { + providerIds.unshift(firstProviderMatch[1]) + } + + console.log( + chalk.green( + `Found ${providerIds.length} total providers: ${providerIds.slice(0, 5).join(", ")}${providerIds.length > 5 ? "..." : ""}`, + ), + ) + + // Filter to only enabled providers + const totalProvidersFound = providerIds.length + const filteredProviderIds = providerIds.filter((id) => ENABLED_PROVIDERS.includes(id)) + const disabledCount = totalProvidersFound - filteredProviderIds.length + + console.log(chalk.cyan(`Filtering to ${filteredProviderIds.length} enabled providers (${disabledCount} disabled)`)) + console.log(chalk.green(` Enabled: ${filteredProviderIds.join(", ")}`)) + + // Validate that all enabled providers exist in the source + const missingProviders = ENABLED_PROVIDERS.filter((id) => !providerIds.includes(id)) + if (missingProviders.length > 0) { + console.log( + chalk.yellow( + ` WARNING: ${missingProviders.length} enabled provider(s) not found in api.ts: ${missingProviders.join(", ")}`, + ), + ) + } + + // Parse ApiHandlerSecrets to auto-discover API key fields + const { parseApiHandlerSecrets, mapProviderToApiKeys, validateApiKeyMappings } = await import("./api-secrets-parser.mjs") + const apiSecretsFields = parseApiHandlerSecrets(content) + const providerApiKeyMap = mapProviderToApiKeys(providerIds, apiSecretsFields) + + // Validate the mapping + const validation = validateApiKeyMappings(providerIds, providerApiKeyMap) + console.log(chalk.green(` Mapped API keys for ${validation.mappedProviders}/${validation.totalProviders} providers`)) + if (validation.warnings.length > 0) { + validation.warnings.forEach((warning) => console.log(chalk.yellow(` ${warning}`))) + } + + // Extract ApiHandlerOptions interface to understand configuration fields + const optionsMatch = content.match(/export interface ApiHandlerOptions \{([\s\S]*?)\}/m) + if (!optionsMatch) { + throw new Error("Could not find ApiHandlerOptions interface") + } + + const optionsContent = optionsMatch[1] + const configFields = parseConfigurationFields(optionsContent, providerApiKeyMap, apiSecretsFields) + + // Extract model definitions for each provider + const modelDefinitions = extractModelDefinitions(content) + + // Extract default model IDs from TypeScript constants + const defaultModelIds = extractDefaultModelIds(content) + + console.log(chalk.green(` Extracted ${Object.keys(defaultModelIds).length} default model IDs`)) + + // Filter config fields to only include whitelisted providers + const filteredConfigFields = configFields.filter( + (field) => + // Include fields for whitelisted providers + filteredProviderIds.includes(field.category) || + // Include general fields that apply to all providers + field.category === "general", + ) + + // Filter model definitions to only include whitelisted providers + const filteredModelDefinitions = Object.fromEntries( + Object.entries(modelDefinitions).filter(([providerId]) => filteredProviderIds.includes(providerId)), + ) + + console.log( + chalk.cyan( + ` Filtered config fields: ${configFields.length} -> ${filteredConfigFields.length} (${configFields.length - filteredConfigFields.length} excluded)`, + ), + ) + console.log( + chalk.cyan( + ` Filtered model definitions: ${Object.keys(modelDefinitions).length} -> ${Object.keys(filteredModelDefinitions).length} (${Object.keys(modelDefinitions).length - Object.keys(filteredModelDefinitions).length} excluded)`, + ), + ) + + return { + providers: filteredProviderIds, + configFields: filteredConfigFields, + modelDefinitions: filteredModelDefinitions, + defaultModelIds, + providerApiKeyMap, + } +} + +/** + * Parse configuration fields from ApiHandlerOptions and ApiHandlerSecrets + */ +function parseConfigurationFields(optionsContent, providerApiKeyMap, apiSecretsFields) { + const fields = [] + + // FIRST: Add API key fields from ApiHandlerSecrets + // These are the actual authentication fields that need to be collected + for (const fieldName of apiSecretsFields.fieldNames) { + const fieldInfo = apiSecretsFields.fields[fieldName] + const lowerName = fieldName.toLowerCase() + + // Determine which provider this field belongs to + let category = "general" + for (const [providerId, apiKeys] of Object.entries(providerApiKeyMap)) { + if (apiKeys.includes(fieldName)) { + category = providerId + break + } + } + + // All API key fields are required for their respective provider + const required = true + const fieldType = "password" + const placeholder = "Enter your API key" + + fields.push({ + name: fieldName, + type: fieldInfo.type, + comment: fieldInfo.comment || "", + category, + required, + fieldType, + placeholder, + }) + } + + // SECOND: Add configuration fields from ApiHandlerOptions + // Match field definitions like: fieldName?: type // comment + const fieldMatches = optionsContent.matchAll(/^\s*([a-zA-Z][a-zA-Z0-9_]*)\?\s*:\s*([^/\n]+)(?:\/\/\s*(.*))?$/gm) + + for (const match of fieldMatches) { + const [, name, type, comment] = match + + // Skip mode-specific fields (we'll handle those separately) + if (name.includes("planMode") || name.includes("actMode")) { + continue + } + + const lowerName = name.toLowerCase() + + // Determine field category based on provider-specific prefixes FIRST + let category = "general" + let required = false + let fieldType = "string" + let placeholder = "" + + // Check for provider-specific prefixes to categorize appropriately + const providerPrefixes = [ + "anthropic", + "openrouter", + "aws", + "bedrock", + "vertex", + "openai", + "ollama", + "lmstudio", + "gemini", + "deepseek", + "qwen", + "doubao", + "mistral", + "litellm", + "moonshot", + "nebius", + "fireworks", + "asksage", + "xai", + "sambanova", + "cerebras", + "sapaicore", + "groq", + "huggingface", + "huawei", + "dify", + "baseten", + "vercel", + "zai", + "requesty", + "together", + "claudecode", + "cline", + ] + + // If field name starts with or contains a provider prefix, categorize it as provider-specific + for (const prefix of providerPrefixes) { + if (lowerName.startsWith(prefix) || lowerName.includes(prefix)) { + category = prefix + break + } + } + + // Set field type metadata for UI rendering + if (lowerName.includes("apikey")) { + fieldType = "password" + placeholder = "Enter your API key" + } else if (lowerName.includes("key") && !lowerName.includes("apikey")) { + fieldType = "password" + placeholder = "Enter your key" + } else if (lowerName.includes("url") || lowerName.includes("endpoint")) { + fieldType = "url" + placeholder = "https://api.example.com" + } else if (lowerName.includes("region")) { + fieldType = "select" + } else if (lowerName.includes("model")) { + // model fields stay with their provider category + } + + // Check if this field is required for any provider using the auto-discovered API key map + // A field is marked as required if it appears in any provider's required fields list + for (const [providerId, requiredFields] of Object.entries(providerApiKeyMap)) { + if (requiredFields.includes(name)) { + required = true + break + } + } + + fields.push({ + name, + type: type.trim(), + comment: comment?.trim() || "", + category, + required, + fieldType, + placeholder, + }) + } + + return fields +} + +/** + * Extract model definitions for each provider + */ +function extractModelDefinitions(content) { + const modelDefinitions = {} + + // Find all model constant definitions like: export const anthropicModels = { + const modelMatches = content.matchAll(/export const (\w+)Models = \{([\s\S]*?)\} as const/g) + + for (const match of modelMatches) { + const [, providerPrefix, modelsContent] = match + + // Parse individual model entries + const models = {} + const modelEntryMatches = modelsContent.matchAll(/"([^"]+)":\s*\{([\s\S]*?)\},?/g) + + for (const modelMatch of modelEntryMatches) { + const [, modelId, modelContent] = modelMatch + + // Parse model properties + const modelInfo = parseModelInfo(modelContent) + models[modelId] = modelInfo + } + + // Map provider prefix to actual provider ID + const providerMapping = { + anthropic: "anthropic", + claudeCode: "claude-code", + bedrock: "bedrock", + vertex: "vertex", + openAiNative: "openai-native", + gemini: "gemini", + deepSeek: "deepseek", + huggingFace: "huggingface", + qwen: "qwen", + doubao: "doubao", + mistral: "mistral", + xai: "xai", + sambanova: "sambanova", + cerebras: "cerebras", + sapAiCore: "sapaicore", + moonshot: "moonshot", + huaweiCloudMaas: "huawei-cloud-maas", + baseten: "baseten", + fireworks: "fireworks", + groq: "groq", + nebius: "nebius", + askSage: "asksage", + qwenCode: "qwen-code", + } + + const providerId = providerMapping[providerPrefix] || providerPrefix.toLowerCase() + if (Object.keys(models).length > 0) { + modelDefinitions[providerId] = models + } + } + + return modelDefinitions +} + +/** + * Parse model information from model definition content + */ +function parseModelInfo(modelContent) { + const info = {} + + // Parse numeric properties + const numericProps = ["maxTokens", "contextWindow", "inputPrice", "outputPrice", "cacheWritesPrice", "cacheReadsPrice"] + for (const prop of numericProps) { + const match = modelContent.match(new RegExp(`${prop}:\\s*([0-9_,]+)`)) + if (match) { + info[prop] = parseInt(match[1].replace(/[_,]/g, "")) + } + } + + // Parse boolean properties + const booleanProps = ["supportsImages", "supportsPromptCache"] + for (const prop of booleanProps) { + const match = modelContent.match(new RegExp(`${prop}:\\s*(true|false)`)) + if (match) { + info[prop] = match[1] === "true" + } + } + + // Parse description + const descMatch = modelContent.match(/description:\s*"([^"]*)"/) + if (descMatch) { + info.description = descMatch[1] + } + + return info +} + +/** + * Generate Go structs from parsed data + */ +function generateGoCode(data) { + console.log(chalk.cyan("Generating Go code...")) + + const { providers, configFields, modelDefinitions } = data + + // Generate provider constants + const providerConstants = providers.map((p) => `\t${p.toUpperCase().replace(/-/g, "_")} = "${p}"`).join("\n") + + // Generate configuration field definitions + const configFieldsJson = JSON.stringify(configFields, null, 2) + .split("\n") + .map((line) => `\t${line}`) + .join("\n") + + // Generate model definitions + const modelDefinitionsJson = JSON.stringify(modelDefinitions, null, 2) + .split("\n") + .map((line) => `\t${line}`) + .join("\n") + + // Generate provider metadata + const providerMetadata = generateProviderMetadata(providers, configFields, modelDefinitions, data.defaultModelIds) + + return `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY +// Generated by scripts/generate-provider-definitions.mjs +// Source: src/shared/api.ts +// +// ============================================================================ +// DATA CONTRACT & DOCUMENTATION +// ============================================================================ +// +// This file provides structured provider metadata extracted from TypeScript source. +// It serves as the bridge between the VSCode extension's TypeScript API definitions +// and the CLI's Go-based setup wizard. +// +// CORE STRUCTURES +// =============== +// +// ConfigField: Individual configuration fields with type, category, and validation metadata +// - Name: Field name as it appears in ApiHandlerOptions (e.g., "cerebrasApiKey") +// - Type: TypeScript type (e.g., "string", "number") +// - Comment: Inline comment from TypeScript source +// - Category: Provider categorization (e.g., "cerebras", "general") +// - Required: Whether this field MUST be collected for any provider +// - FieldType: UI field type hint ("password", "url", "string", "select") +// - Placeholder: Suggested placeholder text for UI input +// +// ModelInfo: Model capabilities, pricing, and limits +// - MaxTokens: Maximum output tokens +// - ContextWindow: Total context window size +// - SupportsImages: Whether model accepts image inputs +// - SupportsPromptCache: Whether model supports prompt caching +// - InputPrice: Cost per 1M input tokens (USD) +// - OutputPrice: Cost per 1M output tokens (USD) +// - CacheWritesPrice: Cost per 1M cached tokens written (USD) +// - CacheReadsPrice: Cost per 1M cached tokens read (USD) +// - Description: Human-readable model description +// +// ProviderDefinition: Complete provider metadata including required/optional fields +// - ID: Provider identifier (e.g., "cerebras", "anthropic") +// - Name: Human-readable display name (e.g., "Cerebras", "Anthropic (Claude)") +// - RequiredFields: Fields that MUST be collected (filtered by category + overrides) +// - OptionalFields: Fields that MAY be collected (filtered by category + overrides) +// - Models: Map of model IDs to ModelInfo +// - DefaultModelID: Recommended default model from TypeScript source +// - HasDynamicModels: Whether provider supports runtime model discovery +// - SetupInstructions: User-facing setup guidance +// +// FIELD FILTERING LOGIC +// ===================== +// +// Fields are categorized during parsing based on provider-specific prefixes in field names: +// - "cerebrasApiKey" → category="cerebras" +// - "awsAccessKey" → category="aws" (used by bedrock) +// - "requestTimeoutMs" → category="general" (applies to all providers) +// +// The getFieldsByProvider() function filters fields using this priority: +// 1. Check field_overrides.go via GetFieldOverride() for manual corrections +// 2. Match field.Category against provider ID (primary filtering) +// 3. Apply hardcoded switch cases for complex provider relationships +// 4. Include universal fields (requestTimeoutMs, ulid, clineAccountId) for all providers +// +// Required vs Optional: +// - Fields are marked as required if they appear in the providerRequiredFields map +// in the generator script (scripts/generate-provider-definitions.mjs) +// - getFieldsByProvider() respects the required parameter to separate required/optional +// +// MODEL SELECTION +// =============== +// +// DefaultModelID extraction priority: +// 1. Exact match from TypeScript constant (e.g., cerebrasDefaultModelId = "llama-3.3-70b") +// 2. Pattern matching on model IDs ("latest", "default", "sonnet", "gpt-4", etc.) +// 3. First model in the models map +// +// Models map contains full capability and pricing data extracted from TypeScript model +// definitions (e.g., cerebrasModels, anthropicModels). +// +// HasDynamicModels indicates providers that support runtime model discovery via API +// (e.g., OpenRouter, Ollama, LM Studio). For these providers, the models map may be +// incomplete or a representative sample. +// +// USAGE EXAMPLE +// ============= +// +// def, err := GetProviderDefinition("cerebras") +// if err != nil { +// return err +// } +// +// // Collect required fields from user +// for _, field := range def.RequiredFields { +// value := promptUser(field.Name, field.Placeholder, field.FieldType == "password") +// config[field.Name] = value +// } +// +// // Use default model or let user choose +// if def.DefaultModelID != "" { +// config["modelId"] = def.DefaultModelID +// } +// +// EXTENDING & OVERRIDING +// ====================== +// +// DO NOT modify this generated file directly. Changes will be lost on regeneration. +// +// To fix incorrect field categorization: +// - Edit cli/pkg/generated/field_overrides.go +// - Add entries to GetFieldOverride() function +// - Example: Force "awsSessionToken" to be relevant for "bedrock" +// +// To change required fields: +// - Edit providerRequiredFields map in scripts/generate-provider-definitions.mjs +// - Rerun: npm run generate-provider-definitions +// +// To add new providers: +// - Add to ApiProvider type in src/shared/api.ts +// - Add fields to ApiHandlerOptions with provider-specific prefixes +// - Optionally add model definitions (e.g., export const newProviderModels = {...}) +// - Rerun generator +// +// To fix default model extraction: +// - Ensure TypeScript source has: export const DefaultModelId = "model-id" +// - Or update extractDefaultModelIds() patterns in generator script +// +// For upstream changes: +// - Submit pull request to src/shared/api.ts in the main repository +// +// ============================================================================ + +package generated + +import ( + "encoding/json" + "fmt" + "strings" +) + +// Provider constants +const ( +${providerConstants} +) + +// AllProviders returns a slice of enabled provider IDs for the CLI build. +// This is a filtered subset of all providers available in the VSCode extension. +// To modify which providers are included, edit ENABLED_PROVIDERS in scripts/cli-providers.mjs +var AllProviders = []string{ +${providers.map((p) => `\t"${p}",`).join("\n")} +} + +// ConfigField represents a configuration field requirement +type ConfigField struct { + Name string \`json:"name"\` + Type string \`json:"type"\` + Comment string \`json:"comment"\` + Category string \`json:"category"\` + Required bool \`json:"required"\` + FieldType string \`json:"fieldType"\` + Placeholder string \`json:"placeholder"\` +} + +// ModelInfo represents model capabilities and pricing +type ModelInfo struct { + MaxTokens int \`json:"maxTokens,omitempty"\` + ContextWindow int \`json:"contextWindow,omitempty"\` + SupportsImages bool \`json:"supportsImages"\` + SupportsPromptCache bool \`json:"supportsPromptCache"\` + InputPrice float64 \`json:"inputPrice,omitempty"\` + OutputPrice float64 \`json:"outputPrice,omitempty"\` + CacheWritesPrice float64 \`json:"cacheWritesPrice,omitempty"\` + CacheReadsPrice float64 \`json:"cacheReadsPrice,omitempty"\` + Description string \`json:"description,omitempty"\` +} + +// ProviderDefinition represents a provider's metadata and requirements +type ProviderDefinition struct { + ID string \`json:"id"\` + Name string \`json:"name"\` + RequiredFields []ConfigField \`json:"requiredFields"\` + OptionalFields []ConfigField \`json:"optionalFields"\` + Models map[string]ModelInfo \`json:"models"\` + DefaultModelID string \`json:"defaultModelId"\` + HasDynamicModels bool \`json:"hasDynamicModels"\` + SetupInstructions string \`json:"setupInstructions"\` +} + +// Raw configuration fields data (parsed from TypeScript) +var rawConfigFields = \`${configFieldsJson.replace(/`/g, '` + "`" + `')}\` + +// Raw model definitions data (parsed from TypeScript) +var rawModelDefinitions = \`${modelDefinitionsJson.replace(/`/g, '` + "`" + `')}\` + +// GetConfigFields returns all configuration fields +func GetConfigFields() ([]ConfigField, error) { + var fields []ConfigField + if err := json.Unmarshal([]byte(rawConfigFields), &fields); err != nil { + return nil, fmt.Errorf("failed to parse config fields: %w", err) + } + return fields, nil +} + +// GetModelDefinitions returns all model definitions +func GetModelDefinitions() (map[string]map[string]ModelInfo, error) { + var models map[string]map[string]ModelInfo + if err := json.Unmarshal([]byte(rawModelDefinitions), &models); err != nil { + return nil, fmt.Errorf("failed to parse model definitions: %w", err) + } + return models, nil +} + +// GetProviderDefinition returns the definition for a specific provider +func GetProviderDefinition(providerID string) (*ProviderDefinition, error) { + definitions, err := GetProviderDefinitions() + if err != nil { + return nil, err + } + + def, exists := definitions[providerID] + if !exists { + return nil, fmt.Errorf("provider %s not found", providerID) + } + + return &def, nil +} + +// GetProviderDefinitions returns all provider definitions +func GetProviderDefinitions() (map[string]ProviderDefinition, error) { + configFields, err := GetConfigFields() + if err != nil { + return nil, err + } + + modelDefinitions, err := GetModelDefinitions() + if err != nil { + return nil, err + } + + definitions := make(map[string]ProviderDefinition) + +${providerMetadata} + + return definitions, nil +} + +// IsValidProvider checks if a provider ID is valid +func IsValidProvider(providerID string) bool { + for _, p := range AllProviders { + if p == providerID { + return true + } + } + return false +} + +// GetProviderDisplayName returns a human-readable name for a provider +func GetProviderDisplayName(providerID string) string { + displayNames := map[string]string{ +${providers.map((p) => `\t\t"${p}": "${getProviderDisplayName(p)}",`).join("\n")} + } + + if name, exists := displayNames[providerID]; exists { + return name + } + return providerID +} + +// getFieldsByProvider filters configuration fields by provider and requirement +// Uses category field as primary filter with override support +func getFieldsByProvider(providerID string, allFields []ConfigField, required bool) []ConfigField { + var fields []ConfigField + + for _, field := range allFields { + fieldName := strings.ToLower(field.Name) + fieldCategory := strings.ToLower(field.Category) + providerName := strings.ToLower(providerID) + + isRelevant := false + + // Priority 1: Check manual overrides FIRST (from GetFieldOverride in this package) + if override, hasOverride := GetFieldOverride(providerID, field.Name); hasOverride { + isRelevant = override + } else if fieldCategory == providerName { + // Priority 2: Direct category match (primary filtering mechanism) + isRelevant = true + } else if fieldCategory == "aws" && providerID == "bedrock" { + // Priority 3: Handle provider-specific category relationships + // AWS fields are used by Bedrock provider + isRelevant = true + } else if fieldCategory == "openai" && providerID == "openai-native" { + // OpenAI fields used by openai-native + isRelevant = true + } else if fieldCategory == "general" { + // Priority 4: Universal fields that apply to all providers + // Note: ulid is excluded as it's auto-generated and users should not set it + universalFields := []string{"requesttimeoutms", "clineaccountid"} + for _, universal := range universalFields { + if fieldName == universal { + isRelevant = true + break + } + } + } + + if isRelevant && field.Required == required { + fields = append(fields, field) + } + } + + return fields +} +` +} + +/** + * Generate provider metadata for each provider + */ +function generateProviderMetadata(providers, configFields, modelDefinitions, defaultModelIds) { + return providers + .map((providerId) => { + const displayName = getProviderDisplayName(providerId) + const models = modelDefinitions[providerId] || {} + const defaultModelId = getDefaultModelId(providerId, models, defaultModelIds) + const hasDynamicModels = hasDynamicModelsSupport(providerId) + const setupInstructions = getSetupInstructions(providerId) + + return `\t// ${displayName} + definitions["${providerId}"] = ProviderDefinition{ + ID: "${providerId}", + Name: "${displayName}", + RequiredFields: getFieldsByProvider("${providerId}", configFields, true), + OptionalFields: getFieldsByProvider("${providerId}", configFields, false), + Models: modelDefinitions["${providerId}"], + DefaultModelID: "${defaultModelId}", + HasDynamicModels: ${hasDynamicModels}, + SetupInstructions: \`${setupInstructions}\`, + }` + }) + .join("\n\n") +} + +/** + * Get human-readable display name for a provider + */ +function getProviderDisplayName(providerId) { + const displayNames = { + anthropic: "Anthropic (Claude)", + "claude-code": "Claude Code", + openrouter: "OpenRouter", + bedrock: "AWS Bedrock", + vertex: "Google Vertex AI", + openai: "OpenAI Compatible", + ollama: "Ollama", + lmstudio: "LM Studio", + gemini: "Google Gemini", + "openai-native": "OpenAI", + requesty: "Requesty", + together: "Together AI", + deepseek: "DeepSeek", + qwen: "Qwen", + "qwen-code": "Qwen Code", + doubao: "Doubao", + mistral: "Mistral AI", + "vscode-lm": "VSCode Language Models", + cline: "Cline", + litellm: "LiteLLM", + moonshot: "Moonshot AI", + nebius: "Nebius AI", + fireworks: "Fireworks AI", + asksage: "AskSage", + xai: "X AI (Grok)", + sambanova: "SambaNova", + cerebras: "Cerebras", + sapaicore: "SAP AI Core", + groq: "Groq", + huggingface: "Hugging Face", + "huawei-cloud-maas": "Huawei Cloud MaaS", + dify: "Dify", + baseten: "Baseten", + "vercel-ai-gateway": "Vercel AI Gateway", + zai: "Z AI", + } + + return displayNames[providerId] || providerId.charAt(0).toUpperCase() + providerId.slice(1) +} + +/** + * Get default model ID for a provider + */ +function getDefaultModelId(providerId, models, defaultModelIds) { + // First, check if we have an extracted default from TypeScript source + if (defaultModelIds && defaultModelIds[providerId]) { + return defaultModelIds[providerId] + } + + // Fallback to pattern matching if no explicit default was found + const modelIds = Object.keys(models) + if (modelIds.length === 0) return "" + + // Look for common default patterns + const defaultPatterns = ["latest", "default", "sonnet", "gpt-4", "claude-3", "gemini-pro"] + + for (const pattern of defaultPatterns) { + const match = modelIds.find((id) => id.toLowerCase().includes(pattern)) + if (match) return match + } + + // Return first model if no pattern matches + return modelIds[0] +} + +/** + * Check if provider supports dynamic model fetching + */ +function hasDynamicModelsSupport(providerId) { + // Providers that support dynamic model fetching + const dynamicProviders = [ + "openrouter", + "openai", + "openai-native", + "ollama", + "lmstudio", + "litellm", + "together", + "fireworks", + "groq", + ] + + return dynamicProviders.includes(providerId) +} + +/** + * Get setup instructions for a provider + */ +function getSetupInstructions(providerId) { + const instructions = { + anthropic: "Get your API key from https://console.anthropic.com/", + openrouter: "Get your API key from https://openrouter.ai/keys", + bedrock: "Configure AWS credentials with Bedrock access permissions", + vertex: "Set up Google Cloud project with Vertex AI API enabled", + openai: "Get your API key from https://platform.openai.com/api-keys", + "openai-native": "Get your API key from your API provider", + ollama: "Install Ollama locally and ensure it's running on the specified port", + lmstudio: "Install LM Studio and start the local server", + gemini: "Get your API key from https://makersuite.google.com/app/apikey", + deepseek: "Get your API key from https://platform.deepseek.com/", + qwen: "Get your API key from Alibaba Cloud DashScope", + doubao: "Get your API key from ByteDance Volcano Engine", + mistral: "Get your API key from https://console.mistral.ai/", + xai: "Get your API key from https://console.x.ai/", + groq: "Get your API key from https://console.groq.com/keys", + cerebras: "Get your API key from https://cloud.cerebras.ai/", + fireworks: "Get your API key from https://fireworks.ai/", + } + + return instructions[providerId] || `Configure ${getProviderDisplayName(providerId)} API credentials` +} + +/** + * Main function to generate provider definitions + */ +async function main() { + try { + console.log(chalk.cyan("Starting provider definitions generation...")) + + // Parse TypeScript API definitions + const data = await parseApiDefinitions() + + // Generate Go code + const goCode = generateGoCode(data) + + // Ensure output directory exists + const outputDir = path.dirname(GO_OUTPUT_FILE) + await fs.mkdir(outputDir, { recursive: true }) + + // Write Go file + await fs.writeFile(GO_OUTPUT_FILE, goCode) + + console.log(chalk.green(`Successfully generated provider definitions:`)) + console.log(chalk.green(` Output: ${GO_OUTPUT_FILE}`)) + console.log(chalk.green(` Providers: ${data.providers.length}`)) + console.log(chalk.green(` Config fields: ${data.configFields.length}`)) + console.log(chalk.green(` Model definitions: ${Object.keys(data.modelDefinitions).length} providers`)) + } catch (error) { + console.error(chalk.red("ERROR generating provider definitions:"), error.message) + if (error.stack) { + console.error(chalk.gray(error.stack)) + } + process.exit(1) + } +} + +// Add helper function to the generated Go code +const helperFunction = ` +// getFieldsByProvider filters configuration fields by provider and requirement +func getFieldsByProvider(providerID string, allFields []ConfigField, required bool) []ConfigField { + var fields []ConfigField + + for _, field := range allFields { + // Check if field is relevant to this provider + fieldName := strings.ToLower(field.Name) + providerName := strings.ToLower(providerID) + + isRelevant := false + + // Direct provider name match + if strings.Contains(fieldName, providerName) { + isRelevant = true + } + + // Provider-specific field mappings + switch providerID { + case "anthropic": + isRelevant = strings.Contains(fieldName, "apikey") || strings.Contains(fieldName, "anthropic") + case "openrouter": + isRelevant = strings.Contains(fieldName, "openrouter") + case "bedrock": + isRelevant = strings.Contains(fieldName, "aws") || strings.Contains(fieldName, "bedrock") + case "vertex": + isRelevant = strings.Contains(fieldName, "vertex") + case "openai", "openai-native": + isRelevant = strings.Contains(fieldName, "openai") + case "ollama": + isRelevant = strings.Contains(fieldName, "ollama") + case "lmstudio": + isRelevant = strings.Contains(fieldName, "lmstudio") + case "gemini": + isRelevant = strings.Contains(fieldName, "gemini") + } + + // General fields that apply to all providers + if field.Category == "general" { + isRelevant = true + } + + if isRelevant && field.Required == required { + fields = append(fields, field) + } + } + + return fields +}` + +// Run if this script is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main() +}