feat: multi-backend proxy with auto rate-limit jailing
Refactor from monolithic proxy to multi-backend architecture (Backend interface: OpenCode Zen + Kilo gateway). - Add auto rate-limit jailing: models returning HTTP 429 are hidden from /v1/models immediately and re-probed every 30 min - Backend interface supports model aliases, custom headers, and per-backend routing - Full Anthropic Messages API support with OpenAI format conversion - 9 free models across two backends with Claude name aliases
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
package proxy
|
||||
|
||||
// ZenBaseURL is the upstream OpenCode Zen API base URL.
|
||||
const ZenBaseURL = "https://opencode.ai/zen/v1"
|
||||
|
||||
// OpenCodeBackend forwards to OpenCode's free Zen models.
|
||||
type OpenCodeBackend struct{}
|
||||
|
||||
// zenModels are the free models exposed by OpenCode Zen (no API key required).
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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.",
|
||||
},
|
||||
}
|
||||
|
||||
// modelAliases maps short/company/Claude names to OpenCode Zen model IDs.
|
||||
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",
|
||||
// Exact IDs
|
||||
"deepseek-v4-flash-free": "deepseek-v4-flash-free",
|
||||
"mimo-v2.5-free": "mimo-v2.5-free",
|
||||
"north-mini-code-free": "north-mini-code-free",
|
||||
"nemotron-3-ultra-free": "nemotron-3-ultra-free",
|
||||
|
||||
// Claude model name aliases (Claude Code validates model names client-side).
|
||||
"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",
|
||||
}
|
||||
|
||||
// Name returns the backend identifier.
|
||||
func (OpenCodeBackend) Name() string { return "opencode" }
|
||||
|
||||
// Models returns the OpenCode Zen free models.
|
||||
func (OpenCodeBackend) Models() []ModelInfo { return zenModels }
|
||||
|
||||
// Resolve maps a requested name to an OpenCode Zen model ID.
|
||||
func (OpenCodeBackend) Resolve(model string) (string, bool) {
|
||||
if mapped, ok := modelAliases[model]; ok {
|
||||
return mapped, true
|
||||
}
|
||||
for _, m := range zenModels {
|
||||
if m.ID == model {
|
||||
return model, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ChatURL is the OpenCode Zen chat completions endpoint.
|
||||
func (OpenCodeBackend) ChatURL() string { return ZenBaseURL + "/chat/completions" }
|
||||
|
||||
// Headers builds the headers OpenCode Zen expects (x-opencode-* + public bearer).
|
||||
func (OpenCodeBackend) Headers(requestID, sessionID string) map[string]string {
|
||||
return map[string]string{
|
||||
"Authorization": "Bearer public",
|
||||
"User-Agent": "opencode/1.15.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13",
|
||||
"x-opencode-client": "cli",
|
||||
"x-opencode-project": "global",
|
||||
"x-opencode-request": requestID,
|
||||
"x-opencode-session": sessionID,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/event-stream, application/json",
|
||||
}
|
||||
}
|
||||
|
||||
// ModelByID returns the OpenCode Zen ModelInfo for an ID, or nil if not found.
|
||||
// Used by the Anthropic path for vision auto-routing.
|
||||
func ModelByID(id string) *ModelInfo {
|
||||
for _, m := range zenModels {
|
||||
if m.ID == id {
|
||||
return &m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ZenChatURL returns the OpenCode Zen chat completions URL.
|
||||
// Kept for backwards compatibility with existing call sites.
|
||||
func ZenChatURL() string { return ZenBaseURL + "/chat/completions" }
|
||||
|
||||
// ZenModelsURL returns the OpenCode Zen models listing URL.
|
||||
func ZenModelsURL() string { return ZenBaseURL + "/models" }
|
||||
Reference in New Issue
Block a user