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:
@@ -652,15 +652,19 @@ func (p *Proxy) ForwardAnthropicMessages(w http.ResponseWriter, r *http.Request,
|
||||
return fmt.Errorf("invalid Anthropic request: %w", err)
|
||||
}
|
||||
|
||||
// Resolve model
|
||||
resolved, _ := p.ResolveModel(ar.Model)
|
||||
// Route to the backend that owns this model (default = pass-through).
|
||||
backend, resolved, known := p.resolveModel(ar.Model)
|
||||
if !known {
|
||||
resolved = ar.Model
|
||||
}
|
||||
// Keep original client-requested model name for response rewriting.
|
||||
// Claude Code validates that the response model matches the request model.
|
||||
clientModel := ar.Model
|
||||
|
||||
// Auto-route to vision model if the request has images and resolved model can't handle them
|
||||
// Auto-route to a vision model only inside the opencode backend (which owns
|
||||
// a vision-capable free model). Kilo models are forwarded as-is.
|
||||
hasImages := requestHasImages(&ar)
|
||||
if hasImages {
|
||||
if hasImages && backend.Name() == "opencode" {
|
||||
mi := ModelByID(resolved)
|
||||
if mi == nil || !mi.SupportsVision {
|
||||
fmt.Printf("[anthropic] model=%s lacks vision, auto-routing to mimo-v2.5-free\n", resolved)
|
||||
@@ -668,8 +672,8 @@ func (p *Proxy) ForwardAnthropicMessages(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[anthropic] model=%s → %s, max_tokens=%d, stream=%v, messages=%d, tools=%d\n",
|
||||
clientModel, resolved, ar.MaxTokens, ar.Stream, len(ar.Messages), len(ar.Tools))
|
||||
fmt.Printf("[anthropic] model=%s → %s via %s, max_tokens=%d, stream=%v, messages=%d, tools=%d\n",
|
||||
clientModel, resolved, backend.Name(), ar.MaxTokens, ar.Stream, len(ar.Messages), len(ar.Tools))
|
||||
|
||||
// Convert Anthropic → OpenAI
|
||||
oaReq := AnthropicToOpenAI(&ar)
|
||||
@@ -686,13 +690,13 @@ func (p *Proxy) ForwardAnthropicMessages(w http.ResponseWriter, r *http.Request,
|
||||
requestID := RequestID()
|
||||
sessionID := p.SessionID("anthropic")
|
||||
|
||||
// Build upstream request
|
||||
upstreamReq, err := http.NewRequestWithContext(r.Context(), "POST", ZenChatURL(), bytes.NewReader(oaBody))
|
||||
// Build upstream request to the chosen backend
|
||||
upstreamReq, err := http.NewRequestWithContext(r.Context(), "POST", backend.ChatURL(), bytes.NewReader(oaBody))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create upstream request: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range UpstreamHeaders(requestID, sessionID) {
|
||||
for k, v := range backend.Headers(requestID, sessionID) {
|
||||
upstreamReq.Header.Set(k, v)
|
||||
}
|
||||
|
||||
@@ -709,6 +713,9 @@ func (p *Proxy) ForwardAnthropicMessages(w http.ResponseWriter, r *http.Request,
|
||||
if resp.StatusCode != 200 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("[anthropic] upstream error body (%d bytes): %s\n", len(bodyBytes), string(bodyBytes))
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
p.markRateLimited(resolved)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
json.NewEncoder(w).Encode(AnthropicError{
|
||||
|
||||
Reference in New Issue
Block a user