初始提交:边缘AI算力机统一AI通讯层
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / security-scan (push) Has been cancelled

This commit is contained in:
freedakgmail
2026-08-03 07:44:05 +08:00
commit 93a469061d
51 changed files with 11565 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package handler
import (
"encoding/json"
"net/http"
"github.com/edgeai/gateway/pkg/api"
"github.com/google/uuid"
)
// ErrorCode constants.
const (
ErrAuthFailed = "AUTH_FAILED"
ErrPermissionDenied = "PERMISSION_DENIED"
ErrPolicyBlocked = "POLICY_BLOCKED"
ErrRateLimited = "RATE_LIMITED"
ErrQuotaExceeded = "QUOTA_EXCEEDED"
ErrQueueFull = "QUEUE_FULL"
ErrInvalidRequest = "INVALID_REQUEST"
ErrContextTooLarge = "CONTEXT_TOO_LARGE"
ErrQueueTimeout = "QUEUE_TIMEOUT"
ErrFirstTokenTimeout = "FIRST_TOKEN_TIMEOUT"
ErrInferenceTimeout = "INFERENCE_TIMEOUT"
ErrRequestCancelled = "REQUEST_CANCELLED"
ErrModelUnavailable = "MODEL_UNAVAILABLE"
ErrResourceExhausted = "RESOURCE_EXHAUSTED"
ErrInternalError = "INTERNAL_ERROR"
)
// httpStatusForCode maps error codes to HTTP status codes.
var httpStatusForCode = map[string]int{
ErrAuthFailed: http.StatusUnauthorized,
ErrPermissionDenied: http.StatusForbidden,
ErrPolicyBlocked: http.StatusForbidden,
ErrRateLimited: http.StatusTooManyRequests,
ErrQuotaExceeded: http.StatusTooManyRequests,
ErrQueueFull: http.StatusTooManyRequests,
ErrInvalidRequest: http.StatusBadRequest,
ErrContextTooLarge: http.StatusBadRequest,
ErrQueueTimeout: http.StatusRequestTimeout,
ErrFirstTokenTimeout: http.StatusRequestTimeout,
ErrInferenceTimeout: http.StatusRequestTimeout,
ErrRequestCancelled: http.StatusConflict,
ErrModelUnavailable: http.StatusServiceUnavailable,
ErrResourceExhausted: http.StatusServiceUnavailable,
ErrInternalError: http.StatusInternalServerError,
}
// GatewayError represents a structured error with code, message, and request ID.
type GatewayError struct {
Code string
Message string
RequestID string
}
func (e *GatewayError) Error() string {
return e.Message
}
// NewGatewayError creates a GatewayError with a generated request ID.
func NewGatewayError(code, message string) *GatewayError {
return &GatewayError{
Code: code,
Message: message,
RequestID: uuid.New().String(),
}
}
// NewGatewayErrorWithID creates a GatewayError with an existing request ID.
func NewGatewayErrorWithID(code, message, requestID string) *GatewayError {
return &GatewayError{
Code: code,
Message: message,
RequestID: requestID,
}
}
// WriteError writes a structured error response.
func WriteError(w http.ResponseWriter, err *GatewayError) {
status, ok := httpStatusForCode[err.Code]
if !ok {
status = http.StatusInternalServerError
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := api.ErrorResponse{
Error: api.ErrorBody{
Code: err.Code,
Message: err.Message,
RequestID: err.RequestID,
},
}
json.NewEncoder(w).Encode(resp)
}
// WriteJSON writes a JSON response with the given status code.
func WriteJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
+139
View File
@@ -0,0 +1,139 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"github.com/edgeai/gateway/internal/adapter"
"github.com/edgeai/gateway/pkg/api"
)
// SSEWriter writes Server-Sent Events to an HTTP response.
type SSEWriter struct {
w http.ResponseWriter
flusher http.Flusher
}
// NewSSEWriter creates a new SSEWriter. Returns nil if streaming is not supported.
func NewSSEWriter(w http.ResponseWriter) *SSEWriter {
flusher, ok := w.(http.Flusher)
if !ok {
return nil
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
return &SSEWriter{w: w, flusher: flusher}
}
// WriteChunk writes a single SSE data event.
func (s *SSEWriter) WriteChunk(data any) error {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("marshal sse data: %w", err)
}
fmt.Fprintf(s.w, "data: %s\n\n", jsonData)
s.flusher.Flush()
return nil
}
// WriteDone writes the [DONE] marker.
func (s *SSEWriter) WriteDone() {
fmt.Fprintf(s.w, "data: [DONE]\n\n")
s.flusher.Flush()
}
// StreamChatCompletion streams chunks from an adapter to the client in OpenAI SSE format.
func StreamChatCompletion(sse *SSEWriter, ch <-chan adapter.StreamChunk, requestID, taskID, model string) (int, int, error) {
inputTokens := 0
outputTokens := 0
for chunk := range ch {
if chunk.Error != nil {
return inputTokens, outputTokens, chunk.Error
}
if chunk.Done {
if chunk.InputTokens > 0 {
inputTokens = chunk.InputTokens
}
if chunk.OutputTokens > 0 {
outputTokens = chunk.OutputTokens
}
// Write final chunk with finish_reason
sseChunk := map[string]any{
"id": requestID,
"object": "chat.completion.chunk",
"model": model,
"choices": []map[string]any{
{
"index": 0,
"delta": map[string]any{},
"finish_reason": chunk.FinishReason,
},
},
}
if inputTokens > 0 || outputTokens > 0 {
sseChunk["usage"] = map[string]int{
"input_tokens": inputTokens,
"output_tokens": outputTokens,
"total_tokens": inputTokens + outputTokens,
}
}
sse.WriteChunk(sseChunk)
sse.WriteDone()
return inputTokens, outputTokens, nil
}
// Write content delta
sseChunk := map[string]any{
"id": requestID,
"object": "chat.completion.chunk",
"model": model,
"choices": []map[string]any{
{
"index": 0,
"delta": map[string]any{
"content": chunk.Delta,
},
"finish_reason": nil,
},
},
}
sse.WriteChunk(sseChunk)
}
return inputTokens, outputTokens, nil
}
// BuildChatResponse creates a non-streaming ChatResponse from adapter result.
func BuildChatResponse(requestID, taskID, logicalModel string, resp *adapter.ChatResponse) api.ChatResponse {
return api.ChatResponse{
RequestID: requestID,
TaskID: taskID,
Status: "completed",
Model: logicalModel,
Choices: []api.Choice{
{
Index: 0,
Message: &api.Message{
Role: "assistant",
Content: resp.Content,
},
FinishReason: resp.FinishReason,
},
},
LogicalModel: logicalModel,
ActualModel: resp.ActualModel,
Usage: &api.Usage{
InputTokens: resp.InputTokens,
OutputTokens: resp.OutputTokens,
TotalTokens: resp.InputTokens + resp.OutputTokens,
},
}
}