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
16 changes: 16 additions & 0 deletions coding-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
火山引擎 订阅计划:
1、个人版、企业版 Coding Plan
BaseURL:
https://ark.cn-beijing.volces.com/api/coding/v3 (兼容OpenAI 协议)
https://ark.cn-beijing.volces.com/api/coding (兼容 Anthropic 接口协议)
阿里云百炼 订阅计划:
1、企业版 token plan:
BaseURL:
https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 (兼容OpenAI 协议)
https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic (兼容 Anthropic 接口协议)
2、个人版 coding plan:
https://coding.dashscope.aliyuncs.com/v1 (兼容OpenAI 协议)
https://coding.dashscope.aliyuncs.com/apps/anthropic (兼容 Anthropic 接口协议)
智普GLM 订阅计划:
1、个人版 coding plan:
https://open.bigmodel.cn/api/coding/paas/v4
8 changes: 8 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,12 @@ var ChannelSpecialBases = map[string]ChannelSpecialBase{
ClaudeBaseURL: "https://ark.cn-beijing.volces.com/api/coding",
OpenAIBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
},
"ali-token-plan": {
ClaudeBaseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic",
OpenAIBaseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
},
"ali-coding-plan": {
ClaudeBaseURL: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
OpenAIBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
},
}
6 changes: 5 additions & 1 deletion controller/channel_upstream_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,11 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
var url string
switch channel.Type {
case constant.ChannelTypeAli:
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
} else {
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
}
case constant.ChannelTypeZhipu_v4:
if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" {
url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL)
Expand Down
61 changes: 32 additions & 29 deletions relay/channel/ali/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"strings"

channelconstant "github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/relay/channel"
Expand Down Expand Up @@ -71,6 +72,9 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt
}

func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
if _, ok := channelconstant.ChannelSpecialBases[info.ChannelBaseUrl]; ok {
return req, nil
}
if supportsAliAnthropicMessages(info.UpstreamModelName) {
return req, nil
}
Expand All @@ -89,49 +93,57 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}

func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
var fullRequestURL string
baseUrl := info.ChannelBaseUrl
specialPlan, hasSpecialPlan := channelconstant.ChannelSpecialBases[baseUrl]

switch info.RelayFormat {
case types.RelayFormatClaude:
if hasSpecialPlan && specialPlan.ClaudeBaseURL != "" {
return fmt.Sprintf("%s/v1/messages", specialPlan.ClaudeBaseURL), nil
}
if supportsAliAnthropicMessages(info.UpstreamModelName) {
fullRequestURL = fmt.Sprintf("%s/apps/anthropic/v1/messages", info.ChannelBaseUrl)
} else {
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/chat/completions", info.ChannelBaseUrl)
return fmt.Sprintf("%s/apps/anthropic/v1/messages", baseUrl), nil
}
return fmt.Sprintf("%s/compatible-mode/v1/chat/completions", baseUrl), nil
default:
if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
return fmt.Sprintf("%s/chat/completions", specialPlan.OpenAIBaseURL), nil
}
Comment on lines 95 to +111

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep special-plan routing mode-aware.

This early return sends every non-Claude special-plan request to /chat/completions, so embeddings, completions, responses, and any image relay that reaches this path will hit the wrong upstream endpoint once ChannelBaseUrl is ali-token-plan or ali-coding-plan. Move the OpenAIBaseURL override inside the RelayMode switch so each mode still maps to its own OpenAI-compatible path.

Suggested direction
 	default:
-		if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
-			return fmt.Sprintf("%s/chat/completions", specialPlan.OpenAIBaseURL), nil
-		}
+		openAIBaseURL := baseUrl
+		if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
+			openAIBaseURL = specialPlan.OpenAIBaseURL
+		}
 		switch info.RelayMode {
 		case constant.RelayModeEmbeddings:
+			if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
+				return fmt.Sprintf("%s/embeddings", openAIBaseURL), nil
+			}
 			return fmt.Sprintf("%s/compatible-mode/v1/embeddings", baseUrl), nil
 		case constant.RelayModeRerank:
 			return fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", baseUrl), nil
 		case constant.RelayModeResponses:
+			if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
+				return fmt.Sprintf("%s/responses", openAIBaseURL), nil
+			}
 			return fmt.Sprintf("%s/api/v2/apps/protocols/compatible-mode/v1/responses", baseUrl), nil
 		case constant.RelayModeImagesGenerations:
 			if isSyncImageModel(info.OriginModelName) {
 				return fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", baseUrl), nil
 			} else {
 				return fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", baseUrl), nil
 			}
 		case constant.RelayModeImagesEdits:
 			if isOldWanModel(info.OriginModelName) {
 				return fmt.Sprintf("%s/api/v1/services/aigc/image2image/image-synthesis", baseUrl), nil
 			} else if isWanModel(info.OriginModelName) {
 				return fmt.Sprintf("%s/api/v1/services/aigc/image-generation/generation", baseUrl), nil
 			} else {
 				return fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", baseUrl), nil
 			}
 		case constant.RelayModeCompletions:
+			if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
+				return fmt.Sprintf("%s/completions", openAIBaseURL), nil
+			}
 			return fmt.Sprintf("%s/compatible-mode/v1/completions", baseUrl), nil
 		default:
+			if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
+				return fmt.Sprintf("%s/chat/completions", openAIBaseURL), nil
+			}
 			return fmt.Sprintf("%s/compatible-mode/v1/chat/completions", baseUrl), nil
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@relay/channel/ali/adaptor.go` around lines 95 - 111, In GetRequestURL, the
special-plan OpenAIBaseURL override is applied before checking info.RelayMode,
causing non-Claude requests for plans like ali-token-plan to always route to
/chat/completions; move the specialPlan.OpenAIBaseURL handling inside the
RelayMode switch (the same place where Claude handling and
supportsAliAnthropicMessages are evaluated) so that for each RelayMode (e.g.,
RelayModeCompletion, RelayModeEmbedding, RelayModeResponse, image relay) you
return the mode-appropriate OpenAI-compatible path using
specialPlan.OpenAIBaseURL when present, falling back to baseUrl paths otherwise;
update logic around info.RelayFormat and info.RelayMode in GetRequestURL to
perform the OpenAIBaseURL override per mode rather than unconditionally.

switch info.RelayMode {
case constant.RelayModeEmbeddings:
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/embeddings", info.ChannelBaseUrl)
return fmt.Sprintf("%s/compatible-mode/v1/embeddings", baseUrl), nil
case constant.RelayModeRerank:
fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", baseUrl), nil
case constant.RelayModeResponses:
fullRequestURL = fmt.Sprintf("%s/api/v2/apps/protocols/compatible-mode/v1/responses", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v2/apps/protocols/compatible-mode/v1/responses", baseUrl), nil
case constant.RelayModeImagesGenerations:
if isSyncImageModel(info.OriginModelName) {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", baseUrl), nil
} else {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v1/services/aigc/text2image/image-synthesis", baseUrl), nil
}
case constant.RelayModeImagesEdits:
if isOldWanModel(info.OriginModelName) {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/image2image/image-synthesis", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v1/services/aigc/image2image/image-synthesis", baseUrl), nil
} else if isWanModel(info.OriginModelName) {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/image-generation/generation", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v1/services/aigc/image-generation/generation", baseUrl), nil
} else {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
return fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", baseUrl), nil
}
case constant.RelayModeCompletions:
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/completions", info.ChannelBaseUrl)
return fmt.Sprintf("%s/compatible-mode/v1/completions", baseUrl), nil
default:
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/chat/completions", info.ChannelBaseUrl)
return fmt.Sprintf("%s/compatible-mode/v1/chat/completions", baseUrl), nil
}
}

return fullRequestURL, nil
}

func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
channel.SetupApiRequestHeader(info, c, req)
req.Set("Authorization", "Bearer "+info.ApiKey)
if _, ok := channelconstant.ChannelSpecialBases[info.ChannelBaseUrl]; ok {
return nil
}
if info.IsStream {
req.Set("X-DashScope-SSE", "enable")
}
Expand All @@ -158,18 +170,9 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
if request == nil {
return nil, errors.New("request is nil")
}
// docs: https://bailian.console.aliyun.com/?tab=api#/api/?type=model&url=2712216
// fix: InternalError.Algo.InvalidParameter: The value of the enable_thinking parameter is restricted to True.
//if strings.Contains(request.Model, "thinking") {
// request.EnableThinking = true
// request.Stream = true
// info.IsStream = true
//}
//// fix: ali parameter.enable_thinking must be set to false for non-streaming calls
//if !info.IsStream {
// request.EnableThinking = false
//}

if _, ok := channelconstant.ChannelSpecialBases[info.ChannelBaseUrl]; ok {
return request, nil
}
switch info.RelayMode {
default:
aliReq := requestOpenAI2Ali(*request)
Expand Down Expand Up @@ -241,7 +244,7 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
switch info.RelayFormat {
case types.RelayFormatClaude:
if supportsAliAnthropicMessages(info.UpstreamModelName) {
if _, ok := channelconstant.ChannelSpecialBases[info.ChannelBaseUrl]; ok || supportsAliAnthropicMessages(info.UpstreamModelName) {
adaptor := claude.Adaptor{}
return adaptor.DoResponse(c, resp, info)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,24 @@ export function ChannelMutateDrawer({
},
})

const {
unlocked: aliApiEditUnlocked,
handleClick: handleAliApiConfigSecretClick,
reset: resetAliApiUnlock,
} = useHiddenClickUnlock({
requiredClicks: 10,
disabled: currentType !== 17,
onUnlock: () => {
toast.info(t('Ali custom API address editing unlocked'))
},
})

useEffect(() => {
if (!open) {
resetDoubaoApiUnlock()
resetAliApiUnlock()
}
}, [open, resetDoubaoApiUnlock])
}, [open, resetDoubaoApiUnlock, resetAliApiUnlock])

// Helper computed values
const isBatchMode =
Expand Down Expand Up @@ -616,6 +629,14 @@ export function ChannelMutateDrawer({
}
}

// Type 17 (Ali) - set default base_url
if (currentType === 17) {
const currentBaseUrlValue = form.getValues('base_url')
if (!currentBaseUrlValue || currentBaseUrlValue === '') {
form.setValue('base_url', 'https://dashscope.aliyuncs.com')
}
}
Comment on lines +632 to +638

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize base_url when switching to Ali type to avoid invalid sentinel carry-over

For type 17, defaulting only on empty values lets incompatible values (e.g. from other channel types like doubao-coding-plan) persist and be submitted. This can break upstream requests.

Suggested fix
+const ALI_DEFAULT_BASE_URL = 'https://dashscope.aliyuncs.com'
+const ALI_PRESET_BASE_URLS = new Set([
+  ALI_DEFAULT_BASE_URL,
+  'ali-token-plan',
+  'ali-coding-plan',
+])

  // Type 17 (Ali) - set default base_url
  if (currentType === 17) {
-   const currentBaseUrlValue = form.getValues('base_url')
-   if (!currentBaseUrlValue || currentBaseUrlValue === '') {
-     form.setValue('base_url', 'https://dashscope.aliyuncs.com')
-   }
+   const currentBaseUrlValue = (form.getValues('base_url') || '').trim()
+   if (!currentBaseUrlValue || !ALI_PRESET_BASE_URLS.has(currentBaseUrlValue)) {
+     form.setValue('base_url', ALI_DEFAULT_BASE_URL)
+   }
  }
📝 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
// Type 17 (Ali) - set default base_url
if (currentType === 17) {
const currentBaseUrlValue = form.getValues('base_url')
if (!currentBaseUrlValue || currentBaseUrlValue === '') {
form.setValue('base_url', 'https://dashscope.aliyuncs.com')
}
}
const ALI_DEFAULT_BASE_URL = 'https://dashscope.aliyuncs.com'
const ALI_PRESET_BASE_URLS = new Set([
ALI_DEFAULT_BASE_URL,
'ali-token-plan',
'ali-coding-plan',
])
// Type 17 (Ali) - set default base_url
if (currentType === 17) {
const currentBaseUrlValue = (form.getValues('base_url') || '').trim()
if (!currentBaseUrlValue || !ALI_PRESET_BASE_URLS.has(currentBaseUrlValue)) {
form.setValue('base_url', ALI_DEFAULT_BASE_URL)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`
around lines 632 - 638, When switching channel type to Ali (the currentType ===
17 branch in channel-mutate-drawer.tsx) we must normalize base_url instead of
only setting it when empty; update the logic in the currentType === 17 handler
(where form.getValues('base_url') and form.setValue('base_url',
'https://dashscope.aliyuncs.com') are used) to either validate the existing
base_url against an expected Ali pattern and replace it with the default if it
doesn't match, or simply overwrite it with 'https://dashscope.aliyuncs.com'
whenever currentType becomes 17 so incompatible sentinel values from other
channel types cannot persist.


