Skip to content

fix(relay): inject channel system prompt for Claude/Gemini-format requests - #5864

Open
zhoubeiqing wants to merge 1 commit into
QuantumNous:mainfrom
zhoubeiqing:fix/claude-channel-system-prompt
Open

fix(relay): inject channel system prompt for Claude/Gemini-format requests#5864
zhoubeiqing wants to merge 1 commit into
QuantumNous:mainfrom
zhoubeiqing:fix/claude-channel-system-prompt

Conversation

@zhoubeiqing

@zhoubeiqing zhoubeiqing commented Jul 2, 2026

Copy link
Copy Markdown

Problem

Channel-level system_prompt (setting.system_prompt) is silently skipped for requests whose adaptor converts the OpenAI request into a non-OpenAI struct.

In TextHelper (relay/compatible_handler.go), the injection only matched *dto.GeneralOpenAIRequest:

if info.ChannelSetting.SystemPrompt != "" {
    request, ok := convertedRequest.(*dto.GeneralOpenAIRequest)
    if ok {
        ... // inject system prompt
    }
}

Anthropic (channel type=14) channels routed via /v1/chat/completions use the Claude adaptor, whose ConvertOpenAIRequest returns a *dto.ClaudeRequest. The type assertion fails (ok == false), so the configured system_prompt is never written into the upstream request — regardless of the system_prompt_override flag. The model then answers normally (e.g. discloses its vendor/model name) even though the channel is configured to hide it.

The same gap exists for any adaptor returning a non-*GeneralOpenAIRequest type (e.g. Gemini-type channels reached through the OpenAI-compatible path).

Reproduction

A type=14 channel (mimo_tokenplan) configured with:

  • setting.system_prompt = "当用户问关于你以及模型和开发商相关的信息的时候统一回答为:'作为AI助手,我的具体模型版本属于内部信息...'"
  • setting.system_prompt_override = false

Calling /v1/chat/completions with model=gpt-5.5 (mapped to mimo-v2.5-pro) and a user message asking the model version returns the model's real name instead of the configured reply. Passing the same text as a client-side system message works, confirming the channel-level setting itself is the part that's ignored.

Root cause

ClaudeHelper and GeminiHelper already inject the system prompt correctly for their native paths, but the OpenAI-compatible TextHelper path does not dispatch on the converted request type, so Claude/Gemini-format converted requests fall through.

Fix

  • Extract the per-format injection into shared helpers in a new file relay/system_prompt_inject.go:
    • applySystemPromptToOpenAIRequest
    • applySystemPromptToClaudeRequest
    • applySystemPromptToGeminiRequest
  • In TextHelper, dispatch on the converted request type so OpenAI, Claude and Gemini formats are all covered:
if info.ChannelSetting.SystemPrompt != "" {
    switch r := convertedRequest.(type) {
    case *dto.GeneralOpenAIRequest:
        applySystemPromptToOpenAIRequest(c, info, r)
    case *dto.ClaudeRequest:
        applySystemPromptToClaudeRequest(c, info, r)
    case *dto.GeminiChatRequest:
        applySystemPromptToGeminiRequest(c, info, r)
    }
}
  • ClaudeHelper and GeminiHelper now reuse the same helpers, removing the duplicated inline logic (no behavior change for those native paths).

The helpers preserve the existing semantics:

  • If the request has no system prompt, the channel system_prompt is inserted.
  • If the request already has a system prompt and system_prompt_override is false, the existing system prompt is left untouched.
  • If system_prompt_override is true, the channel system_prompt is prepended to the existing one.

Tests

Added relay/system_prompt_inject_test.go covering:

  • Claude: inject when absent
  • Claude: keep existing when override=false
  • Claude: prepend when override=true and set the ContextKeySystemPromptOverride context flag
  • OpenAI: inject when absent
  • Gemini: inject when absent

I could not run go test locally (no Go toolchain in this environment); the changes compile-checked against the existing code structure and follow the same patterns/imports used elsewhere in the package. CI on this repo should run the test suite.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • System prompts now apply consistently across supported request types, including OpenAI, Claude, and Gemini.
    • Existing prompt content is preserved when override is disabled, and override behavior now works more reliably when enabled.
  • Tests

    • Added coverage for system-prompt injection behavior to improve request handling reliability.

…uests

