pauper-tarot-chain commited on
Commit
9e48b2b
·
verified ·
1 Parent(s): 0ad5125

Upload 10 files

Browse files
Files changed (4) hide show
  1. .cnb.yml +1 -1
  2. README.md +13 -11
  3. handlers.go +44 -26
  4. utils.go +2 -1
.cnb.yml CHANGED
@@ -1,4 +1,4 @@
1
- "(main)":
2
  push:
3
  - runner:
4
  cpus: 2
 
1
+ "(main|dev)":
2
  push:
3
  - runner:
4
  cpus: 2
README.md CHANGED
@@ -1,11 +1,13 @@
1
- ---
2
- title: Julep2api
3
- emoji: 🔥
4
- colorFrom: pink
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- app_port: 8080
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ ## 部署到 claw cloud
2
+
3
+ image: docker.cnb.cool/0_0/learn/julep
4
+ 端口: 8080
5
+ cpu: 0.2
6
+ 内存: 64M
7
+ 环境变量: JULEP_API_BASE_URL=https://api.julep.ai/api
8
+
9
+ 如果跟 new-api 部署在同一个区域,可以让 new-api 直接用内网地址连接到 julep。无需暴露到公网。
10
+
11
+ ## 致谢
12
+
13
+ https://linux.do/t/topic/577836/27
handlers.go CHANGED
@@ -19,7 +19,7 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
19
  // ... (Keep Method Check, Authentication, Decode Body, Validation as before) ...
20
  // 1. Check Method
21
  if r.Method != http.MethodPost {
22
- writeJSONError(w, reqLogger, http.StatusMethodNotAllowed, fmt.Sprintf("Method %s not allowed", r.Method), stringPtr("invalid_request_error"), nil, nil)
23
  return
24
  }
25
 
@@ -30,7 +30,7 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
30
  if authHeader != "" {
31
  invalidAuthType = "invalid_authorization_type"
32
  }
33
- writeJSONError(w, reqLogger, http.StatusUnauthorized, "Authorization header is required (e.g., 'Bearer YOUR_API_KEY')", &invalidAuthType, nil, nil)
34
  return
35
  }
36
  reqLogger.Debug("Authorization header present") // Changed to Debug
@@ -42,15 +42,15 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
42
  if err != nil {
43
  var syntaxError *json.SyntaxError
44
  var unmarshalTypeError *json.UnmarshalTypeError
45
- errMsg := "Invalid JSON request body"
46
  errCode := "invalid_json"
47
  if errors.As(err, &syntaxError) { // Use errors.As
48
- errMsg = fmt.Sprintf("Invalid JSON syntax at offset %d", syntaxError.Offset)
49
  } else if errors.As(err, &unmarshalTypeError) { // Use errors.As
50
- errMsg = fmt.Sprintf("Invalid type for field '%s', expected %s", unmarshalTypeError.Field, unmarshalTypeError.Type)
51
  errCode = "invalid_field_type"
52
  } else if err.Error() == "http: request body too large" {
53
- errMsg = "Request body exceeds limit (1MB)"
54
  errCode = "request_too_large"
55
  writeJSONError(w, reqLogger, http.StatusRequestEntityTooLarge, errMsg, stringPtr("invalid_request_error"), &errCode, nil)
56
  return
@@ -66,14 +66,14 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
66
  reqLogger.Warn("Validation failed: 'messages' field is empty")
67
  param := "messages"
68
  code := "missing_field"
69
- writeJSONError(w, reqLogger, http.StatusBadRequest, "'messages' is a required field and must be a non-empty array", stringPtr("invalid_request_error"), &code, &param)
70
  return
71
  }
72
  if openaiRequest.Model == "" {
73
  reqLogger.Warn("Validation failed: 'model' field is empty")
74
  param := "model"
75
  code := "missing_field"
76
- writeJSONError(w, reqLogger, http.StatusBadRequest, "'model' is a required field", stringPtr("invalid_request_error"), &code, &param)
77
  return
78
  }
79
  reqLogger.Info("Request decoded and validated", "model", openaiRequest.Model, "stream_requested", openaiRequest.Stream)
@@ -86,12 +86,14 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
86
  reqLogger.Error("Upstream Julep call failed", "error", err, "status_code", upstreamStatusCode)
87
  // Use the status code returned by callJulepChat for the client response
88
  errType := "upstream_error"
 
89
  if upstreamStatusCode == http.StatusGatewayTimeout {
90
  errType = "gateway_timeout"
 
91
  } else if upstreamStatusCode >= 400 && upstreamStatusCode < 500 {
92
  errType = "invalid_request_error" // Or map specific Julep 4xx codes
93
  }
94
- writeJSONError(w, reqLogger, upstreamStatusCode, fmt.Sprintf("Upstream API error: %s", err.Error()), &errType, nil, nil)
95
  return
96
  }
