Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/guides/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/guides/user-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。
Expand Down
4 changes: 3 additions & 1 deletion src/canvas/ModelServiceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,9 @@ export function ModelServiceForm({
/>
</label>
<p className="cw-model-note" id={`${connectionFieldId}-url-note`}>
{t("models.service.urlNote")}
{t(presetId === "bailian-beijing"
? "models.service.beijingWorkspaceUrl"
: "models.service.urlNote")}
</p>
</div>
<div className="cw-model-connection-field">
Expand Down
17 changes: 16 additions & 1 deletion src/canvas/providerPresets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const EN_NAMES: Record<string, string> = {
"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",
Expand Down Expand Up @@ -58,6 +59,7 @@ export interface ProviderPreset {
optionLabel?: string;
kinds: ModelKind[];
url: string;
workspaceUrlPattern?: RegExp;
protocol: ModelConfigInput["protocol"];
connectionType: ModelConfigInput["connectionType"];
authMode: ModelConfigInput["authMode"];
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/messages/en-US/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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://<workspace-id>.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",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/messages/zh-CN/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export const models = {
"models.service.show": "显示",
"models.service.urlNote":
"自部署服务通常填写 /v1 地址;通过 SSH 隧道连接时填写本地转发地址。",
"models.service.beijingWorkspaceUrl":
"使用北京地域的 API Key。如需使用业务空间专属地址,请将 https://<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1 中的 <workspace-id> 替换为你的业务空间 ID。",
"models.service.inputCapabilities": "输入能力",
"models.service.referenceOutput": "参考与输出能力",
"models.service.textInput": "文本",
Expand Down
40 changes: 40 additions & 0 deletions tests/providerPresets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions tests/test_native_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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({
Expand All @@ -473,15 +480,15 @@ 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,
'delta': {'reasoning_content': 'Check the request.'}, 'finish_reason': None}]}
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)
Expand Down
Loading