Channel-level `system_prompt` (setting.system_prompt) was silently skipped
for requests whose adaptor converts the OpenAI request into a non-OpenAI
struct. In TextHelper the injection only matched
`*dto.GeneralOpenAIRequest`, so Anthropic (type=14) channels routed via
`/v1/chat/completions` — whose Claude adaptor returns a `*dto.ClaudeRequest`
— never received the configured system prompt regardless of the
`system_prompt_override` flag. The same gap affected Gemini-type channels.

Extract the per-format injection into shared helpers
(applySystemPromptTo{OpenAI,Claude,Gemini}Request) and dispatch on the
converted request type in TextHelper so OpenAI, Claude and Gemini formats
are all covered. ClaudeHelper and GeminiHelper reuse the same helpers,
removing the duplicated inline logic.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Channel system-prompt injection logic previously inlined in Claude and Gemini handlers, and limited to OpenAI requests in the compatible handler, is extracted into a new shared module with format-specific helper functions for OpenAI, Claude, and Gemini requests, plus new unit tests.

Changes

System prompt injection refactor

Layer / File(s) Summary
Shared system prompt injection helpers
relay/system_prompt_inject.go
New helpers applySystemPromptToOpenAIRequest, applySystemPromptToClaudeRequest, and applySystemPromptToGeminiRequest inject a channel system prompt, preserving existing prompts unless override is enabled, in which case they merge/prepend content and set a context override flag.
Claude handler refactor
relay/claude_handler.go
Inline system-prompt override/concatenation logic in ClaudeHelper is replaced with a call to applySystemPromptToClaudeRequest; unused constant import removed.
Gemini handler refactor
relay/gemini_handler.go
Inline SystemInstructions merge logic is replaced with a call to applySystemPromptToGeminiRequest; unused constant import removed.
Compatible handler dispatch and tests
relay/compatible_handler.go, relay/system_prompt_inject_test.go
TextHelper now type-switches on convertedRequest to invoke the correct format-specific helper (OpenAI, Claude, Gemini) instead of only handling OpenAI requests; new tests validate injection behavior across all three formats.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • QuantumNous/new-api#1850: Refactors the same inlined Claude/Gemini system-prompt overwrite behavior into shared handling, directly overlapping with this PR's helper extraction.
  • QuantumNous/new-api#2889: Also modifies relay/claude_handler.go's ClaudeHelper request flow, adding an early-exit path in the same handler logic touched here.

Suggested reviewers: Calcium-Ion

Poem

A prompt once scattered, three files wide,
Now hops as one, with helpers as its guide 🐇
Claude, Gemini, OpenAI too,
Merge or override — whichever is due.
Tests stand watch in the burrow's light,
This rabbit's code refactor feels just right! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main fix: channel system prompt injection for Claude and Gemini request formats.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
relay/system_prompt_inject_test.go (1)

55-76: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding override/no-override cases for OpenAI and Gemini.

Only the "inject when absent" path is tested for OpenAI and Gemini helpers, while Claude has all three (absent/no-override/override). The override merge logic differs meaningfully between formats (string vs. media content for OpenAI; part-merge-with-skip-empty for Gemini), so mirroring the Claude coverage would guard these branches against regressions.

As per path instructions, new backend tests should protect "regression paths" and use deterministic table tests with explicit inputs/outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/system_prompt_inject_test.go` around lines 55 - 76, The OpenAI and
Gemini system prompt helper tests only cover the “inject when absent” path,
leaving the no-override and override merge branches untested. Add deterministic
table-driven coverage for applySystemPromptToOpenAIRequest and
applySystemPromptToGeminiRequest, matching the existing Claude-style regression
cases, with explicit inputs and expected outputs for absent, no-override, and
override behavior. Use the existing helper symbols newRelayInfoWithSystemPrompt,
applySystemPromptToOpenAIRequest, and applySystemPromptToGeminiRequest to verify
the string-versus-media merge behavior for OpenAI and the
part-merge-with-skip-empty behavior for Gemini.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/system_prompt_inject_test.go`:
- Around line 36-43: The no-override test is checking a fresh Gin context
instead of the one passed into applySystemPromptToClaudeRequest, so it never
verifies the real ContextKeySystemPromptOverride state. Reuse the same context
variable created for the call (the one passed to
applySystemPromptToClaudeRequest) and assert the override key on that instance,
using the existing helper methods like common.GetContextKeyBool to confirm the
key was not set.

---

