Files
vecna/pkg/server/google.go
Hein c7a3fed6e1 feat(server): add support for extra maps in adapter configuration
* Introduced ExtraMapConfig to allow multiple adapter configurations.
* Updated server and handler to utilize extra maps for routing.
* Added dashboard handler for metrics visualization.
2026-04-11 21:43:14 +02:00

176 lines
5.5 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 writeJSON(w, http.StatusNotFound, map[string]string{"error": 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 writeJSON(w, http.StatusNotFound, map[string]string{"error": "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 writeJSON(w, http.StatusNotFound, map[string]string{"error": "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 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "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 writeJSON(w, http.StatusBadGateway, map[string]string{"error": 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 writeJSON(w, http.StatusInternalServerError, map[string]string{"error": 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 writeJSON(w, http.StatusBadRequest, map[string]string{"error": "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 writeJSON(w, http.StatusBadGateway, map[string]string{"error": 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 writeJSON(w, http.StatusInternalServerError, map[string]string{"error": adaptErr.Error()})
}
result[i] = googleEmbeddingValues{Values: adapted}
}
trace.TranslateDuration = time.Since(t1)
writeTraceHeaders(w, trace)
return writeJSON(w, http.StatusOK, googleBatchResponse{Embeddings: result})
}