Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 674393ec12 | |||
| 9f45806106 | |||
| 307ae76ed4 | |||
| 735b21394c | |||
| 9cdef937af |
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
)
|
||||
|
||||
// RequestLogger defines the interface for logging HTTP requests and responses.
|
||||
@@ -485,7 +486,8 @@ func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[st
|
||||
content.WriteString("=== HEADERS ===\n")
|
||||
for key, values := range headers {
|
||||
for _, value := range values {
|
||||
content.WriteString(fmt.Sprintf("%s: %s\n", key, value))
|
||||
masked := util.MaskSensitiveHeaderValue(key, value)
|
||||
content.WriteString(fmt.Sprintf("%s: %s\n", key, masked))
|
||||
}
|
||||
}
|
||||
content.WriteString("\n")
|
||||
|
||||
@@ -275,7 +275,8 @@ func writeHeaders(builder *strings.Builder, headers http.Header) {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
builder.WriteString(fmt.Sprintf("%s: %s\n", key, sanitizeHeaderValue(key, value)))
|
||||
masked := util.MaskSensitiveHeaderValue(key, value)
|
||||
builder.WriteString(fmt.Sprintf("%s: %s\n", key, masked))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,18 +320,3 @@ func formatAuthInfo(info upstreamRequestLog) string {
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func sanitizeHeaderValue(key, value string) string {
|
||||
trimmedValue := strings.TrimSpace(value)
|
||||
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
||||
switch {
|
||||
case strings.Contains(lowerKey, "authorization"),
|
||||
strings.Contains(lowerKey, "api-key"),
|
||||
strings.Contains(lowerKey, "apikey"),
|
||||
strings.Contains(lowerKey, "token"),
|
||||
strings.Contains(lowerKey, "secret"):
|
||||
return util.HideAPIKey(trimmedValue)
|
||||
default:
|
||||
return trimmedValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -180,29 +179,16 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
||||
// Returns:
|
||||
// - string: A Claude Code-compatible JSON response containing all message content and metadata
|
||||
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) string {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
||||
buffer := make([]byte, 20_971_520)
|
||||
scanner.Buffer(buffer, 20_971_520)
|
||||
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if !bytes.HasPrefix(line, dataTag) {
|
||||
continue
|
||||
}
|
||||
payload := bytes.TrimSpace(line[len(dataTag):])
|
||||
if len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
rootResult := gjson.ParseBytes(payload)
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
if rootResult.Get("type").String() != "response.completed" {
|
||||
continue
|
||||
return ""
|
||||
}
|
||||
|
||||
responseData := rootResult.Get("response")
|
||||
if !responseData.Exists() {
|
||||
continue
|
||||
return ""
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
@@ -345,9 +331,6 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
||||
return string(responseJSON)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
||||
func buildReverseMapFromClaudeOriginalShortToOriginal(original []byte) map[string]string {
|
||||
tools := gjson.GetBytes(original, "tools")
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -152,22 +151,11 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
||||
// Returns:
|
||||
// - string: A Gemini-compatible JSON response containing all message content and metadata
|
||||
func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
||||
buffer := make([]byte, 20_971_520)
|
||||
scanner.Buffer(buffer, 20_971_520)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// log.Debug(string(line))
|
||||
if !bytes.HasPrefix(line, dataTag) {
|
||||
continue
|
||||
}
|
||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||
|
||||
rootResult := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Verify this is a response.completed event
|
||||
if rootResult.Get("type").String() != "response.completed" {
|
||||
continue
|
||||
return ""
|
||||
}
|
||||
|
||||
// Base Gemini response template for non-streaming
|
||||
@@ -304,8 +292,6 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
||||
}
|
||||
return template
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// buildReverseMapFromGeminiOriginal builds a map[short]original from original Gemini request tools.
|
||||
func buildReverseMapFromGeminiOriginal(original []byte) map[string]string {
|
||||
|
||||
@@ -466,7 +466,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
},
|
||||
}
|
||||
|
||||
var contentBlocks []interface{}
|
||||
contentBlocks := make([]interface{}, 0)
|
||||
hasToolCall := false
|
||||
|
||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
||||
@@ -477,7 +477,8 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
}
|
||||
|
||||
if message := choice.Get("message"); message.Exists() {
|
||||
if contentArray := message.Get("content"); contentArray.Exists() && contentArray.IsArray() {
|
||||
if contentResult := message.Get("content"); contentResult.Exists() {
|
||||
if contentResult.IsArray() {
|
||||
var textBuilder strings.Builder
|
||||
var thinkingBuilder strings.Builder
|
||||
|
||||
@@ -503,7 +504,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
thinkingBuilder.Reset()
|
||||
}
|
||||
|
||||
for _, item := range contentArray.Array() {
|
||||
for _, item := range contentResult.Array() {
|
||||
typeStr := item.Get("type").String()
|
||||
switch typeStr {
|
||||
case "text":
|
||||
@@ -551,6 +552,15 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
||||
|
||||
flushThinking()
|
||||
flushText()
|
||||
} else if contentResult.Type == gjson.String {
|
||||
textContent := contentResult.String()
|
||||
if textContent != "" {
|
||||
contentBlocks = append(contentBlocks, map[string]interface{}{
|
||||
"type": "text",
|
||||
"text": textContent,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||
)
|
||||
@@ -141,3 +143,48 @@ func HideAPIKey(apiKey string) string {
|
||||
}
|
||||
return apiKey
|
||||
}
|
||||
|
||||
// maskAuthorizationHeader masks the Authorization header value while preserving the auth type prefix.
|
||||
// Common formats: "Bearer <token>", "Basic <credentials>", "ApiKey <key>", etc.
|
||||
// It preserves the prefix (e.g., "Bearer ") and only masks the token/credential part.
|
||||
//
|
||||
// Parameters:
|
||||
// - value: The Authorization header value
|
||||
//
|
||||
// Returns:
|
||||
// - string: The masked Authorization value with prefix preserved
|
||||
func MaskAuthorizationHeader(value string) string {
|
||||
parts := strings.SplitN(strings.TrimSpace(value), " ", 2)
|
||||
if len(parts) < 2 {
|
||||
return HideAPIKey(value)
|
||||
}
|
||||
return parts[0] + " " + HideAPIKey(parts[1])
|
||||
}
|
||||
|
||||
// MaskSensitiveHeaderValue masks sensitive header values while preserving expected formats.
|
||||
//
|
||||
// Behavior by header key (case-insensitive):
|
||||
// - "Authorization": Preserve the auth type prefix (e.g., "Bearer ") and mask only the credential part.
|
||||
// - Headers containing "api-key": Mask the entire value using HideAPIKey.
|
||||
// - Others: Return the original value unchanged.
|
||||
//
|
||||
// Parameters:
|
||||
// - key: The HTTP header name to inspect (case-insensitive matching).
|
||||
// - value: The header value to mask when sensitive.
|
||||
//
|
||||
// Returns:
|
||||
// - string: The masked value according to the header type; unchanged if not sensitive.
|
||||
func MaskSensitiveHeaderValue(key, value string) string {
|
||||
lowerKey := strings.ToLower(strings.TrimSpace(key))
|
||||
switch {
|
||||
case lowerKey == "authorization":
|
||||
return MaskAuthorizationHeader(value)
|
||||
case strings.Contains(lowerKey, "api-key"),
|
||||
strings.Contains(lowerKey, "apikey"),
|
||||
strings.Contains(lowerKey, "token"),
|
||||
strings.Contains(lowerKey, "secret"):
|
||||
return HideAPIKey(value)
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user