Files
vecna/pkg/server/openai.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

116 lines
3.5 KiB
Go

package server
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/uptrace/bunrouter"
"github.com/Warky-Devs/vecna.git/pkg/adapter"
"github.com/Warky-Devs/vecna.git/pkg/embedclient"
)
type openAIEmbedRequest struct {
Input interface{} `json:"input"` // string or []string
Model string `json:"model"`
}
type openAIEmbedResponse struct {
Object string `json:"object"`
Data []openAIEmbedDatum `json:"data"`
Model string `json:"model"`
Usage openAIUsage `json:"usage"`
}
type openAIEmbedDatum struct {
Object string `json:"object"`
Embedding []float32 `json:"embedding"`
Index int `json:"index"`
}
type openAIUsage struct {
PromptTokens int `json:"prompt_tokens"`
TotalTokens int `json:"total_tokens"`
}
func (h *handler) openAIEmbeddings(w http.ResponseWriter, req bunrouter.Request) error {
return h.openAIEmbeddingsWithAdapter(w, req, h.adapter, "")
}
func (h *handler) openAIEmbeddingsMapped(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.openAIEmbeddingsWithAdapter(w, req, em.Adapter, em.ForwardTarget)
}
func (h *handler) openAIEmbeddingsWithAdapter(w http.ResponseWriter, req bunrouter.Request, adp adapter.Adapter, targetOverride string) error {
var body openAIEmbedRequest
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
}
texts, err := toStringSlice(body.Input)
if err != nil {
return writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
}
client, targetName, targetURL := h.resolveClientOverride(targetOverride, body.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: body.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()
data := make([]openAIEmbedDatum, 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()})
}
data[i] = openAIEmbedDatum{Object: "embedding", Embedding: adapted, Index: i}
}
trace.TranslateDuration = time.Since(t1)
writeTraceHeaders(w, trace)
return writeJSON(w, http.StatusOK, openAIEmbedResponse{
Object: "list",
Data: data,
Model: embedResp.Model,
Usage: openAIUsage{PromptTokens: embedResp.Usage.PromptTokens, TotalTokens: embedResp.Usage.TotalTokens},
})
}
// toStringSlice accepts a JSON string or array of strings.
func toStringSlice(v interface{}) ([]string, error) {
switch val := v.(type) {
case string:
return []string{val}, nil
case []interface{}:
out := make([]string, len(val))
for i, item := range val {
s, ok := item.(string)
if !ok {
return nil, fmt.Errorf("input array element %d is not a string", i)
}
out[i] = s
}
return out, nil
default:
return nil, fmt.Errorf("input must be a string or array of strings")
}
}