package proxy import ( "crypto/rand" "encoding/hex" "sync" "time" ) // ZenBaseURL is the upstream OpenCode Zen API base URL. const ZenBaseURL = "https://opencode.ai/zen/v1" // Free models available via OpenCode Zen (no API key required). // These are the 5 models currently offered for free. var zenModels = []ModelInfo{ { ID: "big-pickle", Name: "Big Pickle (Free)", OwnedBy: "opencode-zen", ContextWindow: 200_000, MaxOutputTokens: 32_000, Description: "Permanentemente gratis. Código legacy, refactors, mantenimiento.", SupportsVision: false, }, { ID: "deepseek-v4-flash-free", Name: "DeepSeek V4 Flash (Free)", OwnedBy: "deepseek", ContextWindow: 1_000_000, MaxOutputTokens: 384_000, Description: "El todoterreno. 1M contexto, 284B params (13B activos), razonamiento.", SupportsVision: false, }, { ID: "mimo-v2.5-free", Name: "MiMo V2.5 (Free)", OwnedBy: "xiaomi", ContextWindow: 1_000_000, MaxOutputTokens: 32_000, Description: "Multimodal de Xiaomi. 1M contexto, imágenes → código. MIT license.", SupportsVision: true, }, { ID: "north-mini-code-free", Name: "North Mini Code (Free)", OwnedBy: "cohere", ContextWindow: 256_000, MaxOutputTokens: 64_000, Description: "Cohere. 30B total / 3B activos (MoE). Respuesta rápida, scripts.", SupportsVision: false, }, { ID: "nemotron-3-ultra-free", Name: "Nemotron 3 Ultra (Free)", OwnedBy: "nvidia", ContextWindow: 1_000_000, MaxOutputTokens: 16_384, Description: "NVIDIA. 550B params (55B activos). Mamba-2 + MoE híbrido. Razonamiento.", SupportsVision: false, }, } // Model aliases: short name → full model ID. var modelAliases = map[string]string{ // Short aliases "pickle": "big-pickle", "big-pickle": "big-pickle", "deepseek": "deepseek-v4-flash-free", "deepseek-v4": "deepseek-v4-flash-free", "ds": "deepseek-v4-flash-free", "mimo": "mimo-v2.5-free", "mimo-v2.5": "mimo-v2.5-free", "xiaomi": "mimo-v2.5-free", "north": "north-mini-code-free", "north-mini": "north-mini-code-free", "cohere": "north-mini-code-free", "nemotron": "nemotron-3-ultra-free", "nemotron-3": "nemotron-3-ultra-free", "nvidia": "nemotron-3-ultra-free", // Claude model name aliases (for Claude Code compatibility) // Claude Code validates model names client-side; set ANTHROPIC_MODEL to a Claude name. "claude-sonnet-4-6": "deepseek-v4-flash-free", "claude-sonnet-4-5": "deepseek-v4-flash-free", "claude-sonnet-4": "deepseek-v4-flash-free", "claude-opus-4-8": "deepseek-v4-flash-free", "claude-opus-4-5": "deepseek-v4-flash-free", "claude-opus-4": "deepseek-v4-flash-free", "claude-haiku-4-5": "north-mini-code-free", "claude-haiku-4": "north-mini-code-free", "claude-3.5-sonnet": "north-mini-code-free", "claude-3.5-haiku": "big-pickle", } // ModelInfo describes a single model. type ModelInfo struct { ID string `json:"id"` Name string `json:"name,omitempty"` OwnedBy string `json:"owned_by"` ContextWindow int `json:"context_window"` MaxOutputTokens int `json:"max_output_tokens"` Description string `json:"description"` SupportsVision bool `json:"supports_vision"` } // Proxy handles forwarding OpenAI-format requests to OpenCode Zen. type Proxy struct { apiKey string sessions map[string]*session // keyed by client-provided API key mu sync.Mutex } type session struct { id string expiresAt time.Time } // NewProxy creates a new Proxy instance. func NewProxy(apiKey string) *Proxy { return &Proxy{ apiKey: apiKey, sessions: make(map[string]*session), } } // APIKey returns the configured API key (empty = no auth). func (p *Proxy) APIKey() string { return p.apiKey } // Models returns the list of available free models. func (p *Proxy) Models() []ModelInfo { return zenModels } // ModelByID returns the ModelInfo entry for a given model ID, or nil if not found. func ModelByID(id string) *ModelInfo { for _, m := range zenModels { if m.ID == id { return &m } } return nil } // ResolveModel maps a requested model name to a Zen model ID. // Returns the resolved model ID and whether it was found. func (p *Proxy) ResolveModel(requested string) (string, bool) { // Check aliases first (covers both short names and exact IDs) if mapped, ok := modelAliases[requested]; ok { return mapped, true } // Check if it's already a valid model ID (belt and suspenders) for _, m := range zenModels { if m.ID == requested { return requested, true } } // Pass through unknown models (might be newly added upstream) return requested, false } // ZenChatURL returns the full upstream URL for chat completions. func ZenChatURL() string { return ZenBaseURL + "/chat/completions" } // ZenModelsURL returns the full upstream URL for model listing. func ZenModelsURL() string { return ZenBaseURL + "/models" } // SessionID returns a session ID for the given user key, rotating every 30 minutes. func (p *Proxy) SessionID(userKey string) string { p.mu.Lock() defer p.mu.Unlock() if s, ok := p.sessions[userKey]; ok && time.Now().Before(s.expiresAt) { return s.id } p.sessions[userKey] = &session{ id: "ses_" + randomHex(12), expiresAt: time.Now().Add(30 * time.Minute), } return p.sessions[userKey].id } // RequestID generates a new unique request ID. func RequestID() string { return "msg_" + randomHex(12) } func randomHex(n int) string { b := make([]byte, n) rand.Read(b) return hex.EncodeToString(b) }