diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 1d91d33ef..84c413d0f 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -397,6 +397,12 @@ common provider keys into worker containers: - `AI_GATEWAY_API_KEY` (Vercel AI Gateway, `vercel/...` models) - `OPENAI_API_KEY` - `ANTHROPIC_API_KEY` +- `DASHSCOPE_API_KEY` (Alibaba Cloud Model Studio, `alibaba/...` models, with + `ALIBABA_REGION`) +- `ALIBABA_CODING_PLAN_API_KEY` (Alibaba Coding Plan, + `alibaba-coding-plan/...` models, with `ALIBABA_CODING_PLAN_REGION`) +- `ALIBABA_TOKEN_PLAN_API_KEY` (Alibaba Token Plan, + `alibaba-token-plan/...` models, with `ALIBABA_TOKEN_PLAN_REGION`) - `MOONSHOT_API_KEY` - `KIMI_API_KEY` (Kimi for Coding, `kimi-for-coding/...` models) - `MINIMAX_API_KEY` diff --git a/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts b/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts index d05d701dd..954fa349a 100644 --- a/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts +++ b/apps/api/src/handlers/inference/__tests__/inference-gateway.test.ts @@ -125,6 +125,9 @@ describe('inference gateway', () => { // Region lookups resolve separately from API keys. if ( nameList.includes('AWS_REGION') || + nameList.includes('ALIBABA_REGION') || + nameList.includes('ALIBABA_CODING_PLAN_REGION') || + nameList.includes('ALIBABA_TOKEN_PLAN_REGION') || nameList.includes('ZAI_REGION') || nameList.includes('ZAI_CODING_PLAN_REGION') ) { @@ -232,6 +235,18 @@ describe('inference gateway', () => { const app = createApp(createRunToken()); const cases: Array<[string, string]> = [ + [ + 'alibaba', + 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions', + ], + [ + 'alibaba-coding-plan', + 'https://coding-intl.dashscope.aliyuncs.com/v1/chat/completions', + ], + [ + 'alibaba-token-plan', + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/chat/completions', + ], ['requesty', 'https://router.requesty.ai/v1/chat/completions'], ['baseten', 'https://inference.baseten.co/v1/chat/completions'], ['togetherai', 'https://api.together.xyz/v1/chat/completions'], @@ -259,6 +274,50 @@ describe('inference gateway', () => { } }); + it('proxies Alibaba providers to their China endpoints when selected', async () => { + const cases = [ + { + providerId: 'alibaba', + regionEnvVarName: 'ALIBABA_REGION', + expectedUrl: + 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', + }, + { + providerId: 'alibaba-coding-plan', + regionEnvVarName: 'ALIBABA_CODING_PLAN_REGION', + expectedUrl: + 'https://coding.dashscope.aliyuncs.com/v1/chat/completions', + }, + { + providerId: 'alibaba-token-plan', + regionEnvVarName: 'ALIBABA_TOKEN_PLAN_REGION', + expectedUrl: + 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions', + }, + ]; + + for (const { providerId, regionEnvVarName, expectedUrl } of cases) { + mockResolveModelProviderEnvValue.mockImplementation( + async (names: string | readonly string[]) => { + const nameList = typeof names === 'string' ? [names] : names; + return nameList.includes(regionEnvVarName) + ? 'china' + : 'provider-secret-key'; + }, + ); + const fetchMock = stubUpstreamFetch(); + + const response = await postMessages( + createApp(createRunToken()), + `/api/inference/${providerId}/v1/chat/completions`, + ); + + expect(response.status).toBe(200); + const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe(expectedUrl); + } + }); + it.each([ [ 'azure', diff --git a/apps/docs/docs.json b/apps/docs/docs.json index 07ef19fff..2e3196777 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -46,6 +46,9 @@ "expanded": false, "pages": [ "models", + "providers/inference/alibaba-coding-plan", + "providers/inference/alibaba-model-studio", + "providers/inference/alibaba-token-plan", "providers/inference/amazon-bedrock", "providers/inference/anthropic", "providers/inference/azure-foundry", diff --git a/apps/docs/environment-variables.mdx b/apps/docs/environment-variables.mdx index 911b4a86b..f6fb7a863 100644 --- a/apps/docs/environment-variables.mdx +++ b/apps/docs/environment-variables.mdx @@ -184,6 +184,12 @@ as per-task auth tokens or workspace paths. | `AZURE_COGNITIVE_SERVICES_API_KEY` | Provider key | Azure AI Foundry / AI Services API key. | | `AZURE_COGNITIVE_SERVICES_RESOURCE_NAME` | Provider config | Azure AI Foundry / AI Services resource name, without the domain or URL. | | `ANTHROPIC_API_KEY` | Provider key | Anthropic API key. | +| `DASHSCOPE_API_KEY` | Provider key | Alibaba Cloud Model Studio API key for `alibaba/...` models in the region selected by `ALIBABA_REGION`. | +| `ALIBABA_REGION` | Provider config | Alibaba Model Studio region: `global` (International) or `china`. Defaults to `global` when unset. | +| `ALIBABA_CODING_PLAN_API_KEY` | Provider key | Alibaba Coding Plan API key for `alibaba-coding-plan/...` models. | +| `ALIBABA_CODING_PLAN_REGION` | Provider config | Alibaba Coding Plan region: `global` (International) or `china`. Defaults to `global` when unset. | +| `ALIBABA_TOKEN_PLAN_API_KEY` | Provider key | Alibaba Token Plan API key for `alibaba-token-plan/...` models. | +| `ALIBABA_TOKEN_PLAN_REGION` | Provider config | Alibaba Token Plan region: `global` (International) or `china`. Defaults to `global` when unset. | | `XAI_API_KEY` | Provider key | xAI / Grok API key. Optional when a SuperGrok / eligible X Premium+ subscription is connected from **Settings > Models**. | | `XAI_OAUTH_CLIENT_ID` | Optional | Override for the public Grok CLI OAuth client id used by the SuperGrok device-code connect flow. Defaults to the public CLI client id. | | `MOONSHOT_API_KEY` | Provider key | Moonshot AI / Kimi Open Platform API key. | diff --git a/apps/docs/models.mdx b/apps/docs/models.mdx index 64ce7fa69..dc7946e21 100644 --- a/apps/docs/models.mdx +++ b/apps/docs/models.mdx @@ -42,6 +42,7 @@ These connections use metered API billing or a provider-managed gateway: | Provider | Credential | Cost source | | --- | --- | --- | +| [Alibaba Cloud Model Studio (Qwen)](/providers/inference/alibaba-model-studio) | Model Studio API key | Alibaba Cloud account | | [Amazon Bedrock](/providers/inference/amazon-bedrock) | Mantle API key and optional AWS region | AWS account | | [Anthropic](/providers/inference/anthropic) | Anthropic API key | Anthropic API organization | | [Azure AI Foundry](/providers/inference/azure-foundry) | Azure AI Services API key and resource name | Azure subscription | @@ -66,6 +67,8 @@ general API balance: | Provider | Connection | Usage behavior | | --- | --- | --- | | [ChatGPT Subscription](/providers/inference/chatgpt) | OpenAI device authorization | ChatGPT plan windows | +| [Alibaba Coding Plan](/providers/inference/alibaba-coding-plan) | Regional Coding Plan API key | Coding Plan allowance | +| [Alibaba Token Plan](/providers/inference/alibaba-token-plan) | Regional Token Plan API key | Token Plan allowance | | [GitHub Copilot](/providers/inference/github-copilot) | GitHub device authorization | Copilot plan and premium requests | | [Kimi for Coding](/providers/inference/kimi-for-coding) | Kimi membership key | Coding membership allowance | | [xAI Grok Subscription](/providers/inference/xai-subscription) | xAI account authorization | SuperGrok or eligible X plan allowance | diff --git a/apps/docs/providers/inference/alibaba-coding-plan.mdx b/apps/docs/providers/inference/alibaba-coding-plan.mdx new file mode 100644 index 000000000..8538e6307 --- /dev/null +++ b/apps/docs/providers/inference/alibaba-coding-plan.mdx @@ -0,0 +1,40 @@ +--- +title: Alibaba Coding Plan +icon: 'https://unpkg.com/@lobehub/icons-static-svg@1.94.0/icons/qwen.svg' +description: Use Qwen models through an Alibaba Coding Plan in the international or China region. +--- + +Alibaba Coding Plan provides a plan allowance for coding-focused model access. +Its keys and endpoints are separate from metered [Model Studio +API](/providers/inference/alibaba-model-studio) and Token Plan connections. + +## Get a Coding Plan key + +Activate a plan and follow the [Alibaba Coding Plan documentation](https://www.alibabacloud.com/help/en/model-studio/coding-plan). +Create the key in the same international or China region you select in Roomote. + +## Configuration + +Add **Alibaba Coding Plan** in **Settings > Models**, paste the plan key, and +select its region. Environment-variable configuration uses: + +```sh +ALIBABA_CODING_PLAN_API_KEY=... +ALIBABA_CODING_PLAN_REGION=global # or china +``` + +Models use the `alibaba-coding-plan/` prefix. + +## Verify setup + +1. save the Coding Plan key and matching region +2. confirm Alibaba Coding Plan models appear in **Settings > Models** +3. enable an `alibaba-coding-plan/...` model and assign it to a role +4. run a small task and confirm usage in the matching plan console + +## Common issues + +- **The key is rejected.** Confirm it is a Coding Plan key and that the selected + region matches. +- **The plan allowance is exhausted.** Wait for its reset or switch the role to + a separately billed provider. diff --git a/apps/docs/providers/inference/alibaba-model-studio.mdx b/apps/docs/providers/inference/alibaba-model-studio.mdx new file mode 100644 index 000000000..50bff29a7 --- /dev/null +++ b/apps/docs/providers/inference/alibaba-model-studio.mdx @@ -0,0 +1,50 @@ +--- +title: Alibaba Cloud Model Studio (Qwen) +icon: 'https://unpkg.com/@lobehub/icons-static-svg@1.94.0/icons/qwen.svg' +description: Use Qwen models through the international or China Alibaba Cloud Model Studio API. +--- + +Alibaba Cloud Model Studio provides metered API access to Qwen models. Roomote +supports both the international and China Model Studio endpoints; the key and +region must match. + +## Get an API key + +Create an API key in the [Model Studio console](https://modelstudio.console.alibabacloud.com/?tab=globalset#/efm/api_key). +Keys are region-specific. Coding Plan and Token Plan keys use separate Roomote +connections. + +## Configuration + +Add **Alibaba Cloud Model Studio (Qwen)** in **Settings > Models**, paste the API +key, and select its region. Environment-variable configuration uses: + +```sh +DASHSCOPE_API_KEY=... +ALIBABA_REGION=global # or china +``` + +Models use the `alibaba/` prefix. Roomote recommends Qwen3.6 Plus as a coding +and multimodal model; you can add other Model Studio model IDs after connecting. + +## Cost behavior + +Requests draw from the Alibaba Cloud account associated with the key and use +Model Studio's metered API pricing. Roomote records token usage and estimates +cost from model metadata; the Model Studio billing console is authoritative. + +## Verify setup + +1. save `DASHSCOPE_API_KEY` with the matching `ALIBABA_REGION` +2. confirm Alibaba Cloud Model Studio models appear in **Settings > Models** +3. enable an `alibaba/...` model and assign it to a role +4. run a small task and confirm usage appears in the Model Studio console + +## Common issues + +- **The key is rejected.** Confirm the selected region matches the endpoint + where the key was created and that it is not a subscription-plan key. +- **A model is unavailable.** Confirm the model is enabled in the same Alibaba + Cloud region as the API key. +- **A model lacks a required capability.** Choose a Qwen model that supports the + task's tool calling, context, or image-input requirements. diff --git a/apps/docs/providers/inference/alibaba-token-plan.mdx b/apps/docs/providers/inference/alibaba-token-plan.mdx new file mode 100644 index 000000000..c60345c4e --- /dev/null +++ b/apps/docs/providers/inference/alibaba-token-plan.mdx @@ -0,0 +1,40 @@ +--- +title: Alibaba Token Plan +icon: 'https://unpkg.com/@lobehub/icons-static-svg@1.94.0/icons/qwen.svg' +description: Use Qwen models through an Alibaba Token Plan in the international or China region. +--- + +Alibaba Token Plan provides prepaid token allowance through plan-specific +endpoints. Its keys are separate from metered [Model Studio +API](/providers/inference/alibaba-model-studio) and Coding Plan connections. + +## Get a Token Plan key + +Activate a plan and follow the [Alibaba Token Plan documentation](https://www.alibabacloud.com/help/en/model-studio/token-plan-overview). +Create the key in the same international or China region you select in Roomote. + +## Configuration + +Add **Alibaba Token Plan** in **Settings > Models**, paste the plan key, and +select its region. Environment-variable configuration uses: + +```sh +ALIBABA_TOKEN_PLAN_API_KEY=... +ALIBABA_TOKEN_PLAN_REGION=global # or china +``` + +Models use the `alibaba-token-plan/` prefix. + +## Verify setup + +1. save the Token Plan key and matching region +2. confirm Alibaba Token Plan models appear in **Settings > Models** +3. enable an `alibaba-token-plan/...` model and assign it to a role +4. run a small task and confirm usage in the matching plan console + +## Common issues + +- **The key is rejected.** Confirm it is a Token Plan key and that the selected + region matches. +- **The plan allowance is exhausted.** Wait for its reset or switch the role to + a separately billed provider. diff --git a/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx b/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx index fbcc143e6..6fa8e7228 100644 --- a/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx +++ b/apps/web/src/app/(onboarding)/setup/SetupDocs.client.test.tsx @@ -36,6 +36,9 @@ describe('SetupDocs', () => { }); it.each([ + ['alibaba', 'alibaba-model-studio'], + ['alibaba-coding-plan', 'alibaba-coding-plan'], + ['alibaba-token-plan', 'alibaba-token-plan'], ['amazon-bedrock', 'amazon-bedrock'], ['anthropic', 'anthropic'], ['azure', 'azure-openai'], diff --git a/apps/web/src/app/(onboarding)/setup/setup-docs.ts b/apps/web/src/app/(onboarding)/setup/setup-docs.ts index acba0809f..910f9baa1 100644 --- a/apps/web/src/app/(onboarding)/setup/setup-docs.ts +++ b/apps/web/src/app/(onboarding)/setup/setup-docs.ts @@ -50,6 +50,9 @@ const COMPUTE_PROVIDER_DOC_PATHS: Record = { const MODEL_PROVIDER_DOC_PATHS: Partial< Record > = { + alibaba: 'providers/inference/alibaba-model-studio', + 'alibaba-coding-plan': 'providers/inference/alibaba-coding-plan', + 'alibaba-token-plan': 'providers/inference/alibaba-token-plan', 'amazon-bedrock': 'providers/inference/amazon-bedrock', anthropic: 'providers/inference/anthropic', azure: 'providers/inference/azure-openai', diff --git a/apps/web/src/trpc/commands/task-models/index.test.ts b/apps/web/src/trpc/commands/task-models/index.test.ts index 59d91576a..40bda9776 100644 --- a/apps/web/src/trpc/commands/task-models/index.test.ts +++ b/apps/web/src/trpc/commands/task-models/index.test.ts @@ -90,6 +90,12 @@ const PROVIDER_ENV_VAR_NAMES = [ 'AZURE_COGNITIVE_SERVICES_API_KEY', 'AZURE_COGNITIVE_SERVICES_RESOURCE_NAME', 'ANTHROPIC_API_KEY', + 'DASHSCOPE_API_KEY', + 'ALIBABA_REGION', + 'ALIBABA_CODING_PLAN_API_KEY', + 'ALIBABA_CODING_PLAN_REGION', + 'ALIBABA_TOKEN_PLAN_API_KEY', + 'ALIBABA_TOKEN_PLAN_REGION', 'MOONSHOT_API_KEY', 'KIMI_API_KEY', 'MINIMAX_API_KEY', @@ -1391,6 +1397,9 @@ describe('task model provider commands', () => { expect(mockGetPersistedEnvironmentVariableValues).toHaveBeenCalledWith([ 'AZURE_RESOURCE_NAME', 'AZURE_COGNITIVE_SERVICES_RESOURCE_NAME', + 'ALIBABA_REGION', + 'ALIBABA_CODING_PLAN_REGION', + 'ALIBABA_TOKEN_PLAN_REGION', 'AWS_REGION', 'ZAI_REGION', 'ZAI_CODING_PLAN_REGION', diff --git a/deploy/compose/docker-compose.prod.yml b/deploy/compose/docker-compose.prod.yml index bb2c147fd..50015537f 100644 --- a/deploy/compose/docker-compose.prod.yml +++ b/deploy/compose/docker-compose.prod.yml @@ -36,6 +36,12 @@ x-roomote-inference-env: &roomote-inference-env AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + DASHSCOPE_API_KEY: ${DASHSCOPE_API_KEY:-} + ALIBABA_REGION: ${ALIBABA_REGION:-} + ALIBABA_CODING_PLAN_API_KEY: ${ALIBABA_CODING_PLAN_API_KEY:-} + ALIBABA_CODING_PLAN_REGION: ${ALIBABA_CODING_PLAN_REGION:-} + ALIBABA_TOKEN_PLAN_API_KEY: ${ALIBABA_TOKEN_PLAN_API_KEY:-} + ALIBABA_TOKEN_PLAN_REGION: ${ALIBABA_TOKEN_PLAN_REGION:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-} AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK:-} diff --git a/docker-compose.production.yml b/docker-compose.production.yml index bb4d76510..a982bbf84 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -39,6 +39,12 @@ x-roomote-production-env: &roomote-production-env AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + DASHSCOPE_API_KEY: ${DASHSCOPE_API_KEY:-} + ALIBABA_REGION: ${ALIBABA_REGION:-} + ALIBABA_CODING_PLAN_API_KEY: ${ALIBABA_CODING_PLAN_API_KEY:-} + ALIBABA_CODING_PLAN_REGION: ${ALIBABA_CODING_PLAN_REGION:-} + ALIBABA_TOKEN_PLAN_API_KEY: ${ALIBABA_TOKEN_PLAN_API_KEY:-} + ALIBABA_TOKEN_PLAN_REGION: ${ALIBABA_TOKEN_PLAN_REGION:-} GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK:-} diff --git a/docker-compose.self-host.yml b/docker-compose.self-host.yml index 99050bb97..1aa175c3f 100644 --- a/docker-compose.self-host.yml +++ b/docker-compose.self-host.yml @@ -37,6 +37,12 @@ x-roomote-env: &roomote-env AI_GATEWAY_API_KEY: ${AI_GATEWAY_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + DASHSCOPE_API_KEY: ${DASHSCOPE_API_KEY:-} + ALIBABA_REGION: ${ALIBABA_REGION:-} + ALIBABA_CODING_PLAN_API_KEY: ${ALIBABA_CODING_PLAN_API_KEY:-} + ALIBABA_CODING_PLAN_REGION: ${ALIBABA_CODING_PLAN_REGION:-} + ALIBABA_TOKEN_PLAN_API_KEY: ${ALIBABA_TOKEN_PLAN_API_KEY:-} + ALIBABA_TOKEN_PLAN_REGION: ${ALIBABA_TOKEN_PLAN_REGION:-} GEMINI_API_KEY: ${GEMINI_API_KEY:-} GOOGLE_GENERATIVE_AI_API_KEY: ${GOOGLE_GENERATIVE_AI_API_KEY:-} AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK:-} diff --git a/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts b/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts index faa24b9a4..ec4254226 100644 --- a/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts +++ b/packages/cloud-agents/src/server/__tests__/opencode-runtime.test.ts @@ -58,6 +58,40 @@ describe('buildOpenCodeCliEnv', () => { }); }); + it.each([ + [ + 'alibaba/qwen3.6-plus', + 'ALIBABA_REGION', + 'https://dashscope.aliyuncs.com/compatible-mode/v1', + ], + [ + 'alibaba-coding-plan/qwen3.6-plus', + 'ALIBABA_CODING_PLAN_REGION', + 'https://coding.dashscope.aliyuncs.com/v1', + ], + [ + 'alibaba-token-plan/qwen3.6-plus', + 'ALIBABA_TOKEN_PLAN_REGION', + 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1', + ], + ] as const)( + 'configures the China endpoint for direct %s calls', + (model, regionEnvVarName, baseURL) => { + const env = buildOpenCodeCliEnv({ + R_MODEL: model, + [regionEnvVarName]: 'china', + }); + const providerId = model.split('/')[0]!; + + expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT ?? '{}')).toMatchObject({ + model, + provider: { + [providerId]: { options: { baseURL } }, + }, + }); + }, + ); + it('applies per-role reasoning options to the model-backed config', () => { const env = buildOpenCodeCliEnv({ R_MODEL: 'openrouter/openai/gpt-5.4', diff --git a/packages/cloud-agents/src/server/opencode-runtime.ts b/packages/cloud-agents/src/server/opencode-runtime.ts index fa015f1bf..53140f66c 100644 --- a/packages/cloud-agents/src/server/opencode-runtime.ts +++ b/packages/cloud-agents/src/server/opencode-runtime.ts @@ -7,6 +7,7 @@ import { CHATGPT_FAST_MODE_ENV_VAR_NAME, DISABLED_MODEL_PROVIDER_ENV_VAR_NAMES, isTaskModelIdDisabled, + mergeAlibabaOpenCodeProviderOptions, mergeOpenCodeModelReasoningOptions, mergeOpenCodeChatGptFastModeOptions, mergeOpenRouterVariantAliasModels, @@ -86,9 +87,10 @@ function buildModelBackedOpenCodeConfigContent( smallModel, ]) : providerReasoningConfig; - const providerConfig = mergeOpenRouterVariantAliasModels( - providerModelConfig, - variantAliases, + const providerConfig = mergeAlibabaOpenCodeProviderOptions( + mergeOpenRouterVariantAliasModels(providerModelConfig, variantAliases), + [model, smallModel], + env, ); return JSON.stringify({ diff --git a/packages/db/src/lib/model-runtime-config.test.ts b/packages/db/src/lib/model-runtime-config.test.ts index ba5d3099f..de85b18cf 100644 --- a/packages/db/src/lib/model-runtime-config.test.ts +++ b/packages/db/src/lib/model-runtime-config.test.ts @@ -583,6 +583,42 @@ describe('resolveEffectiveModelRuntimeEnv', () => { }); }); + it.each([ + ['alibaba/qwen3.6-plus', 'DASHSCOPE_API_KEY', 'ALIBABA_REGION'], + [ + 'alibaba-coding-plan/qwen3.6-plus', + 'ALIBABA_CODING_PLAN_API_KEY', + 'ALIBABA_CODING_PLAN_REGION', + ], + [ + 'alibaba-token-plan/qwen3.6-plus', + 'ALIBABA_TOKEN_PLAN_API_KEY', + 'ALIBABA_TOKEN_PLAN_REGION', + ], + ] as const)( + 'propagates the China region for direct %s control-plane calls', + async (model, apiKeyEnvVarName, regionEnvVarName) => { + mockDeploymentSettingsFindFirst.mockResolvedValue({ + runtimeModelConfig: { roomoteModel: model }, + }); + + const env = await resolveEffectiveModelRuntimeEnv({ + runtimeEnv: {}, + deploymentEnvVars: { + [apiKeyEnvVarName]: 'alibaba-key', + [regionEnvVarName]: 'china', + }, + }); + + expect(env).toMatchObject({ + R_MODEL: model, + R_MODEL_ENV_KEYS: `${apiKeyEnvVarName},${regionEnvVarName}`, + [apiKeyEnvVarName]: 'alibaba-key', + [regionEnvVarName]: 'china', + }); + }, + ); + it('ignores disabled provider roles and forwards enabled provider credentials', async () => { mockDeploymentSettingsFindFirst.mockResolvedValue({ runtimeModelConfig: { diff --git a/packages/types/src/__tests__/inference-gateway.test.ts b/packages/types/src/__tests__/inference-gateway.test.ts index c47b61b4f..9bdb3ff59 100644 --- a/packages/types/src/__tests__/inference-gateway.test.ts +++ b/packages/types/src/__tests__/inference-gateway.test.ts @@ -54,6 +54,61 @@ describe('inference gateway URL builders', () => { ); }); + it.each([ + [ + 'alibaba', + 'DASHSCOPE_API_KEY', + 'ALIBABA_REGION', + 'https://dashscope-intl.aliyuncs.com/compatible-mode', + 'https://dashscope.aliyuncs.com/compatible-mode', + ], + [ + 'alibaba-coding-plan', + 'ALIBABA_CODING_PLAN_API_KEY', + 'ALIBABA_CODING_PLAN_REGION', + 'https://coding-intl.dashscope.aliyuncs.com', + 'https://coding.dashscope.aliyuncs.com', + ], + [ + 'alibaba-token-plan', + 'ALIBABA_TOKEN_PLAN_API_KEY', + 'ALIBABA_TOKEN_PLAN_REGION', + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode', + 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode', + ], + ] as const)( + 'registers %s as a regional OpenAI-compatible provider', + ( + providerId, + apiKeyEnvVarName, + regionEnvVarName, + globalBaseUrl, + chinaBaseUrl, + ) => { + const provider = getInferenceGatewayProvider(providerId); + expect(provider).toMatchObject({ + envVarNames: [apiKeyEnvVarName], + authHeader: { name: 'authorization', scheme: 'bearer' }, + openCodeBaseUrlSuffix: '/v1', + region: { + envVarName: regionEnvVarName, + default: 'global', + baseUrls: { global: globalBaseUrl, china: chinaBaseUrl }, + }, + }); + expect(provider?.allowedPaths).toContain('/v1/chat/completions'); + expect( + buildInferenceGatewayOpenCodeBaseUrl( + 'https://api.example.com/api/inference', + provider!, + ), + ).toBe(`https://api.example.com/api/inference/${providerId}/v1`); + expect( + getInferenceGatewayProviderByEnvVarName(apiKeyEnvVarName)?.id, + ).toBe(providerId); + }, + ); + it.each([ [ 'azure', @@ -247,6 +302,9 @@ describe('inference gateway key lookups', () => { ); expect(regionProviders.map((provider) => provider.id)).toEqual([ + 'alibaba', + 'alibaba-coding-plan', + 'alibaba-token-plan', 'zai', 'zai-coding-plan', ]); diff --git a/packages/types/src/inference-gateway.ts b/packages/types/src/inference-gateway.ts index b0a185e6b..be1450af4 100644 --- a/packages/types/src/inference-gateway.ts +++ b/packages/types/src/inference-gateway.ts @@ -1,4 +1,7 @@ -import type { SetupModelProviderId } from './model-provider-config'; +import { + ALIBABA_PROVIDER_REGION_CONFIG, + type SetupModelProviderId, +} from './model-provider-config'; import { getOpenAiCompatibleProviderInstance, isOpenAiCompatibleProviderEnvVarName, @@ -228,6 +231,33 @@ export const INFERENCE_GATEWAY_PROVIDERS: readonly InferenceGatewayProvider[] = allowedPaths: OPENAI_COMPATIBLE_INFERENCE_PATHS, openCodeBaseUrlSuffix: '/v1', }, + { + id: 'alibaba', + name: 'Alibaba Cloud Model Studio', + envVarNames: ['DASHSCOPE_API_KEY'], + authHeader: { name: 'authorization', scheme: 'bearer' }, + allowedPaths: OPENAI_COMPATIBLE_INFERENCE_PATHS, + openCodeBaseUrlSuffix: '/v1', + region: ALIBABA_PROVIDER_REGION_CONFIG.alibaba, + }, + { + id: 'alibaba-coding-plan', + name: 'Alibaba Coding Plan', + envVarNames: ['ALIBABA_CODING_PLAN_API_KEY'], + authHeader: { name: 'authorization', scheme: 'bearer' }, + allowedPaths: OPENAI_COMPATIBLE_INFERENCE_PATHS, + openCodeBaseUrlSuffix: '/v1', + region: ALIBABA_PROVIDER_REGION_CONFIG['alibaba-coding-plan'], + }, + { + id: 'alibaba-token-plan', + name: 'Alibaba Token Plan', + envVarNames: ['ALIBABA_TOKEN_PLAN_API_KEY'], + authHeader: { name: 'authorization', scheme: 'bearer' }, + allowedPaths: OPENAI_COMPATIBLE_INFERENCE_PATHS, + openCodeBaseUrlSuffix: '/v1', + region: ALIBABA_PROVIDER_REGION_CONFIG['alibaba-token-plan'], + }, { id: 'anthropic', name: 'Anthropic', diff --git a/packages/types/src/model-provider-config.test.ts b/packages/types/src/model-provider-config.test.ts index 8e8b63416..f8a896555 100644 --- a/packages/types/src/model-provider-config.test.ts +++ b/packages/types/src/model-provider-config.test.ts @@ -226,6 +226,9 @@ describe('SETUP_MODEL_PROVIDER_CATALOG', () => { 'openai', 'azure', 'azure-cognitive-services', + 'alibaba', + 'alibaba-coding-plan', + 'alibaba-token-plan', 'anthropic', 'moonshotai', 'kimi-for-coding', @@ -936,6 +939,20 @@ describe('getModelProviderEnvKeyCandidates', () => { it('publishes the flattened default provider env key list', () => { expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain('AI_GATEWAY_API_KEY'); + expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain('DASHSCOPE_API_KEY'); + expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain('ALIBABA_REGION'); + expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain( + 'ALIBABA_CODING_PLAN_API_KEY', + ); + expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain( + 'ALIBABA_CODING_PLAN_REGION', + ); + expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain( + 'ALIBABA_TOKEN_PLAN_API_KEY', + ); + expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain( + 'ALIBABA_TOKEN_PLAN_REGION', + ); expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain('AZURE_API_KEY'); expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain('AZURE_RESOURCE_NAME'); expect(DEFAULT_MODEL_PROVIDER_ENV_KEYS).toContain( diff --git a/packages/types/src/model-provider-config.ts b/packages/types/src/model-provider-config.ts index 67e3baf51..bec462959 100644 --- a/packages/types/src/model-provider-config.ts +++ b/packages/types/src/model-provider-config.ts @@ -136,6 +136,92 @@ export const ZAI_REGION_OPTIONS = [ { value: 'china', label: 'China' }, ] as const; +export const ALIBABA_REGION_OPTIONS = [ + { value: 'global', label: 'International' }, + { value: 'china', label: 'China' }, +] as const; + +export const ALIBABA_PROVIDER_REGION_CONFIG = { + alibaba: { + envVarName: 'ALIBABA_REGION', + default: 'global', + baseUrls: { + global: 'https://dashscope-intl.aliyuncs.com/compatible-mode', + china: 'https://dashscope.aliyuncs.com/compatible-mode', + }, + }, + 'alibaba-coding-plan': { + envVarName: 'ALIBABA_CODING_PLAN_REGION', + default: 'global', + baseUrls: { + global: 'https://coding-intl.dashscope.aliyuncs.com', + china: 'https://coding.dashscope.aliyuncs.com', + }, + }, + 'alibaba-token-plan': { + envVarName: 'ALIBABA_TOKEN_PLAN_REGION', + default: 'global', + baseUrls: { + global: + 'https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode', + china: 'https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode', + }, + }, +} as const; + +type AlibabaProviderId = keyof typeof ALIBABA_PROVIDER_REGION_CONFIG; + +export function mergeAlibabaOpenCodeProviderOptions( + config: Record, + modelIds: readonly (string | undefined)[], + runtimeEnv: Partial>, +): Record { + const providerIds = new Set( + modelIds.flatMap((modelId) => { + const providerId = modelId?.split('/')[0] as + | AlibabaProviderId + | undefined; + return providerId && providerId in ALIBABA_PROVIDER_REGION_CONFIG + ? [providerId] + : []; + }), + ); + + let merged = config; + for (const providerId of providerIds) { + const regionConfig = ALIBABA_PROVIDER_REGION_CONFIG[providerId]; + const region = + runtimeEnv[regionConfig.envVarName]?.trim() || regionConfig.default; + const baseURL = regionConfig.baseUrls[ + region as keyof typeof regionConfig.baseUrls + ] as string | undefined; + + if (!baseURL) { + continue; + } + + const existingProvider = + typeof merged[providerId] === 'object' && merged[providerId] !== null + ? (merged[providerId] as Record) + : {}; + const existingOptions = + typeof existingProvider.options === 'object' && + existingProvider.options !== null + ? (existingProvider.options as Record) + : {}; + + merged = { + ...merged, + [providerId]: { + ...existingProvider, + options: { ...existingOptions, baseURL: `${baseURL}/v1` }, + }, + }; + } + + return merged; +} + export type SetupModelProviderDescriptor = { id: SetupModelProviderId; label: string; @@ -418,6 +504,91 @@ export const SETUP_MODEL_PROVIDER_CATALOG = [ planning: 'azure-cognitive-services/gpt-5.6-sol', }, }, + { + // Regional gateway routing lets one Roomote connection cover OpenCode's + // separate alibaba and alibaba-cn provider endpoints. + id: 'alibaba', + label: 'Alibaba Cloud Model Studio (Qwen)', + envVarName: 'DASHSCOPE_API_KEY', + defaultRoomoteModel: 'alibaba/qwen3.6-plus', + authKind: 'api-key', + credentialHelp: { + text: 'Paste a Model Studio API key for the selected region. Coding Plan and Token Plan keys belong on their separate connections.', + href: 'https://modelstudio.console.alibabacloud.com/?tab=globalset#/efm/api_key', + linkLabel: 'Open Model Studio API keys', + }, + additionalEnvFields: [ + { + envVarName: 'ALIBABA_REGION', + label: 'Region', + secret: false, + required: true, + options: ALIBABA_REGION_OPTIONS, + }, + ], + suggestedTaskModels: mapRecommendedTaskModels({ + 'qwen3-6-plus': 'alibaba/qwen3.6-plus', + }), + recommendedRoleModels: { + vision: 'alibaba/qwen3.6-plus', + }, + }, + { + id: 'alibaba-coding-plan', + label: 'Alibaba Coding Plan', + envVarName: 'ALIBABA_CODING_PLAN_API_KEY', + envVarLabel: 'Alibaba Coding Plan API key', + defaultRoomoteModel: 'alibaba-coding-plan/qwen3.6-plus', + authKind: 'api-key', + credentialHelp: { + text: 'Paste a Coding Plan key for the selected region. Do not use a general Model Studio or Token Plan key here.', + href: 'https://www.alibabacloud.com/help/en/model-studio/coding-plan', + linkLabel: 'Open Alibaba Coding Plan docs', + }, + additionalEnvFields: [ + { + envVarName: 'ALIBABA_CODING_PLAN_REGION', + label: 'Region', + secret: false, + required: true, + options: ALIBABA_REGION_OPTIONS, + }, + ], + suggestedTaskModels: mapRecommendedTaskModels({ + 'qwen3-6-plus': 'alibaba-coding-plan/qwen3.6-plus', + }), + recommendedRoleModels: { + vision: 'alibaba-coding-plan/qwen3.6-plus', + }, + }, + { + id: 'alibaba-token-plan', + label: 'Alibaba Token Plan', + envVarName: 'ALIBABA_TOKEN_PLAN_API_KEY', + envVarLabel: 'Alibaba Token Plan API key', + defaultRoomoteModel: 'alibaba-token-plan/qwen3.6-plus', + authKind: 'api-key', + credentialHelp: { + text: 'Paste a Token Plan key for the selected region. Do not use a general Model Studio or Coding Plan key here.', + href: 'https://www.alibabacloud.com/help/en/model-studio/token-plan-overview', + linkLabel: 'Open Alibaba Token Plan docs', + }, + additionalEnvFields: [ + { + envVarName: 'ALIBABA_TOKEN_PLAN_REGION', + label: 'Region', + secret: false, + required: true, + options: ALIBABA_REGION_OPTIONS, + }, + ], + suggestedTaskModels: mapRecommendedTaskModels({ + 'qwen3-6-plus': 'alibaba-token-plan/qwen3.6-plus', + }), + recommendedRoleModels: { + vision: 'alibaba-token-plan/qwen3.6-plus', + }, + }, { id: 'anthropic', label: 'Anthropic', diff --git a/packages/types/src/task-models.ts b/packages/types/src/task-models.ts index 2434e4adb..9a0539046 100644 --- a/packages/types/src/task-models.ts +++ b/packages/types/src/task-models.ts @@ -25,6 +25,9 @@ export const ENABLED_DIRECT_TASK_MODEL_PROVIDER_IDS = [ 'azure', 'azure-cognitive-services', 'anthropic', + 'alibaba', + 'alibaba-coding-plan', + 'alibaba-token-plan', 'moonshotai', 'kimi-for-coding', 'minimax',