Skip to content
Closed
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
51 changes: 51 additions & 0 deletions .github/workflows/docker-build-ghcr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Build Docker image to GHCR

on:
workflow_dispatch:
inputs:
branch:
description: "要构建的分支名"
required: true
type: string
default: "feat/system-prompt-variables"

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ inputs.branch }}
Comment on lines +19 to +22

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Checkout persists GitHub token in .git/config.

actions/checkout defaults to persist-credentials: true, which writes the GITHUB_TOKEN into the local git config. During a Docker build, untrusted RUN steps or a malicious Dockerfile could read and exfiltrate it. Set persist-credentials: false since this job only needs the source tree, not git push access.

🔒 Proposed fix
       - name: Check out branch
         uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
         with:
           ref: ${{ inputs.branch }}
+          persist-credentials: false
📝 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
- name: Check out branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ inputs.branch }}
- name: Check out branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ inputs.branch }}
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 19-22: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 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 @.github/workflows/docker-build-ghcr.yml around lines 19 - 22, Update the
actions/checkout step in the workflow to set persist-credentials to false, while
preserving the existing inputs.branch ref and checkout behavior. This prevents
the GitHub token from being written to the repository’s git configuration during
the Docker build.

Source: Linters/SAST tools


- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f

- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Resolve image tag
id: tag
run: |
REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
TAG=$(echo "${{ inputs.branch }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_.-]/-/g')
Comment on lines +37 to +38

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.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Script injection via unescaped inputs.branch still present (previously flagged).

${{ inputs.branch }} is still interpolated directly into the shell script. A dispatcher with write access can pass a branch name containing shell metacharacters to achieve arbitrary code execution. Pass it through an environment variable instead.

🔒 Proposed fix
       - name: Resolve image tag
         id: tag
+        env:
+          BRANCH: ${{ inputs.branch }}
         run: |
           REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
-          TAG=$(echo "${{ inputs.branch }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_.-]/-/g')
+          TAG=$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_.-]/-/g')
           echo "repo=$REPO" >> "$GITHUB_OUTPUT"
           echo "tag=$TAG" >> "$GITHUB_OUTPUT"
           echo "Building image: ghcr.io/${REPO}:$TAG"
🧰 Tools
🪛 zizmor (1.26.1)

[error] 38-38: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 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 @.github/workflows/docker-build-ghcr.yml around lines 37 - 38, Update the
workflow’s TAG computation to avoid directly interpolating inputs.branch into
shell code. Expose the branch input through the step’s environment and have the
shell command read that environment variable before applying lowercase
conversion and sanitization; leave the REPO computation unchanged.

Source: Linters/SAST tools

echo "repo=$REPO" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Building image: ghcr.io/${REPO}:$TAG"

- name: Build & push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
with:
context: .
push: true
tags: |
ghcr.io/${{ steps.tag.outputs.repo }}:${{ steps.tag.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 2 additions & 1 deletion relay/channel/codex/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/openai"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/types"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -56,7 +57,7 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
isCompact := info != nil && info.RelayMode == relayconstant.RelayModeResponsesCompact

if info != nil && info.ChannelSetting.SystemPrompt != "" {
systemPrompt := info.ChannelSetting.SystemPrompt
systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)

if len(request.Instructions) == 0 {
if b, err := common.Marshal(systemPrompt); err == nil {
Expand Down
8 changes: 5 additions & 3 deletions relay/chat_completions_via_responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
openaichannel "github.com/QuantumNous/new-api/relay/channel/openai"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/types"

Expand All @@ -26,6 +27,7 @@ func applySystemPromptIfNeeded(c *gin.Context, info *relaycommon.RelayInfo, requ
return
}

systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
systemRole := request.GetSystemRoleName()

containSystemPrompt := false
Expand All @@ -38,7 +40,7 @@ func applySystemPromptIfNeeded(c *gin.Context, info *relaycommon.RelayInfo, requ
if !containSystemPrompt {
systemMessage := dto.Message{
Role: systemRole,
Content: info.ChannelSetting.SystemPrompt,
Content: systemPrompt,
}
request.Messages = append([]dto.Message{systemMessage}, request.Messages...)
return
Expand All @@ -54,14 +56,14 @@ func applySystemPromptIfNeeded(c *gin.Context, info *relaycommon.RelayInfo, requ
continue
}
if message.IsStringContent() {
request.Messages[i].SetStringContent(info.ChannelSetting.SystemPrompt + "\n" + message.StringContent())
request.Messages[i].SetStringContent(systemPrompt + "\n" + message.StringContent())
return
}
contents := message.ParseContent()
contents = append([]dto.MediaContent{
{
Type: dto.ContentTypeText,
Text: info.ChannelSetting.SystemPrompt,
Text: systemPrompt,
},
}, contents...)
request.Messages[i].Content = contents
Expand Down
9 changes: 5 additions & 4 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,22 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
}

if info.ChannelSetting.SystemPrompt != "" {
systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
if request.System == nil {
request.SetStringSystem(info.ChannelSetting.SystemPrompt)
request.SetStringSystem(systemPrompt)
} else if info.ChannelSetting.SystemPromptOverride {
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
if request.IsStringSystem() {
existing := strings.TrimSpace(request.GetStringSystem())
if existing == "" {
request.SetStringSystem(info.ChannelSetting.SystemPrompt)
request.SetStringSystem(systemPrompt)
} else {
request.SetStringSystem(info.ChannelSetting.SystemPrompt + "\n" + existing)
request.SetStringSystem(systemPrompt + "\n" + existing)
}
} else {
systemContents := request.ParseSystem()
newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText}
newSystem.SetText(info.ChannelSetting.SystemPrompt)
newSystem.SetText(systemPrompt)
if len(systemContents) == 0 {
request.System = []dto.ClaudeMediaMessage{newSystem}
} else {
Expand Down
8 changes: 4 additions & 4 deletions relay/compatible_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)

if info.ChannelSetting.SystemPrompt != "" {
// 如果有系统提示,则将其添加到请求中
systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
request, ok := convertedRequest.(*dto.GeneralOpenAIRequest)
if ok {
containSystemPrompt := false
Expand All @@ -127,7 +127,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
// 如果没有系统提示,则添加系统提示
systemMessage := dto.Message{
Role: request.GetSystemRoleName(),
Content: info.ChannelSetting.SystemPrompt,
Content: systemPrompt,
}
request.Messages = append([]dto.Message{systemMessage}, request.Messages...)
} else if info.ChannelSetting.SystemPromptOverride {
Expand All @@ -136,13 +136,13 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
for i, message := range request.Messages {
if message.Role == request.GetSystemRoleName() {
if message.IsStringContent() {
request.Messages[i].SetStringContent(info.ChannelSetting.SystemPrompt + "\n" + message.StringContent())
request.Messages[i].SetStringContent(systemPrompt + "\n" + message.StringContent())
} else {
contents := message.ParseContent()
contents = append([]dto.MediaContent{
{
Type: dto.ContentTypeText,
Text: info.ChannelSetting.SystemPrompt,
Text: systemPrompt,
},
}, contents...)
request.Messages[i].Content = contents
Expand Down
9 changes: 5 additions & 4 deletions relay/gemini_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,28 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
adaptor.Init(info)

if info.ChannelSetting.SystemPrompt != "" {
systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
if request.SystemInstructions == nil {
request.SystemInstructions = &dto.GeminiChatContent{
Parts: []dto.GeminiPart{
{Text: info.ChannelSetting.SystemPrompt},
{Text: systemPrompt},
},
}
} else if len(request.SystemInstructions.Parts) == 0 {
request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}
request.SystemInstructions.Parts = []dto.GeminiPart{{Text: systemPrompt}}
} else if info.ChannelSetting.SystemPromptOverride {
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
merged := false
for i := range request.SystemInstructions.Parts {
if request.SystemInstructions.Parts[i].Text == "" {
continue
}
request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
request.SystemInstructions.Parts[i].Text = systemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
merged = true
break
}
if !merged {
request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...)
request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: systemPrompt}}, request.SystemInstructions.Parts...)
Comment on lines +99 to +120

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

{model_name} may substitute the modified model name instead of the originally requested one.

The thinking adapter at lines 73–89 can modify info.OriginModelName (appending -nothinking) before the system prompt expansion at line 99. This means {model_name} would resolve to e.g. gemini-2.5-flash-nothinking rather than the user's originally requested model name, inconsistent with the PR's stated behavior.

The system prompt expansion block and the thinking adapter block are independent (one modifies request.SystemInstructions, the other modifies request.GenerationConfig.ThinkingConfig and info.OriginModelName), so moving the expansion before the thinking adapter is safe.

♻️ Proposed fix: move system prompt expansion before thinking adapter
 	adaptor.Init(info)

+	if info.ChannelSetting.SystemPrompt != "" {
+		systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
+		if request.SystemInstructions == nil {
+			request.SystemInstructions = &dto.GeminiChatContent{
+				Parts: []dto.GeminiPart{
+					{Text: systemPrompt},
+				},
+			}
+		} else if len(request.SystemInstructions.Parts) == 0 {
+			request.SystemInstructions.Parts = []dto.GeminiPart{{Text: systemPrompt}}
+		} else if info.ChannelSetting.SystemPromptOverride {
+			common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
+			merged := false
+			for i := range request.SystemInstructions.Parts {
+				if request.SystemInstructions.Parts[i].Text == "" {
+					continue
+				}
+				request.SystemInstructions.Parts[i].Text = systemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
+				merged = true
+				break
+			}
+			if !merged {
+				request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: systemPrompt}}, request.SystemInstructions.Parts...)
+			}
+		}
+	}
+
 	if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
 		if isNoThinkingRequest(request) {
 			// check is thinking
 			if !strings.Contains(info.OriginModelName, "-nothinking") {
 				// try to get no thinking model price
 				noThinkingModelName := info.OriginModelName + "-nothinking"
 				containPrice := helper.HasModelBillingConfig(noThinkingModelName)
 				if containPrice {
 					info.OriginModelName = noThinkingModelName
 					info.UpstreamModelName = noThinkingModelName
 				}
 			}
 		}
 		if request.GenerationConfig.ThinkingConfig == nil {
 			gemini.ThinkingAdaptor(request, info)
 		}
 	}

-	if info.ChannelSetting.SystemPrompt != "" {
-		systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
-		...
-	}
📝 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
systemPrompt := helper.ApplyChannelSystemPromptVariables(info.ChannelSetting.SystemPrompt, info)
if request.SystemInstructions == nil {
request.SystemInstructions = &dto.GeminiChatContent{
Parts: []dto.GeminiPart{
{Text: info.ChannelSetting.SystemPrompt},
{Text: systemPrompt},
},
}
} else if len(request.SystemInstructions.Parts) == 0 {
request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}
request.SystemInstructions.Parts = []dto.GeminiPart{{Text: systemPrompt}}
} else if info.ChannelSetting.SystemPromptOverride {
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
merged := false
for i := range request.SystemInstructions.Parts {
if request.SystemInstructions.Parts[i].Text == "" {
continue
}
request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
request.SystemInstructions.Parts[i].Text = systemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
merged = true
break
}
if !merged {
request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...)
request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: systemPrompt}}, request.SystemInstructions.Parts...)
🤖 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/gemini_handler.go` around lines 99 - 120, Move the system prompt
expansion block identified by ApplyChannelSystemPromptVariables and
request.SystemInstructions before the thinking adapter that can modify
info.OriginModelName. Keep the existing prompt initialization, merge behavior,
and override context handling unchanged so {model_name} expands using the
originally requested model name.

}
}
}
Expand Down
22 changes: 22 additions & 0 deletions relay/helper/channel_system_prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package helper

import (
"strings"

"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/system_setting"
)

func ApplyChannelSystemPromptVariables(systemPrompt string, info *relaycommon.RelayInfo) string {
if systemPrompt == "" {
return systemPrompt
}
result := systemPrompt
result = strings.ReplaceAll(result, "{site_name}", common.SystemName)
result = strings.ReplaceAll(result, "{site_url}", system_setting.ServerAddress)
if info != nil && info.OriginModelName != "" {
result = strings.ReplaceAll(result, "{model_name}", info.OriginModelName)
}
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -4157,6 +4157,10 @@ export function ChannelMutateDrawer({
{t(
'Default system prompt for this channel'
)}
<br />
<span className='text-muted-foreground'>
{t('Available variables')}: {'{model_name}'}, {'{site_name}'}, {'{site_url}'}
</span>
</FormDescription>
<FormMessage />
</FormItem>
Expand Down
1 change: 1 addition & 0 deletions web/default/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@
"Automatically sync model list when upstream changes are detected": "Automatically sync model list when upstream changes are detected",
"Availability (last 24h)": "Availability (last 24h)",
"Available": "Available",
"Available variables": "Available variables",
"Available credits are ordered by soonest expiration.": "Available credits are ordered by soonest expiration.",
"Available disk space": "Available disk space",
"Available Models": "Available Models",
Expand Down
1 change: 1 addition & 0 deletions web/default/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@
"Automatically sync model list when upstream changes are detected": "检测到上游模型变更时自动同步模型列表",
"Availability (last 24h)": "可用率(最近 24 小时)",
"Available": "可用",
"Available variables": "可用变量",
"Available credits are ordered by soonest expiration.": "可用次数按最早到期排序。",
"Available disk space": "可用磁盘空间",
"Available Models": "可用模型",
Expand Down