Files
Enterprise-Knowledge-Base/internal/utils/extraheaders.go
T
wizardchen fd3e2992b1 feat(models): support custom HTTP headers across all remote model calls
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.
2026-04-23 23:36:40 +08:00

87 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"net/http"
"strings"
)
// reservedHeaderKeys 列出不允许被用户自定义头覆盖的关键请求头。
// 这些头由各 provider 的签名、鉴权或 SSE 流程控制,覆盖后可能直接导致调用失败。
var reservedHeaderKeys = map[string]struct{}{
"authorization": {},
"api-key": {},
"x-api-key": {},
"content-type": {},
"content-length": {},
"accept-encoding": {},
"host": {},
"connection": {},
"transfer-encoding": {},
}
// IsReservedHeader 判断某个 header key 是否为保留 header,保留 header 不允许被自定义头覆盖。
func IsReservedHeader(key string) bool {
_, ok := reservedHeaderKeys[strings.ToLower(strings.TrimSpace(key))]
return ok
}
// ApplyCustomHeaders 将用户自定义的 header 写入 http.Request。
// 保留 header(Authorization、api-key、Content-Type 等)会被跳过以避免破坏鉴权/签名。
// 其它 header 会直接覆盖同名条目,允许用户替换默认值(例如 Accept)。
func ApplyCustomHeaders(req *http.Request, headers map[string]string) {
if req == nil || len(headers) == 0 {
return
}
for k, v := range headers {
name := strings.TrimSpace(k)
if name == "" {
continue
}
if IsReservedHeader(name) {
continue
}
req.Header.Set(name, v)
}
}
// CustomHeadersRoundTripper 是一个 http.RoundTripper 包装器,
// 会在每个 HTTP 请求发出前注入用户自定义的 header。
// 用于无法直接拿到底层 *http.Request 的场景(如 go-openai SDK)。
type CustomHeadersRoundTripper struct {
Headers map[string]string
Base http.RoundTripper
}
// RoundTrip 实现 http.RoundTripper 接口。
func (t *CustomHeadersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
base := t.Base
if base == nil {
base = http.DefaultTransport
}
if len(t.Headers) > 0 {
// 复制请求以避免修改调用方传入的对象。
cloned := req.Clone(req.Context())
ApplyCustomHeaders(cloned, t.Headers)
return base.RoundTrip(cloned)
}
return base.RoundTrip(req)
}
// WrapHTTPClientWithHeaders 返回一个新的 *http.Client,在原有 client 基础上注入自定义 header。
// 如果 headers 为空则直接返回原 client,避免不必要的开销。
func WrapHTTPClientWithHeaders(client *http.Client, headers map[string]string) *http.Client {
if len(headers) == 0 {
return client
}
if client == nil {
client = &http.Client{}
}
base := client.Transport
if base == nil {
base = http.DefaultTransport
}
wrapped := *client
wrapped.Transport = &CustomHeadersRoundTripper{Headers: headers, Base: base}
return &wrapped
}