-
Notifications
You must be signed in to change notification settings - Fork 89
feat(bailian): add Bailian (Alibaba Cloud) as an AI provider #421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
F915
wants to merge
11
commits into
Zoo-Code-Org:main
Choose a base branch
from
F915:bailian
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+21,858
−18,435
Draft
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
8cc5b2f
feat(bailian): add Bailian (Alibaba Cloud) as an AI provider (#420)(#…
F915 32b64ba
fix(bailian): initialize binary reasoning state, add null guard, fix …
F915 ef6b1f9
fix(bailian): add missing workspaceId tests, fix supportsPromptCache …
F915 719c0a8
feat(bailian): dynamic model fetching, provider hardening, and bug fi…
F915 6e22197
docs: add implementation plan for 4 coderabbit review fixes
F915 4eb9654
fix(bailian): use matched preset metadata for versioned model IDs on …
F915 73241c7
docs(bailian): document why shared cache key across regions is safe
F915 b160cbb
test(bailian): add suiteTeardown to restore default provider config
F915 1ec1a0b
fix(bailian): read region/workspaceId from message.values for refresh…
F915 141b567
test(bailian): add bailian:{} to requestRouterModels test assertions
F915 e0c26f5
fix(bailian): show correct model params for auto-fetched models in UI…
F915 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "fixtures": [] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| import * as assert from "assert" | ||
|
|
||
| import { RooCodeEventName, type ClineMessage } from "@roo-code/types" | ||
|
|
||
| import { setDefaultSuiteTimeout } from "../test-utils" | ||
| import { waitUntilCompleted } from "../utils" | ||
|
|
||
| const BAILIAN_API_KEY = process.env.BAILIAN_API_KEY | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Fetch interceptor | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** @typedef {{ model?: string; enable_thinking?: boolean; thinking_budget?: number; reasoning_effort?: string; probeTag?: string }} BailianRequestCapture */ | ||
|
|
||
| type BailianRequestCapture = { | ||
| url?: string | ||
| model?: string | ||
| enable_thinking?: boolean | ||
| thinking_budget?: number | ||
| reasoning_effort?: string | ||
| probeTag?: string | ||
| } | ||
|
|
||
| /** | ||
| * @param {BailianRequestCapture[]} capture | ||
| * @param {boolean} [passthrough] | ||
| * @returns {() => void} restore function | ||
| */ | ||
| function installBailianFetchInterceptor(capture: BailianRequestCapture[], passthrough?: boolean): () => void { | ||
| const original = globalThis.fetch | ||
|
|
||
| globalThis.fetch = async function (input: RequestInfo | URL, init?: RequestInit) { | ||
| const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url | ||
|
|
||
| const isBailianUrl = url.includes("dashscope.aliyuncs.com") || url.includes("maas.aliyuncs.com") | ||
|
|
||
| if (isBailianUrl && url.includes("/chat/completions")) { | ||
| const body = init?.body ? JSON.parse(init.body as string) : {} | ||
| const messages = body.messages ?? [] | ||
| const allMessagesText = JSON.stringify(messages) | ||
| const probeTag = allMessagesText.match(/bailian-e2e:[^"\s]+/)?.[0] | ||
|
|
||
| capture.push({ | ||
| url, | ||
| model: body.model, | ||
| enable_thinking: body.enable_thinking, | ||
| thinking_budget: body.thinking_budget, | ||
| reasoning_effort: body.reasoning_effort, | ||
| probeTag, | ||
| }) | ||
|
|
||
| if (passthrough) { | ||
| return original.call(globalThis, input, init) | ||
| } | ||
|
|
||
| // In mock mode, return a simple SSE response with a completion | ||
| const enc = new TextEncoder() | ||
| const body2 = new ReadableStream({ | ||
| start(controller) { | ||
| controller.enqueue( | ||
| enc.encode( | ||
| `data: ${JSON.stringify({ | ||
| id: "chatcmpl-mock", | ||
| object: "chat.completion.chunk", | ||
| created: Math.floor(Date.now() / 1000), | ||
| model: body.model || "qwen3.6-plus", | ||
| choices: [{ index: 0, delta: { content: "bailian-e2e:" }, finish_reason: null }], | ||
| })}\n\n`, | ||
| ), | ||
| ) | ||
| controller.enqueue( | ||
| enc.encode( | ||
| `data: ${JSON.stringify({ | ||
| id: "chatcmpl-mock", | ||
| object: "chat.completion.chunk", | ||
| created: Math.floor(Date.now() / 1000), | ||
| model: body.model || "qwen3.6-plus", | ||
| choices: [{ index: 0, delta: { content: "mock-ok" }, finish_reason: null }], | ||
| })}\n\n`, | ||
| ), | ||
| ) | ||
| controller.enqueue( | ||
| enc.encode( | ||
| `data: ${JSON.stringify({ | ||
| id: "chatcmpl-mock", | ||
| object: "chat.completion.chunk", | ||
| created: Math.floor(Date.now() / 1000), | ||
| model: body.model || "qwen3.6-plus", | ||
| choices: [{ index: 0, delta: {}, finish_reason: "stop" }], | ||
| usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, | ||
| })}\n\n`, | ||
| ), | ||
| ) | ||
| controller.enqueue(enc.encode("data: [DONE]\n\n")) | ||
| controller.close() | ||
| }, | ||
| }) | ||
| return new Response(body2, { | ||
| status: 200, | ||
| headers: { "content-type": "text/event-stream" }, | ||
| }) | ||
| } | ||
|
|
||
| return original.call(globalThis, input, init) | ||
| } as typeof globalThis.fetch | ||
|
|
||
| return () => { | ||
| globalThis.fetch = original | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Suite | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| describe("Bailian Provider", function () { | ||
| setDefaultSuiteTimeout(this) | ||
|
|
||
| /** @type {import("../../../src/extension/api").API} */ | ||
| let api: any | ||
|
|
||
| /** @type {import("../../../src/extension/api").ProviderSettings} */ | ||
| let originalConfig: any | ||
|
|
||
| before(async function () { | ||
| api = globalThis.api | ||
| if (!api) { | ||
| throw new Error("E2E API not found — ensure the test runner initializes globalThis.api") | ||
| } | ||
| originalConfig = await api.getConfiguration() | ||
| }) | ||
|
|
||
| suiteTeardown(async function () { | ||
| if (originalConfig) { | ||
| await api.setConfiguration(originalConfig) | ||
| } | ||
| }) | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // Beijing region — basic streaming smoke test | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| it("completes a task on Beijing region with Qwen model", async function () { | ||
| const requests: BailianRequestCapture[] = [] | ||
| const restore = installBailianFetchInterceptor(requests) | ||
|
|
||
| const messages: ClineMessage[] = [] | ||
| const messageHandler = ({ message }: { message: ClineMessage }) => { | ||
| if (message.type === "say" && message.partial === false) { | ||
| messages.push(message) | ||
| } | ||
| } | ||
| api.on(RooCodeEventName.Message, messageHandler) | ||
|
|
||
| try { | ||
| await api.setConfiguration({ | ||
| apiProvider: "bailian", | ||
| bailianApiKey: BAILIAN_API_KEY ?? "mock-key", | ||
| bailianRegion: "beijing", | ||
| apiModelId: "qwen3.6-plus", | ||
| enableReasoningEffort: false, | ||
| }) | ||
|
|
||
| const taskId = await api.startNewTask({ | ||
| configuration: { mode: "ask", autoApprovalEnabled: true }, | ||
| text: "bailian-e2e:beijing-basic: echo 'hello'", | ||
| }) | ||
| await waitUntilCompleted({ api, taskId }) | ||
|
|
||
| const completion = messages.find((m) => m.type === "say" && m.say === "completion_result") | ||
| assert.ok(completion, "Task should complete successfully") | ||
| assert.ok( | ||
| completion.text?.includes("bailian-e2e:mock-ok") || completion.text?.includes("mock-ok"), | ||
| `Completion should contain mock response, got: ${completion.text?.slice(0, 200)}`, | ||
| ) | ||
| } finally { | ||
| api.off(RooCodeEventName.Message, messageHandler) | ||
| restore() | ||
| } | ||
| }) | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // DeepSeek V4 reasoning_effort parameter | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| it("sends reasoning_effort for DeepSeek V4 model", async function () { | ||
| const requests: BailianRequestCapture[] = [] | ||
| const restore = installBailianFetchInterceptor(requests) | ||
|
|
||
| const messages: ClineMessage[] = [] | ||
| const messageHandler = ({ message }: { message: ClineMessage }) => { | ||
| if (message.type === "say" && message.partial === false) { | ||
| messages.push(message) | ||
| } | ||
| } | ||
| api.on(RooCodeEventName.Message, messageHandler) | ||
|
|
||
| try { | ||
| await api.setConfiguration({ | ||
| apiProvider: "bailian", | ||
| bailianApiKey: BAILIAN_API_KEY ?? "mock-key", | ||
| bailianRegion: "beijing", | ||
| apiModelId: "deepseek-v4-pro", | ||
| reasoningEffort: "high", | ||
| }) | ||
|
|
||
| const taskId = await api.startNewTask({ | ||
| configuration: { mode: "ask", autoApprovalEnabled: true }, | ||
| text: "bailian-e2e:deepseek-reasoning: echo 'hello'", | ||
| }) | ||
| await waitUntilCompleted({ api, taskId }) | ||
|
|
||
| const reasoningRequest = requests.find((r) => r.reasoning_effort === "high") | ||
| assert.ok(reasoningRequest, "Should send reasoning_effort: high for DeepSeek V4 model") | ||
| } finally { | ||
| api.off(RooCodeEventName.Message, messageHandler) | ||
| restore() | ||
| } | ||
| }) | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // Binary reasoning enable_thinking parameter (Qwen) | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| it("sends enable_thinking for binary reasoning model (Qwen)", async function () { | ||
| const requests: BailianRequestCapture[] = [] | ||
| const restore = installBailianFetchInterceptor(requests) | ||
|
|
||
| const messages: ClineMessage[] = [] | ||
| const messageHandler = ({ message }: { message: ClineMessage }) => { | ||
| if (message.type === "say" && message.partial === false) { | ||
| messages.push(message) | ||
| } | ||
| } | ||
| api.on(RooCodeEventName.Message, messageHandler) | ||
|
|
||
| try { | ||
| await api.setConfiguration({ | ||
| apiProvider: "bailian", | ||
| bailianApiKey: BAILIAN_API_KEY ?? "mock-key", | ||
| bailianRegion: "beijing", | ||
| apiModelId: "qwen3.7-max", | ||
| enableReasoningEffort: true, | ||
| }) | ||
|
|
||
| const taskId = await api.startNewTask({ | ||
| configuration: { mode: "ask", autoApprovalEnabled: true }, | ||
| text: "bailian-e2e:qwen-thinking: echo 'hello'", | ||
| }) | ||
| await waitUntilCompleted({ api, taskId }) | ||
|
|
||
| const thinkingRequest = requests.find((r) => r.enable_thinking === true) | ||
| assert.ok(thinkingRequest, "Should send enable_thinking: true for Qwen binary reasoning model") | ||
| } finally { | ||
| api.off(RooCodeEventName.Message, messageHandler) | ||
| restore() | ||
| } | ||
| }) | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // Workspace ID for Frankfurt region | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| it("uses Frankfurt workspaceId-based URL", async function () { | ||
| const requests: BailianRequestCapture[] = [] | ||
| const restore = installBailianFetchInterceptor(requests) | ||
|
|
||
| const messages: ClineMessage[] = [] | ||
| const messageHandler = ({ message }: { message: ClineMessage }) => { | ||
| if (message.type === "say" && message.partial === false) { | ||
| messages.push(message) | ||
| } | ||
| } | ||
| api.on(RooCodeEventName.Message, messageHandler) | ||
|
|
||
| try { | ||
| await api.setConfiguration({ | ||
| apiProvider: "bailian", | ||
| bailianApiKey: BAILIAN_API_KEY ?? "mock-key", | ||
| bailianRegion: "frankfurt", | ||
| bailianWorkspaceId: "ws-test-123", | ||
| apiModelId: "qwen3.6-flash", | ||
| enableReasoningEffort: false, | ||
| }) | ||
|
|
||
| const taskId = await api.startNewTask({ | ||
| configuration: { mode: "ask", autoApprovalEnabled: true }, | ||
| text: "bailian-e2e:frankfurt: echo 'hello'", | ||
| }) | ||
| await waitUntilCompleted({ api, taskId }) | ||
|
|
||
| assert.ok( | ||
| messages.some((m) => m.type === "say" && m.say === "completion_result"), | ||
| "Task should complete with Frankfurt endpoint", | ||
| ) | ||
| const frankfurtRequest = requests.find((r) => r.url?.includes("ws-test-123.eu-central-1.maas.aliyuncs.com")) | ||
| assert.ok(frankfurtRequest, "Should use workspaceId-prefixed URL for Frankfurt region") | ||
| } finally { | ||
| api.off(RooCodeEventName.Message, messageHandler) | ||
| restore() | ||
| } | ||
| }) | ||
| }) | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restore the default provider config in suite teardown.
This suite persists Bailian settings via
api.setConfiguration(...)but never restores the default OpenRouter config, so later e2e suites can inherit Bailian unexpectedly.As per coding guidelines, "Always restore the default OpenRouter config in
suiteTeardownwhen tests change persisted provider/model settings, so subsequent suites are unaffected."🤖 Prompt for AI Agents