97
 
@@ -101,7 +103,8 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
101
  if !isStreaming {
102
  // 6.a. Send Non-Streaming Response
103
  reqLogger.Info("Sending non-streaming response")
104
- w.Header().Set("Content-Type", "application/json")
 
105
  w.WriteHeader(http.StatusOK) // Status OK as the overall operation succeeded
106
  if err := json.NewEncoder(w).Encode(finalOpenAIResponse); err != nil {
107
  reqLogger.Error("Failed to encode non-streaming response", "error", err)
@@ -110,7 +113,8 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
110
  // 6.b. Send Simulated Streaming Response from the complete Julep data
111
  reqLogger.Info("Sending simulated streaming response based on Julep result")
112
 
113
- w.Header().Set("Content-Type", "text/event-stream")
 
114
  w.Header().Set("Cache-Control", "no-cache")
115
  w.Header().Set("Connection", "keep-alive")
116
  // Optional: w.Header().Set("X-Accel-Buffering", "no")
@@ -119,11 +123,12 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
119
  if !ok {
120
  reqLogger.Error("Streaming unsupported: ResponseWriter does not implement http.Flusher")
121
  errType := "internal_server_error"
 
122
  // It's likely too late to send a proper JSON error here if headers were already flushed implicitly.
123
  // Best effort: log and potentially send plain text error before trying to write stream headers.
124
- http.Error(w, "Internal Server Error: Streaming unsupported", http.StatusInternalServerError)
125
  // Attempt to write JSON error anyway, might fail.
126
- writeJSONError(w, reqLogger, http.StatusInternalServerError, "Streaming is not supported by the server configuration", &errType, nil, nil)
127
  return
128
  }
129
 
@@ -144,13 +149,14 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
144
 
145
  // streamFullResponseAsChunks takes a complete OpenAIResponse and sends it
146
  // to the client as a series of SSE chunks, simulating a real stream.
 
147
  func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, fullResp *OpenAIResponse) error {
148
  if len(fullResp.Choices) == 0 {
149
  logger.Warn("Full response has no choices to stream")
150
  // Send DONE immediately if no choices
151
  _, err := fmt.Fprintf(w, "data: [DONE]\n\n")
152
  if err != nil {
153
- return fmt.Errorf("failed to write [DONE] message: %w", err)
154
  }
155
  flusher.Flush()
156
  return nil
@@ -175,7 +181,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
175
  },
176
  }
177
  if err := sendChunk(w, flusher, logger, roleChunk); err != nil {
178
- return fmt.Errorf("failed to send role chunk: %w", err)
179
  }
180
  time.Sleep(10 * time.Millisecond) // Small delay
181
  }
@@ -185,12 +191,21 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
185
  if content != "" {
186
  // Simulate streaming by breaking content into smaller parts
187
  const chunkSize = 5 // Small chunk size for demonstration
188
- for i := 0; i < len(content); i += chunkSize {
189
- end := i + chunkSize
190
- if end > len(content) {
191
- end = len(content)
 
 
 
 
 
 
 
 
 
192
  }
193
- contentPiece := content[i:end]
194
 
195
  contentChunk := OpenAIChunk{
196
  ID: fullResp.ID,
@@ -206,7 +221,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
206
  },
207
  }
208
  if err := sendChunk(w, flusher, logger, contentChunk); err != nil {
209
- return fmt.Errorf("failed to send content chunk: %w", err)
210
  }
211
  time.Sleep(30 * time.Millisecond) // Simulate generation time between chunks
212
  }
@@ -228,7 +243,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
228
  },
229
  }