// Type 18 (Xunfei) - set default other (version)
if (currentType === 18) {
const currentOther = form.getValues('other')
Expand Down Expand Up @@ -1728,6 +1749,76 @@ export function ChannelMutateDrawer({
/>
)}

{/* Ali/DashScope (type 17) */}
{currentType === 17 && !aliApiEditUnlocked && (
<FormField
control={form.control}
name='base_url'
render={({ field }) => (
<FormItem>
<FormLabel
className='cursor-pointer select-none'
onClick={handleAliApiConfigSecretClick}
>
Comment on lines +1759 to +1762

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the hidden unlock interaction keyboard-operable

The unlock trigger is click-only on a label; keyboard users cannot perform this interaction.

Suggested fix
<FormLabel
-  className='cursor-pointer select-none'
+  className='cursor-pointer select-none'
+  role='button'
+  tabIndex={0}
   onClick={handleAliApiConfigSecretClick}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.preventDefault()
+      handleAliApiConfigSecretClick()
+    }
+  }}
>

As per coding guidelines "Ensure keyboard operability and logical focus order; use ARIA attributes (aria-label, aria-expanded, aria-hidden) when necessary".

📝 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
<FormLabel
className='cursor-pointer select-none'
onClick={handleAliApiConfigSecretClick}
>
<FormLabel
className='cursor-pointer select-none'
role='button'
tabIndex={0}
onClick={handleAliApiConfigSecretClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
handleAliApiConfigSecretClick()
}
}}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`
around lines 1759 - 1762, The FormLabel currently only handles mouse clicks
(onClick={handleAliApiConfigSecretClick}) which is not keyboard-accessible; make
the unlock trigger keyboard-operable by giving the FormLabel a semantics and
keyboard handlers: add a focusable attribute (tabIndex=0), semantic role
(role="button"), an onKeyDown handler that calls handleAliApiConfigSecretClick
when Enter or Space is pressed, and include appropriate ARIA attributes (e.g.,
aria-label describing the action and aria-expanded or aria-pressed as
applicable) so screen readers and keyboard users can operate the hidden-unlock
control.

{t('API Base URL *')}
</FormLabel>
<Select
onValueChange={field.onChange}
value={
field.value || 'https://dashscope.aliyuncs.com'
}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='https://dashscope.aliyuncs.com'>
{t('https://dashscope.aliyuncs.com')}
</SelectItem>
<SelectItem value='ali-token-plan'>
{t('Ali Token Plan')}
</SelectItem>
<SelectItem value='ali-coding-plan'>
{t('Ali Coding Plan')}
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
{t('Select the API endpoint')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}

{/* Ali/DashScope (type 17) - Custom API URL (unlocked) */}
{currentType === 17 && aliApiEditUnlocked && (
<FormField
control={form.control}
name='base_url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('API Base URL *')}</FormLabel>
<FormControl>
<Input
placeholder={t(
'e.g., https://dashscope.aliyuncs.com'
)}
{...field}
/>
</FormControl>
<FormDescription>
{t('Enter custom API endpoint URL')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}

{/* Coze (type 49) */}
{currentType === 49 && (
<FormField
Expand All @@ -1752,7 +1843,7 @@ export function ChannelMutateDrawer({
)}

{/* General base_url for other types */}
{![3, 8, 22, 36, 45].includes(currentType) && (
{![3, 8, 17, 22, 36, 45].includes(currentType) && (
<FormField
control={form.control}
name='base_url'
Expand Down
Loading