Create main.go
Browse files
main.go
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"bytes"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"io"
|
| 9 |
+
"log"
|
| 10 |
+
"net/http"
|
| 11 |
+
"os"
|
| 12 |
+
"strings"
|
| 13 |
+
"time"
|
| 14 |
+
|
| 15 |
+
"github.com/google/uuid"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
const (
|
| 19 |
+
waveAPIURL = "https://cfapi.waveterm.dev/api/waveai"
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
type ResponsesRequest struct {
|
| 23 |
+
Model string `json:"model"`
|
| 24 |
+
Input json.RawMessage `json:"input"`
|
| 25 |
+
Instructions string `json:"instructions,omitempty"`
|
| 26 |
+
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
| 27 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 28 |
+
TopP float64 `json:"top_p,omitempty"`
|
| 29 |
+
Stream *bool `json:"stream,omitempty"`
|
| 30 |
+
Tools json.RawMessage `json:"tools,omitempty"`
|
| 31 |
+
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
| 32 |
+
Reasoning *ReasoningConfig `json:"reasoning,omitempty"`
|
| 33 |
+
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
type ReasoningConfig struct {
|
| 37 |
+
Effort string `json:"effort,omitempty"`
|
| 38 |
+
Summary string `json:"summary,omitempty"`
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
type WaveRequest struct {
|
| 42 |
+
Input json.RawMessage `json:"input"`
|
| 43 |
+
Instructions string `json:"instructions,omitempty"`
|
| 44 |
+
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
| 45 |
+
Model string `json:"model"`
|
| 46 |
+
Reasoning *ReasoningConfig `json:"reasoning,omitempty"`
|
| 47 |
+
Stream bool `json:"stream"`
|
| 48 |
+
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
| 49 |
+
Text *TextConfig `json:"text,omitempty"`
|
| 50 |
+
Tools json.RawMessage `json:"tools,omitempty"`
|
| 51 |
+
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
| 52 |
+
Temperature float64 `json:"temperature,omitempty"`
|
| 53 |
+
TopP float64 `json:"top_p,omitempty"`
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
type StreamOptions struct {
|
| 57 |
+
IncludeObfuscation bool `json:"include_obfuscation"`
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
type TextConfig struct {
|
| 61 |
+
Verbosity string `json:"verbosity,omitempty"`
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func main() {
|
| 65 |
+
// Hugging Face 必须监听 7860
|
| 66 |
+
port := os.Getenv("PORT")
|
| 67 |
+
if port == "" {
|
| 68 |
+
port = "7860"
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
http.HandleFunc("/v1/responses", handleResponses)
|
| 72 |
+
http.HandleFunc("/responses", handleResponses)
|
| 73 |
+
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
| 74 |
+
w.Write([]byte(`{"status":"ok"}`))
|
| 75 |
+
})
|
| 76 |
+
|
| 77 |
+
log.Printf("Server starting on :%s", port)
|
| 78 |
+
if err := http.ListenAndServe(":"+port, nil); err != nil {
|
| 79 |
+
log.Fatalf("Server failed: %v", err)
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
func handleResponses(w http.ResponseWriter, r *http.Request) {
|
| 84 |
+
reqID := uuid.New().String()[:8]
|
| 85 |
+
if r.Method != http.MethodPost {
|
| 86 |
+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
body, _ := io.ReadAll(r.Body)
|
| 91 |
+
var req ResponsesRequest
|
| 92 |
+
json.Unmarshal(body, &req)
|
| 93 |
+
|
| 94 |
+
waveReq := convertToWaveRequest(&req)
|
| 95 |
+
stream := true
|
| 96 |
+
if req.Stream != nil {
|
| 97 |
+
stream = *req.Stream
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if stream {
|
| 101 |
+
handleStreamingResponse(w, waveReq, reqID)
|
| 102 |
+
} else {
|
| 103 |
+
handleNonStreamingResponse(w, waveReq, reqID)
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
func convertToWaveRequest(req *ResponsesRequest) *WaveRequest {
|
| 108 |
+
maxTokens := req.MaxOutputTokens
|
| 109 |
+
if maxTokens == 0 {
|
| 110 |
+
maxTokens = 4096
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return &WaveRequest{
|
| 114 |
+
Input: req.Input,
|
| 115 |
+
Instructions: req.Instructions,
|
| 116 |
+
MaxOutputTokens: maxTokens,
|
| 117 |
+
Model: "gpt-5.1",
|
| 118 |
+
Stream: true,
|
| 119 |
+
StreamOptions: &StreamOptions{IncludeObfuscation: false},
|
| 120 |
+
Text: &TextConfig{Verbosity: "low"},
|
| 121 |
+
Reasoning: &ReasoningConfig{Effort: "medium", Summary: "auto"},
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
func makeWaveRequest(waveReq *WaveRequest) (*http.Response, error) {
|
| 126 |
+
body, _ := json.Marshal(waveReq)
|
| 127 |
+
httpReq, _ := http.NewRequest(http.MethodPost, waveAPIURL, bytes.NewReader(body))
|
| 128 |
+
|
| 129 |
+
httpReq.Header.Set("Content-Type", "application/json")
|
| 130 |
+
httpReq.Header.Set("Accept", "text/event-stream")
|
| 131 |
+
httpReq.Header.Set("X-Wave-Apitype", "openai-responses")
|
| 132 |
+
httpReq.Header.Set("X-Wave-Clientid", uuid.New().String())
|
| 133 |
+
|
| 134 |
+
client := &http.Client{Timeout: 5 * time.Minute}
|
| 135 |
+
return client.Do(httpReq)
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
func handleStreamingResponse(w http.ResponseWriter, waveReq *WaveRequest, reqID string) {
|
| 139 |
+
resp, err := makeWaveRequest(waveReq)
|
| 140 |
+
if err != nil {
|
| 141 |
+
http.Error(w, err.Error(), http.StatusBadGateway)
|
| 142 |
+
return
|
| 143 |
+
}
|
| 144 |
+
defer resp.Body.Close()
|
| 145 |
+
|
| 146 |
+
w.Header().Set("Content-Type", "text/event-stream")
|
| 147 |
+
flusher, _ := w.(http.Flusher)
|
| 148 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 149 |
+
for scanner.Scan() {
|
| 150 |
+
w.Write([]byte(scanner.Text() + "\n"))
|
| 151 |
+
flusher.Flush()
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
func handleNonStreamingResponse(w http.ResponseWriter, waveReq *WaveRequest, reqID string) {
|
| 156 |
+
resp, _ := makeWaveRequest(waveReq)
|
| 157 |
+
defer resp.Body.Close()
|
| 158 |
+
|
| 159 |
+
var outputText strings.Builder
|
| 160 |
+
scanner := bufio.NewScanner(resp.Body)
|
| 161 |
+
for scanner.Scan() {
|
| 162 |
+
line := scanner.Text()
|
| 163 |
+
if strings.HasPrefix(line, "data: ") {
|
| 164 |
+
var event map[string]interface{}
|
| 165 |
+
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &event)
|
| 166 |
+
if delta, ok := event["delta"].(string); ok {
|
| 167 |
+
outputText.WriteString(delta)
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
w.Header().Set("Content-Type", "application/json")
|
| 173 |
+
json.NewEncoder(w).Encode(map[string]interface{}{
|
| 174 |
+
"choices": []map[string]interface{}{
|
| 175 |
+
{"message": map[string]string{"content": outputText.String()}},
|
| 176 |
+
},
|
| 177 |
+
})
|
| 178 |
+
}
|