mirror of
https://github.com/Warky-Devs/vecna.git
synced 2026-08-07 10:47:38 +00:00
* implement proxy handler for various OpenAI API routes * add error handling for request body and response streaming * introduce new error response format for API compatibility * add tests for recover middleware to handle panics gracefully
176 lines
5.3 KiB
Go
176 lines
5.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/uptrace/bunrouter"
|
|
|
|
"github.com/Warky-Devs/vecna.git/pkg/adapter"
|
|
"github.com/Warky-Devs/vecna.git/pkg/embedclient"
|
|
)
|
|
|
|
// googleDispatch parses the wildcard "model:action" segment and routes to the
|
|
// correct Google handler. The colon is a literal method separator in the
|
|
// Google embedding API, not a bunrouter parameter prefix.
|
|
func (h *handler) googleDispatch(w http.ResponseWriter, req bunrouter.Request) error {
|
|
return h.googleDispatchWithAdapter(w, req, h.adapter, "")
|
|
}
|
|
|
|
func (h *handler) googleDispatchMapped(w http.ResponseWriter, req bunrouter.Request) error {
|
|
em, err := h.resolveExtraMap(req.Param("mapping"))
|
|
if err != nil {
|
|
return writeError(w, http.StatusNotFound, err.Error())
|
|
}
|
|
return h.googleDispatchWithAdapter(w, req, em.Adapter, em.ForwardTarget)
|
|
}
|
|
|
|
func (h *handler) googleDispatchWithAdapter(w http.ResponseWriter, req bunrouter.Request, adp adapter.Adapter, targetOverride string) error {
|
|
modelaction := req.Param("modelaction") // e.g. "text-embedding-foo:embedContent"
|
|
idx := strings.LastIndex(modelaction, ":")
|
|
if idx < 0 {
|
|
return writeError(w, http.StatusNotFound, "invalid Google API path")
|
|
}
|
|
model := modelaction[:idx]
|
|
action := modelaction[idx+1:]
|
|
|
|
ctx := context.WithValue(req.Context(), modelKey, model)
|
|
req = req.WithContext(ctx)
|
|
|
|
switch action {
|
|
case "embedContent":
|
|
return h.googleEmbedContentWithAdapter(w, req, adp, targetOverride)
|
|
case "batchEmbedContents":
|
|
return h.googleBatchEmbedContentsWithAdapter(w, req, adp, targetOverride)
|
|
default:
|
|
return writeError(w, http.StatusNotFound, "unknown Google API method: "+action)
|
|
}
|
|
}
|
|
|
|
// --- single embedContent ---
|
|
|
|
type googleEmbedContentRequest struct {
|
|
Content googleContent `json:"content"`
|
|
TaskType string `json:"taskType,omitempty"`
|
|
}
|
|
|
|
type googleContent struct {
|
|
Parts []googlePart `json:"parts"`
|
|
}
|
|
|
|
type googlePart struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type googleEmbedContentResponse struct {
|
|
Embedding googleEmbeddingValues `json:"embedding"`
|
|
}
|
|
|
|
type googleEmbeddingValues struct {
|
|
Values []float32 `json:"values"`
|
|
}
|
|
|
|
func (h *handler) googleEmbedContentWithAdapter(w http.ResponseWriter, req bunrouter.Request, adp adapter.Adapter, targetOverride string) error {
|
|
model, _ := req.Context().Value(modelKey).(string)
|
|
|
|
var body googleEmbedContentRequest
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
return writeError(w, http.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
texts := make([]string, len(body.Content.Parts))
|
|
for i, p := range body.Content.Parts {
|
|
texts[i] = p.Text
|
|
}
|
|
|
|
client, targetName, targetURL := h.resolveClientOverride(targetOverride, model)
|
|
trace := TraceFromContext(req.Context())
|
|
trace.ForwardTarget = targetName
|
|
trace.ForwardURL = targetURL
|
|
|
|
t0 := time.Now()
|
|
embedResp, err := client.Embed(req.Context(), embedclient.Request{Texts: texts, Model: model})
|
|
trace.ForwardDuration = time.Since(t0)
|
|
if err != nil {
|
|
return writeError(w, http.StatusBadGateway, err.Error())
|
|
}
|
|
trace.ForwardModel = embedResp.Model
|
|
trace.PromptTokens = embedResp.Usage.PromptTokens
|
|
trace.TotalTokens = embedResp.Usage.TotalTokens
|
|
|
|
t1 := time.Now()
|
|
var adapted []float32
|
|
if len(embedResp.Embeddings) > 0 {
|
|
adapted, err = adp.Adapt(embedResp.Embeddings[0])
|
|
if err != nil {
|
|
return writeError(w, http.StatusInternalServerError, err.Error())
|
|
}
|
|
}
|
|
trace.TranslateDuration = time.Since(t1)
|
|
|
|
writeTraceHeaders(w, trace)
|
|
|
|
return writeJSON(w, http.StatusOK, googleEmbedContentResponse{
|
|
Embedding: googleEmbeddingValues{Values: adapted},
|
|
})
|
|
}
|
|
|
|
// --- batch batchEmbedContents ---
|
|
|
|
type googleBatchRequest struct {
|
|
Requests []googleEmbedContentRequest `json:"requests"`
|
|
}
|
|
|
|
type googleBatchResponse struct {
|
|
Embeddings []googleEmbeddingValues `json:"embeddings"`
|
|
}
|
|
|
|
func (h *handler) googleBatchEmbedContentsWithAdapter(w http.ResponseWriter, req bunrouter.Request, adp adapter.Adapter, targetOverride string) error {
|
|
model, _ := req.Context().Value(modelKey).(string)
|
|
|
|
var body googleBatchRequest
|
|
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
|
return writeError(w, http.StatusBadRequest, "invalid request body")
|
|
}
|
|
|
|
var texts []string
|
|
for _, r := range body.Requests {
|
|
for _, p := range r.Content.Parts {
|
|
texts = append(texts, p.Text)
|
|
}
|
|
}
|
|
|
|
client, targetName, targetURL := h.resolveClientOverride(targetOverride, model)
|
|
trace := TraceFromContext(req.Context())
|
|
trace.ForwardTarget = targetName
|
|
trace.ForwardURL = targetURL
|
|
|
|
t0 := time.Now()
|
|
embedResp, err := client.Embed(req.Context(), embedclient.Request{Texts: texts, Model: model})
|
|
trace.ForwardDuration = time.Since(t0)
|
|
if err != nil {
|
|
return writeError(w, http.StatusBadGateway, err.Error())
|
|
}
|
|
trace.ForwardModel = embedResp.Model
|
|
trace.PromptTokens = embedResp.Usage.PromptTokens
|
|
trace.TotalTokens = embedResp.Usage.TotalTokens
|
|
|
|
t1 := time.Now()
|
|
result := make([]googleEmbeddingValues, len(embedResp.Embeddings))
|
|
for i, vec := range embedResp.Embeddings {
|
|
adapted, adaptErr := adp.Adapt(vec)
|
|
if adaptErr != nil {
|
|
return writeError(w, http.StatusInternalServerError, adaptErr.Error())
|
|
}
|
|
result[i] = googleEmbeddingValues{Values: adapted}
|
|
}
|
|
trace.TranslateDuration = time.Since(t1)
|
|
|
|
writeTraceHeaders(w, trace)
|
|
|
|
return writeJSON(w, http.StatusOK, googleBatchResponse{Embeddings: result})
|
|
}
|