Merge pull request #2802 from 21307369/fix/alibaba-endpoint-selection-v2

feat(alibaba-coding-plan): add endpoint selection for China/International/Custom
This commit is contained in:
Can Bölük
2026-06-17 11:36:02 +02:00
committed by GitHub
5 changed files with 326 additions and 20 deletions
+4 -1
View File
@@ -1786,7 +1786,10 @@ export class AuthStorage {
return;
}
const newCredential: OAuthCredential = { type: "oauth", ...result };
await this.#upsertOAuthCredential(def.storeCredentialsAs ?? provider, newCredential);
// Use set() instead of #upsertOAuthCredential to replace ALL existing credentials
// (including legacy api_key rows from older versions) with the new OAuth credential.
// This ensures getApiKey() doesn't match an old api_key row before the new OAuth row.
await this.set(def.storeCredentialsAs ?? provider, newCredential);
}
/**
@@ -1283,6 +1283,19 @@ async function createRequestSetup(
copilotPremiumRequests = copilot.premiumRequests;
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
}
if (model.provider === "alibaba-coding-plan") {
try {
const parsed = JSON.parse(rawApiKey);
if (typeof parsed?.token === "string") {
apiKey = parsed.token;
}
if (typeof parsed?.enterpriseUrl === "string") {
baseUrl = parsed.enterpriseUrl;
}
} catch {
// Not JSON — use raw apiKey and catalog baseUrl
}
}
// Azure OpenAI requires /deployments/{id}/chat/completions?api-version=YYYY-MM-DD.
// The generic openai-completions path adds neither, producing silent 404s.
let azureDefaultQuery: Record<string, string> | undefined;
+55 -10
View File
@@ -1,19 +1,58 @@
import { validateOpenAICompatibleApiKey } from "./api-key-validation";
import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types";
import * as apiKeyValidation from "./api-key-validation";
import type { OAuthController, OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types";
import type { ProviderDefinition } from "./types";
const AUTH_URL = "https://modelstudio.console.alibabacloud.com/";
const API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
const DEFAULT_AUTH_URL = "https://modelstudio.console.alibabacloud.com/";
const CHINA_AUTH_URL = "https://dashscope.console.aliyun.com/";
const DEFAULT_API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
const CHINA_API_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1";
const VALIDATION_MODEL = "qwen3.5-plus";
export async function loginAlibabaCodingPlan(options: OAuthController): Promise<string> {
export async function loginAlibabaCodingPlan(options: OAuthController): Promise<OAuthCredentials> {
if (!options.onPrompt) {
throw new Error("Alibaba Coding Plan login requires onPrompt callback");
}
// Ask which endpoint to use
const endpointChoice = await options.onPrompt({
message: "Select Alibaba Coding Plan endpoint: 1=International (default), 2=China, 3=Custom — enter 1, 2, or 3",
placeholder: "1",
});
// Check for abort after endpoint selection (Escape returns "")
if (options.signal?.aborted) {
throw new Error("Login cancelled");
}
const choice = endpointChoice.trim();
let baseUrl: string;
let authUrl: string;
let instructions: string;
if (choice === "2") {
baseUrl = CHINA_API_BASE_URL;
authUrl = CHINA_AUTH_URL;
instructions = "Copy your API key from the Alibaba Cloud DashScope console (China mainland)";
} else if (choice === "3") {
const customUrl = await options.onPrompt({
message: "Enter custom base URL",
placeholder: "https://your-proxy.com/v1",
});
const trimmedUrl = customUrl.trim().replace(/\/+$/, "");
if (!trimmedUrl) {
throw new Error("Custom URL is required for option 3");
}
baseUrl = trimmedUrl;
authUrl = DEFAULT_AUTH_URL;
instructions = "Copy your API key from the Alibaba Cloud DashScope console";
} else {
baseUrl = DEFAULT_API_BASE_URL;
authUrl = DEFAULT_AUTH_URL;
instructions = "Copy your API key from the Alibaba Cloud DashScope console (International)";
}
options.onAuth?.({
url: AUTH_URL,
instructions: "Copy your API key from the Alibaba Cloud DashScope console",
url: authUrl,
instructions,
});
const apiKey = await options.onPrompt({
@@ -31,19 +70,25 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise<
}
options.onProgress?.("Validating API key...");
await validateOpenAICompatibleApiKey({
await apiKeyValidation.validateOpenAICompatibleApiKey({
provider: "Alibaba Coding Plan",
apiKey: trimmed,
baseUrl: API_BASE_URL,
baseUrl,
model: VALIDATION_MODEL,
signal: options.signal,
});
return trimmed;
return {
access: trimmed,
refresh: trimmed,
expires: Number.MAX_SAFE_INTEGER,
enterpriseUrl: baseUrl,
};
}
export const alibabaCodingPlanProvider = {
id: "alibaba-coding-plan",
name: "Alibaba Coding Plan",
login: (cb: OAuthLoginCallbacks) => loginAlibabaCodingPlan(cb),
getApiKey: (credentials) => credentials.access,
} as const satisfies ProviderDefinition;
+12 -9
View File
@@ -136,17 +136,20 @@ export async function getOAuthApiKey(
}
// For providers that need request-time credential metadata, return JSON.
const needsStructuredApiKey =
provider === "github-copilot" || provider === "google-gemini-cli" || provider === "google-antigravity";
provider === "github-copilot" ||
provider === "google-gemini-cli" ||
provider === "google-antigravity" ||
provider === "alibaba-coding-plan";
const apiKey = needsStructuredApiKey
? JSON.stringify({
token: creds.access,
enterpriseUrl: creds.enterpriseUrl,
projectId: creds.projectId,
refreshToken: creds.refresh,
expiresAt: creds.expires,
email: creds.email,
accountId: creds.accountId,
})
token: creds.access,
enterpriseUrl: creds.enterpriseUrl,
projectId: creds.projectId,
refreshToken: creds.refresh,
expiresAt: creds.expires,
email: creds.email,
accountId: creds.accountId,
})
: creds.access;
return { newCredentials: creds, apiKey };
}
@@ -0,0 +1,242 @@
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
import type { OAuthController, OAuthCredentials } from "../src/registry/oauth/types";
import * as apiKeyValidation from "../src/registry/api-key-validation";
import { loginAlibabaCodingPlan, alibabaCodingPlanProvider } from "../src/registry/alibaba-coding-plan";
import { getOAuthApiKey } from "../src/registry/oauth/index";
import type { Mock } from "bun:test";
describe("alibaba-coding-plan endpoint selection", () => {
let validateSpy: Mock<typeof apiKeyValidation.validateOpenAICompatibleApiKey>;
beforeEach(() => {
validateSpy = spyOn(apiKeyValidation, "validateOpenAICompatibleApiKey").mockResolvedValue(undefined);
});
afterEach(() => {
validateSpy.mockRestore();
});
it("option 1 uses international endpoint and auth URL", async () => {
let capturedAuth: { url: string; instructions?: string } | undefined;
const options: OAuthController = {
onAuth: (info) => { capturedAuth = info; },
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "1";
if (prompt.message.includes("Paste your")) return "sk-test-key";
return "";
},
};
const result = await loginAlibabaCodingPlan(options);
expect(result.access).toBe("sk-test-key");
expect(result.refresh).toBe("sk-test-key");
expect(result.enterpriseUrl).toBe("https://coding-intl.dashscope.aliyuncs.com/v1");
expect(capturedAuth?.url).toBe("https://modelstudio.console.alibabacloud.com/");
expect(capturedAuth?.instructions).toContain("International");
expect(validateSpy).toHaveBeenCalledWith({
provider: "Alibaba Coding Plan",
apiKey: "sk-test-key",
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
model: "qwen3.5-plus",
signal: undefined,
});
});
it("option 2 uses China endpoint and auth URL", async () => {
let capturedAuth: { url: string; instructions?: string } | undefined;
const options: OAuthController = {
onAuth: (info) => { capturedAuth = info; },
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "2";
if (prompt.message.includes("Paste your")) return "sk-cn-key";
return "";
},
};
const result = await loginAlibabaCodingPlan(options);
expect(result.access).toBe("sk-cn-key");
expect(result.refresh).toBe("sk-cn-key");
expect(result.enterpriseUrl).toBe("https://coding.dashscope.aliyuncs.com/v1");
expect(capturedAuth?.url).toBe("https://dashscope.console.aliyun.com/");
expect(capturedAuth?.instructions).toContain("China mainland");
expect(validateSpy).toHaveBeenCalledWith({
provider: "Alibaba Coding Plan",
apiKey: "sk-cn-key",
baseUrl: "https://coding.dashscope.aliyuncs.com/v1",
model: "qwen3.5-plus",
signal: undefined,
});
});
it("option 3 prompts for custom URL and uses it", async () => {
let capturedAuth: { url: string; instructions?: string } | undefined;
const options: OAuthController = {
onAuth: (info) => { capturedAuth = info; },
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "3";
if (prompt.message.includes("custom base URL")) return "https://my-proxy.com/v1";
if (prompt.message.includes("Paste your")) return "sk-custom-key";
return "";
},
};
const result = await loginAlibabaCodingPlan(options);
expect(result.access).toBe("sk-custom-key");
expect(result.refresh).toBe("sk-custom-key");
expect(result.enterpriseUrl).toBe("https://my-proxy.com/v1");
expect(capturedAuth?.url).toBe("https://modelstudio.console.alibabacloud.com/");
expect(validateSpy).toHaveBeenCalledWith({
provider: "Alibaba Coding Plan",
apiKey: "sk-custom-key",
baseUrl: "https://my-proxy.com/v1",
model: "qwen3.5-plus",
signal: undefined,
});
});
it("empty input defaults to international endpoint and auth URL", async () => {
const options: OAuthController = {
onAuth: () => {},
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "";
if (prompt.message.includes("Paste your")) return "sk-test-key";
return "";
},
};
const result = await loginAlibabaCodingPlan(options);
expect(result.enterpriseUrl).toBe("https://coding-intl.dashscope.aliyuncs.com/v1");
});
it("strips trailing slashes from custom URL", async () => {
const options: OAuthController = {
onAuth: () => {},
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "3";
if (prompt.message.includes("custom base URL")) return "https://my-proxy.com/v1///";
if (prompt.message.includes("Paste your")) return "sk-test-key";
return "";
},
};
const result = await loginAlibabaCodingPlan(options);
expect(result.enterpriseUrl).toBe("https://my-proxy.com/v1");
});
it("throws error when custom URL is empty", async () => {
const options: OAuthController = {
onAuth: () => {},
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "3";
if (prompt.message.includes("custom base URL")) return "";
return "";
},
};
await expect(loginAlibabaCodingPlan(options)).rejects.toThrow(
"Custom URL is required for option 3"
);
});
it("throws error when API key is empty", async () => {
const options: OAuthController = {
onAuth: () => {},
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) return "1";
if (prompt.message.includes("Paste your")) return "";
return "";
},
};
await expect(loginAlibabaCodingPlan(options)).rejects.toThrow(
"API key is required"
);
});
it("checks abort signal after endpoint selection", async () => {
const controller = new AbortController();
const options: OAuthController = {
onAuth: () => {},
onProgress: () => {},
onPrompt: async (prompt) => {
if (prompt.message.includes("Select Alibaba")) {
controller.abort();
return "";
}
return "";
},
signal: controller.signal,
};
await expect(loginAlibabaCodingPlan(options)).rejects.toThrow(
"Login cancelled"
);
});
});
describe("alibaba-coding-plan JSON apiKey", () => {
it("getOAuthApiKey returns JSON with token and enterpriseUrl", async () => {
const credentials = {
"alibaba-coding-plan": {
access: "sk-test-key",
refresh: "refresh-token",
expires: Date.now() + 3600000,
enterpriseUrl: "https://coding.dashscope.aliyuncs.com/v1",
},
};
const result = await getOAuthApiKey("alibaba-coding-plan", credentials);
expect(result).not.toBeNull();
const parsed = JSON.parse(result!.apiKey);
expect(parsed.token).toBe("sk-test-key");
expect(parsed.enterpriseUrl).toBe("https://coding.dashscope.aliyuncs.com/v1");
});
it("JSON apiKey parsing extracts token for Bearer header", () => {
const rawApiKey = JSON.stringify({
token: "sk-bearer-token",
enterpriseUrl: "https://custom.endpoint.com/v1",
});
const parsed = JSON.parse(rawApiKey);
const apiKey = typeof parsed?.token === "string" ? parsed.token : rawApiKey;
expect(apiKey).toBe("sk-bearer-token");
});
it("JSON apiKey parsing extracts enterpriseUrl for baseUrl", () => {
const rawApiKey = JSON.stringify({
token: "sk-test",
enterpriseUrl: "https://china.dashscope.aliyuncs.com/v1",
});
const parsed = JSON.parse(rawApiKey);
const baseUrl = typeof parsed?.enterpriseUrl === "string" ? parsed.enterpriseUrl : undefined;
expect(baseUrl).toBe("https://china.dashscope.aliyuncs.com/v1");
});
it("non-JSON apiKey falls back to raw value", () => {
const rawApiKey = "sk-plain-key";
let apiKey = rawApiKey;
try {
const parsed = JSON.parse(rawApiKey);
if (typeof parsed?.token === "string") {
apiKey = parsed.token;
}
} catch {
// Not JSON — use raw apiKey
}
expect(apiKey).toBe("sk-plain-key");
});
});