Delete main.go
Browse files
main.go
DELETED
@@ -1,1184 +0,0 @@
|
|
1 |
-
package main
|
2 |
-
|
3 |
-
import (
|
4 |
-
"bufio"
|
5 |
-
"bytes"
|
6 |
-
"context"
|
7 |
-
"encoding/json"
|
8 |
-
"flag"
|
9 |
-
"fmt"
|
10 |
-
"io"
|
11 |
-
"log"
|
12 |
-
"math/rand"
|
13 |
-
"net"
|
14 |
-
"net/http"
|
15 |
-
"net/url"
|
16 |
-
"os"
|
17 |
-
"os/signal"
|
18 |
-
"path/filepath"
|
19 |
-
"regexp"
|
20 |
-
"strings"
|
21 |
-
"sync"
|
22 |
-
"syscall"
|
23 |
-
"time"
|
24 |
-
|
25 |
-
"github.com/google/uuid"
|
26 |
-
"github.com/spf13/viper"
|
27 |
-
"golang.org/x/net/proxy"
|
28 |
-
)
|
29 |
-
|
30 |
-
// ---------------------- 配置结构 ----------------------
|
31 |
-
|
32 |
-
type Config struct {
|
33 |
-
ThinkingServices []ThinkingService `mapstructure:"thinking_services"`
|
34 |
-
Channels map[string]Channel `mapstructure:"channels"`
|
35 |
-
Global GlobalConfig `mapstructure:"global"`
|
36 |
-
}
|
37 |
-
|
38 |
-
type ThinkingService struct {
|
39 |
-
ID int `mapstructure:"id"`
|
40 |
-
Name string `mapstructure:"name"`
|
41 |
-
Model string `mapstructure:"model"`
|
42 |
-
BaseURL string `mapstructure:"base_url"`
|
43 |
-
APIPath string `mapstructure:"api_path"`
|
44 |
-
APIKey string `mapstructure:"api_key"`
|
45 |
-
Timeout int `mapstructure:"timeout"`
|
46 |
-
Retry int `mapstructure:"retry"`
|
47 |
-
Weight int `mapstructure:"weight"`
|
48 |
-
Proxy string `mapstructure:"proxy"`
|
49 |
-
Mode string `mapstructure:"mode"` // "standard" 或 "full"
|
50 |
-
ReasoningEffort string `mapstructure:"reasoning_effort"`
|
51 |
-
ReasoningFormat string `mapstructure:"reasoning_format"`
|
52 |
-
Temperature *float64 `mapstructure:"temperature"`
|
53 |
-
ForceStopDeepThinking bool `mapstructure:"force_stop_deep_thinking"` // 配置项:标准模式下遇到 content 时是否立即停止
|
54 |
-
}
|
55 |
-
|
56 |
-
func (s *ThinkingService) GetFullURL() string {
|
57 |
-
return s.BaseURL + s.APIPath
|
58 |
-
}
|
59 |
-
|
60 |
-
type Channel struct {
|
61 |
-
Name string `mapstructure:"name"`
|
62 |
-
BaseURL string `mapstructure:"base_url"`
|
63 |
-
APIPath string `mapstructure:"api_path"`
|
64 |
-
Timeout int `mapstructure:"timeout"`
|
65 |
-
Proxy string `mapstructure:"proxy"`
|
66 |
-
}
|
67 |
-
|
68 |
-
func (c *Channel) GetFullURL() string {
|
69 |
-
return c.BaseURL + c.APIPath
|
70 |
-
}
|
71 |
-
|
72 |
-
type LogConfig struct {
|
73 |
-
Level string `mapstructure:"level"`
|
74 |
-
Format string `mapstructure:"format"`
|
75 |
-
Output string `mapstructure:"output"`
|
76 |
-
FilePath string `mapstructure:"file_path"`
|
77 |
-
Debug DebugConfig `mapstructure:"debug"`
|
78 |
-
}
|
79 |
-
|
80 |
-
type DebugConfig struct {
|
81 |
-
Enabled bool `mapstructure:"enabled"`
|
82 |
-
PrintRequest bool `mapstructure:"print_request"`
|
83 |
-
PrintResponse bool `mapstructure:"print_response"`
|
84 |
-
MaxContentLength int `mapstructure:"max_content_length"`
|
85 |
-
}
|
86 |
-
|
87 |
-
type ProxyConfig struct {
|
88 |
-
Enabled bool `mapstructure:"enabled"`
|
89 |
-
Default string `mapstructure:"default"`
|
90 |
-
AllowInsecure bool `mapstructure:"allow_insecure"`
|
91 |
-
}
|
92 |
-
|
93 |
-
type GlobalConfig struct {
|
94 |
-
MaxRetries int `mapstructure:"max_retries"`
|
95 |
-
DefaultTimeout int `mapstructure:"default_timeout"`
|
96 |
-
ErrorCodes struct {
|
97 |
-
RetryOn []int `mapstructure:"retry_on"`
|
98 |
-
} `mapstructure:"error_codes"`
|
99 |
-
Log LogConfig `mapstructure:"log"`
|
100 |
-
Server ServerConfig `mapstructure:"server"`
|
101 |
-
Proxy ProxyConfig `mapstructure:"proxy"`
|
102 |
-
ConfigPaths []string `mapstructure:"config_paths"`
|
103 |
-
Thinking ThinkingConfig `mapstructure:"thinking"`
|
104 |
-
}
|
105 |
-
|
106 |
-
type ServerConfig struct {
|
107 |
-
Port int `mapstructure:"port"`
|
108 |
-
Host string `mapstructure:"host"`
|
109 |
-
ReadTimeout int `mapstructure:"read_timeout"`
|
110 |
-
WriteTimeout int `mapstructure:"write_timeout"`
|
111 |
-
IdleTimeout int `mapstructure:"idle_timeout"`
|
112 |
-
}
|
113 |
-
|
114 |
-
type ThinkingConfig struct {
|
115 |
-
Enabled bool `mapstructure:"enabled"`
|
116 |
-
AddToAllRequests bool `mapstructure:"add_to_all_requests"`
|
117 |
-
Timeout int `mapstructure:"timeout"`
|
118 |
-
}
|
119 |
-
|
120 |
-
// ---------------------- API 相关结构 ----------------------
|
121 |
-
|
122 |
-
type ChatCompletionRequest struct {
|
123 |
-
Model string `json:"model"`
|
124 |
-
Messages []ChatCompletionMessage `json:"messages"`
|
125 |
-
Temperature float64 `json:"temperature,omitempty"`
|
126 |
-
MaxTokens int `json:"max_tokens,omitempty"`
|
127 |
-
Stream bool `json:"stream,omitempty"`
|
128 |
-
APIKey string `json:"-"` // 内部传递,不序列化
|
129 |
-
}
|
130 |
-
|
131 |
-
type ChatCompletionMessage struct {
|
132 |
-
Role string `json:"role"`
|
133 |
-
Content string `json:"content"`
|
134 |
-
ReasoningContent interface{} `json:"reasoning_content,omitempty"`
|
135 |
-
}
|
136 |
-
|
137 |
-
type ChatCompletionResponse struct {
|
138 |
-
ID string `json:"id"`
|
139 |
-
Object string `json:"object"`
|
140 |
-
Created int64 `json:"created"`
|
141 |
-
Model string `json:"model"`
|
142 |
-
Choices []Choice `json:"choices"`
|
143 |
-
Usage Usage `json:"usage"`
|
144 |
-
}
|
145 |
-
|
146 |
-
type Choice struct {
|
147 |
-
Index int `json:"index"`
|
148 |
-
Message ChatCompletionMessage `json:"message"`
|
149 |
-
FinishReason string `json:"finish_reason"`
|
150 |
-
}
|
151 |
-
|
152 |
-
type Usage struct {
|
153 |
-
PromptTokens int `json:"prompt_tokens"`
|
154 |
-
CompletionTokens int `json:"completion_tokens"`
|
155 |
-
TotalTokens int `json:"total_tokens"`
|
156 |
-
}
|
157 |
-
|
158 |
-
// ---------------------- 日志工具 ----------------------
|
159 |
-
|
160 |
-
type RequestLogger struct {
|
161 |
-
RequestID string
|
162 |
-
Model string
|
163 |
-
StartTime time.Time
|
164 |
-
logs []string
|
165 |
-
config *Config
|
166 |
-
}
|
167 |
-
|
168 |
-
func NewRequestLogger(config *Config) *RequestLogger {
|
169 |
-
return &RequestLogger{
|
170 |
-
RequestID: uuid.New().String(),
|
171 |
-
StartTime: time.Now(),
|
172 |
-
logs: make([]string, 0),
|
173 |
-
config: config,
|
174 |
-
}
|
175 |
-
}
|
176 |
-
|
177 |
-
func (l *RequestLogger) Log(format string, args ...interface{}) {
|
178 |
-
msg := fmt.Sprintf(format, args...)
|
179 |
-
l.logs = append(l.logs, fmt.Sprintf("[%s] %s", time.Now().Format(time.RFC3339), msg))
|
180 |
-
log.Printf("[RequestID: %s] %s", l.RequestID, msg)
|
181 |
-
}
|
182 |
-
|
183 |
-
func (l *RequestLogger) LogContent(contentType string, content interface{}, maxLength int) {
|
184 |
-
if !l.config.Global.Log.Debug.Enabled {
|
185 |
-
return
|
186 |
-
}
|
187 |
-
sanitizedContent := sanitizeJSON(content)
|
188 |
-
truncatedContent := truncateContent(sanitizedContent, maxLength)
|
189 |
-
l.Log("%s Content:\n%s", contentType, truncatedContent)
|
190 |
-
}
|
191 |
-
|
192 |
-
func truncateContent(content string, maxLength int) string {
|
193 |
-
if len(content) <= maxLength {
|
194 |
-
return content
|
195 |
-
}
|
196 |
-
return content[:maxLength] + "... (truncated)"
|
197 |
-
}
|
198 |
-
|
199 |
-
func sanitizeJSON(data interface{}) string {
|
200 |
-
sanitized, err := json.Marshal(data)
|
201 |
-
if err != nil {
|
202 |
-
return "Failed to marshal JSON"
|
203 |
-
}
|
204 |
-
content := string(sanitized)
|
205 |
-
sensitivePattern := `"api_key":\s*"[^"]*"`
|
206 |
-
content = regexp.MustCompile(sensitivePattern).ReplaceAllString(content, `"api_key":"****"`)
|
207 |
-
return content
|
208 |
-
}
|
209 |
-
|
210 |
-
func extractRealAPIKey(fullKey string) string {
|
211 |
-
parts := strings.Split(fullKey, "-")
|
212 |
-
if len(parts) >= 3 && (parts[0] == "deep" || parts[0] == "openai") {
|
213 |
-
return strings.Join(parts[2:], "-")
|
214 |
-
}
|
215 |
-
return fullKey
|
216 |
-
}
|
217 |
-
|
218 |
-
func extractChannelID(fullKey string) string {
|
219 |
-
parts := strings.Split(fullKey, "-")
|
220 |
-
if len(parts) >= 2 && (parts[0] == "deep" || parts[0] == "openai") {
|
221 |
-
return parts[1]
|
222 |
-
}
|
223 |
-
return "1" // 默认渠道
|
224 |
-
}
|
225 |
-
|
226 |
-
func logAPIKey(key string) string {
|
227 |
-
if len(key) <= 8 {
|
228 |
-
return "****"
|
229 |
-
}
|
230 |
-
return key[:4] + "..." + key[len(key)-4:]
|
231 |
-
}
|
232 |
-
|
233 |
-
// ---------------------- Server 结构 ----------------------
|
234 |
-
|
235 |
-
type Server struct {
|
236 |
-
config *Config
|
237 |
-
srv *http.Server
|
238 |
-
}
|
239 |
-
|
240 |
-
var (
|
241 |
-
randMu sync.Mutex
|
242 |
-
randGen = rand.New(rand.NewSource(time.Now().UnixNano()))
|
243 |
-
)
|
244 |
-
|
245 |
-
func NewServer(config *Config) *Server {
|
246 |
-
return &Server{
|
247 |
-
config: config,
|
248 |
-
}
|
249 |
-
}
|
250 |
-
|
251 |
-
func (s *Server) Start() error {
|
252 |
-
mux := http.NewServeMux()
|
253 |
-
mux.HandleFunc("/v1/chat/completions", s.handleOpenAIRequests)
|
254 |
-
mux.HandleFunc("/v1/models", s.handleOpenAIRequests)
|
255 |
-
mux.HandleFunc("/health", s.handleHealth)
|
256 |
-
|
257 |
-
s.srv = &http.Server{
|
258 |
-
Addr: fmt.Sprintf("%s:%d", s.config.Global.Server.Host, s.config.Global.Server.Port),
|
259 |
-
Handler: mux,
|
260 |
-
ReadTimeout: time.Duration(s.config.Global.Server.ReadTimeout) * time.Second,
|
261 |
-
WriteTimeout: time.Duration(s.config.Global.Server.WriteTimeout) * time.Second,
|
262 |
-
IdleTimeout: time.Duration(s.config.Global.Server.IdleTimeout) * time.Second,
|
263 |
-
}
|
264 |
-
|
265 |
-
log.Printf("Server starting on %s\n", s.srv.Addr)
|
266 |
-
return s.srv.ListenAndServe()
|
267 |
-
}
|
268 |
-
|
269 |
-
func (s *Server) Shutdown(ctx context.Context) error {
|
270 |
-
return s.srv.Shutdown(ctx)
|
271 |
-
}
|
272 |
-
|
273 |
-
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
274 |
-
w.WriteHeader(http.StatusOK)
|
275 |
-
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
|
276 |
-
}
|
277 |
-
|
278 |
-
func (s *Server) handleOpenAIRequests(w http.ResponseWriter, r *http.Request) {
|
279 |
-
logger := NewRequestLogger(s.config)
|
280 |
-
|
281 |
-
fullAPIKey := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
282 |
-
apiKey := extractRealAPIKey(fullAPIKey)
|
283 |
-
channelID := extractChannelID(fullAPIKey)
|
284 |
-
|
285 |
-
logger.Log("Received request for %s with API Key: %s", r.URL.Path, logAPIKey(fullAPIKey))
|
286 |
-
logger.Log("Extracted channel ID: %s", channelID)
|
287 |
-
logger.Log("Extracted real API Key: %s", logAPIKey(apiKey))
|
288 |
-
|
289 |
-
targetChannel, ok := s.config.Channels[channelID]
|
290 |
-
if !ok {
|
291 |
-
http.Error(w, "Invalid channel", http.StatusBadRequest)
|
292 |
-
return
|
293 |
-
}
|
294 |
-
|
295 |
-
if r.URL.Path == "/v1/models" {
|
296 |
-
if r.Method != http.MethodGet {
|
297 |
-
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
298 |
-
return
|
299 |
-
}
|
300 |
-
req := &ChatCompletionRequest{APIKey: apiKey}
|
301 |
-
s.forwardModelsRequest(w, r.Context(), req, targetChannel)
|
302 |
-
return
|
303 |
-
}
|
304 |
-
|
305 |
-
if r.Method != http.MethodPost {
|
306 |
-
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
307 |
-
return
|
308 |
-
}
|
309 |
-
|
310 |
-
body, err := io.ReadAll(r.Body)
|
311 |
-
if err != nil {
|
312 |
-
logger.Log("Error reading request body: %v", err)
|
313 |
-
http.Error(w, "Failed to read request", http.StatusBadRequest)
|
314 |
-
return
|
315 |
-
}
|
316 |
-
r.Body.Close()
|
317 |
-
r.Body = io.NopCloser(bytes.NewBuffer(body))
|
318 |
-
|
319 |
-
if s.config.Global.Log.Debug.PrintRequest {
|
320 |
-
logger.LogContent("Request", string(body), s.config.Global.Log.Debug.MaxContentLength)
|
321 |
-
}
|
322 |
-
|
323 |
-
var req ChatCompletionRequest
|
324 |
-
if err := json.NewDecoder(bytes.NewBuffer(body)).Decode(&req); err != nil {
|
325 |
-
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
326 |
-
return
|
327 |
-
}
|
328 |
-
req.APIKey = apiKey
|
329 |
-
|
330 |
-
thinkingService := s.getWeightedRandomThinkingService()
|
331 |
-
logger.Log("Using thinking service: %s with API Key: %s", thinkingService.Name, logAPIKey(thinkingService.APIKey))
|
332 |
-
|
333 |
-
if req.Stream {
|
334 |
-
handler, err := NewStreamHandler(w, thinkingService, targetChannel, s.config)
|
335 |
-
if err != nil {
|
336 |
-
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
|
337 |
-
return
|
338 |
-
}
|
339 |
-
if err := handler.HandleRequest(r.Context(), &req); err != nil {
|
340 |
-
logger.Log("Stream handler error: %v", err)
|
341 |
-
}
|
342 |
-
} else {
|
343 |
-
thinkingResp, err := s.processThinkingContent(r.Context(), &req, thinkingService)
|
344 |
-
if err != nil {
|
345 |
-
logger.Log("Error processing thinking content: %v", err)
|
346 |
-
http.Error(w, "Thinking service error: "+err.Error(), http.StatusInternalServerError)
|
347 |
-
return
|
348 |
-
}
|
349 |
-
enhancedReq := s.prepareEnhancedRequest(&req, thinkingResp, thinkingService)
|
350 |
-
s.forwardRequest(w, r.Context(), enhancedReq, targetChannel)
|
351 |
-
}
|
352 |
-
}
|
353 |
-
|
354 |
-
func (s *Server) getWeightedRandomThinkingService() ThinkingService {
|
355 |
-
thinkingServices := s.config.ThinkingServices
|
356 |
-
if len(thinkingServices) == 0 {
|
357 |
-
return ThinkingService{}
|
358 |
-
}
|
359 |
-
totalWeight := 0
|
360 |
-
for _, svc := range thinkingServices {
|
361 |
-
totalWeight += svc.Weight
|
362 |
-
}
|
363 |
-
if totalWeight <= 0 {
|
364 |
-
log.Println("Warning: Total weight of thinking services is not positive, using first service as default.")
|
365 |
-
return thinkingServices[0]
|
366 |
-
}
|
367 |
-
randMu.Lock()
|
368 |
-
randNum := randGen.Intn(totalWeight)
|
369 |
-
randMu.Unlock()
|
370 |
-
currentSum := 0
|
371 |
-
for _, svc := range thinkingServices {
|
372 |
-
currentSum += svc.Weight
|
373 |
-
if randNum < currentSum {
|
374 |
-
return svc
|
375 |
-
}
|
376 |
-
}
|
377 |
-
return thinkingServices[0]
|
378 |
-
}
|
379 |
-
|
380 |
-
// ---------------------- 非流式处理思考服务 ----------------------
|
381 |
-
|
382 |
-
type ThinkingResponse struct {
|
383 |
-
Content string
|
384 |
-
ReasoningContent string
|
385 |
-
}
|
386 |
-
|
387 |
-
func (s *Server) processThinkingContent(ctx context.Context, req *ChatCompletionRequest, svc ThinkingService) (*ThinkingResponse, error) {
|
388 |
-
logger := NewRequestLogger(s.config)
|
389 |
-
log.Printf("Getting thinking content from service: %s (mode=%s)", svc.Name, svc.Mode)
|
390 |
-
|
391 |
-
thinkingReq := *req
|
392 |
-
thinkingReq.Model = svc.Model
|
393 |
-
thinkingReq.APIKey = svc.APIKey
|
394 |
-
|
395 |
-
var systemPrompt string
|
396 |
-
if svc.Mode == "full" {
|
397 |
-
systemPrompt = "Provide a detailed step-by-step analysis of the question. Your entire response will be used as reasoning and won't be shown to the user directly."
|
398 |
-
} else {
|
399 |
-
systemPrompt = "Please provide a detailed reasoning process for your response. Think step by step."
|
400 |
-
}
|
401 |
-
thinkingReq.Messages = append([]ChatCompletionMessage{
|
402 |
-
{Role: "system", Content: systemPrompt},
|
403 |
-
}, thinkingReq.Messages...)
|
404 |
-
|
405 |
-
temp := 0.7
|
406 |
-
if svc.Temperature != nil {
|
407 |
-
temp = *svc.Temperature
|
408 |
-
}
|
409 |
-
payload := map[string]interface{}{
|
410 |
-
"model": svc.Model,
|
411 |
-
"messages": thinkingReq.Messages,
|
412 |
-
"stream": false,
|
413 |
-
"temperature": temp,
|
414 |
-
}
|
415 |
-
if isValidReasoningEffort(svc.ReasoningEffort) {
|
416 |
-
payload["reasoning_effort"] = svc.ReasoningEffort
|
417 |
-
}
|
418 |
-
if isValidReasoningFormat(svc.ReasoningFormat) {
|
419 |
-
payload["reasoning_format"] = svc.ReasoningFormat
|
420 |
-
}
|
421 |
-
|
422 |
-
if s.config.Global.Log.Debug.PrintRequest {
|
423 |
-
logger.LogContent("Thinking Service Request", payload, s.config.Global.Log.Debug.MaxContentLength)
|
424 |
-
}
|
425 |
-
|
426 |
-
jsonData, err := json.Marshal(payload)
|
427 |
-
if err != nil {
|
428 |
-
return nil, fmt.Errorf("failed to marshal thinking request: %v", err)
|
429 |
-
}
|
430 |
-
client, err := createHTTPClient(svc.Proxy, time.Duration(svc.Timeout)*time.Second)
|
431 |
-
if err != nil {
|
432 |
-
return nil, fmt.Errorf("failed to create HTTP client: %v", err)
|
433 |
-
}
|
434 |
-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, svc.GetFullURL(), bytes.NewBuffer(jsonData))
|
435 |
-
if err != nil {
|
436 |
-
return nil, fmt.Errorf("failed to create request: %v", err)
|
437 |
-
}
|
438 |
-
httpReq.Header.Set("Content-Type", "application/json")
|
439 |
-
httpReq.Header.Set("Authorization", "Bearer "+svc.APIKey)
|
440 |
-
|
441 |
-
resp, err := client.Do(httpReq)
|
442 |
-
if err != nil {
|
443 |
-
return nil, fmt.Errorf("failed to send thinking request: %v", err)
|
444 |
-
}
|
445 |
-
defer resp.Body.Close()
|
446 |
-
|
447 |
-
respBody, err := io.ReadAll(resp.Body)
|
448 |
-
if err != nil {
|
449 |
-
return nil, fmt.Errorf("failed to read response body: %v", err)
|
450 |
-
}
|
451 |
-
if s.config.Global.Log.Debug.PrintResponse {
|
452 |
-
logger.LogContent("Thinking Service Response", string(respBody), s.config.Global.Log.Debug.MaxContentLength)
|
453 |
-
}
|
454 |
-
if resp.StatusCode != http.StatusOK {
|
455 |
-
return nil, fmt.Errorf("thinking service returned %d: %s", resp.StatusCode, string(respBody))
|
456 |
-
}
|
457 |
-
|
458 |
-
var thinkingResp ChatCompletionResponse
|
459 |
-
if err := json.Unmarshal(respBody, &thinkingResp); err != nil {
|
460 |
-
return nil, fmt.Errorf("failed to unmarshal thinking response: %v", err)
|
461 |
-
}
|
462 |
-
if len(thinkingResp.Choices) == 0 {
|
463 |
-
return nil, fmt.Errorf("thinking service returned no choices")
|
464 |
-
}
|
465 |
-
|
466 |
-
result := &ThinkingResponse{}
|
467 |
-
choice := thinkingResp.Choices[0]
|
468 |
-
|
469 |
-
if svc.Mode == "full" {
|
470 |
-
result.ReasoningContent = choice.Message.Content
|
471 |
-
result.Content = "Based on the above detailed analysis."
|
472 |
-
} else {
|
473 |
-
if choice.Message.ReasoningContent != nil {
|
474 |
-
switch v := choice.Message.ReasoningContent.(type) {
|
475 |
-
case string:
|
476 |
-
result.ReasoningContent = v
|
477 |
-
case map[string]interface{}:
|
478 |
-
if j, err := json.Marshal(v); err == nil {
|
479 |
-
result.ReasoningContent = string(j)
|
480 |
-
}
|
481 |
-
}
|
482 |
-
}
|
483 |
-
if result.ReasoningContent == "" {
|
484 |
-
result.ReasoningContent = choice.Message.Content
|
485 |
-
}
|
486 |
-
result.Content = "Based on the above reasoning."
|
487 |
-
}
|
488 |
-
return result, nil
|
489 |
-
}
|
490 |
-
|
491 |
-
func (s *Server) prepareEnhancedRequest(originalReq *ChatCompletionRequest, thinkingResp *ThinkingResponse, svc ThinkingService) *ChatCompletionRequest {
|
492 |
-
newReq := *originalReq
|
493 |
-
var systemPrompt string
|
494 |
-
if svc.Mode == "full" {
|
495 |
-
systemPrompt = fmt.Sprintf(`Consider the following detailed analysis (not shown to user):
|
496 |
-
%s
|
497 |
-
|
498 |
-
Provide a clear, concise response that incorporates insights from this analysis.`, thinkingResp.ReasoningContent)
|
499 |
-
} else {
|
500 |
-
systemPrompt = fmt.Sprintf(`Previous thinking process:
|
501 |
-
%s
|
502 |
-
Please consider the above thinking process in your response.`, thinkingResp.ReasoningContent)
|
503 |
-
}
|
504 |
-
newReq.Messages = append([]ChatCompletionMessage{
|
505 |
-
{Role: "system", Content: systemPrompt},
|
506 |
-
}, newReq.Messages...)
|
507 |
-
return &newReq
|
508 |
-
}
|
509 |
-
|
510 |
-
func (s *Server) forwardRequest(w http.ResponseWriter, ctx context.Context, req *ChatCompletionRequest, channel Channel) {
|
511 |
-
logger := NewRequestLogger(s.config)
|
512 |
-
if s.config.Global.Log.Debug.PrintRequest {
|
513 |
-
logger.LogContent("Forward Request", req, s.config.Global.Log.Debug.MaxContentLength)
|
514 |
-
}
|
515 |
-
jsonData, err := json.Marshal(req)
|
516 |
-
if err != nil {
|
517 |
-
http.Error(w, "Failed to marshal request", http.StatusInternalServerError)
|
518 |
-
return
|
519 |
-
}
|
520 |
-
|
521 |
-
client, err := createHTTPClient(channel.Proxy, time.Duration(channel.Timeout)*time.Second)
|
522 |
-
if err != nil {
|
523 |
-
http.Error(w, fmt.Sprintf("Failed to create HTTP client: %v", err), http.StatusInternalServerError)
|
524 |
-
return
|
525 |
-
}
|
526 |
-
|
527 |
-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, channel.GetFullURL(), bytes.NewBuffer(jsonData))
|
528 |
-
if err != nil {
|
529 |
-
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
530 |
-
return
|
531 |
-
}
|
532 |
-
httpReq.Header.Set("Content-Type", "application/json")
|
533 |
-
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
|
534 |
-
|
535 |
-
resp, err := client.Do(httpReq)
|
536 |
-
if err != nil {
|
537 |
-
http.Error(w, fmt.Sprintf("Failed to forward request: %v", err), http.StatusInternalServerError)
|
538 |
-
return
|
539 |
-
}
|
540 |
-
defer resp.Body.Close()
|
541 |
-
|
542 |
-
respBody, err := io.ReadAll(resp.Body)
|
543 |
-
if err != nil {
|
544 |
-
http.Error(w, "Failed to read response", http.StatusInternalServerError)
|
545 |
-
return
|
546 |
-
}
|
547 |
-
if s.config.Global.Log.Debug.PrintResponse {
|
548 |
-
logger.LogContent("Forward Response", string(respBody), s.config.Global.Log.Debug.MaxContentLength)
|
549 |
-
}
|
550 |
-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
551 |
-
http.Error(w, fmt.Sprintf("Target server error: %s", resp.Status), resp.StatusCode)
|
552 |
-
return
|
553 |
-
}
|
554 |
-
|
555 |
-
for k, vals := range resp.Header {
|
556 |
-
for _, v := range vals {
|
557 |
-
w.Header().Add(k, v)
|
558 |
-
}
|
559 |
-
}
|
560 |
-
w.WriteHeader(resp.StatusCode)
|
561 |
-
w.Write(respBody)
|
562 |
-
}
|
563 |
-
|
564 |
-
func (s *Server) forwardModelsRequest(w http.ResponseWriter, ctx context.Context, req *ChatCompletionRequest, targetChannel Channel) {
|
565 |
-
logger := NewRequestLogger(s.config)
|
566 |
-
if s.config.Global.Log.Debug.PrintRequest {
|
567 |
-
logger.LogContent("/v1/models Request", req, s.config.Global.Log.Debug.MaxContentLength)
|
568 |
-
}
|
569 |
-
fullChatURL := targetChannel.GetFullURL()
|
570 |
-
parsedURL, err := url.Parse(fullChatURL)
|
571 |
-
if err != nil {
|
572 |
-
http.Error(w, "Failed to parse channel URL", http.StatusInternalServerError)
|
573 |
-
return
|
574 |
-
}
|
575 |
-
baseURL := parsedURL.Scheme + "://" + parsedURL.Host
|
576 |
-
modelsURL := strings.TrimSuffix(baseURL, "/") + "/v1/models"
|
577 |
-
|
578 |
-
client, err := createHTTPClient(targetChannel.Proxy, time.Duration(targetChannel.Timeout)*time.Second)
|
579 |
-
if err != nil {
|
580 |
-
http.Error(w, fmt.Sprintf("Failed to create HTTP client: %v", err), http.StatusInternalServerError)
|
581 |
-
return
|
582 |
-
}
|
583 |
-
|
584 |
-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, modelsURL, nil)
|
585 |
-
if err != nil {
|
586 |
-
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
587 |
-
return
|
588 |
-
}
|
589 |
-
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
|
590 |
-
|
591 |
-
resp, err := client.Do(httpReq)
|
592 |
-
if err != nil {
|
593 |
-
http.Error(w, fmt.Sprintf("Failed to forward request: %v", err), http.StatusInternalServerError)
|
594 |
-
return
|
595 |
-
}
|
596 |
-
defer resp.Body.Close()
|
597 |
-
|
598 |
-
respBody, err := io.ReadAll(resp.Body)
|
599 |
-
if err != nil {
|
600 |
-
http.Error(w, "Failed to read response", http.StatusInternalServerError)
|
601 |
-
return
|
602 |
-
}
|
603 |
-
if s.config.Global.Log.Debug.PrintResponse {
|
604 |
-
logger.LogContent("/v1/models Response", string(respBody), s.config.Global.Log.Debug.MaxContentLength)
|
605 |
-
}
|
606 |
-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
607 |
-
http.Error(w, fmt.Sprintf("Target server error: %s", resp.Status), resp.StatusCode)
|
608 |
-
return
|
609 |
-
}
|
610 |
-
|
611 |
-
for k, vals := range resp.Header {
|
612 |
-
for _, v := range vals {
|
613 |
-
w.Header().Add(k, v)
|
614 |
-
}
|
615 |
-
}
|
616 |
-
w.WriteHeader(resp.StatusCode)
|
617 |
-
w.Write(respBody)
|
618 |
-
}
|
619 |
-
|
620 |
-
// ---------------------- 流式处理 ----------------------
|
621 |
-
|
622 |
-
// collectedReasoningBuffer 用于收集思考服务返回的 reasoning_content(标准模式只收集 reasoning_content;full 模式收集全部)
|
623 |
-
type collectedReasoningBuffer struct {
|
624 |
-
builder strings.Builder
|
625 |
-
mode string
|
626 |
-
}
|
627 |
-
|
628 |
-
func (cb *collectedReasoningBuffer) append(text string) {
|
629 |
-
cb.builder.WriteString(text)
|
630 |
-
}
|
631 |
-
|
632 |
-
func (cb *collectedReasoningBuffer) get() string {
|
633 |
-
return cb.builder.String()
|
634 |
-
}
|
635 |
-
|
636 |
-
type StreamHandler struct {
|
637 |
-
thinkingService ThinkingService
|
638 |
-
targetChannel Channel
|
639 |
-
writer http.ResponseWriter
|
640 |
-
flusher http.Flusher
|
641 |
-
config *Config
|
642 |
-
}
|
643 |
-
|
644 |
-
func NewStreamHandler(w http.ResponseWriter, thinkingService ThinkingService, targetChannel Channel, config *Config) (*StreamHandler, error) {
|
645 |
-
flusher, ok := w.(http.Flusher)
|
646 |
-
if !ok {
|
647 |
-
return nil, fmt.Errorf("streaming not supported by this writer")
|
648 |
-
}
|
649 |
-
return &StreamHandler{
|
650 |
-
thinkingService: thinkingService,
|
651 |
-
targetChannel: targetChannel,
|
652 |
-
writer: w,
|
653 |
-
flusher: flusher,
|
654 |
-
config: config,
|
655 |
-
}, nil
|
656 |
-
}
|
657 |
-
|
658 |
-
// HandleRequest 分两阶段:先流式接收思考服务 SSE 并转发给客户端(只转发包含 reasoning_content 的 chunk,标准模式遇到纯 content 则中断);
|
659 |
-
// 然后使用收集到的 reasoning_content 构造最终请求,发起目标 Channel 的 SSE 并转发给客户端。
|
660 |
-
func (h *StreamHandler) HandleRequest(ctx context.Context, req *ChatCompletionRequest) error {
|
661 |
-
h.writer.Header().Set("Content-Type", "text/event-stream")
|
662 |
-
h.writer.Header().Set("Cache-Control", "no-cache")
|
663 |
-
h.writer.Header().Set("Connection", "keep-alive")
|
664 |
-
|
665 |
-
logger := NewRequestLogger(h.config)
|
666 |
-
reasonBuf := &collectedReasoningBuffer{mode: h.thinkingService.Mode}
|
667 |
-
|
668 |
-
// 阶段一:流式接收思考服务 SSE
|
669 |
-
if err := h.streamThinkingService(ctx, req, reasonBuf, logger); err != nil {
|
670 |
-
return err
|
671 |
-
}
|
672 |
-
|
673 |
-
// 阶段二:构造最终请求,并流式转发目标 Channel SSE
|
674 |
-
finalReq := h.prepareFinalRequest(req, reasonBuf.get())
|
675 |
-
if err := h.streamFinalChannel(ctx, finalReq, logger); err != nil {
|
676 |
-
return err
|
677 |
-
}
|
678 |
-
return nil
|
679 |
-
}
|
680 |
-
|
681 |
-
// streamThinkingService 连接思考服务 SSE,原样转发包含 reasoning_content 的 SSE chunk给客户端,同时收集 reasoning_content。
|
682 |
-
// 在标准模式下,一旦遇到只返回 non-empty 的 content(且 reasoning_content 为空),则立即中断,不转发该 chunk。
|
683 |
-
func (h *StreamHandler) streamThinkingService(ctx context.Context, req *ChatCompletionRequest, reasonBuf *collectedReasoningBuffer, logger *RequestLogger) error {
|
684 |
-
thinkingReq := *req
|
685 |
-
thinkingReq.Model = h.thinkingService.Model
|
686 |
-
thinkingReq.APIKey = h.thinkingService.APIKey
|
687 |
-
|
688 |
-
var systemPrompt string
|
689 |
-
if h.thinkingService.Mode == "full" {
|
690 |
-
systemPrompt = "Provide a detailed step-by-step analysis of the question. Your entire response will be used as reasoning and won't be shown to the user directly."
|
691 |
-
} else {
|
692 |
-
systemPrompt = "Please provide a detailed reasoning process for your response. Think step by step."
|
693 |
-
}
|
694 |
-
messages := append([]ChatCompletionMessage{
|
695 |
-
{Role: "system", Content: systemPrompt},
|
696 |
-
}, thinkingReq.Messages...)
|
697 |
-
|
698 |
-
temp := 0.7
|
699 |
-
if h.thinkingService.Temperature != nil {
|
700 |
-
temp = *h.thinkingService.Temperature
|
701 |
-
}
|
702 |
-
bodyMap := map[string]interface{}{
|
703 |
-
"model": thinkingReq.Model,
|
704 |
-
"messages": messages,
|
705 |
-
"temperature": temp,
|
706 |
-
"stream": true,
|
707 |
-
}
|
708 |
-
if isValidReasoningEffort(h.thinkingService.ReasoningEffort) {
|
709 |
-
bodyMap["reasoning_effort"] = h.thinkingService.ReasoningEffort
|
710 |
-
}
|
711 |
-
if isValidReasoningFormat(h.thinkingService.ReasoningFormat) {
|
712 |
-
bodyMap["reasoning_format"] = h.thinkingService.ReasoningFormat
|
713 |
-
}
|
714 |
-
|
715 |
-
jsonData, err := json.Marshal(bodyMap)
|
716 |
-
if err != nil {
|
717 |
-
return err
|
718 |
-
}
|
719 |
-
|
720 |
-
client, err := createHTTPClient(h.thinkingService.Proxy, time.Duration(h.thinkingService.Timeout)*time.Second)
|
721 |
-
if err != nil {
|
722 |
-
return err
|
723 |
-
}
|
724 |
-
|
725 |
-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, h.thinkingService.GetFullURL(), bytes.NewBuffer(jsonData))
|
726 |
-
if err != nil {
|
727 |
-
return err
|
728 |
-
}
|
729 |
-
httpReq.Header.Set("Content-Type", "application/json")
|
730 |
-
httpReq.Header.Set("Authorization", "Bearer "+h.thinkingService.APIKey)
|
731 |
-
|
732 |
-
log.Printf("Starting ThinkingService SSE => %s (mode=%s, force_stop=%v)", h.thinkingService.GetFullURL(), h.thinkingService.Mode, h.thinkingService.ForceStopDeepThinking)
|
733 |
-
resp, err := client.Do(httpReq)
|
734 |
-
if err != nil {
|
735 |
-
return err
|
736 |
-
}
|
737 |
-
defer resp.Body.Close()
|
738 |
-
|
739 |
-
if resp.StatusCode != http.StatusOK {
|
740 |
-
b, _ := io.ReadAll(resp.Body)
|
741 |
-
return fmt.Errorf("thinking service status=%d, body=%s", resp.StatusCode, string(b))
|
742 |
-
}
|
743 |
-
|
744 |
-
reader := bufio.NewReader(resp.Body)
|
745 |
-
var lastLine string
|
746 |
-
// 标识是否需中断思考 SSE(标准模式下)
|
747 |
-
forceStop := false
|
748 |
-
|
749 |
-
for {
|
750 |
-
select {
|
751 |
-
case <-ctx.Done():
|
752 |
-
return ctx.Err()
|
753 |
-
default:
|
754 |
-
}
|
755 |
-
line, err := reader.ReadString('\n')
|
756 |
-
if err != nil {
|
757 |
-
if err == io.EOF {
|
758 |
-
break
|
759 |
-
}
|
760 |
-
return err
|
761 |
-
}
|
762 |
-
line = strings.TrimSpace(line)
|
763 |
-
if line == "" || line == lastLine {
|
764 |
-
continue
|
765 |
-
}
|
766 |
-
lastLine = line
|
767 |
-
|
768 |
-
if !strings.HasPrefix(line, "data: ") {
|
769 |
-
continue
|
770 |
-
}
|
771 |
-
dataPart := strings.TrimPrefix(line, "data: ")
|
772 |
-
if dataPart == "[DONE]" {
|
773 |
-
// 思考服务结束:直接中断(不发送特殊标记)
|
774 |
-
break
|
775 |
-
}
|
776 |
-
|
777 |
-
var chunk struct {
|
778 |
-
Choices []struct {
|
779 |
-
Delta struct {
|
780 |
-
Content string `json:"content,omitempty"`
|
781 |
-
ReasoningContent string `json:"reasoning_content,omitempty"`
|
782 |
-
} `json:"delta"`
|
783 |
-
FinishReason *string `json:"finish_reason,omitempty"`
|
784 |
-
} `json:"choices"`
|
785 |
-
}
|
786 |
-
if err := json.Unmarshal([]byte(dataPart), &chunk); err != nil {
|
787 |
-
// 如果解析失败,直接原样转发
|
788 |
-
h.writer.Write([]byte("data: " + dataPart + "\n\n"))
|
789 |
-
h.flusher.Flush()
|
790 |
-
continue
|
791 |
-
}
|
792 |
-
|
793 |
-
if len(chunk.Choices) > 0 {
|
794 |
-
c := chunk.Choices[0]
|
795 |
-
if h.config.Global.Log.Debug.PrintResponse {
|
796 |
-
logger.LogContent("Thinking SSE chunk", chunk, h.config.Global.Log.Debug.MaxContentLength)
|
797 |
-
}
|
798 |
-
|
799 |
-
if h.thinkingService.Mode == "full" {
|
800 |
-
// full 模式:收集所有内容
|
801 |
-
if c.Delta.ReasoningContent != "" {
|
802 |
-
reasonBuf.append(c.Delta.ReasoningContent)
|
803 |
-
}
|
804 |
-
if c.Delta.Content != "" {
|
805 |
-
reasonBuf.append(c.Delta.Content)
|
806 |
-
}
|
807 |
-
// 原样转发整 chunk
|
808 |
-
forwardLine := "data: " + dataPart + "\n\n"
|
809 |
-
h.writer.Write([]byte(forwardLine))
|
810 |
-
h.flusher.Flush()
|
811 |
-
} else {
|
812 |
-
// standard 模式:只收集 reasoning_content
|
813 |
-
if c.Delta.ReasoningContent != "" {
|
814 |
-
reasonBuf.append(c.Delta.ReasoningContent)
|
815 |
-
// 转发该 chunk
|
816 |
-
forwardLine := "data: " + dataPart + "\n\n"
|
817 |
-
h.writer.Write([]byte(forwardLine))
|
818 |
-
h.flusher.Flush()
|
819 |
-
}
|
820 |
-
// 如果遇到 chunk 中只有 content(且 reasoning_content 为空),认为思考链结束,不转发该 chunk
|
821 |
-
if c.Delta.Content != "" && strings.TrimSpace(c.Delta.ReasoningContent) == "" {
|
822 |
-
forceStop = true
|
823 |
-
}
|
824 |
-
}
|
825 |
-
|
826 |
-
// 如果 finishReason 非空,也认为结束
|
827 |
-
if c.FinishReason != nil && *c.FinishReason != "" {
|
828 |
-
forceStop = true
|
829 |
-
}
|
830 |
-
}
|
831 |
-
|
832 |
-
if forceStop {
|
833 |
-
break
|
834 |
-
}
|
835 |
-
}
|
836 |
-
// 读空剩余数据
|
837 |
-
io.Copy(io.Discard, reader)
|
838 |
-
return nil
|
839 |
-
}
|
840 |
-
|
841 |
-
func (h *StreamHandler) prepareFinalRequest(originalReq *ChatCompletionRequest, reasoningCollected string) *ChatCompletionRequest {
|
842 |
-
req := *originalReq
|
843 |
-
var systemPrompt string
|
844 |
-
if h.thinkingService.Mode == "full" {
|
845 |
-
systemPrompt = fmt.Sprintf(
|
846 |
-
`Consider the following detailed analysis (not shown to user):
|
847 |
-
%s
|
848 |
-
|
849 |
-
Provide a clear, concise response that incorporates insights from this analysis.`,
|
850 |
-
reasoningCollected,
|
851 |
-
)
|
852 |
-
} else {
|
853 |
-
systemPrompt = fmt.Sprintf(
|
854 |
-
`Previous thinking process:
|
855 |
-
%s
|
856 |
-
Please consider the above thinking process in your response.`,
|
857 |
-
reasoningCollected,
|
858 |
-
)
|
859 |
-
}
|
860 |
-
req.Messages = append([]ChatCompletionMessage{
|
861 |
-
{Role: "system", Content: systemPrompt},
|
862 |
-
}, req.Messages...)
|
863 |
-
return &req
|
864 |
-
}
|
865 |
-
|
866 |
-
func (h *StreamHandler) streamFinalChannel(ctx context.Context, req *ChatCompletionRequest, logger *RequestLogger) error {
|
867 |
-
if h.config.Global.Log.Debug.PrintRequest {
|
868 |
-
logger.LogContent("Final Request to Channel", req, h.config.Global.Log.Debug.MaxContentLength)
|
869 |
-
}
|
870 |
-
|
871 |
-
jsonData, err := json.Marshal(req)
|
872 |
-
if err != nil {
|
873 |
-
return err
|
874 |
-
}
|
875 |
-
client, err := createHTTPClient(h.targetChannel.Proxy, time.Duration(h.targetChannel.Timeout)*time.Second)
|
876 |
-
if err != nil {
|
877 |
-
return err
|
878 |
-
}
|
879 |
-
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, h.targetChannel.GetFullURL(), bytes.NewBuffer(jsonData))
|
880 |
-
if err != nil {
|
881 |
-
return err
|
882 |
-
}
|
883 |
-
httpReq.Header.Set("Content-Type", "application/json")
|
884 |
-
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
|
885 |
-
|
886 |
-
resp, err := client.Do(httpReq)
|
887 |
-
if err != nil {
|
888 |
-
return err
|
889 |
-
}
|
890 |
-
defer resp.Body.Close()
|
891 |
-
|
892 |
-
if resp.StatusCode != http.StatusOK {
|
893 |
-
b, _ := io.ReadAll(resp.Body)
|
894 |
-
return fmt.Errorf("target channel status=%d, body=%s", resp.StatusCode, string(b))
|
895 |
-
}
|
896 |
-
|
897 |
-
reader := bufio.NewReader(resp.Body)
|
898 |
-
var lastLine string
|
899 |
-
|
900 |
-
for {
|
901 |
-
select {
|
902 |
-
case <-ctx.Done():
|
903 |
-
return ctx.Err()
|
904 |
-
default:
|
905 |
-
}
|
906 |
-
line, err := reader.ReadString('\n')
|
907 |
-
if err != nil {
|
908 |
-
if err == io.EOF {
|
909 |
-
break
|
910 |
-
}
|
911 |
-
return err
|
912 |
-
}
|
913 |
-
line = strings.TrimSpace(line)
|
914 |
-
if line == "" || line == lastLine {
|
915 |
-
continue
|
916 |
-
}
|
917 |
-
lastLine = line
|
918 |
-
|
919 |
-
if !strings.HasPrefix(line, "data: ") {
|
920 |
-
continue
|
921 |
-
}
|
922 |
-
data := strings.TrimPrefix(line, "data: ")
|
923 |
-
if data == "[DONE]" {
|
924 |
-
doneLine := "data: [DONE]\n\n"
|
925 |
-
h.writer.Write([]byte(doneLine))
|
926 |
-
h.flusher.Flush()
|
927 |
-
break
|
928 |
-
}
|
929 |
-
forwardLine := "data: " + data + "\n\n"
|
930 |
-
h.writer.Write([]byte(forwardLine))
|
931 |
-
h.flusher.Flush()
|
932 |
-
|
933 |
-
if h.config.Global.Log.Debug.PrintResponse {
|
934 |
-
logger.LogContent("Channel SSE chunk", forwardLine, h.config.Global.Log.Debug.MaxContentLength)
|
935 |
-
}
|
936 |
-
}
|
937 |
-
io.Copy(io.Discard, reader)
|
938 |
-
return nil
|
939 |
-
}
|
940 |
-
|
941 |
-
// ---------------------- HTTP Client 工具 ----------------------
|
942 |
-
|
943 |
-
func createHTTPClient(proxyURL string, timeout time.Duration) (*http.Client, error) {
|
944 |
-
transport := &http.Transport{
|
945 |
-
DialContext: (&net.Dialer{
|
946 |
-
Timeout: 30 * time.Second,
|
947 |
-
KeepAlive: 30 * time.Second,
|
948 |
-
}).DialContext,
|
949 |
-
ForceAttemptHTTP2: true,
|
950 |
-
MaxIdleConns: 100,
|
951 |
-
IdleConnTimeout: 90 * time.Second,
|
952 |
-
TLSHandshakeTimeout: 10 * time.Second,
|
953 |
-
ExpectContinueTimeout: 1 * time.Second,
|
954 |
-
}
|
955 |
-
if proxyURL != "" {
|
956 |
-
parsedURL, err := url.Parse(proxyURL)
|
957 |
-
if err != nil {
|
958 |
-
return nil, fmt.Errorf("invalid proxy URL: %v", err)
|
959 |
-
}
|
960 |
-
switch parsedURL.Scheme {
|
961 |
-
case "http", "https":
|
962 |
-
transport.Proxy = http.ProxyURL(parsedURL)
|
963 |
-
case "socks5":
|
964 |
-
dialer, err := proxy.FromURL(parsedURL, proxy.Direct)
|
965 |
-
if err != nil {
|
966 |
-
return nil, fmt.Errorf("failed to create SOCKS5 dialer: %v", err)
|
967 |
-
}
|
968 |
-
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
969 |
-
return dialer.Dial(network, addr)
|
970 |
-
}
|
971 |
-
default:
|
972 |
-
return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme)
|
973 |
-
}
|
974 |
-
}
|
975 |
-
return &http.Client{
|
976 |
-
Transport: transport,
|
977 |
-
Timeout: timeout,
|
978 |
-
}, nil
|
979 |
-
}
|
980 |
-
|
981 |
-
func maskSensitiveHeaders(headers http.Header) http.Header {
|
982 |
-
masked := make(http.Header)
|
983 |
-
for k, vals := range headers {
|
984 |
-
if strings.ToLower(k) == "authorization" {
|
985 |
-
masked[k] = []string{"Bearer ****"}
|
986 |
-
} else {
|
987 |
-
masked[k] = vals
|
988 |
-
}
|
989 |
-
}
|
990 |
-
return masked
|
991 |
-
}
|
992 |
-
|
993 |
-
func isValidReasoningEffort(effort string) bool {
|
994 |
-
switch strings.ToLower(effort) {
|
995 |
-
case "low", "medium", "high":
|
996 |
-
return true
|
997 |
-
}
|
998 |
-
return false
|
999 |
-
}
|
1000 |
-
|
1001 |
-
func isValidReasoningFormat(format string) bool {
|
1002 |
-
switch strings.ToLower(format) {
|
1003 |
-
case "parsed", "raw", "hidden":
|
1004 |
-
return true
|
1005 |
-
}
|
1006 |
-
return false
|
1007 |
-
}
|
1008 |
-
|
1009 |
-
// ---------------------- 配置加载与验证 ----------------------
|
1010 |
-
|
1011 |
-
// 首先添加这些辅助函数
|
1012 |
-
func getEnvOrDefault(key, defaultValue string) string {
|
1013 |
-
if value := os.Getenv(key); value != "" {
|
1014 |
-
return value
|
1015 |
-
}
|
1016 |
-
return defaultValue
|
1017 |
-
}
|
1018 |
-
|
1019 |
-
func getEnvIntOrDefault(key string, defaultValue int) int {
|
1020 |
-
if value := os.Getenv(key); value != "" {
|
1021 |
-
if intValue, err := strconv.Atoi(value); err == nil {
|
1022 |
-
return intValue
|
1023 |
-
}
|
1024 |
-
}
|
1025 |
-
return defaultValue
|
1026 |
-
}
|
1027 |
-
|
1028 |
-
func getEnvBoolOrDefault(key string, defaultValue bool) bool {
|
1029 |
-
if value := os.Getenv(key); value != "" {
|
1030 |
-
if boolValue, err := strconv.ParseBool(value); err == nil {
|
1031 |
-
return boolValue
|
1032 |
-
}
|
1033 |
-
}
|
1034 |
-
return defaultValue
|
1035 |
-
}
|
1036 |
-
|
1037 |
-
func getEnvFloatPtr(key string, defaultValue float64) *float64 {
|
1038 |
-
if value := os.Getenv(key); value != "" {
|
1039 |
-
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
1040 |
-
return &floatValue
|
1041 |
-
}
|
1042 |
-
}
|
1043 |
-
return &defaultValue
|
1044 |
-
}
|
1045 |
-
|
1046 |
-
// 然后替换原有的 loadConfig 函数
|
1047 |
-
func loadConfig() (*Config, error) {
|
1048 |
-
// 优先从环境变量加载完整配置
|
1049 |
-
if envConfig := os.Getenv("CONFIG_JSON"); envConfig != "" {
|
1050 |
-
var cfg Config
|
1051 |
-
if err := json.Unmarshal([]byte(envConfig), &cfg); err == nil {
|
1052 |
-
if err := validateConfig(&cfg); err == nil {
|
1053 |
-
log.Println("Using configuration from CONFIG_JSON environment variable")
|
1054 |
-
return &cfg, nil
|
1055 |
-
}
|
1056 |
-
}
|
1057 |
-
}
|
1058 |
-
|
1059 |
-
// 如果没有完整的 JSON 配置,则从独立的环境变量构建配置
|
1060 |
-
log.Println("Building configuration from individual environment variables")
|
1061 |
-
cfg := &Config{
|
1062 |
-
ThinkingServices: []ThinkingService{
|
1063 |
-
{
|
1064 |
-
ID: 1,
|
1065 |
-
Name: getEnvOrDefault("THINKING_SERVICE_NAME", "modelscope-deepseek-thinking"),
|
1066 |
-
Mode: getEnvOrDefault("THINKING_SERVICE_MODE", "standard"),
|
1067 |
-
Model: getEnvOrDefault("THINKING_SERVICE_MODEL", "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"),
|
1068 |
-
BaseURL: getEnvOrDefault("THINKING_SERVICE_BASE_URL", "https://api-inference.modelscope.cn"),
|
1069 |
-
APIPath: getEnvOrDefault("THINKING_SERVICE_API_PATH", "/v1/chat/completions"),
|
1070 |
-
APIKey: os.Getenv("THINKING_SERVICE_API_KEY"), // 敏感信息必须从环境变量读取
|
1071 |
-
Timeout: getEnvIntOrDefault("THINKING_SERVICE_TIMEOUT", 6000),
|
1072 |
-
Weight: getEnvIntOrDefault("THINKING_SERVICE_WEIGHT", 100),
|
1073 |
-
Proxy: getEnvOrDefault("THINKING_SERVICE_PROXY", ""),
|
1074 |
-
ReasoningEffort: getEnvOrDefault("THINKING_SERVICE_REASONING_EFFORT", "high"),
|
1075 |
-
ReasoningFormat: getEnvOrDefault("THINKING_SERVICE_REASONING_FORMAT", "parsed"),
|
1076 |
-
Temperature: getEnvFloatPtr("THINKING_SERVICE_TEMPERATURE", 0.8),
|
1077 |
-
ForceStopDeepThinking: getEnvBoolOrDefault("THINKING_SERVICE_FORCE_STOP", false),
|
1078 |
-
},
|
1079 |
-
},
|
1080 |
-
Channels: map[string]Channel{
|
1081 |
-
"1": {
|
1082 |
-
Name: getEnvOrDefault("CHANNEL_NAME", "Gemini-channel"),
|
1083 |
-
BaseURL: getEnvOrDefault("CHANNEL_BASE_URL", "https://Richardlsr-gemini-balance.hf.space/hf"),
|
1084 |
-
APIPath: getEnvOrDefault("CHANNEL_API_PATH", "/v1/chat/completions"),
|
1085 |
-
Timeout: getEnvIntOrDefault("CHANNEL_TIMEOUT", 600),
|
1086 |
-
Proxy: getEnvOrDefault("CHANNEL_PROXY", ""),
|
1087 |
-
},
|
1088 |
-
},
|
1089 |
-
Global: GlobalConfig{
|
1090 |
-
Server: ServerConfig{
|
1091 |
-
Port: getEnvIntOrDefault("SERVER_PORT", 7860), // HF 默认端口
|
1092 |
-
Host: getEnvOrDefault("SERVER_HOST", "0.0.0.0"),
|
1093 |
-
ReadTimeout: getEnvIntOrDefault("SERVER_READ_TIMEOUT", 600),
|
1094 |
-
WriteTimeout: getEnvIntOrDefault("SERVER_WRITE_TIMEOUT", 600),
|
1095 |
-
IdleTimeout: getEnvIntOrDefault("SERVER_IDLE_TIMEOUT", 600),
|
1096 |
-
},
|
1097 |
-
Log: LogConfig{
|
1098 |
-
Level: getEnvOrDefault("LOG_LEVEL", "info"),
|
1099 |
-
Format: getEnvOrDefault("LOG_FORMAT", "json"),
|
1100 |
-
Output: getEnvOrDefault("LOG_OUTPUT", "console"), // HF 环境下默认输出到控制台
|
1101 |
-
FilePath: getEnvOrDefault("LOG_FILE_PATH", "./logs/deepai.log"),
|
1102 |
-
Debug: DebugConfig{
|
1103 |
-
Enabled: getEnvBoolOrDefault("LOG_DEBUG_ENABLED", true),
|
1104 |
-
PrintRequest: getEnvBoolOrDefault("LOG_PRINT_REQUEST", true),
|
1105 |
-
PrintResponse: getEnvBoolOrDefault("LOG_PRINT_RESPONSE", true),
|
1106 |
-
MaxContentLength: getEnvIntOrDefault("LOG_MAX_CONTENT_LENGTH", 1000),
|
1107 |
-
},
|
1108 |
-
},
|
1109 |
-
},
|
1110 |
-
}
|
1111 |
-
|
1112 |
-
// 验证必需的环境变量
|
1113 |
-
if cfg.ThinkingServices[0].APIKey == "" {
|
1114 |
-
return nil, fmt.Errorf("THINKING_SERVICE_API_KEY environment variable is required")
|
1115 |
-
}
|
1116 |
-
|
1117 |
-
if err := validateConfig(cfg); err != nil {
|
1118 |
-
return nil, fmt.Errorf("config validation error: %v", err)
|
1119 |
-
}
|
1120 |
-
|
1121 |
-
return cfg, nil
|
1122 |
-
}
|
1123 |
-
|
1124 |
-
func validateConfig(config *Config) error {
|
1125 |
-
if len(config.ThinkingServices) == 0 {
|
1126 |
-
return fmt.Errorf("no thinking services configured")
|
1127 |
-
}
|
1128 |
-
if len(config.Channels) == 0 {
|
1129 |
-
return fmt.Errorf("no channels configured")
|
1130 |
-
}
|
1131 |
-
for i, svc := range config.ThinkingServices {
|
1132 |
-
if svc.BaseURL == "" {
|
1133 |
-
return fmt.Errorf("thinking service %s has empty baseURL", svc.Name)
|
1134 |
-
}
|
1135 |
-
if svc.APIKey == "" {
|
1136 |
-
return fmt.Errorf("thinking service %s has empty apiKey", svc.Name)
|
1137 |
-
}
|
1138 |
-
if svc.Timeout <= 0 {
|
1139 |
-
return fmt.Errorf("thinking service %s has invalid timeout", svc.Name)
|
1140 |
-
}
|
1141 |
-
if svc.Model == "" {
|
1142 |
-
return fmt.Errorf("thinking service %s has empty model", svc.Name)
|
1143 |
-
}
|
1144 |
-
if svc.Mode == "" {
|
1145 |
-
config.ThinkingServices[i].Mode = "standard"
|
1146 |
-
} else if svc.Mode != "standard" && svc.Mode != "full" {
|
1147 |
-
return fmt.Errorf("thinking service %s unknown mode=%s", svc.Name, svc.Mode)
|
1148 |
-
}
|
1149 |
-
}
|
1150 |
-
return nil
|
1151 |
-
}
|
1152 |
-
|
1153 |
-
func main() {
|
1154 |
-
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
|
1155 |
-
|
1156 |
-
cfg, err := loadConfig()
|
1157 |
-
if err != nil {
|
1158 |
-
log.Fatalf("Failed to load config: %v", err)
|
1159 |
-
}
|
1160 |
-
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
1161 |
-
|
1162 |
-
server := NewServer(cfg)
|
1163 |
-
|
1164 |
-
done := make(chan os.Signal, 1)
|
1165 |
-
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
1166 |
-
|
1167 |
-
go func() {
|
1168 |
-
if err := server.Start(); err != nil && err != http.ErrServerClosed {
|
1169 |
-
log.Fatalf("start server error: %v", err)
|
1170 |
-
}
|
1171 |
-
}()
|
1172 |
-
log.Printf("Server started successfully")
|
1173 |
-
|
1174 |
-
<-done
|
1175 |
-
log.Print("Server stopping...")
|
1176 |
-
|
1177 |
-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
1178 |
-
defer cancel()
|
1179 |
-
|
1180 |
-
if err := server.Shutdown(ctx); err != nil {
|
1181 |
-
log.Printf("Server forced to shutdown: %v", err)
|
1182 |
-
}
|
1183 |
-
log.Print("Server stopped")
|
1184 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|