diff --git a/docs/guides/user-guide.md b/docs/guides/user-guide.md
index 0faf8aff..7ca3ec6d 100644
--- a/docs/guides/user-guide.md
+++ b/docs/guides/user-guide.md
@@ -27,6 +27,8 @@ A service can provide multiple models within the same category. After entering i
The service URL and Model ID must match your model service. Self-hosted service URLs typically include `/v1`; use the model name actually exposed by the service as the Model ID. For third-party services, follow the information provided by the service.
+For Qwen assistant chat, choose **Alibaba Cloud Model Studio (Beijing)** and enter a Beijing API key. The preset selects `qwen3.8-max`. You can keep the shared Beijing URL or enter your Beijing workspace's OpenAI-compatible Base URL ending in `/compatible-mode/v1`. [Alibaba Cloud endpoint guide](https://help.aliyun.com/en/model-studio/compatibility-of-openai-with-dashscope)
+
For a self-hosted chat service, allow at least 8192 tokens of context: the
application sends `max_tokens: 8192`, so a smaller `--max-model-len` (for
example vLLM's 4096 default) rejects every request with HTTP 400.
diff --git a/docs/guides/user-guide.zh-CN.md b/docs/guides/user-guide.zh-CN.md
index 31be205f..e5a6b74c 100644
--- a/docs/guides/user-guide.zh-CN.md
+++ b/docs/guides/user-guide.zh-CN.md
@@ -27,6 +27,8 @@
服务地址和 Model ID 应与自己的模型服务一致。自部署服务的地址通常包含 `/v1`,Model ID 使用服务实际公开的模型名称;第三方服务按其提供的信息填写。
+使用千问对话助手时,选择 **阿里云百炼(北京)** 并填写北京地域的 API Key。预设会选中 `qwen3.8-max`。服务地址可保留默认的北京通用地址,也可填写以 `/compatible-mode/v1` 结尾的北京业务空间专属地址。[百炼 OpenAI 兼容接口指南](https://help.aliyun.com/zh/model-studio/compatibility-of-openai-with-dashscope)
+
自部署的对话服务需要至少 8192 token 的上下文:应用固定发送 `max_tokens: 8192`,
若服务端的 `--max-model-len` 更小(例如 vLLM 的 4096 默认值),每次请求都会返回
HTTP 400。
diff --git a/src/canvas/ModelServiceForm.tsx b/src/canvas/ModelServiceForm.tsx
index c3d775ad..70832187 100644
--- a/src/canvas/ModelServiceForm.tsx
+++ b/src/canvas/ModelServiceForm.tsx
@@ -437,7 +437,9 @@ export function ModelServiceForm({
/>
- {t("models.service.urlNote")}
+ {t(presetId === "bailian-beijing"
+ ? "models.service.beijingWorkspaceUrl"
+ : "models.service.urlNote")}
diff --git a/src/canvas/providerPresets.ts b/src/canvas/providerPresets.ts
index 08bea610..f5a48160 100644
--- a/src/canvas/providerPresets.ts
+++ b/src/canvas/providerPresets.ts
@@ -7,6 +7,7 @@ const EN_NAMES: Record = {
"OpenAI 兼容": "OpenAI compatible",
"MiniMax 官方视频": "MiniMax official video",
"智谱 BigModel": "Zhipu BigModel",
+ "阿里云百炼(北京)": "Alibaba Cloud Model Studio (Beijing)",
"可灵官方视频": "Kling official video",
"MiniMax 官方图片": "MiniMax Official Images",
"MOSS 开放平台": "MOSS Open Platform",
@@ -58,6 +59,7 @@ export interface ProviderPreset {
optionLabel?: string;
kinds: ModelKind[];
url: string;
+ workspaceUrlPattern?: RegExp;
protocol: ModelConfigInput["protocol"];
connectionType: ModelConfigInput["connectionType"];
authMode: ModelConfigInput["authMode"];
@@ -131,6 +133,18 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [
connectionType: "api",
authMode: "bearer",
},
+ {
+ id: "bailian-beijing",
+ logoOrganization: "Qwen",
+ name: "阿里云百炼(北京)",
+ kinds: ["agent"],
+ url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ workspaceUrlPattern: /^https:\/\/[a-z0-9-]+\.cn-beijing\.maas\.aliyuncs\.com\/compatible-mode\/v1$/,
+ protocol: "openai",
+ connectionType: "api",
+ authMode: "bearer",
+ modelsByKind: { agent: ["qwen3.8-max"] },
+ },
{
id: "zhipu",
logoOrganization: "zai-org",
@@ -225,12 +239,13 @@ export function providerPresetFor(model: ModelConfig): string {
);
if (deployed && model.connectionType === "self_hosted") return deployed.id;
if (model.connectionType === "self_hosted") return "custom";
+ const url = model.url.replace(/\/+$/, "");
return (
PROVIDER_PRESETS.find(
(preset) =>
preset.kinds.includes(model.kind) &&
preset.protocol === model.protocol &&
- preset.url === model.url.replace(/\/+$/, ""),
+ (preset.url === url || preset.workspaceUrlPattern?.test(url)),
)?.id ?? "custom"
);
}
diff --git a/src/i18n/messages/en-US/models.ts b/src/i18n/messages/en-US/models.ts
index 6e2d6c99..8188f0f3 100644
--- a/src/i18n/messages/en-US/models.ts
+++ b/src/i18n/messages/en-US/models.ts
@@ -72,6 +72,8 @@ export const models = {
"models.service.show": "Show",
"models.service.urlNote":
"Self-hosted services usually use a /v1 URL; when connecting through an SSH tunnel, enter the local forwarded address.",
+ "models.service.beijingWorkspaceUrl":
+ "Use a Beijing API key. To use your workspace endpoint, enter https://.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 with your workspace ID.",
"models.service.inputCapabilities": "Input capabilities",
"models.service.referenceOutput": "References and output",
"models.service.textInput": "Text",
diff --git a/src/i18n/messages/zh-CN/models.ts b/src/i18n/messages/zh-CN/models.ts
index 2e9c132c..cf39e6db 100644
--- a/src/i18n/messages/zh-CN/models.ts
+++ b/src/i18n/messages/zh-CN/models.ts
@@ -63,6 +63,8 @@ export const models = {
"models.service.show": "显示",
"models.service.urlNote":
"自部署服务通常填写 /v1 地址;通过 SSH 隧道连接时填写本地转发地址。",
+ "models.service.beijingWorkspaceUrl":
+ "使用北京地域的 API Key。如需使用业务空间专属地址,请将 https://.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 中的 替换为你的业务空间 ID。",
"models.service.inputCapabilities": "输入能力",
"models.service.referenceOutput": "参考与输出能力",
"models.service.textInput": "文本",
diff --git a/tests/providerPresets.test.ts b/tests/providerPresets.test.ts
index 6c66bf57..ae72c77c 100644
--- a/tests/providerPresets.test.ts
+++ b/tests/providerPresets.test.ts
@@ -17,6 +17,46 @@ test("provider presets separate official protocol from self-hosted model names",
assert.deepEqual(matchingConnections([{ ...model, providerId: "shared" }, { ...model, id: "two", providerId: "shared" }, { ...model, id: "three", providerId: "other" }], "video", { ...model, protocol: "minimax_video_v2", authMode: "bearer", apiKey: "" }).map((item) => item.id), ["one", "three"]);
});
+test("Beijing Model Studio offers Qwen chat in both languages", () => {
+ const preset = PROVIDER_PRESETS.find((item) => item.id === "bailian-beijing");
+ assert.ok(preset);
+ assert.deepEqual(preset.kinds, ["agent"]);
+ assert.deepEqual(preset.modelsByKind, { agent: ["qwen3.8-max"] });
+ assert.equal(preset.url, "https://dashscope.aliyuncs.com/compatible-mode/v1");
+ assert.equal(preset.connectionType, "api");
+ assert.equal(preset.protocol, "openai");
+ assert.equal(preset.authMode, "bearer");
+ const previous = getActiveLocale();
+ try {
+ for (const [locale, label] of [["zh-CN", "阿里云百炼(北京)"], ["en-US", "Alibaba Cloud Model Studio (Beijing)"]] as const) {
+ setActiveLocale(locale);
+ assert.equal(providerOptions("agent").find((item) => item.id === preset.id)?.name, label);
+ }
+ } finally {
+ setActiveLocale(previous);
+ }
+});
+
+test("saved Beijing workspace connections retain their preset", () => {
+ const model: ModelConfig = {
+ id: "saved-qwen", kind: "agent", modelId: "qwen3.8-max", connectionType: "api",
+ protocol: "openai", authMode: "bearer", url: "https://dashscope.aliyuncs.com/compatible-mode/v1/",
+ inputRules: { profile: "agent", allowedKinds: [], maxImages: 0, maxVideos: 0, maxAudios: 0, videoModes: [] },
+ };
+ assert.equal(providerPresetFor(model), "bailian-beijing");
+ const workspace = "https://ws-example.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
+ assert.equal(providerPresetFor({ ...model, url: workspace + "/" }), "bailian-beijing");
+ for (const url of [
+ workspace.replace("cn-beijing", "ap-southeast-1"),
+ workspace.replace("aliyuncs.com", "aliyuncs.com.example.org"),
+ workspace.replace("/compatible-mode/v1", "/v1"),
+ "https://proxy.example.org/v1",
+ ]) {
+ assert.equal(providerPresetFor({ ...model, url }), "custom");
+ }
+ assert.equal(providerPresetFor({ ...model, kind: "image", url: workspace }), "custom");
+});
+
test("MOSS presets offer the supported image and speech models by kind", () => {
const preset = PROVIDER_PRESETS.find((item) => item.id === "moss");
assert.ok(preset);
diff --git a/tests/test_native_runtime.py b/tests/test_native_runtime.py
index c02640a9..d62cbdb3 100644
--- a/tests/test_native_runtime.py
+++ b/tests/test_native_runtime.py
@@ -445,12 +445,17 @@ async def no_skills(*args):
def test_real_opencode_native_skill_and_gateway_write(tmp_path, runtime, resume, monkeypatch, real_opencode_worker):
if not real_opencode_worker:
return
+ # Exercise the Beijing workspace endpoint with an in-process provider double.
+ base_url = 'https://ws-test.cn-beijing.maas.aliyuncs.com/compatible-mode/v1'
+ model_id = 'qwen3.8-max'
requests = []
write_receipts = []
def model(request):
payload = json.loads(request.content)
if payload.get('tools'):
requests.append(payload)
+ assert str(request.url) == f'{base_url}/chat/completions'
+ assert payload['model'] == model_id and payload['stream'] is True
assert request.headers['authorization'] == 'Bearer server-only-key'
count = len(requests)
print(f'stage: model request {count} (tools={bool(payload.get("tools"))})', flush=True)
@@ -460,6 +465,8 @@ def model(request):
assert any(tool['function']['name'] == 'skill' for tool in payload['tools'])
call = {'name':'skill','arguments':json.dumps({'name':'character-replacement'})}
elif count == 2:
+ previous = next(message for message in payload['messages'] if message.get('tool_calls'))
+ assert previous['reasoning_content'] == 'Check the request.'
assert 'Create a 24-FPS constant-frame-rate working copy' in json.dumps(payload['messages'])
assert not any('opendirector_' in tool['function']['name'] for tool in payload['tools'])
call = {'name': 'bash', 'arguments': json.dumps({
@@ -473,7 +480,7 @@ def model(request):
write_receipts.append(receipt)
call = None
delta = {'tool_calls':[{'index':0,'id':f'call-{count}','type':'function','function':call}]} if call else {'content':'OpenCode ready.'}
- chunk = {'id':f'completion-{count}','object':'chat.completion.chunk','created':1,'model':'test-model',
+ chunk = {'id':f'completion-{count}','object':'chat.completion.chunk','created':1,'model':model_id,
'choices':[{'index':0,'delta':delta,'finish_reason':None}]}
finish = {**chunk,'choices':[{'index':0,'delta':{},'finish_reason':'tool_calls' if call else 'stop'}]}
reasoning = {**chunk, 'choices': [{'index': 0,
@@ -481,7 +488,7 @@ def model(request):
return httpx.Response(200,headers={'content-type':'text/event-stream'},content=(
'data: '+json.dumps(reasoning)+'\n\ndata: '+json.dumps(chunk)+'\n\ndata: '+json.dumps(finish)+'\n\ndata: [DONE]\n\n').encode())
local = LocalProjects(tmp_path)
- config = ModelConfigs(tmp_path).save('agent','http://test.invalid/v1','test-model','server-only-key')
+ config = ModelConfigs(tmp_path).save('agent',base_url,model_id,'server-only-key')
provider = ChatCompletions(httpx.MockTransport(model))
with TestClient(create_app(local, chat_provider=provider), headers={'X-OpenDirector-Request':'1','X-OpenDirector-Session':str(uuid4())}) as client:
path = project(client)