feat: openrouter provider - #2884
Conversation
fix: optimize Codex relay
…al-usage fix: charge local input tokens when Gemini returns empty response
- Remove claude-instant-1.2, claude-2, claude-2.0, claude-2.1 from model lists - Remove /v1/complete endpoint support (legacy completion API) - Remove RequestModeCompletion and related code paths - Simplify handler functions by removing requestMode parameter - Update all channel adaptors that referenced claude handlers
remove: drop support for claude-2 and claude-1 series models
- Introduce Provider interface pattern for standard OAuth protocols - Create unified controller/oauth.go with common OAuth logic - Add OAuthError type for translatable error messages - Add i18n keys and translations (zh/en) for OAuth messages - Use common.ApiErrorI18n/ApiSuccessI18n for consistent responses - Preserve backward compatibility for existing routes and data
- Add support for custom OAuth providers, including creation, retrieval, updating, and deletion. - Introduce new model and controller for managing custom OAuth providers. - Enhance existing OAuth logic to accommodate custom providers. - Update API routes for custom OAuth provider management. - Include i18n support for custom OAuth-related messages.
…th user creation and binding - Improve error handling in DeleteCustomOAuthProvider to log and return errors when fetching binding counts. - Refactor user creation and OAuth binding logic to use transactions for atomic operations, ensuring data integrity. - Add unique constraints to UserOAuthBinding model to prevent duplicate bindings. - Enhance GitHub OAuth provider error logging for non-200 responses. - Update AccountManagement component to provide clearer error messages on API failures.
…ers for optional fields - Change fields in UpdateCustomOAuthProviderRequest struct to use pointers for optional values, allowing for better handling of nil cases. - Update UpdateCustomOAuthProvider function to check for nil before assigning optional fields, ensuring existing values are preserved when not provided.
…al file types for LF normalization and binary detection
Mitigate XSS vulnerabilities in the playground where AI-generated content is rendered without sanitization, allowing potential script injection via prompt injection attacks. MarkdownRenderer.jsx: - Replace dangerouslySetInnerHTML with a sandboxed iframe for HTML preview - Use sandbox="allow-same-origin" to block script execution while allowing CSS rendering and iframe height auto-sizing - Add SandboxedHtmlPreview component with automatic height adjustment CodeViewer.jsx: - Add escapeHtml() utility to encode HTML entities before rendering - Rewrite highlightJson() to process tokens iteratively, escaping each token and structural text before wrapping in syntax highlighting spans - Escape non-JSON and very-large content paths that previously bypassed sanitization - Update linkRegex to correctly match URLs containing & entities These changes only affect the playground (AI output rendering). Admin- configured content (home page, about page, footer, notices) remains unaffected as they use separate code paths and are within the trusted admin boundary.
🔒 fix(security): sanitize AI-generated HTML to prevent XSS in playground
…ty-text Revert "Fix/aws non empty text"
…ty-text Revert "fix: aws text content blocks must be non-empty"
fix: change token model_limits column from varchar(1024) to text
Return error when model price/ratio unset
* feat: add upstream model update detection with scheduled sync and manual apply flows * feat: support upstream model removal sync and selectable deletes in update modal * feat: add detect-only upstream updates and show compact +/- model badges * feat: improve upstream model update UX * feat: improve upstream model update UX * fix: respect model_mapping in upstream update detection * feat: improve upstream update modal to prevent missed add/remove actions * feat: add admin upstream model update notifications with digest and truncation * fix: avoid repeated partial-submit confirmation in upstream update modal * feat: improve ui/ux * feat: suppress upstream update alerts for unchanged channel-count within 24h * fix: submit upstream update choices even when no models are selected * feat: improve upstream model update flow and split frontend updater * fix merge conflict
…tips Fix/auto fetch upstream model tips
…yles and color variants
…5c19f0556e4655bc fix: update task billing log content to include reason
…d-path Feature/param override wildcard path
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
relay/channel/openrouter/adaptor.go (3)
160-161:⚠️ Potential issue | 🟠 MajorUse
common.Unmarshalfor THINKING decode.This path directly calls
json.Unmarshal; use the project JSON wrapper for consistency and policy compliance.Suggested fix
- if err := json.Unmarshal(request.THINKING, &thinking); err != nil { + if err := common.Unmarshal(request.THINKING, &thinking); err != nil { return nil, fmt.Errorf("error Unmarshal thinking: %w", err) }As per coding guidelines
**/*.go: All JSON marshal/unmarshal operations MUST use wrapper functions fromcommon/json.go, and business code must not directly callencoding/json.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@relay/channel/openrouter/adaptor.go` around lines 160 - 161, Replace the direct call to json.Unmarshal when decoding request.THINKING with the project's JSON wrapper: call common.Unmarshal(request.THINKING, &thinking) instead of json.Unmarshal to comply with the coding guideline; update the error message in the surrounding function (the THINKING decode block in adaptor.go) to wrap/return the error from common.Unmarshal (e.g., fmt.Errorf("error unmarshal thinking: %w", err)) so the code references common.Unmarshal from common/json.go rather than encoding/json.
183-212:⚠️ Potential issue | 🟠 MajorReasoning-model detection is too broad on provider-qualified names.
Checking
strings.HasPrefix(info.UpstreamModelName, "o")matches provider prefixes (e.g.openai/...) and can apply reasoning-model mutations to non-reasoning models.Suggested fix
- if strings.HasPrefix(info.UpstreamModelName, "o") || strings.HasPrefix(info.UpstreamModelName, "gpt-5") { + modelPart := info.UpstreamModelName + if idx := strings.LastIndex(modelPart, "/"); idx >= 0 { + modelPart = modelPart[idx+1:] + } + isOReasoningModel := strings.HasPrefix(modelPart, "o1-") || strings.HasPrefix(modelPart, "o3-") || strings.HasPrefix(modelPart, "o4-") + isGpt5 := strings.HasPrefix(modelPart, "gpt-5") + if isOReasoningModel || isGpt5 { ... - if strings.HasPrefix(info.UpstreamModelName, "o") { + if isOReasoningModel { request.Temperature = nil } - if strings.HasPrefix(info.UpstreamModelName, "gpt-5") { + if isGpt5 { request.Temperature = nil request.TopP = nil request.LogProbs = nil } ... - if !strings.HasPrefix(info.UpstreamModelName, "o1-mini") && !strings.HasPrefix(info.UpstreamModelName, "o1-preview") { + if !strings.HasPrefix(modelPart, "o1-mini") && !strings.HasPrefix(modelPart, "o1-preview") { if len(request.Messages) > 0 && request.Messages[0].Role == "system" { request.Messages[0].Role = "developer" } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@relay/channel/openrouter/adaptor.go` around lines 183 - 212, The prefix checks on info.UpstreamModelName (e.g., strings.HasPrefix(info.UpstreamModelName, "o")) are matching provider-qualified names like "openai/..." and causing incorrect reasoning-model mutations; fix by extracting the base model name (the substring after the last '/' or final segment) into a local variable (e.g., baseModel := strings.Split(info.UpstreamModelName, "/")[len-1]) and use baseModel for all HasPrefix checks and for parseReasoningEffortFromModelSuffix; when you update request.Model or info.UpstreamModelName preserve or recombine the original provider prefix if needed (or set to the originModel only for the base part) so other code that expects the provider-qualified name still works, and ensure request.ReasoningEffort is set from the parsed baseModel result.
128-177:⚠️ Potential issue | 🟠 Major
ReasoningEffortis cleared too early, which drops valid intent.Current ordering loses effort context before THINKING conversion and also fails to emit reasoning config for some valid effort cases.
Suggested ordering fix
+ reasoningEffort := request.ReasoningEffort + if !model_setting.ShouldPreserveThinkingSuffix(info.OriginModelName) && strings.HasSuffix(info.UpstreamModelName, "-thinking") { ... if len(request.Reasoning) == 0 { reasoning := map[string]any{"enabled": true} - if request.ReasoningEffort != "" && request.ReasoningEffort != "none" { - reasoning["effort"] = request.ReasoningEffort + if reasoningEffort != "" { + reasoning["effort"] = reasoningEffort } ... } - request.ReasoningEffort = "" } else { - if len(request.Reasoning) == 0 && request.ReasoningEffort != "" { + if len(request.Reasoning) == 0 && reasoningEffort != "" { reasoning := map[string]any{"enabled": true} - if request.ReasoningEffort != "none" { - reasoning["effort"] = request.ReasoningEffort - marshal, err := common.Marshal(reasoning) - if err != nil { - return nil, fmt.Errorf("error marshalling reasoning: %w", err) - } - request.Reasoning = marshal - } + reasoning["effort"] = reasoningEffort + marshal, err := common.Marshal(reasoning) + if err != nil { + return nil, fmt.Errorf("error marshalling reasoning: %w", err) + } + request.Reasoning = marshal } - request.ReasoningEffort = "" } if request.THINKING != nil && strings.HasPrefix(info.UpstreamModelName, "anthropic") { ... reasoning := dto.OpenRouterRequestReasoning{ Enabled: true, MaxTokens: *thinking.BudgetTokens, + Effort: reasoningEffort, } ... } + request.ReasoningEffort = ""🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@relay/channel/openrouter/adaptor.go` around lines 128 - 177, The code clears request.ReasoningEffort too early which loses intent before the THINKING -> Reasoning conversion; move the request.ReasoningEffort = "" assignment so it executes only after all THINKING-handling and reasoning-population logic (i.e., after the block that unmarshals dto.Thinking and sets request.Reasoning), and ensure both branches that create reasoning (the "-thinking" trim branch and the else branch, plus the anthropic THINKING branch that produces dto.OpenRouterRequestReasoning) consult request.ReasoningEffort while building the reasoning payload when request.Reasoning is empty so valid effort values are preserved and emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dto/openrouter.go`:
- Around line 5-10: Change the optional scalar fields on the
OpenRouterRequestReasoning struct to pointer types so explicit client values are
preserved: make Effort a *string, MaxTokens a *int, and Exclude a *bool (keep
`omitempty` tags). Update any call sites that construct or read
OpenRouterRequestReasoning (creators, unmarshallers, or code that inspects
fields) to handle nil pointers (nil means absent) and dereference safely when
using values. Ensure any JSON re-marshal paths rely on the new pointer semantics
so false/0 are serialized when explicitly provided.
In `@service/convert.go`:
- Around line 36-63: The new OpenRouter conversion block is calling json.Marshal
directly; replace those calls with the project wrapper common.Marshal and
propagate errors the same way. Specifically, in the isOpenRouter branch where
effortBytes := json.Marshal(effort) (used to set openAIRequest.Verbosity) and
where reasoningJSON := json.Marshal(reasoning) (used to set
openAIRequest.Reasoning), call common.Marshal(effort) and
common.Marshal(reasoning) instead and keep the existing error handling
(returning fmt.Errorf("failed to marshal ...: %w", err)); the change applies
around claudeRequest.GetEfforts, claudeRequest.Thinking handling, and
dto.OpenRouterRequestReasoning construction.
---
Duplicate comments:
In `@relay/channel/openrouter/adaptor.go`:
- Around line 160-161: Replace the direct call to json.Unmarshal when decoding
request.THINKING with the project's JSON wrapper: call
common.Unmarshal(request.THINKING, &thinking) instead of json.Unmarshal to
comply with the coding guideline; update the error message in the surrounding
function (the THINKING decode block in adaptor.go) to wrap/return the error from
common.Unmarshal (e.g., fmt.Errorf("error unmarshal thinking: %w", err)) so the
code references common.Unmarshal from common/json.go rather than encoding/json.
- Around line 183-212: The prefix checks on info.UpstreamModelName (e.g.,
strings.HasPrefix(info.UpstreamModelName, "o")) are matching provider-qualified
names like "openai/..." and causing incorrect reasoning-model mutations; fix by
extracting the base model name (the substring after the last '/' or final
segment) into a local variable (e.g., baseModel :=
strings.Split(info.UpstreamModelName, "/")[len-1]) and use baseModel for all
HasPrefix checks and for parseReasoningEffortFromModelSuffix; when you update
request.Model or info.UpstreamModelName preserve or recombine the original
provider prefix if needed (or set to the originModel only for the base part) so
other code that expects the provider-qualified name still works, and ensure
request.ReasoningEffort is set from the parsed baseModel result.
- Around line 128-177: The code clears request.ReasoningEffort too early which
loses intent before the THINKING -> Reasoning conversion; move the
request.ReasoningEffort = "" assignment so it executes only after all
THINKING-handling and reasoning-population logic (i.e., after the block that
unmarshals dto.Thinking and sets request.Reasoning), and ensure both branches
that create reasoning (the "-thinking" trim branch and the else branch, plus the
anthropic THINKING branch that produces dto.OpenRouterRequestReasoning) consult
request.ReasoningEffort while building the reasoning payload when
request.Reasoning is empty so valid effort values are preserved and emitted.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9e4f07f0-f0e0-429a-839c-21bd314831eb
📒 Files selected for processing (4)
dto/claude.godto/openrouter.gorelay/channel/openrouter/adaptor.goservice/convert.go
| type OpenRouterRequestReasoning struct { | ||
| Enabled bool `json:"enabled"` | ||
| Effort string `json:"effort,omitempty"` | ||
| MaxTokens int `json:"max_tokens,omitempty"` | ||
| Exclude bool `json:"exclude,omitempty"` | ||
| } |
There was a problem hiding this comment.
Optional relay scalars should be pointers to preserve explicit values.
Effort, MaxTokens, and Exclude are optional request scalars but non-pointer, so explicit client values like false / 0 can be lost on re-marshal.
Suggested DTO fix
type OpenRouterRequestReasoning struct {
Enabled bool `json:"enabled"`
- Effort string `json:"effort,omitempty"`
- MaxTokens int `json:"max_tokens,omitempty"`
- Exclude bool `json:"exclude,omitempty"`
+ Effort *string `json:"effort,omitempty"`
+ MaxTokens *int `json:"max_tokens,omitempty"`
+ Exclude *bool `json:"exclude,omitempty"`
}As per coding guidelines dto/**/*.go: For upstream relay request DTOs parsed from client JSON and re-marshaled to providers, optional scalar fields MUST use pointer types with omitempty.
📝 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.
| type OpenRouterRequestReasoning struct { | |
| Enabled bool `json:"enabled"` | |
| Effort string `json:"effort,omitempty"` | |
| MaxTokens int `json:"max_tokens,omitempty"` | |
| Exclude bool `json:"exclude,omitempty"` | |
| } | |
| type OpenRouterRequestReasoning struct { | |
| Enabled bool `json:"enabled"` | |
| Effort *string `json:"effort,omitempty"` | |
| MaxTokens *int `json:"max_tokens,omitempty"` | |
| Exclude *bool `json:"exclude,omitempty"` | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@dto/openrouter.go` around lines 5 - 10, Change the optional scalar fields on
the OpenRouterRequestReasoning struct to pointer types so explicit client values
are preserved: make Effort a *string, MaxTokens a *int, and Exclude a *bool
(keep `omitempty` tags). Update any call sites that construct or read
OpenRouterRequestReasoning (creators, unmarshallers, or code that inspects
fields) to handle nil pointers (nil means absent) and dereference safely when
using values. Ensure any JSON re-marshal paths rely on the new pointer semantics
so false/0 are serialized when explicitly provided.
| if isOpenRouter { | ||
| if effort := claudeRequest.GetEfforts(); effort != "" { | ||
| effortBytes, err := json.Marshal(effort) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to marshal reasoning: %w", err) | ||
| return nil, fmt.Errorf("failed to marshal verbosity: %w", err) | ||
| } | ||
| openAIRequest.Reasoning = reasoningJSON | ||
| } else { | ||
| thinkingSuffix := "-thinking" | ||
| if strings.HasSuffix(info.OriginModelName, thinkingSuffix) && | ||
| !strings.HasSuffix(openAIRequest.Model, thinkingSuffix) { | ||
| openAIRequest.Model = openAIRequest.Model + thinkingSuffix | ||
| openAIRequest.Verbosity = effortBytes | ||
| } | ||
| if claudeRequest.Thinking != nil { | ||
| var reasoning *dto.OpenRouterRequestReasoning | ||
| switch claudeRequest.Thinking.Type { | ||
| case "enabled": | ||
| reasoning = &dto.OpenRouterRequestReasoning{ | ||
| Enabled: true, | ||
| MaxTokens: claudeRequest.Thinking.GetBudgetTokens(), | ||
| } | ||
| case "adaptive": | ||
| reasoning = &dto.OpenRouterRequestReasoning{ | ||
| Enabled: true, | ||
| } | ||
| } | ||
| if reasoning != nil { | ||
| reasoningJSON, err := json.Marshal(reasoning) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to marshal reasoning: %w", err) | ||
| } | ||
| openAIRequest.Reasoning = reasoningJSON | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -name "service" -type d 2>/dev/null | head -20Repository: QuantumNous/new-api
Length of output: 45
🏁 Script executed:
# First, let's find the file and inspect it
find . -name "convert.go" -path "*/service/*" 2>/dev/nullRepository: QuantumNous/new-api
Length of output: 83
🏁 Script executed:
# List files in service directory to understand structure
git ls-files | grep "^service/" | head -20Repository: QuantumNous/new-api
Length of output: 581
🏁 Script executed:
# Read the service/convert.go file to check lines 36-63
wc -l service/convert.goRepository: QuantumNous/new-api
Length of output: 85
🏁 Script executed:
# Get the imports and the code section around lines 36-63
head -80 service/convert.go | cat -nRepository: QuantumNous/new-api
Length of output: 3024
🏁 Script executed:
# Check if common/json.go exists and contains the Marshal function
ls -la common/json.go 2>/dev/null || echo "File not found"Repository: QuantumNous/new-api
Length of output: 125
🏁 Script executed:
# Search for the Marshal function in common package
rg "func.*Marshal\(" common/ --type goRepository: QuantumNous/new-api
Length of output: 115
🏁 Script executed:
# Read the common/json.go file to see available wrapper functions
cat -n common/json.goRepository: QuantumNous/new-api
Length of output: 1132
Use common.Marshal in this new OpenRouter conversion block.
Lines 38 and 58 use json.Marshal directly; these should use the project wrapper function. The common package is already imported in the file.
Suggested fix
- effortBytes, err := json.Marshal(effort)
+ effortBytes, err := common.Marshal(effort)
if err != nil {
return nil, fmt.Errorf("failed to marshal verbosity: %w", err)
}
openAIRequest.Verbosity = effortBytes
}
...
- reasoningJSON, err := json.Marshal(reasoning)
+ reasoningJSON, err := common.Marshal(reasoning)
if err != nil {
return nil, fmt.Errorf("failed to marshal reasoning: %w", err)
}
openAIRequest.Reasoning = reasoningJSON🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/convert.go` around lines 36 - 63, The new OpenRouter conversion block
is calling json.Marshal directly; replace those calls with the project wrapper
common.Marshal and propagate errors the same way. Specifically, in the
isOpenRouter branch where effortBytes := json.Marshal(effort) (used to set
openAIRequest.Verbosity) and where reasoningJSON := json.Marshal(reasoning)
(used to set openAIRequest.Reasoning), call common.Marshal(effort) and
common.Marshal(reasoning) instead and keep the existing error handling
(returning fmt.Errorf("failed to marshal ...: %w", err)); the change applies
around claudeRequest.GetEfforts, claudeRequest.Thinking handling, and
dto.OpenRouterRequestReasoning construction.
独立openrouter provider,支持原生 /v1/messages
#2822
暂时不合并,根据 #2791 反馈,OpenRouter的claude计费usage和官方不一致,需要进一步确认。
Summary by CodeRabbit
New Features
Refactor