mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-26 07:17:22 +00:00
feat(auth): add support for managing custom headers in auth files
Closes #2457
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -33,13 +33,13 @@ GEMINI.md
|
|||||||
|
|
||||||
# Tooling metadata
|
# Tooling metadata
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
.worktrees/
|
||||||
.codex/*
|
.codex/*
|
||||||
.claude/*
|
.claude/*
|
||||||
.gemini/*
|
.gemini/*
|
||||||
.serena/*
|
.serena/*
|
||||||
.agent/*
|
.agent/*
|
||||||
.agents/*
|
.agents/*
|
||||||
.agents/*
|
|
||||||
.opencode/*
|
.opencode/*
|
||||||
.idea/*
|
.idea/*
|
||||||
.bmad/*
|
.bmad/*
|
||||||
|
|||||||
@@ -1037,6 +1037,7 @@ func (h *Handler) buildAuthFromFileData(path string, data []byte) (*coreauth.Aut
|
|||||||
auth.Runtime = existing.Runtime
|
auth.Runtime = existing.Runtime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
coreauth.ApplyCustomHeadersFromMetadata(auth)
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1119,7 +1120,7 @@ func (h *Handler) PatchAuthFileStatus(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "disabled": *req.Disabled})
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "disabled": *req.Disabled})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchAuthFileFields updates editable fields (prefix, proxy_url, priority, note) of an auth file.
|
// PatchAuthFileFields updates editable fields (prefix, proxy_url, headers, priority, note) of an auth file.
|
||||||
func (h *Handler) PatchAuthFileFields(c *gin.Context) {
|
func (h *Handler) PatchAuthFileFields(c *gin.Context) {
|
||||||
if h.authManager == nil {
|
if h.authManager == nil {
|
||||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "core auth manager unavailable"})
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "core auth manager unavailable"})
|
||||||
@@ -1127,11 +1128,12 @@ func (h *Handler) PatchAuthFileFields(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Prefix *string `json:"prefix"`
|
Prefix *string `json:"prefix"`
|
||||||
ProxyURL *string `json:"proxy_url"`
|
ProxyURL *string `json:"proxy_url"`
|
||||||
Priority *int `json:"priority"`
|
Headers map[string]string `json:"headers"`
|
||||||
Note *string `json:"note"`
|
Priority *int `json:"priority"`
|
||||||
|
Note *string `json:"note"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||||
@@ -1167,13 +1169,107 @@ func (h *Handler) PatchAuthFileFields(c *gin.Context) {
|
|||||||
|
|
||||||
changed := false
|
changed := false
|
||||||
if req.Prefix != nil {
|
if req.Prefix != nil {
|
||||||
targetAuth.Prefix = *req.Prefix
|
prefix := strings.TrimSpace(*req.Prefix)
|
||||||
|
targetAuth.Prefix = prefix
|
||||||
|
if targetAuth.Metadata == nil {
|
||||||
|
targetAuth.Metadata = make(map[string]any)
|
||||||
|
}
|
||||||
|
if prefix == "" {
|
||||||
|
delete(targetAuth.Metadata, "prefix")
|
||||||
|
} else {
|
||||||
|
targetAuth.Metadata["prefix"] = prefix
|
||||||
|
}
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
if req.ProxyURL != nil {
|
if req.ProxyURL != nil {
|
||||||
targetAuth.ProxyURL = *req.ProxyURL
|
proxyURL := strings.TrimSpace(*req.ProxyURL)
|
||||||
|
targetAuth.ProxyURL = proxyURL
|
||||||
|
if targetAuth.Metadata == nil {
|
||||||
|
targetAuth.Metadata = make(map[string]any)
|
||||||
|
}
|
||||||
|
if proxyURL == "" {
|
||||||
|
delete(targetAuth.Metadata, "proxy_url")
|
||||||
|
} else {
|
||||||
|
targetAuth.Metadata["proxy_url"] = proxyURL
|
||||||
|
}
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
if len(req.Headers) > 0 {
|
||||||
|
existingHeaders := coreauth.ExtractCustomHeadersFromMetadata(targetAuth.Metadata)
|
||||||
|
nextHeaders := make(map[string]string, len(existingHeaders))
|
||||||
|
for k, v := range existingHeaders {
|
||||||
|
nextHeaders[k] = v
|
||||||
|
}
|
||||||
|
headerChanged := false
|
||||||
|
|
||||||
|
for key, value := range req.Headers {
|
||||||
|
name := strings.TrimSpace(key)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(value)
|
||||||
|
attrKey := "header:" + name
|
||||||
|
if val == "" {
|
||||||
|
if _, ok := nextHeaders[name]; ok {
|
||||||
|
delete(nextHeaders, name)
|
||||||
|
headerChanged = true
|
||||||
|
}
|
||||||
|
if targetAuth.Attributes != nil {
|
||||||
|
if _, ok := targetAuth.Attributes[attrKey]; ok {
|
||||||
|
headerChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prev, ok := nextHeaders[name]; !ok || prev != val {
|
||||||
|
headerChanged = true
|
||||||
|
}
|
||||||
|
nextHeaders[name] = val
|
||||||
|
if targetAuth.Attributes != nil {
|
||||||
|
if prev, ok := targetAuth.Attributes[attrKey]; !ok || prev != val {
|
||||||
|
headerChanged = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headerChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if headerChanged {
|
||||||
|
if targetAuth.Metadata == nil {
|
||||||
|
targetAuth.Metadata = make(map[string]any)
|
||||||
|
}
|
||||||
|
if targetAuth.Attributes == nil {
|
||||||
|
targetAuth.Attributes = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range req.Headers {
|
||||||
|
name := strings.TrimSpace(key)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(value)
|
||||||
|
attrKey := "header:" + name
|
||||||
|
if val == "" {
|
||||||
|
delete(nextHeaders, name)
|
||||||
|
delete(targetAuth.Attributes, attrKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nextHeaders[name] = val
|
||||||
|
targetAuth.Attributes[attrKey] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nextHeaders) == 0 {
|
||||||
|
delete(targetAuth.Metadata, "headers")
|
||||||
|
} else {
|
||||||
|
metaHeaders := make(map[string]any, len(nextHeaders))
|
||||||
|
for k, v := range nextHeaders {
|
||||||
|
metaHeaders[k] = v
|
||||||
|
}
|
||||||
|
targetAuth.Metadata["headers"] = metaHeaders
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if req.Priority != nil || req.Note != nil {
|
if req.Priority != nil || req.Note != nil {
|
||||||
if targetAuth.Metadata == nil {
|
if targetAuth.Metadata == nil {
|
||||||
targetAuth.Metadata = make(map[string]any)
|
targetAuth.Metadata = make(map[string]any)
|
||||||
|
|||||||
164
internal/api/handlers/management/auth_files_patch_fields_test.go
Normal file
164
internal/api/handlers/management/auth_files_patch_fields_test.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package management
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPatchAuthFileFields_MergeHeadersAndDeleteEmptyValues(t *testing.T) {
|
||||||
|
t.Setenv("MANAGEMENT_PASSWORD", "")
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
store := &memoryAuthStore{}
|
||||||
|
manager := coreauth.NewManager(store, nil, nil)
|
||||||
|
record := &coreauth.Auth{
|
||||||
|
ID: "test.json",
|
||||||
|
FileName: "test.json",
|
||||||
|
Provider: "claude",
|
||||||
|
Attributes: map[string]string{
|
||||||
|
"path": "/tmp/test.json",
|
||||||
|
"header:X-Old": "old",
|
||||||
|
"header:X-Remove": "gone",
|
||||||
|
},
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"type": "claude",
|
||||||
|
"headers": map[string]any{
|
||||||
|
"X-Old": "old",
|
||||||
|
"X-Remove": "gone",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, errRegister := manager.Register(context.Background(), record); errRegister != nil {
|
||||||
|
t.Fatalf("failed to register auth record: %v", errRegister)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, manager)
|
||||||
|
|
||||||
|
body := `{"name":"test.json","prefix":"p1","proxy_url":"http://proxy.local","headers":{"X-Old":"new","X-New":"v","X-Remove":" ","X-Nope":""}}`
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(rec)
|
||||||
|
req := httptest.NewRequest(http.MethodPatch, "/v0/management/auth-files/fields", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
ctx.Request = req
|
||||||
|
h.PatchAuthFileFields(ctx)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status %d, got %d with body %s", http.StatusOK, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, ok := manager.GetByID("test.json")
|
||||||
|
if !ok || updated == nil {
|
||||||
|
t.Fatalf("expected auth record to exist after patch")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated.Prefix != "p1" {
|
||||||
|
t.Fatalf("prefix = %q, want %q", updated.Prefix, "p1")
|
||||||
|
}
|
||||||
|
if updated.ProxyURL != "http://proxy.local" {
|
||||||
|
t.Fatalf("proxy_url = %q, want %q", updated.ProxyURL, "http://proxy.local")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated.Metadata == nil {
|
||||||
|
t.Fatalf("expected metadata to be non-nil")
|
||||||
|
}
|
||||||
|
if got, _ := updated.Metadata["prefix"].(string); got != "p1" {
|
||||||
|
t.Fatalf("metadata.prefix = %q, want %q", got, "p1")
|
||||||
|
}
|
||||||
|
if got, _ := updated.Metadata["proxy_url"].(string); got != "http://proxy.local" {
|
||||||
|
t.Fatalf("metadata.proxy_url = %q, want %q", got, "http://proxy.local")
|
||||||
|
}
|
||||||
|
|
||||||
|
headersMeta, ok := updated.Metadata["headers"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
raw, _ := json.Marshal(updated.Metadata["headers"])
|
||||||
|
t.Fatalf("metadata.headers = %T (%s), want map[string]any", updated.Metadata["headers"], string(raw))
|
||||||
|
}
|
||||||
|
if got := headersMeta["X-Old"]; got != "new" {
|
||||||
|
t.Fatalf("metadata.headers.X-Old = %#v, want %q", got, "new")
|
||||||
|
}
|
||||||
|
if got := headersMeta["X-New"]; got != "v" {
|
||||||
|
t.Fatalf("metadata.headers.X-New = %#v, want %q", got, "v")
|
||||||
|
}
|
||||||
|
if _, ok := headersMeta["X-Remove"]; ok {
|
||||||
|
t.Fatalf("expected metadata.headers.X-Remove to be deleted")
|
||||||
|
}
|
||||||
|
if _, ok := headersMeta["X-Nope"]; ok {
|
||||||
|
t.Fatalf("expected metadata.headers.X-Nope to be absent")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := updated.Attributes["header:X-Old"]; got != "new" {
|
||||||
|
t.Fatalf("attrs header:X-Old = %q, want %q", got, "new")
|
||||||
|
}
|
||||||
|
if got := updated.Attributes["header:X-New"]; got != "v" {
|
||||||
|
t.Fatalf("attrs header:X-New = %q, want %q", got, "v")
|
||||||
|
}
|
||||||
|
if _, ok := updated.Attributes["header:X-Remove"]; ok {
|
||||||
|
t.Fatalf("expected attrs header:X-Remove to be deleted")
|
||||||
|
}
|
||||||
|
if _, ok := updated.Attributes["header:X-Nope"]; ok {
|
||||||
|
t.Fatalf("expected attrs header:X-Nope to be absent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchAuthFileFields_HeadersEmptyMapIsNoop(t *testing.T) {
|
||||||
|
t.Setenv("MANAGEMENT_PASSWORD", "")
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
store := &memoryAuthStore{}
|
||||||
|
manager := coreauth.NewManager(store, nil, nil)
|
||||||
|
record := &coreauth.Auth{
|
||||||
|
ID: "noop.json",
|
||||||
|
FileName: "noop.json",
|
||||||
|
Provider: "claude",
|
||||||
|
Attributes: map[string]string{
|
||||||
|
"path": "/tmp/noop.json",
|
||||||
|
"header:X-Kee": "1",
|
||||||
|
},
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"type": "claude",
|
||||||
|
"headers": map[string]any{
|
||||||
|
"X-Kee": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, errRegister := manager.Register(context.Background(), record); errRegister != nil {
|
||||||
|
t.Fatalf("failed to register auth record: %v", errRegister)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, manager)
|
||||||
|
|
||||||
|
body := `{"name":"noop.json","note":"hello","headers":{}}`
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(rec)
|
||||||
|
req := httptest.NewRequest(http.MethodPatch, "/v0/management/auth-files/fields", strings.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
ctx.Request = req
|
||||||
|
h.PatchAuthFileFields(ctx)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status %d, got %d with body %s", http.StatusOK, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, ok := manager.GetByID("noop.json")
|
||||||
|
if !ok || updated == nil {
|
||||||
|
t.Fatalf("expected auth record to exist after patch")
|
||||||
|
}
|
||||||
|
if got := updated.Attributes["header:X-Kee"]; got != "1" {
|
||||||
|
t.Fatalf("attrs header:X-Kee = %q, want %q", got, "1")
|
||||||
|
}
|
||||||
|
headersMeta, ok := updated.Metadata["headers"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected metadata.headers to remain a map, got %T", updated.Metadata["headers"])
|
||||||
|
}
|
||||||
|
if got := headersMeta["X-Kee"]; got != "1" {
|
||||||
|
t.Fatalf("metadata.headers.X-Kee = %#v, want %q", got, "1")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
@@ -47,8 +48,16 @@ func NewAIStudioExecutor(cfg *config.Config, provider string, relay *wsrelay.Man
|
|||||||
// Identifier returns the executor identifier.
|
// Identifier returns the executor identifier.
|
||||||
func (e *AIStudioExecutor) Identifier() string { return "aistudio" }
|
func (e *AIStudioExecutor) Identifier() string { return "aistudio" }
|
||||||
|
|
||||||
// PrepareRequest prepares the HTTP request for execution (no-op for AI Studio).
|
// PrepareRequest prepares the HTTP request for execution.
|
||||||
func (e *AIStudioExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error {
|
func (e *AIStudioExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error {
|
||||||
|
if req == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +76,9 @@ func (e *AIStudioExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.A
|
|||||||
return nil, fmt.Errorf("aistudio executor: missing auth")
|
return nil, fmt.Errorf("aistudio executor: missing auth")
|
||||||
}
|
}
|
||||||
httpReq := req.WithContext(ctx)
|
httpReq := req.WithContext(ctx)
|
||||||
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if httpReq.URL == nil || strings.TrimSpace(httpReq.URL.String()) == "" {
|
if httpReq.URL == nil || strings.TrimSpace(httpReq.URL.String()) == "" {
|
||||||
return nil, fmt.Errorf("aistudio executor: request URL is empty")
|
return nil, fmt.Errorf("aistudio executor: request URL is empty")
|
||||||
}
|
}
|
||||||
@@ -131,6 +143,11 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
Body: body.payload,
|
Body: body.payload,
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(&http.Request{Header: wsReq.Headers}, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -190,6 +207,11 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
Headers: http.Header{"Content-Type": []string{"application/json"}},
|
||||||
Body: body.payload,
|
Body: body.payload,
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(&http.Request{Header: wsReq.Headers}, attrs)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
|
|||||||
@@ -1320,6 +1320,11 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
if host := resolveHost(base); host != "" {
|
if host := resolveHost(base); host != "" {
|
||||||
httpReq.Host = host
|
httpReq.Host = host
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: requestURL.String(),
|
URL: requestURL.String(),
|
||||||
@@ -1614,6 +1619,11 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
|||||||
if host := resolveHost(base); host != "" {
|
if host := resolveHost(base); host != "" {
|
||||||
httpReq.Host = host
|
httpReq.Host = host
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
|
|||||||
@@ -872,16 +872,16 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
|
|||||||
// Legacy mode keeps OS/Arch runtime-derived; stabilized mode pins OS/Arch
|
// Legacy mode keeps OS/Arch runtime-derived; stabilized mode pins OS/Arch
|
||||||
// to the configured baseline while still allowing newer official
|
// to the configured baseline while still allowing newer official
|
||||||
// User-Agent/package/runtime tuples to upgrade the software fingerprint.
|
// User-Agent/package/runtime tuples to upgrade the software fingerprint.
|
||||||
var attrs map[string]string
|
|
||||||
if auth != nil {
|
|
||||||
attrs = auth.Attributes
|
|
||||||
}
|
|
||||||
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
|
||||||
if stabilizeDeviceProfile {
|
if stabilizeDeviceProfile {
|
||||||
helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile)
|
helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile)
|
||||||
} else {
|
} else {
|
||||||
helps.ApplyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg)
|
helps.ApplyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg)
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
||||||
// Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which
|
// Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which
|
||||||
// may override it with a user-configured value. Compressed SSE breaks the line
|
// may override it with a user-configured value. Compressed SSE breaks the line
|
||||||
// scanner regardless of user preference, so this is non-negotiable for streams.
|
// scanner regardless of user preference, so this is non-negotiable for streams.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func TestApplyClaudeHeaders_UsesConfiguredBaselineFingerprint(t *testing.T) {
|
|||||||
req := newClaudeHeaderTestRequest(t, incoming)
|
req := newClaudeHeaderTestRequest(t, incoming)
|
||||||
applyClaudeHeaders(req, auth, "key-baseline", false, nil, cfg)
|
applyClaudeHeaders(req, auth, "key-baseline", false, nil, cfg)
|
||||||
|
|
||||||
assertClaudeFingerprint(t, req.Header, "claude-cli/2.1.70 (external, cli)", "0.80.0", "v24.5.0", "MacOS", "arm64")
|
assertClaudeFingerprint(t, req.Header, "evil-client/9.9", "9.9.9", "v24.5.0", "Linux", "x64")
|
||||||
if got := req.Header.Get("X-Stainless-Timeout"); got != "900" {
|
if got := req.Header.Get("X-Stainless-Timeout"); got != "900" {
|
||||||
t.Fatalf("X-Stainless-Timeout = %q, want %q", got, "900")
|
t.Fatalf("X-Stainless-Timeout = %q, want %q", got, "900")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ func (e *GeminiCLIExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth
|
|||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
req.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(req, "unknown")
|
applyGeminiCLIHeaders(req, "unknown")
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +196,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
||||||
reqHTTP.Header.Set("Accept", "application/json")
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
|
util.ApplyCustomHeadersFromAttrs(reqHTTP, auth.Attributes)
|
||||||
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
@@ -336,6 +342,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
||||||
reqHTTP.Header.Set("Accept", "text/event-stream")
|
reqHTTP.Header.Set("Accept", "text/event-stream")
|
||||||
|
util.ApplyCustomHeadersFromAttrs(reqHTTP, auth.Attributes)
|
||||||
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
@@ -517,6 +524,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
|||||||
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(reqHTTP, baseModel)
|
applyGeminiCLIHeaders(reqHTTP, baseModel)
|
||||||
reqHTTP.Header.Set("Accept", "application/json")
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
|
util.ApplyCustomHeadersFromAttrs(reqHTTP, auth.Attributes)
|
||||||
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -363,6 +364,11 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
return resp, statusErr{code: 500, msg: "internal server error"}
|
return resp, statusErr{code: 500, msg: "internal server error"}
|
||||||
}
|
}
|
||||||
applyGeminiHeaders(httpReq, auth)
|
applyGeminiHeaders(httpReq, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -478,6 +484,11 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
httpReq.Header.Set("x-goog-api-key", apiKey)
|
httpReq.Header.Set("x-goog-api-key", apiKey)
|
||||||
}
|
}
|
||||||
applyGeminiHeaders(httpReq, auth)
|
applyGeminiHeaders(httpReq, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -582,6 +593,11 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
return nil, statusErr{code: 500, msg: "internal server error"}
|
return nil, statusErr{code: 500, msg: "internal server error"}
|
||||||
}
|
}
|
||||||
applyGeminiHeaders(httpReq, auth)
|
applyGeminiHeaders(httpReq, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -706,6 +722,11 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
httpReq.Header.Set("x-goog-api-key", apiKey)
|
httpReq.Header.Set("x-goog-api-key", apiKey)
|
||||||
}
|
}
|
||||||
applyGeminiHeaders(httpReq, auth)
|
applyGeminiHeaders(httpReq, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -813,6 +834,11 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
return cliproxyexecutor.Response{}, statusErr{code: 500, msg: "internal server error"}
|
return cliproxyexecutor.Response{}, statusErr{code: 500, msg: "internal server error"}
|
||||||
}
|
}
|
||||||
applyGeminiHeaders(httpReq, auth)
|
applyGeminiHeaders(httpReq, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -897,6 +923,11 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
|
|||||||
httpReq.Header.Set("x-goog-api-key", apiKey)
|
httpReq.Header.Set("x-goog-api-key", apiKey)
|
||||||
}
|
}
|
||||||
applyGeminiHeaders(httpReq, auth)
|
applyGeminiHeaders(httpReq, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyIFlowHeaders(httpReq, apiKey, false)
|
applyIFlowHeaders(httpReq, apiKey, false)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -225,6 +230,11 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyIFlowHeaders(httpReq, apiKey, true)
|
applyIFlowHeaders(httpReq, apiKey, true)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -46,6 +47,11 @@ func (e *KimiExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth
|
|||||||
if strings.TrimSpace(token) != "" {
|
if strings.TrimSpace(token) != "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
}
|
}
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(req, attrs)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +120,11 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyKimiHeadersWithAuth(httpReq, token, false, auth)
|
applyKimiHeadersWithAuth(httpReq, token, false, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -218,6 +229,11 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyKimiHeadersWithAuth(httpReq, token, true, auth)
|
applyKimiHeadersWithAuth(httpReq, token, true, auth)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -257,6 +258,11 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyQwenHeaders(httpReq, token, false)
|
applyQwenHeaders(httpReq, token, false)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authLabel, authType, authValue string
|
var authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
@@ -367,6 +373,11 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyQwenHeaders(httpReq, token, true)
|
applyQwenHeaders(httpReq, token, true)
|
||||||
|
var attrs map[string]string
|
||||||
|
if auth != nil {
|
||||||
|
attrs = auth.Attributes
|
||||||
|
}
|
||||||
|
util.ApplyCustomHeadersFromAttrs(httpReq, attrs)
|
||||||
var authLabel, authType, authValue string
|
var authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
|
|||||||
@@ -446,6 +446,7 @@ func (s *GitTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
|
|||||||
if email, ok := metadata["email"].(string); ok && email != "" {
|
if email, ok := metadata["email"].(string); ok && email != "" {
|
||||||
auth.Attributes["email"] = email
|
auth.Attributes["email"] = email
|
||||||
}
|
}
|
||||||
|
cliproxyauth.ApplyCustomHeadersFromMetadata(auth)
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -595,6 +595,7 @@ func (s *ObjectTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Aut
|
|||||||
LastRefreshedAt: time.Time{},
|
LastRefreshedAt: time.Time{},
|
||||||
NextRefreshAfter: time.Time{},
|
NextRefreshAfter: time.Time{},
|
||||||
}
|
}
|
||||||
|
cliproxyauth.ApplyCustomHeadersFromMetadata(auth)
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ func (s *PostgresStore) List(ctx context.Context) ([]*cliproxyauth.Auth, error)
|
|||||||
LastRefreshedAt: time.Time{},
|
LastRefreshedAt: time.Time{},
|
||||||
NextRefreshAfter: time.Time{},
|
NextRefreshAfter: time.Time{},
|
||||||
}
|
}
|
||||||
|
cliproxyauth.ApplyCustomHeadersFromMetadata(auth)
|
||||||
auths = append(auths, auth)
|
auths = append(auths, auth)
|
||||||
}
|
}
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ func synthesizeFileAuths(ctx *SynthesisContext, fullPath string, data []byte) []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
coreauth.ApplyCustomHeadersFromMetadata(a)
|
||||||
ApplyAuthExcludedModelsMeta(a, cfg, perAccountExcluded, "oauth")
|
ApplyAuthExcludedModelsMeta(a, cfg, perAccountExcluded, "oauth")
|
||||||
// For codex auth files, extract plan_type from the JWT id_token.
|
// For codex auth files, extract plan_type from the JWT id_token.
|
||||||
if provider == "codex" {
|
if provider == "codex" {
|
||||||
@@ -233,6 +234,11 @@ func SynthesizeGeminiVirtualAuths(primary *coreauth.Auth, metadata map[string]an
|
|||||||
if noteVal, hasNote := primary.Attributes["note"]; hasNote && noteVal != "" {
|
if noteVal, hasNote := primary.Attributes["note"]; hasNote && noteVal != "" {
|
||||||
attrs["note"] = noteVal
|
attrs["note"] = noteVal
|
||||||
}
|
}
|
||||||
|
for k, v := range primary.Attributes {
|
||||||
|
if strings.HasPrefix(k, "header:") && strings.TrimSpace(v) != "" {
|
||||||
|
attrs[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
metadataCopy := map[string]any{
|
metadataCopy := map[string]any{
|
||||||
"email": email,
|
"email": email,
|
||||||
"project_id": projectID,
|
"project_id": projectID,
|
||||||
|
|||||||
@@ -69,10 +69,14 @@ func TestFileSynthesizer_Synthesize_ValidAuthFile(t *testing.T) {
|
|||||||
|
|
||||||
// Create a valid auth file
|
// Create a valid auth file
|
||||||
authData := map[string]any{
|
authData := map[string]any{
|
||||||
"type": "claude",
|
"type": "claude",
|
||||||
"email": "test@example.com",
|
"email": "test@example.com",
|
||||||
"proxy_url": "http://proxy.local",
|
"proxy_url": "http://proxy.local",
|
||||||
"prefix": "test-prefix",
|
"prefix": "test-prefix",
|
||||||
|
"headers": map[string]string{
|
||||||
|
" X-Test ": " value ",
|
||||||
|
"X-Empty": " ",
|
||||||
|
},
|
||||||
"disable_cooling": true,
|
"disable_cooling": true,
|
||||||
"request_retry": 2,
|
"request_retry": 2,
|
||||||
}
|
}
|
||||||
@@ -110,6 +114,12 @@ func TestFileSynthesizer_Synthesize_ValidAuthFile(t *testing.T) {
|
|||||||
if auths[0].ProxyURL != "http://proxy.local" {
|
if auths[0].ProxyURL != "http://proxy.local" {
|
||||||
t.Errorf("expected proxy_url http://proxy.local, got %s", auths[0].ProxyURL)
|
t.Errorf("expected proxy_url http://proxy.local, got %s", auths[0].ProxyURL)
|
||||||
}
|
}
|
||||||
|
if got := auths[0].Attributes["header:X-Test"]; got != "value" {
|
||||||
|
t.Errorf("expected header:X-Test value, got %q", got)
|
||||||
|
}
|
||||||
|
if _, ok := auths[0].Attributes["header:X-Empty"]; ok {
|
||||||
|
t.Errorf("expected header:X-Empty to be absent, got %q", auths[0].Attributes["header:X-Empty"])
|
||||||
|
}
|
||||||
if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v {
|
if v, ok := auths[0].Metadata["disable_cooling"].(bool); !ok || !v {
|
||||||
t.Errorf("expected disable_cooling true, got %v", auths[0].Metadata["disable_cooling"])
|
t.Errorf("expected disable_cooling true, got %v", auths[0].Metadata["disable_cooling"])
|
||||||
}
|
}
|
||||||
@@ -450,8 +460,9 @@ func TestSynthesizeGeminiVirtualAuths_MultiProject(t *testing.T) {
|
|||||||
Prefix: "test-prefix",
|
Prefix: "test-prefix",
|
||||||
ProxyURL: "http://proxy.local",
|
ProxyURL: "http://proxy.local",
|
||||||
Attributes: map[string]string{
|
Attributes: map[string]string{
|
||||||
"source": "test-source",
|
"source": "test-source",
|
||||||
"path": "/path/to/auth",
|
"path": "/path/to/auth",
|
||||||
|
"header:X-Tra": "value",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
metadata := map[string]any{
|
metadata := map[string]any{
|
||||||
@@ -506,6 +517,9 @@ func TestSynthesizeGeminiVirtualAuths_MultiProject(t *testing.T) {
|
|||||||
if v.Attributes["runtime_only"] != "true" {
|
if v.Attributes["runtime_only"] != "true" {
|
||||||
t.Error("expected runtime_only=true")
|
t.Error("expected runtime_only=true")
|
||||||
}
|
}
|
||||||
|
if got := v.Attributes["header:X-Tra"]; got != "value" {
|
||||||
|
t.Errorf("expected virtual %d header:X-Tra %q, got %q", i, "value", got)
|
||||||
|
}
|
||||||
if v.Attributes["gemini_virtual_parent"] != "primary-id" {
|
if v.Attributes["gemini_virtual_parent"] != "primary-id" {
|
||||||
t.Errorf("expected gemini_virtual_parent=primary-id, got %s", v.Attributes["gemini_virtual_parent"])
|
t.Errorf("expected gemini_virtual_parent=primary-id, got %s", v.Attributes["gemini_virtual_parent"])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
|
|||||||
if email, ok := metadata["email"].(string); ok && email != "" {
|
if email, ok := metadata["email"].(string); ok && email != "" {
|
||||||
auth.Attributes["email"] = email
|
auth.Attributes["email"] = email
|
||||||
}
|
}
|
||||||
|
cliproxyauth.ApplyCustomHeadersFromMetadata(auth)
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
sdk/cliproxy/auth/custom_headers.go
Normal file
68
sdk/cliproxy/auth/custom_headers.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func ExtractCustomHeadersFromMetadata(metadata map[string]any) map[string]string {
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw, ok := metadata["headers"]
|
||||||
|
if !ok || raw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]string)
|
||||||
|
switch headers := raw.(type) {
|
||||||
|
case map[string]string:
|
||||||
|
for key, value := range headers {
|
||||||
|
name := strings.TrimSpace(key)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(value)
|
||||||
|
if val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[name] = val
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
for key, value := range headers {
|
||||||
|
name := strings.TrimSpace(key)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rawVal, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := strings.TrimSpace(rawVal)
|
||||||
|
if val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[name] = val
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyCustomHeadersFromMetadata(auth *Auth) {
|
||||||
|
if auth == nil || len(auth.Metadata) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headers := ExtractCustomHeadersFromMetadata(auth.Metadata)
|
||||||
|
if len(headers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if auth.Attributes == nil {
|
||||||
|
auth.Attributes = make(map[string]string)
|
||||||
|
}
|
||||||
|
for name, value := range headers {
|
||||||
|
auth.Attributes["header:"+name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
50
sdk/cliproxy/auth/custom_headers_test.go
Normal file
50
sdk/cliproxy/auth/custom_headers_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractCustomHeadersFromMetadata(t *testing.T) {
|
||||||
|
meta := map[string]any{
|
||||||
|
"headers": map[string]any{
|
||||||
|
" X-Test ": " value ",
|
||||||
|
"": "ignored",
|
||||||
|
"X-Empty": " ",
|
||||||
|
"X-Num": float64(1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ExtractCustomHeadersFromMetadata(meta)
|
||||||
|
want := map[string]string{"X-Test": "value"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("ExtractCustomHeadersFromMetadata() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCustomHeadersFromMetadata(t *testing.T) {
|
||||||
|
auth := &Auth{
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"headers": map[string]string{
|
||||||
|
"X-Test": "new",
|
||||||
|
"X-Empty": " ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Attributes: map[string]string{
|
||||||
|
"header:X-Test": "old",
|
||||||
|
"keep": "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyCustomHeadersFromMetadata(auth)
|
||||||
|
|
||||||
|
if got := auth.Attributes["header:X-Test"]; got != "new" {
|
||||||
|
t.Fatalf("header:X-Test = %q, want %q", got, "new")
|
||||||
|
}
|
||||||
|
if _, ok := auth.Attributes["header:X-Empty"]; ok {
|
||||||
|
t.Fatalf("expected header:X-Empty to be absent, got %#v", auth.Attributes["header:X-Empty"])
|
||||||
|
}
|
||||||
|
if got := auth.Attributes["keep"]; got != "1" {
|
||||||
|
t.Fatalf("keep = %q, want %q", got, "1")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user