230
  if err := sendChunk(w, flusher, logger, toolChunk); err != nil {
231
- return fmt.Errorf("failed to send tool_calls chunk: %w", err)
232
  }
233
  time.Sleep(10 * time.Millisecond) // Small delay
234
  }
@@ -248,7 +263,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
248
  },
249
  }
250
  if err := sendChunk(w, flusher, logger, finalChunk); err != nil {
251
- return fmt.Errorf("failed to send final chunk: %w", err)
252
  }
253
 
254
  // --- Send DONE message ---
@@ -257,7 +272,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
257
  if err != nil {
258
  // Log error, but might not reach client if connection closed
259
  logger.Error("Failed to write [DONE] message", "error", err)
260
- return fmt.Errorf("failed to write [DONE] message: %w", err)
261
  }
262
  flusher.Flush() // Ensure DONE is sent
263
 
@@ -265,19 +280,22 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
265
  }
266
 
267
  // sendChunk encodes the chunk to JSON and writes it in SSE format.
 
268
  func sendChunk(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, chunk OpenAIChunk) error {
 
269
  chunkBytes, err := json.Marshal(chunk)
270
  if err != nil {
271
  logger.Error("Failed to marshal stream chunk", "error", err, "chunk_id", chunk.ID)
272
- return fmt.Errorf("failed to marshal chunk: %w", err)
273
  }
274
 
275
  // Write in Server-Sent Event format: data: <json>\n\n
 
276
  _, err = fmt.Fprintf(w, "data: %s\n\n", string(chunkBytes))
277
  if err != nil {
278
  // Log error, connection might be closed by client
279
  logger.Error("Failed to write chunk to response writer", "error", err, "chunk_id", chunk.ID)
280
- return fmt.Errorf("failed to write chunk: %w", err)
281
  }
282
 
283
  // Flush the buffer to send the chunk immediately
 
19
  // ... (Keep Method Check, Authentication, Decode Body, Validation as before) ...
20
  // 1. Check Method
21
  if r.Method != http.MethodPost {
22
+ writeJSONError(w, reqLogger, http.StatusMethodNotAllowed, fmt.Sprintf("方法 %s 不允许", r.Method), stringPtr("invalid_request_error"), nil, nil) // Example Chinese error
23
  return
24
  }
25
 
 
30
  if authHeader != "" {
31
  invalidAuthType = "invalid_authorization_type"
32
  }
33
+ writeJSONError(w, reqLogger, http.StatusUnauthorized, "需要 Authorization 标头 (例如, 'Bearer YOUR_API_KEY')", &invalidAuthType, nil, nil) // Example Chinese error
34
  return
35
  }
36
  reqLogger.Debug("Authorization header present") // Changed to Debug
 
42
  if err != nil {
43
  var syntaxError *json.SyntaxError
44
  var unmarshalTypeError *json.UnmarshalTypeError
45
+ errMsg := "无效的 JSON 请求体" // Example Chinese error
46
  errCode := "invalid_json"
47
  if errors.As(err, &syntaxError) { // Use errors.As
48
+ errMsg = fmt.Sprintf("偏移量 %d JSON 语法无效", syntaxError.Offset) // Example Chinese error
49
  } else if errors.As(err, &unmarshalTypeError) { // Use errors.As
50
+ errMsg = fmt.Sprintf("字段 '%s' 类型无效,应为 %s", unmarshalTypeError.Field, unmarshalTypeError.Type) // Example Chinese error
51
  errCode = "invalid_field_type"
52
  } else if err.Error() == "http: request body too large" {
53
+ errMsg = "请求体超过限制 (1MB)" // Example Chinese error
54
  errCode = "request_too_large"
55
  writeJSONError(w, reqLogger, http.StatusRequestEntityTooLarge, errMsg, stringPtr("invalid_request_error"), &errCode, nil)
56
  return
 
66
  reqLogger.Warn("Validation failed: 'messages' field is empty")
67
  param := "messages"
68
  code := "missing_field"
69
+ writeJSONError(w, reqLogger, http.StatusBadRequest, "'messages' 是必填字段且必须是非空数组", stringPtr("invalid_request_error"), &code, &param) // Example Chinese error
70
  return
71
  }
72
  if openaiRequest.Model == "" {
73
  reqLogger.Warn("Validation failed: 'model' field is empty")
74
  param := "model"
75
  code := "missing_field"
76
+ writeJSONError(w, reqLogger, http.StatusBadRequest, "'model' 是必填字段", stringPtr("invalid_request_error"), &code, &param) // Example Chinese error
77
  return
78
  }
79
  reqLogger.Info("Request decoded and validated", "model", openaiRequest.Model, "stream_requested", openaiRequest.Stream)
 
86
  reqLogger.Error("Upstream Julep call failed", "error", err, "status_code", upstreamStatusCode)
87
  // Use the status code returned by callJulepChat for the client response
88
  errType := "upstream_error"
89
+ errMsg := fmt.Sprintf("上游 API 错误: %s", err.Error()) // Example Chinese error
90
  if upstreamStatusCode == http.StatusGatewayTimeout {
91
  errType = "gateway_timeout"
92
+ errMsg = fmt.Sprintf("上游 API 超时: %s", err.Error()) // Example Chinese error
93
  } else if upstreamStatusCode >= 400 && upstreamStatusCode < 500 {
94
  errType = "invalid_request_error" // Or map specific Julep 4xx codes
95
  }
96
+ writeJSONError(w, reqLogger, upstreamStatusCode, errMsg, &errType, nil, nil)
97
  return
98
  }