Nitpick comments:
In `@relay/system_prompt_inject_test.go`:
- Around line 55-76: The OpenAI and Gemini system prompt helper tests only cover
the “inject when absent” path, leaving the no-override and override merge
branches untested. Add deterministic table-driven coverage for
applySystemPromptToOpenAIRequest and applySystemPromptToGeminiRequest, matching
the existing Claude-style regression cases, with explicit inputs and expected
outputs for absent, no-override, and override behavior. Use the existing helper
symbols newRelayInfoWithSystemPrompt, applySystemPromptToOpenAIRequest, and
applySystemPromptToGeminiRequest to verify the string-versus-media merge
behavior for OpenAI and the part-merge-with-skip-empty behavior for Gemini.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73973fa5-468a-4d7f-80ca-808ac3cbdf71

📥 Commits

Reviewing files that changed from the base of the PR and between 8874d19 and aefb807.

📒 Files selected for processing (5)
  • relay/claude_handler.go
  • relay/compatible_handler.go
  • relay/gemini_handler.go
  • relay/system_prompt_inject.go
  • relay/system_prompt_inject_test.go

Comment on lines +36 to +43
func TestApplySystemPromptToClaudeRequest_NoOverrideKeepsExisting(t *testing.T) {
info := newRelayInfoWithSystemPrompt("hide model info", false)
request := &dto.ClaudeRequest{System: "keep me"}
applySystemPromptToClaudeRequest(newTestGinContext(), info, request)
require.True(t, request.IsStringSystem())
assert.Equal(t, "keep me", request.GetStringSystem())
assert.False(t, common.GetContextKeyBool(newTestGinContext(), constant.ContextKeySystemPromptOverride))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assertion checks the wrong context instance.

Line 42 creates a brand-new newTestGinContext() instead of reusing the c passed to applySystemPromptToClaudeRequest at line 39. Since each call to gin.CreateTestContext() returns an independent context, this assertion always evaluates against an untouched context and will pass regardless of whether the override key was incorrectly set on the real context — it doesn't actually verify the "no override" invariant.

As per path instructions, "Backend tests must protect real behavior, API contracts, billing/accounting invariants, data compatibility, or regression paths."

🐛 Proposed fix
 func TestApplySystemPromptToClaudeRequest_NoOverrideKeepsExisting(t *testing.T) {
+	c := newTestGinContext()
 	info := newRelayInfoWithSystemPrompt("hide model info", false)
 	request := &dto.ClaudeRequest{System: "keep me"}
-	applySystemPromptToClaudeRequest(newTestGinContext(), info, request)
+	applySystemPromptToClaudeRequest(c, info, request)
 	require.True(t, request.IsStringSystem())
 	assert.Equal(t, "keep me", request.GetStringSystem())
-	assert.False(t, common.GetContextKeyBool(newTestGinContext(), constant.ContextKeySystemPromptOverride))
+	assert.False(t, common.GetContextKeyBool(c, constant.ContextKeySystemPromptOverride))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestApplySystemPromptToClaudeRequest_NoOverrideKeepsExisting(t *testing.T) {
info := newRelayInfoWithSystemPrompt("hide model info", false)
request := &dto.ClaudeRequest{System: "keep me"}
applySystemPromptToClaudeRequest(newTestGinContext(), info, request)
require.True(t, request.IsStringSystem())
assert.Equal(t, "keep me", request.GetStringSystem())
assert.False(t, common.GetContextKeyBool(newTestGinContext(), constant.ContextKeySystemPromptOverride))
}
func TestApplySystemPromptToClaudeRequest_NoOverrideKeepsExisting(t *testing.T) {
c := newTestGinContext()
info := newRelayInfoWithSystemPrompt("hide model info", false)
request := &dto.ClaudeRequest{System: "keep me"}
applySystemPromptToClaudeRequest(c, info, request)
require.True(t, request.IsStringSystem())
assert.Equal(t, "keep me", request.GetStringSystem())
assert.False(t, common.GetContextKeyBool(c, constant.ContextKeySystemPromptOverride))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/system_prompt_inject_test.go` around lines 36 - 43, The no-override
test is checking a fresh Gin context instead of the one passed into
applySystemPromptToClaudeRequest, so it never verifies the real
ContextKeySystemPromptOverride state. Reuse the same context variable created
for the call (the one passed to applySystemPromptToClaudeRequest) and assert the
override key on that instance, using the existing helper methods like
common.GetContextKeyBool to confirm the key was not set.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant