mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-19 02:18:25 +08:00
Introduce per-model `custom_headers` config (similar to OpenAI Python SDK's `extra_headers`) so users can inject gateway auth tokens, trace IDs, etc. into every outbound API request for chat / embedding / rerank / VLLM / ASR models. Reserved headers like Authorization / Content-Type are always preserved to avoid breaking auth or signing flows. Along the way, unify production and "test connection" paths onto a single `ConfigFromModel(*types.Model, appID, appSecret)` constructor in each model package. Both `service.modelService.GetXxxModel` and the four `handler.initialization.Check*Model` / `TestEmbeddingModel` endpoints now go through the same field mapping, so new parameters only need to be added in one place. Backend: - Add `CustomHeaders map[string]string` to `types.ModelParameters` with JSON/YAML `omitempty` tags for forward compatibility. - New `internal/utils/extraheaders.go` providing `ApplyCustomHeaders` (for hand-rolled HTTP paths) and `WrapHTTPClientWithHeaders` / `CustomHeadersRoundTripper` (for SDK paths like go-openai). Reserved headers (Authorization, api-key, Content-Type, Accept, Host, Content-Length, User-Agent) are filtered out. - Inject headers in every remote model path: chat (SDK + raw HTTP), embedding (openai/aliyun/jina/volcengine/nvidia/azure_openai), rerank (remote_api/aliyun/jina/nvidia/zhipu — via shared `customHeaderSetter` interface), VLM and ASR (via http.Client wrapping). - Add `ConfigFromModel` to each of chat/embedding/rerank/vlm/asr, consolidating field mapping (ExtraConfig, CustomHeaders, InterfaceType defaulting, WeKnoraCloud credentials, etc.) and cover it with dedicated unit tests per package. - Refactor `service/model.go`: replace ~100 lines of hand-written Config literals with one-line `ConfigFromModel` calls; drop the now-unused `stringMapToAnyMap` helper. - Refactor `handler/initialization.go` test-connection endpoints: merge four ad-hoc request structs into one `ModelTestRequest`, extract `buildTestModel` and `resolveTenantWeKnoraCloudCreds` helpers, and route all four endpoints through `ConfigFromModel` + `NewXxx` so the test path is now behaviorally identical to production. Frontend: - Add `custom_headers` to the `ModelConfig` API type. - `ModelEditorDialog.vue`: new Key-Value header editor (add/remove rows), auto-converts map <-> array when loading/saving, and passes the current header set to the `Test Connection` button so the preview reflects exactly what production will send. - `ModelSettings.vue`: serialize the header array back into a map for the backend (drops empty rows). - i18n: add custom-header labels / descriptions / placeholders to zh-CN, en-US, ru-RU, ko-KR. Tests: - `internal/utils/extraheaders_test.go` covers reserved-header filtering and round-tripper wrapping (including nil-client fallback). - New `config_from_model_test.go` in chat / embedding / rerank / vlm / asr verifies all fields (CustomHeaders, ExtraConfig, InterfaceType defaulting, AppID/AppSecret) are propagated end-to-end.
87 lines
2.4 KiB
Go
87 lines
2.4 KiB
Go
package utils
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestApplyCustomHeaders_SkipReserved(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "https://example.com", nil)
|
|
req.Header.Set("Authorization", "Bearer original")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
ApplyCustomHeaders(req, map[string]string{
|
|
"Authorization": "Bearer injected",
|
|
"Content-Type": "text/plain",
|
|
"X-Trace-Id": "trace-123",
|
|
"X-Route": "edge",
|
|
"": "empty-key-should-be-skipped",
|
|
})
|
|
|
|
if got := req.Header.Get("Authorization"); got != "Bearer original" {
|
|
t.Fatalf("authorization overwritten: %q", got)
|
|
}
|
|
if got := req.Header.Get("Content-Type"); got != "application/json" {
|
|
t.Fatalf("content-type overwritten: %q", got)
|
|
}
|
|
if got := req.Header.Get("X-Trace-Id"); got != "trace-123" {
|
|
t.Fatalf("X-Trace-Id not injected: %q", got)
|
|
}
|
|
if got := req.Header.Get("X-Route"); got != "edge" {
|
|
t.Fatalf("X-Route not injected: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestApplyCustomHeaders_NilSafe(t *testing.T) {
|
|
ApplyCustomHeaders(nil, map[string]string{"x": "y"})
|
|
req, _ := http.NewRequest("GET", "https://example.com", nil)
|
|
ApplyCustomHeaders(req, nil)
|
|
if len(req.Header) != 0 {
|
|
t.Fatalf("unexpected headers added: %+v", req.Header)
|
|
}
|
|
}
|
|
|
|
func TestWrapHTTPClientWithHeaders(t *testing.T) {
|
|
gotTrace := ""
|
|
gotAuth := ""
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotTrace = r.Header.Get("X-Trace-Id")
|
|
gotAuth = r.Header.Get("Authorization")
|
|
io.Copy(io.Discard, r.Body)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := WrapHTTPClientWithHeaders(nil, map[string]string{
|
|
"X-Trace-Id": "rt-1",
|
|
"Authorization": "Bearer should-not-override",
|
|
})
|
|
|
|
req, _ := http.NewRequest("POST", srv.URL, strings.NewReader("{}"))
|
|
req.Header.Set("Authorization", "Bearer kept")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("request failed: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if gotTrace != "rt-1" {
|
|
t.Fatalf("expected custom header injected, got %q", gotTrace)
|
|
}
|
|
if gotAuth != "Bearer kept" {
|
|
t.Fatalf("reserved header must not be overridden, got %q", gotAuth)
|
|
}
|
|
}
|
|
|
|
func TestWrapHTTPClientWithHeaders_EmptyReturnsOriginal(t *testing.T) {
|
|
orig := &http.Client{}
|
|
wrapped := WrapHTTPClientWithHeaders(orig, nil)
|
|
if wrapped != orig {
|
|
t.Fatalf("expected original client returned when headers empty")
|
|
}
|
|
}
|