Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
}
}
jsonData, err := json.Marshal(convertedRequest)
jsonData, err := common.Marshal(convertedRequest)
if err != nil {
return testResult{
context: c,
Expand All @@ -385,8 +385,15 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
//}

if len(info.ParamOverride) > 0 {
jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride, relaycommon.BuildParamOverrideContext(info))
jsonData, err = relaycommon.ApplyParamOverrideWithRelayInfo(jsonData, info)
if err != nil {
if fixedErr, ok := relaycommon.AsParamOverrideReturnError(err); ok {
return testResult{
context: c,
localErr: fixedErr,
newAPIError: relaycommon.NewAPIErrorFromParamOverride(fixedErr),
}
}
return testResult{
context: c,
localErr: err,
Expand Down
5 changes: 5 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,11 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
ModelName: relayInfo.OriginModelName,
Retry: common.GetPointer(0),
}
relayInfo.RetryIndex = 0
relayInfo.LastError = nil

for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() {
relayInfo.RetryIndex = retryParam.GetRetry()
channel, channelErr := getChannel(c, relayInfo, retryParam)
if channelErr != nil {
logger.LogError(c, channelErr.Error())
Expand Down Expand Up @@ -216,10 +219,12 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
}

if newAPIError == nil {
relayInfo.LastError = nil
return
}

newAPIError = service.NormalizeViolationFeeError(newAPIError)
relayInfo.LastError = newAPIError

processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)

Expand Down
9 changes: 7 additions & 2 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,13 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
common.SetContextKey(c, constant.ContextKeyChannelCreateTime, channel.CreatedTime)
common.SetContextKey(c, constant.ContextKeyChannelSetting, channel.GetSetting())
common.SetContextKey(c, constant.ContextKeyChannelOtherSetting, channel.GetOtherSettings())
common.SetContextKey(c, constant.ContextKeyChannelParamOverride, channel.GetParamOverride())
common.SetContextKey(c, constant.ContextKeyChannelHeaderOverride, channel.GetHeaderOverride())
paramOverride := channel.GetParamOverride()
headerOverride := channel.GetHeaderOverride()
if mergedParam, applied := service.ApplyChannelAffinityOverrideTemplate(c, paramOverride); applied {
paramOverride = mergedParam
}
common.SetContextKey(c, constant.ContextKeyChannelParamOverride, paramOverride)
common.SetContextKey(c, constant.ContextKeyChannelHeaderOverride, headerOverride)
if nil != channel.OpenAIOrganization && *channel.OpenAIOrganization != "" {
common.SetContextKey(c, constant.ContextKeyChannelOrganization, *channel.OpenAIOrganization)
}
Expand Down
20 changes: 12 additions & 8 deletions relay/channel/api_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,17 @@ func applyHeaderOverridePlaceholders(template string, c *gin.Context, apiKey str
// Passthrough rules are applied first, then normal overrides are applied, so explicit overrides win.
func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]string, error) {
headerOverride := make(map[string]string)
if info == nil {
return headerOverride, nil
}

headerOverrideSource := common.GetEffectiveHeaderOverride(info)

passAll := false
var passthroughRegex []*regexp.Regexp
if !info.IsChannelTest {
for k := range info.HeadersOverride {
key := strings.TrimSpace(k)
for k := range headerOverrideSource {
key := strings.TrimSpace(strings.ToLower(k))
if key == "" {
continue
}
Expand All @@ -183,12 +188,11 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
continue
}

lower := strings.ToLower(key)
var pattern string
switch {
case strings.HasPrefix(lower, headerPassthroughRegexPrefix):
case strings.HasPrefix(key, headerPassthroughRegexPrefix):
pattern = strings.TrimSpace(key[len(headerPassthroughRegexPrefix):])
case strings.HasPrefix(lower, headerPassthroughRegexPrefixV2):
case strings.HasPrefix(key, headerPassthroughRegexPrefixV2):
pattern = strings.TrimSpace(key[len(headerPassthroughRegexPrefixV2):])
default:
continue
Expand Down Expand Up @@ -229,15 +233,15 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
if value == "" {
continue
}
headerOverride[name] = value
headerOverride[strings.ToLower(strings.TrimSpace(name))] = value
}
}

for k, v := range info.HeadersOverride {
for k, v := range headerOverrideSource {
if isHeaderPassthroughRuleKey(k) {
continue
}
key := strings.TrimSpace(k)
key := strings.TrimSpace(strings.ToLower(k))
if key == "" {
continue
}
Expand Down
93 changes: 89 additions & 4 deletions relay/channel/api_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestProcessHeaderOverride_ChannelTestSkipsClientHeaderPlaceholder(t *testin

headers, err := processHeaderOverride(info, ctx)
require.NoError(t, err)
_, ok := headers["X-Upstream-Trace"]
_, ok := headers["x-upstream-trace"]
require.False(t, ok)
}

Expand All @@ -77,7 +77,38 @@ func TestProcessHeaderOverride_NonTestKeepsClientHeaderPlaceholder(t *testing.T)

headers, err := processHeaderOverride(info, ctx)
require.NoError(t, err)
require.Equal(t, "trace-123", headers["X-Upstream-Trace"])
require.Equal(t, "trace-123", headers["x-upstream-trace"])
}

func TestProcessHeaderOverride_RuntimeOverrideIsFinalHeaderMap(t *testing.T) {
t.Parallel()

gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)

info := &relaycommon.RelayInfo{
IsChannelTest: false,
UseRuntimeHeadersOverride: true,
RuntimeHeadersOverride: map[string]any{
"x-static": "runtime-value",
"x-runtime": "runtime-only",
},
ChannelMeta: &relaycommon.ChannelMeta{
HeadersOverride: map[string]any{
"X-Static": "legacy-value",
"X-Legacy": "legacy-only",
},
},
}

headers, err := processHeaderOverride(info, ctx)
require.NoError(t, err)
require.Equal(t, "runtime-value", headers["x-static"])
require.Equal(t, "runtime-only", headers["x-runtime"])
_, exists := headers["x-legacy"]
require.False(t, exists)
}

func TestProcessHeaderOverride_PassthroughSkipsAcceptEncoding(t *testing.T) {
Expand All @@ -101,8 +132,62 @@ func TestProcessHeaderOverride_PassthroughSkipsAcceptEncoding(t *testing.T) {

headers, err := processHeaderOverride(info, ctx)
require.NoError(t, err)
require.Equal(t, "trace-123", headers["X-Trace-Id"])
require.Equal(t, "trace-123", headers["x-trace-id"])

_, hasAcceptEncoding := headers["Accept-Encoding"]
_, hasAcceptEncoding := headers["accept-encoding"]
require.False(t, hasAcceptEncoding)
}

func TestProcessHeaderOverride_PassHeadersTemplateSetsRuntimeHeaders(t *testing.T) {
t.Parallel()

gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
ctx.Request.Header.Set("Originator", "Codex CLI")
ctx.Request.Header.Set("Session_id", "sess-123")

info := &relaycommon.RelayInfo{
IsChannelTest: false,
RequestHeaders: map[string]string{
"Originator": "Codex CLI",
"Session_id": "sess-123",
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ChannelMeta: &relaycommon.ChannelMeta{
ParamOverride: map[string]any{
"operations": []any{
map[string]any{
"mode": "pass_headers",
"value": []any{"Originator", "Session_id", "X-Codex-Beta-Features"},
},
},
},
HeadersOverride: map[string]any{
"X-Static": "legacy-value",
},
},
}

_, err := relaycommon.ApplyParamOverrideWithRelayInfo([]byte(`{"model":"gpt-4.1"}`), info)
require.NoError(t, err)
require.True(t, info.UseRuntimeHeadersOverride)
require.Equal(t, "Codex CLI", info.RuntimeHeadersOverride["originator"])
require.Equal(t, "sess-123", info.RuntimeHeadersOverride["session_id"])
_, exists := info.RuntimeHeadersOverride["x-codex-beta-features"]
require.False(t, exists)
require.Equal(t, "legacy-value", info.RuntimeHeadersOverride["x-static"])

headers, err := processHeaderOverride(info, ctx)
require.NoError(t, err)
require.Equal(t, "Codex CLI", headers["originator"])
require.Equal(t, "sess-123", headers["session_id"])
_, exists = headers["x-codex-beta-features"]
require.False(t, exists)

upstreamReq := httptest.NewRequest(http.MethodPost, "https://example.com/v1/responses", nil)
applyHeaderOverrideToRequest(upstreamReq, headers)
require.Equal(t, "Codex CLI", upstreamReq.Header.Get("Originator"))
require.Equal(t, "sess-123", upstreamReq.Header.Get("Session_id"))
require.Empty(t, upstreamReq.Header.Get("X-Codex-Beta-Features"))
}
5 changes: 2 additions & 3 deletions relay/chat_completions_via_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ func applySystemPromptIfNeeded(c *gin.Context, info *relaycommon.RelayInfo, requ
}

func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, adaptor channel.Adaptor, request *dto.GeneralOpenAIRequest) (*dto.Usage, *types.NewAPIError) {
overrideCtx := relaycommon.BuildParamOverrideContext(info)
chatJSON, err := common.Marshal(request)
if err != nil {
return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
Expand All @@ -82,9 +81,9 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad
}

if len(info.ParamOverride) > 0 {
chatJSON, err = relaycommon.ApplyParamOverride(chatJSON, info.ParamOverride, overrideCtx)
chatJSON, err = relaycommon.ApplyParamOverrideWithRelayInfo(chatJSON, info)
if err != nil {
return nil, types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid, types.ErrOptionWithSkipRetry())
return nil, newAPIErrorFromParamOverride(err)
}
}

Expand Down
4 changes: 2 additions & 2 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ

// apply param override
if len(info.ParamOverride) > 0 {
jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride, relaycommon.BuildParamOverrideContext(info))
jsonData, err = relaycommon.ApplyParamOverrideWithRelayInfo(jsonData, info)
if err != nil {
return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid, types.ErrOptionWithSkipRetry())
return newAPIErrorFromParamOverride(err)
}
}

Expand Down
Loading