From 4131208115889780ecbf77dc53d696896f56fb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Manuel=20M=C3=A9ndez=20Rey?= Date: Wed, 23 Sep 2026 03:29:31 +0200 Subject: [PATCH] feat(lumo): add a --web-search flag to serve The OpenAI-compatible server never enabled Lumo's native web search, so a model served through it could only answer from its training data. The client already supports it (the interactive chat gates lumo.ToolWebSearch behind a flag); serve just did not pass any tools. Add `proton lumo serve --web-search` (and $LUMO_WEB_SEARCH), which requests lumo.ToolWebSearch for every generation. When set, answers can draw on up-to-date web results; off by default, so behaviour is unchanged otherwise. --- internal/cli/lumo/handler.go | 25 +++++++++++++++++++------ internal/cli/lumo/serve.go | 9 ++++++++- internal/cli/lumo/websearch_test.go | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 internal/cli/lumo/websearch_test.go diff --git a/internal/cli/lumo/handler.go b/internal/cli/lumo/handler.go index 18228bd..34cef66 100644 --- a/internal/cli/lumo/handler.go +++ b/internal/cli/lumo/handler.go @@ -13,9 +13,20 @@ import ( "github.com/major0/proton-utils/api/lumo" ) +// webSearchTools returns the tool list to pass to Generate: Lumo's native +// web_search when enabled, otherwise none. Kept in one place so the streaming +// and non-streaming paths request the same tools. +func webSearchTools(enabled bool) []lumo.ToolName { + if enabled { + return []lumo.ToolName{lumo.ToolWebSearch} + } + return nil +} + // chatHandler returns an http.HandlerFunc that proxies OpenAI-format chat -// completion requests to Lumo via the provided client. -func chatHandler(client *lumo.Client) http.HandlerFunc { +// completion requests to Lumo via the provided client. When webSearch is set, +// Lumo's native web_search tool is enabled for every request. +func chatHandler(client *lumo.Client, webSearch bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req lumo.ChatCompletionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -28,15 +39,15 @@ func chatHandler(client *lumo.Client) http.HandlerFunc { model := "lumo" if req.Stream { - streamResponse(r.Context(), w, client, turns, id, model) + streamResponse(r.Context(), w, client, turns, id, model, webSearch) } else { - nonStreamResponse(r.Context(), w, client, turns, id, model) + nonStreamResponse(r.Context(), w, client, turns, id, model, webSearch) } } } // streamResponse handles streaming (SSE) chat completions. -func streamResponse(ctx context.Context, w http.ResponseWriter, client *lumo.Client, turns []lumo.Turn, id, model string) { +func streamResponse(ctx context.Context, w http.ResponseWriter, client *lumo.Client, turns []lumo.Turn, id, model string, webSearch bool) { flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "server_error", "Streaming not supported") @@ -49,6 +60,7 @@ func streamResponse(ctx context.Context, w http.ResponseWriter, client *lumo.Cli w.WriteHeader(http.StatusOK) err := client.Generate(ctx, turns, lumo.GenerateOpts{ + Tools: webSearchTools(webSearch), ChunkCallback: func(msg lumo.GenerationResponseMessage) { chunk, ok := ChunkToSSEEvent(msg, id, model) if !ok { @@ -84,10 +96,11 @@ func streamResponse(ctx context.Context, w http.ResponseWriter, client *lumo.Cli } // nonStreamResponse handles non-streaming chat completions. -func nonStreamResponse(ctx context.Context, w http.ResponseWriter, client *lumo.Client, turns []lumo.Turn, id, model string) { +func nonStreamResponse(ctx context.Context, w http.ResponseWriter, client *lumo.Client, turns []lumo.Turn, id, model string, webSearch bool) { var content strings.Builder err := client.Generate(ctx, turns, lumo.GenerateOpts{ + Tools: webSearchTools(webSearch), ChunkCallback: func(msg lumo.GenerationResponseMessage) { if msg.Type == "token_data" && msg.Content != "" { content.WriteString(msg.Content) diff --git a/internal/cli/lumo/serve.go b/internal/cli/lumo/serve.go index 73724e1..be677c8 100644 --- a/internal/cli/lumo/serve.go +++ b/internal/cli/lumo/serve.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -23,6 +24,7 @@ type serveFlags struct { tlsCert string tlsKey string noTLS bool + webSearch bool } var sFlags serveFlags @@ -46,6 +48,10 @@ func init() { if v := os.Getenv("LUMO_API_KEY"); v != "" { apiKeyDefault = v } + webSearchDefault := false + if v := os.Getenv("LUMO_WEB_SEARCH"); v == "1" || strings.EqualFold(v, "true") { + webSearchDefault = true + } serveCmd.Flags().StringVar(&sFlags.addr, "addr", addrDefault, "Listen address and port [$LUMO_ENDPOINT]") serveCmd.Flags().StringVar(&sFlags.apiKey, "api-key", apiKeyDefault, "Use this API key (not persisted) [$LUMO_API_KEY]") @@ -53,6 +59,7 @@ func init() { serveCmd.Flags().StringVar(&sFlags.tlsCert, "tls-cert", "", "Custom TLS certificate path") serveCmd.Flags().StringVar(&sFlags.tlsKey, "tls-key", "", "Custom TLS key path") serveCmd.Flags().BoolVar(&sFlags.noTLS, "no-tls", false, "Disable TLS, serve plain HTTP") + serveCmd.Flags().BoolVar(&sFlags.webSearch, "web-search", webSearchDefault, "Enable Lumo's native web search tool [$LUMO_WEB_SEARCH]") } func runServe(cmd *cobra.Command, _ []string) error { @@ -76,7 +83,7 @@ func runServe(cmd *cobra.Command, _ []string) error { } mux := http.NewServeMux() - mux.HandleFunc("POST /v1/chat/completions", chatHandler(client)) + mux.HandleFunc("POST /v1/chat/completions", chatHandler(client, sFlags.webSearch)) mux.HandleFunc("GET /v1/models", modelsHandler()) handler := authMiddleware(apiKey, loggingMiddleware(mux)) diff --git a/internal/cli/lumo/websearch_test.go b/internal/cli/lumo/websearch_test.go new file mode 100644 index 0000000..c8fc509 --- /dev/null +++ b/internal/cli/lumo/websearch_test.go @@ -0,0 +1,17 @@ +package lumoCmd + +import ( + "testing" + + "github.com/major0/proton-utils/api/lumo" +) + +func TestWebSearchTools(t *testing.T) { + if got := webSearchTools(false); got != nil { + t.Fatalf("webSearchTools(false) = %v, want nil", got) + } + got := webSearchTools(true) + if len(got) != 1 || got[0] != lumo.ToolWebSearch { + t.Fatalf("webSearchTools(true) = %v, want [%q]", got, lumo.ToolWebSearch) + } +}