99
 
 
103
  if !isStreaming {
104
  // 6.a. Send Non-Streaming Response
105
  reqLogger.Info("Sending non-streaming response")
106
+ // CORRECTED: Explicitly add charset=utf-8
107
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
108
  w.WriteHeader(http.StatusOK) // Status OK as the overall operation succeeded
109
  if err := json.NewEncoder(w).Encode(finalOpenAIResponse); err != nil {
110
  reqLogger.Error("Failed to encode non-streaming response", "error", err)
 
113
  // 6.b. Send Simulated Streaming Response from the complete Julep data
114
  reqLogger.Info("Sending simulated streaming response based on Julep result")
115
 
116
+ // CORRECTED: Explicitly add charset=utf-8
117
+ w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
118
  w.Header().Set("Cache-Control", "no-cache")
119
  w.Header().Set("Connection", "keep-alive")
120
  // Optional: w.Header().Set("X-Accel-Buffering", "no")
 
123
  if !ok {
124
  reqLogger.Error("Streaming unsupported: ResponseWriter does not implement http.Flusher")
125
  errType := "internal_server_error"
126
+ errMsg := "内部服务器错误:流式传输不受支持" // Example Chinese error
127
  // It's likely too late to send a proper JSON error here if headers were already flushed implicitly.
128
  // Best effort: log and potentially send plain text error before trying to write stream headers.
129
+ http.Error(w, errMsg, http.StatusInternalServerError)
130
  // Attempt to write JSON error anyway, might fail.
131
+ writeJSONError(w, reqLogger, http.StatusInternalServerError, "服务器配置不支持流式传输", &errType, nil, nil) // Example Chinese error
132
  return
133
  }
134
 
 
149
 
150
  // streamFullResponseAsChunks takes a complete OpenAIResponse and sends it
151
  // to the client as a series of SSE chunks, simulating a real stream.
152
+ // (No changes needed inside this function itself, as it relies on the Content-Type set before calling it)
153
  func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, fullResp *OpenAIResponse) error {
154
  if len(fullResp.Choices) == 0 {
155
  logger.Warn("Full response has no choices to stream")
156
  // Send DONE immediately if no choices
157
  _, err := fmt.Fprintf(w, "data: [DONE]\n\n")
158
  if err != nil {
159
+ return fmt.Errorf("写入 [DONE] 消息失败: %w", err) // Example Chinese error
160
  }
161
  flusher.Flush()
162
  return nil
 
181
  },
182
  }
183
  if err := sendChunk(w, flusher, logger, roleChunk); err != nil {
184
+ return fmt.Errorf("发送角色块失败: %w", err) // Example Chinese error
185
  }
186
  time.Sleep(10 * time.Millisecond) // Small delay
187
  }
 
