freebuff2api / server.go
xingshang3084's picture
Deploy Freebuff2API Docker Space
c0adbd0 verified
Raw
History Blame Contribute Delete
17.7 kB
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
type Server struct {
cfg Config
logger *log.Logger
client *UpstreamClient
runs *RunManager
registry *ModelRegistry
started time.Time
}
func NewServer(cfg Config, logger *log.Logger, registry *ModelRegistry) *Server {
client := NewUpstreamClient(cfg)
runManager := NewRunManager(cfg, client, logger)
return &Server{
cfg: cfg,
logger: logger,
client: client,
runs: runManager,
registry: registry,
started: time.Now(),
}
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", s.handleHealthz)
mux.HandleFunc("/v1/models", s.handleModels)
mux.HandleFunc("/v1/chat/completions", s.handleChatCompletions)
return s.withMiddleware(mux)
}
func (s *Server) Start(ctx context.Context) {
s.runs.Start(ctx, s.registry.AgentIDs())
}
func (s *Server) Shutdown(ctx context.Context) {
s.runs.Close(ctx)
}
func (s *Server) withMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if len(s.cfg.APIKeys) > 0 && !s.authorized(r) {
writeOpenAIError(w, http.StatusUnauthorized, "invalid proxy api key", "authentication_error", "")
return
}
next.ServeHTTP(w, r)
})
}
func (s *Server) authorized(r *http.Request) bool {
authorization := strings.TrimSpace(r.Header.Get("Authorization"))
if authorization == "" {
return false
}
const prefix = "Bearer "
if !strings.HasPrefix(authorization, prefix) {
return false
}
apiKey := strings.TrimSpace(strings.TrimPrefix(authorization, prefix))
return containsString(s.cfg.APIKeys, apiKey)
}
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeOpenAIError(w, http.StatusMethodNotAllowed, "method not allowed", "invalid_request_error", "")
return
}
response := map[string]any{
"ok": true,
"started_at": s.started.UTC(),
"uptime_sec": int(time.Since(s.started).Seconds()),
"token_state": s.runs.Snapshots(),
}
writeJSON(w, http.StatusOK, response)
}
func (s *Server) handleModels(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeOpenAIError(w, http.StatusMethodNotAllowed, "method not allowed", "invalid_request_error", "")
return
}
created := s.started.Unix()
modelsList := s.registry.Models()
models := make([]map[string]any, 0, len(modelsList))
for _, model := range modelsList {
models = append(models, map[string]any{
"id": model,
"object": "model",
"created": created,
"owned_by": "Freebuff2API",
"root": model,
"permission": []any{},
})
}
writeJSON(w, http.StatusOK, map[string]any{
"object": "list",
"data": models,
})
}
func (s *Server) handleChatCompletions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeOpenAIError(w, http.StatusMethodNotAllowed, "method not allowed", "invalid_request_error", "")
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
writeOpenAIError(w, http.StatusBadRequest, "failed to read request body", "invalid_request_error", "")
return
}
var payload map[string]any
if err := json.Unmarshal(requestBody, &payload); err != nil {
writeOpenAIError(w, http.StatusBadRequest, "request body must be valid JSON", "invalid_request_error", "")
return
}
requestedModel, _ := payload["model"].(string)
requestedModel = strings.TrimSpace(requestedModel)
if requestedModel == "" {
writeOpenAIError(w, http.StatusBadRequest, "model is required", "invalid_request_error", "")
return
}
agentID, ok := s.registry.AgentForModel(requestedModel)
if !ok {
writeOpenAIError(w, http.StatusBadRequest, fmt.Sprintf("unsupported model %q", requestedModel), "invalid_request_error", "model_not_found")
return
}
startTime := time.Now()
for attempt := 0; attempt < 2; attempt++ {
lease, err := s.runs.Acquire(r.Context(), agentID)
if err != nil {
writeOpenAIError(w, http.StatusBadGateway, "no healthy upstream auth token available", "server_error", "")
return
}
s.logger.Printf("[%s] Routing request (model: %s) via run: %s", lease.pool.name, requestedModel, lease.run.id)
upstreamBody, err := s.injectUpstreamMetadata(payload, requestedModel, lease.run.id, lease.pool.currentSessionInstanceID())
if err != nil {
s.runs.Release(lease)
writeOpenAIError(w, http.StatusBadRequest, err.Error(), "invalid_request_error", "")
return
}
resp, errorBody, err := s.client.ChatCompletions(r.Context(), lease.pool.token, upstreamBody)
if err != nil {
s.runs.Release(lease)
writeOpenAIError(w, http.StatusBadGateway, err.Error(), "server_error", "")
return
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
defer resp.Body.Close()
copyHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
if err := copyResponseBody(w, resp.Body); err != nil && !errors.Is(err, context.Canceled) {
s.logger.Printf("[%s] proxy response copy failed: %v", lease.pool.name, err)
}
s.logger.Printf("[%s] Request completed successfully in %v (status: %d)", lease.pool.name, time.Since(startTime).Round(time.Millisecond), resp.StatusCode)
s.runs.Release(lease)
return
}
if isSessionInvalid(resp.StatusCode, errorBody) {
s.logger.Printf("%s: free session invalid, refreshing and retrying", lease.pool.name)
lease.pool.invalidateSession(strings.TrimSpace(string(errorBody)))
s.runs.Release(lease)
continue
}
if isRunInvalid(resp.StatusCode, errorBody) {
s.logger.Printf("%s: run %s invalid, rotating and retrying", lease.pool.name, lease.run.id)
s.runs.Invalidate(lease, strings.TrimSpace(string(errorBody)))
s.runs.Release(lease)
continue
}
if resp.StatusCode == http.StatusUnauthorized {
s.runs.Cooldown(lease, 30*time.Minute, "upstream auth rejected token")
lease.pool.invalidateSession("upstream auth rejected token")
}
s.runs.Release(lease)
s.logger.Printf("[%s] upstream error response: %s", lease.pool.name, string(errorBody))
writePassthroughError(w, resp.StatusCode, errorBody)
return
}
writeOpenAIError(w, http.StatusBadGateway, "upstream run expired twice in a row", "server_error", "")
}
func (s *Server) injectUpstreamMetadata(payload map[string]any, requestedModel, runID, sessionInstanceID string) ([]byte, error) {
cloned := cloneMap(payload)
cloned["model"] = requestedModel
// Normalize tool parameter schemas into a conservative subset the upstream
// backend can parse. This keeps LobeChat-style schemas working without
// changing non-tool requests.
if tools, ok := cloned["tools"].([]any); ok {
normalizeToolSchemas(tools)
}
metadata, ok := cloned["codebuff_metadata"].(map[string]any)
if !ok || metadata == nil {
metadata = make(map[string]any)
}
metadata["run_id"] = runID
metadata["cost_mode"] = "free"
metadata["client_id"] = generateClientSessionId()
if strings.TrimSpace(sessionInstanceID) != "" {
metadata["freebuff_instance_id"] = sessionInstanceID
}
cloned["codebuff_metadata"] = metadata
body, err := json.Marshal(cloned)
if err != nil {
return nil, fmt.Errorf("marshal upstream request: %w", err)
}
return body, nil
}
func isSessionInvalid(statusCode int, errorBody []byte) bool {
if statusCode < 400 {
return false
}
var payload struct {
Error string `json:"error"`
}
if err := json.Unmarshal(errorBody, &payload); err != nil {
return false
}
switch strings.TrimSpace(payload.Error) {
case "freebuff_update_required", "waiting_room_required", "waiting_room_queued", "session_superseded", "session_expired":
return true
default:
return false
}
}
// normalizeToolSchemas rewrites tool parameter schemas into a conservative JSON
// Schema subset. Today that means resolving local $ref values and simplifying
// common nullable constructs emitted by clients like LobeChat.
func normalizeToolSchemas(tools []any) {
for _, tool := range tools {
toolMap, ok := tool.(map[string]any)
if !ok {
continue
}
fn, ok := toolMap["function"].(map[string]any)
if !ok {
continue
}
params, ok := fn["parameters"].(map[string]any)
if !ok {
continue
}
fn["parameters"] = normalizeSchemaMap(params, extractDefinitions(params), 12)
}
}
// extractDefinitions returns the combined definitions map from "definitions" and "$defs".
func extractDefinitions(schema map[string]any) map[string]any {
merged := make(map[string]any)
if d, ok := schema["definitions"].(map[string]any); ok {
for key, value := range d {
merged[key] = value
}
}
if d, ok := schema["$defs"].(map[string]any); ok {
for key, value := range d {
merged[key] = value
}
}
if len(merged) == 0 {
return nil
}
return merged
}
func mergeDefinitions(parent, local map[string]any) map[string]any {
if len(parent) == 0 {
return local
}
if len(local) == 0 {
return parent
}
merged := make(map[string]any, len(parent)+len(local))
for key, value := range parent {
merged[key] = value
}
for key, value := range local {
merged[key] = value
}
return merged
}
func normalizeSchemaValue(value any, defs map[string]any, maxDepth int) any {
switch typed := value.(type) {
case map[string]any:
return normalizeSchemaMap(typed, defs, maxDepth)
case []any:
return normalizeSchemaSlice(typed, defs, maxDepth)
default:
return value
}
}
func normalizeSchemaMap(node map[string]any, defs map[string]any, maxDepth int) map[string]any {
if maxDepth <= 0 {
return cloneMap(node)
}
defs = mergeDefinitions(defs, extractDefinitions(node))
if replaced := tryResolveRef(node, defs); replaced != nil {
if replacedMap, ok := replaced.(map[string]any); ok {
return normalizeSchemaMap(replacedMap, defs, maxDepth-1)
}
return cloneMap(node)
}
normalized := make(map[string]any, len(node))
for key, value := range node {
normalized[key] = normalizeSchemaValue(value, defs, maxDepth-1)
}
delete(normalized, "definitions")
delete(normalized, "$defs")
delete(normalized, "nullable")
normalized = simplifyNullableCombinator(normalized, "anyOf")
normalized = simplifyNullableCombinator(normalized, "oneOf")
normalizeTypeField(normalized)
normalizeEnumField(normalized)
normalizeConstField(normalized)
return normalized
}
func normalizeSchemaSlice(slice []any, defs map[string]any, maxDepth int) []any {
if maxDepth <= 0 {
return cloneSlice(slice)
}
normalized := make([]any, len(slice))
for i, value := range slice {
normalized[i] = normalizeSchemaValue(value, defs, maxDepth-1)
}
return normalized
}
func simplifyNullableCombinator(schema map[string]any, key string) map[string]any {
rawOptions, ok := schema[key].([]any)
if !ok {
return schema
}
filtered := make([]any, 0, len(rawOptions))
for _, option := range rawOptions {
if optionMap, ok := option.(map[string]any); ok && isNullSchema(optionMap) {
continue
}
filtered = append(filtered, option)
}
if len(filtered) == 0 {
delete(schema, key)
return schema
}
if len(filtered) == 1 {
if optionMap, ok := filtered[0].(map[string]any); ok {
merged := make(map[string]any, len(schema)+len(optionMap))
for existingKey, existingValue := range schema {
if existingKey == key {
continue
}
merged[existingKey] = existingValue
}
for optionKey, optionValue := range optionMap {
merged[optionKey] = optionValue
}
return merged
}
}
schema[key] = filtered
return schema
}
func normalizeTypeField(schema map[string]any) {
rawType, ok := schema["type"]
if !ok {
return
}
if _, ok := rawType.(string); ok {
return
}
types, ok := rawType.([]any)
if !ok {
return
}
nonNullTypes := make([]string, 0, len(types))
for _, entry := range types {
typeName, ok := entry.(string)
if !ok || typeName == "null" || strings.TrimSpace(typeName) == "" {
continue
}
nonNullTypes = append(nonNullTypes, typeName)
}
switch len(nonNullTypes) {
case 0:
delete(schema, "type")
case 1:
schema["type"] = nonNullTypes[0]
default:
// Upstream expects a single primitive type. Keep the first non-null type
// rather than failing the whole request.
schema["type"] = nonNullTypes[0]
}
}
func normalizeEnumField(schema map[string]any) {
enumValues, ok := schema["enum"].([]any)
if !ok {
return
}
filtered := make([]any, 0, len(enumValues))
seen := make(map[string]struct{}, len(enumValues))
for _, entry := range enumValues {
if entry == nil {
continue
}
key := fmt.Sprintf("%T:%v", entry, entry)
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
filtered = append(filtered, entry)
}
if len(filtered) == 0 {
delete(schema, "enum")
return
}
schema["enum"] = filtered
}
func normalizeConstField(schema map[string]any) {
if value, ok := schema["const"]; ok && value == nil {
delete(schema, "const")
}
}
func isNullSchema(schema map[string]any) bool {
if typeName, ok := schema["type"].(string); ok && typeName == "null" {
return true
}
if constValue, ok := schema["const"]; ok && constValue == nil {
return true
}
if enumValues, ok := schema["enum"].([]any); ok && len(enumValues) == 1 && enumValues[0] == nil {
return true
}
return false
}
// tryResolveRef checks if a node is a $ref object like {"$ref": "#/definitions/Foo"}
// and returns the cloned definition if found.
func tryResolveRef(node map[string]any, defs map[string]any) any {
ref, ok := node["$ref"].(string)
if !ok || len(node) != 1 {
return nil
}
// Support both "#/definitions/X" and "#/$defs/X"
var name string
if strings.HasPrefix(ref, "#/definitions/") {
name = strings.TrimPrefix(ref, "#/definitions/")
} else if strings.HasPrefix(ref, "#/$defs/") {
name = strings.TrimPrefix(ref, "#/$defs/")
}
if name == "" {
return nil
}
def, ok := defs[name]
if !ok {
return nil
}
// Clone to avoid mutating the original definition
if defMap, ok := def.(map[string]any); ok {
return cloneMap(defMap)
}
return def
}
func cloneMap(input map[string]any) map[string]any {
output := make(map[string]any, len(input))
for key, value := range input {
switch typed := value.(type) {
case map[string]any:
output[key] = cloneMap(typed)
case []any:
output[key] = cloneSlice(typed)
default:
output[key] = value
}
}
return output
}
func cloneSlice(input []any) []any {
output := make([]any, len(input))
for index, value := range input {
switch typed := value.(type) {
case map[string]any:
output[index] = cloneMap(typed)
case []any:
output[index] = cloneSlice(typed)
default:
output[index] = value
}
}
return output
}
func copyHeaders(dst, src http.Header) {
for key, values := range src {
if strings.EqualFold(key, "Content-Length") {
continue
}
dst.Del(key)
for _, value := range values {
dst.Add(key, value)
}
}
}
func copyResponseBody(w http.ResponseWriter, body io.Reader) error {
flusher, _ := w.(http.Flusher)
buffer := make([]byte, 32*1024)
for {
n, err := body.Read(buffer)
if n > 0 {
if _, writeErr := w.Write(buffer[:n]); writeErr != nil {
return writeErr
}
if flusher != nil {
flusher.Flush()
}
}
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
}
}
func isRunInvalid(statusCode int, body []byte) bool {
if statusCode != http.StatusBadRequest {
return false
}
message := strings.ToLower(string(body))
return strings.Contains(message, "runid not found") || strings.Contains(message, "runid not running")
}
func writePassthroughError(w http.ResponseWriter, statusCode int, body []byte) {
trimmed := bytes.TrimSpace(body)
if len(trimmed) > 0 && json.Valid(trimmed) {
message, errorType, code := extractUpstreamError(trimmed)
writeOpenAIError(w, statusCode, message, errorType, code)
return
}
writeOpenAIError(w, statusCode, strings.TrimSpace(string(trimmed)), "upstream_error", "")
}
func extractUpstreamError(body []byte) (message, errorType, code string) {
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
return strings.TrimSpace(string(body)), "upstream_error", ""
}
errorType = "upstream_error"
if rawError, ok := payload["error"]; ok {
switch typed := rawError.(type) {
case string:
code = typed
case map[string]any:
if value, ok := typed["message"].(string); ok && strings.TrimSpace(value) != "" {
message = value
}
if value, ok := typed["type"].(string); ok && strings.TrimSpace(value) != "" {
errorType = value
}
if value, ok := typed["code"].(string); ok && strings.TrimSpace(value) != "" {
code = value
}
}
}
if value, ok := payload["message"].(string); ok && strings.TrimSpace(value) != "" {
message = value
}
if message == "" {
message = strings.TrimSpace(string(body))
}
return message, errorType, code
}
func writeOpenAIError(w http.ResponseWriter, statusCode int, message, errorType, code string) {
if message == "" {
message = http.StatusText(statusCode)
}
payload := map[string]any{
"error": map[string]any{
"message": message,
"type": errorType,
},
}
if code != "" {
payload["error"].(map[string]any)["code"] = code
}
writeJSON(w, statusCode, payload)
}
func writeJSON(w http.ResponseWriter, statusCode int, payload any) {
body, err := json.Marshal(payload)
if err != nil {
http.Error(w, `{"error":{"message":"failed to encode response","type":"server_error"}}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
_, _ = w.Write(body)
}
func maxDuration(a, b time.Duration) time.Duration {
if a > b {
return a
}
return b
}