mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
Add local reverse proxy support for third-party Claude providers with: - Thinking parameter override rewriting - Bearer token auth compatibility - Provider clearing functionality - Configurable thinking modes - Improved provider management UX - Enhanced markdown rendering for Feishu generated by llmgit Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
3.7 KiB
Go
140 lines
3.7 KiB
Go
package core
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ProviderProxy is a lightweight local reverse proxy that rewrites
|
|
// incompatible Anthropic API fields for third-party providers.
|
|
//
|
|
// Some providers (e.g. SiliconFlow) don't support thinking.type "adaptive"
|
|
// sent by Claude Code 2.x. The proxy rewrites the thinking field to
|
|
// the configured override value before forwarding.
|
|
type ProviderProxy struct {
|
|
targetURL string
|
|
thinkingOverride string
|
|
listener net.Listener
|
|
server *http.Server
|
|
once sync.Once
|
|
}
|
|
|
|
// NewProviderProxy creates and starts a local reverse proxy for the
|
|
// given upstream URL. thinkingOverride controls what thinking.type to
|
|
// rewrite "adaptive" to (e.g. "disabled" or "enabled").
|
|
// Returns the local URL to use as ANTHROPIC_BASE_URL.
|
|
func NewProviderProxy(targetURL, thinkingOverride string) (*ProviderProxy, string, error) {
|
|
target, err := url.Parse(strings.TrimRight(targetURL, "/"))
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("providerproxy: parse target: %w", err)
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("providerproxy: listen: %w", err)
|
|
}
|
|
|
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
origDirector := proxy.Director
|
|
proxy.Director = func(req *http.Request) {
|
|
origDirector(req)
|
|
req.Host = target.Host
|
|
}
|
|
proxy.FlushInterval = -1 // flush SSE events immediately
|
|
|
|
override := thinkingOverride
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/messages") {
|
|
rewriteThinkingInRequest(r, override)
|
|
}
|
|
proxy.ServeHTTP(w, r)
|
|
})
|
|
|
|
pp := &ProviderProxy{
|
|
targetURL: targetURL,
|
|
thinkingOverride: thinkingOverride,
|
|
listener: listener,
|
|
server: &http.Server{
|
|
Handler: mux,
|
|
ReadTimeout: 10 * time.Minute,
|
|
WriteTimeout: 10 * time.Minute,
|
|
},
|
|
}
|
|
|
|
go func() {
|
|
if err := pp.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
|
slog.Error("providerproxy: serve error", "error", err)
|
|
}
|
|
}()
|
|
|
|
localURL := fmt.Sprintf("http://127.0.0.1:%d", listener.Addr().(*net.TCPAddr).Port)
|
|
slog.Info("providerproxy: started", "target", targetURL, "local", localURL, "thinking", thinkingOverride)
|
|
return pp, localURL, nil
|
|
}
|
|
|
|
// Close shuts down the proxy.
|
|
func (pp *ProviderProxy) Close() {
|
|
pp.once.Do(func() {
|
|
pp.server.Close()
|
|
})
|
|
}
|
|
|
|
// rewriteThinkingInRequest reads the request body and rewrites
|
|
// thinking.type "adaptive" to the given override value.
|
|
func rewriteThinkingInRequest(r *http.Request, override string) {
|
|
if r.Body == nil || override == "" {
|
|
return
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
r.Body.Close()
|
|
if err != nil {
|
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
|
return
|
|
}
|
|
|
|
var data map[string]any
|
|
if err := json.Unmarshal(body, &data); err != nil {
|
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
|
r.ContentLength = int64(len(body))
|
|
return
|
|
}
|
|
|
|
modified := false
|
|
if thinking, ok := data["thinking"].(map[string]any); ok {
|
|
if t, ok := thinking["type"].(string); ok && t == "adaptive" {
|
|
thinking["type"] = override
|
|
if override == "disabled" {
|
|
delete(thinking, "budget_tokens")
|
|
}
|
|
modified = true
|
|
slog.Debug("providerproxy: rewrote thinking adaptive →", "override", override)
|
|
}
|
|
}
|
|
|
|
if !modified {
|
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
|
r.ContentLength = int64(len(body))
|
|
return
|
|
}
|
|
|
|
newBody, err := json.Marshal(data)
|
|
if err != nil {
|
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
|
r.ContentLength = int64(len(body))
|
|
return
|
|
}
|
|
r.Body = io.NopCloser(bytes.NewReader(newBody))
|
|
r.ContentLength = int64(len(newBody))
|
|
}
|