191
  if content != "" {
192
  // Simulate streaming by breaking content into smaller parts
193
  const chunkSize = 5 // Small chunk size for demonstration
194
+ // IMPORTANT: This simple byte-wise chunking can break multi-byte UTF-8 characters.
195
+ // For proper UTF-8 streaming, chunking should happen at rune boundaries.
196
+ // However, since we are simulating from a *complete* response here, and
197
+ // sendChunk uses json.Marshal which handles UTF-8 correctly, this simulation
198
+ // *should* still produce valid JSON payloads per chunk. The potential issue
199
+ // is less severe than if we were reading a raw byte stream.
200
+ // A more robust simulation would iterate over runes.
201
+ runeContent := []rune(content)
202
+ const runeChunkSize = 3 // Chunk runes instead of bytes
203
+ for i := 0; i < len(runeContent); i += runeChunkSize {
204
+ end := i + runeChunkSize
205
+ if end > len(runeContent) {
206
+ end = len(runeContent)
207
  }
208
+ contentPiece := string(runeContent[i:end]) // Convert rune slice back to string
209
 
210
  contentChunk := OpenAIChunk{
211
  ID: fullResp.ID,
 
221
  },
222
  }
223
  if err := sendChunk(w, flusher, logger, contentChunk); err != nil {
224
+ return fmt.Errorf("发送内容块失败: %w", err) // Example Chinese error
225
  }
226
  time.Sleep(30 * time.Millisecond) // Simulate generation time between chunks
227
  }
 
243
  },
244
  }
245
  if err := sendChunk(w, flusher, logger, toolChunk); err != nil {
246
+ return fmt.Errorf("发送 tool_calls 块失败: %w", err) // Example Chinese error
247
  }
248
  time.Sleep(10 * time.Millisecond) // Small delay
249
  }
 
263
  },
264
  }
265
  if err := sendChunk(w, flusher, logger, finalChunk); err != nil {
266
+ return fmt.Errorf("发送最终块失败: %w", err) // Example Chinese error
267
  }
268
 
269
  // --- Send DONE message ---
 
272
  if err != nil {
273
  // Log error, but might not reach client if connection closed
274
  logger.Error("Failed to write [DONE] message", "error", err)
275
+ return fmt.Errorf("写入 [DONE] 消息失败: %w", err) // Example Chinese error
276
  }
277
  flusher.Flush() // Ensure DONE is sent
278
 
 
280
  }
281
 
282
  // sendChunk encodes the chunk to JSON and writes it in SSE format.
283
+ // (No changes needed here as json.Marshal produces UTF-8 and Fprintf respects it)
284
  func sendChunk(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, chunk OpenAIChunk) error {
285
+ // json.Marshal correctly handles Go strings (UTF-8) into JSON strings (UTF-8)
286
  chunkBytes, err := json.Marshal(chunk)
287
  if err != nil {
288
  logger.Error("Failed to marshal stream chunk", "error", err, "chunk_id", chunk.ID)
289
+ return fmt.Errorf("块编码失败: %w", err) // Example Chinese error
290
  }
291
 
292
  // Write in Server-Sent Event format: data: <json>\n\n
293
+ // fmt.Fprintf handles UTF-8 correctly when writing strings/byte slices
294
  _, err = fmt.Fprintf(w, "data: %s\n\n", string(chunkBytes))
295
  if err != nil {
296
  // Log error, connection might be closed by client
297
  logger.Error("Failed to write chunk to response writer", "error", err, "chunk_id", chunk.ID)
298
+ return fmt.Errorf("块写入失败: %w", err) // Example Chinese error
299
  }
300
 
301
  // Flush the buffer to send the chunk immediately
utils.go CHANGED
@@ -10,7 +10,8 @@ import (
10
 
11
  // writeJSONError sends a standard JSON error response.
12
  func writeJSONError(w http.ResponseWriter, logger *slog.Logger, statusCode int, message string, errType *string, errCode *string, param *string) {
13
- w.Header().Set("Content-Type", "application/json")
 
14
  w.WriteHeader(statusCode)
15
  resp := ErrorResponse{
16
  Error: APIError{
 
10
 
11
  // writeJSONError sends a standard JSON error response.
12
  func writeJSONError(w http.ResponseWriter, logger *slog.Logger, statusCode int, message string, errType *string, errCode *string, param *string) {
13
+ // CORRECTED: Explicitly add charset=utf-8
14
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
15
  w.WriteHeader(statusCode)
16
  resp := ErrorResponse{
17
  Error: APIError{