feat(providers): add qoder-cn support for Qoder CN (qoder.com.cn)

This commit is contained in:
Liang.Xu
2026-09-21 20:45:19 +07:00
parent f67d5a0c93
commit 402745dc1f
30 changed files with 487 additions and 234 deletions
@@ -13,10 +13,10 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
const isOllamaLocal = provider === "ollama-local";
const isCookie = authType === "cookie";
const isXaiApiKey = provider === "xai" && !isCookie;
const credentialLabel = isCookie ? "Cookie Value" : provider === "qoder" ? "Personal Access Token (PAT)" : "API Key";
const credentialLabel = isCookie ? "Cookie Value" : provider === "qoder" || provider === "qoder-cn" ? "Personal Access Token (PAT)" : "API Key";
const credentialPlaceholder = isCookie
? (provider === "grok-web" ? "sso=xxxxx... or just the raw value" : "eyJhbGciOi...")
: (isXaiApiKey ? "xai-..." : provider === "qoder" ? "pt-..." : "");
: (isXaiApiKey ? "xai-..." : provider === "qoder" || provider === "qoder-cn" ? "pt-..." : "");
const isAzure = provider === "azure";
const isCloudflareAi = provider === "cloudflare-ai";
@@ -44,7 +44,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
const [saving, setSaving] = useState(false);
const bulkPlaceholder = isCloudflareAi
? `name1|sk-key1|acc123456\nname2|sk-key2|def789012\nsk-key-only-auto-named`
: provider === "qoder"
: provider === "qoder" || provider === "qoder-cn"
? `name1|pt-xxxxx\nname2|pt-yyyyy\npt-only-auto-named`
: BULK_PLACEHOLDER;
@@ -201,7 +201,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
<p className="text-xs text-text-muted">
{isCloudflareAi
? <>One key per line. Format: <code>name|apiKey|accountId</code> or just <code>apiKey</code> (auto-named by index).</>
: provider === "qoder"
: provider === "qoder" || provider === "qoder-cn"
? <>One PAT per line. Format: <code>name|pt-...</code> or just <code>pt-...</code> (auto-named by index).</>
: <>One key per line. Format: <code>name|apiKey</code> or just <code>apiKey</code> (auto-named by index).</>
}
@@ -172,7 +172,7 @@ export default function ProviderDetailPage() {
const apiKeyConnectionLabel =
providerId === "xai" ? "xAI API Key"
: providerId === "kimi" ? "Kimi API Key"
: providerId === "qoder" ? "PAT"
: (providerId === "qoder" || providerId === "qoder-cn") ? "PAT"
: "API Key";
// Resolve suffix "(level)" for a model when a thinking level is picked and the model supports it.
const resolveThinkingSuffix = (modelId) => {
@@ -612,8 +612,9 @@ export default function ProviderDetailPage() {
const modelId = model.id || model.name;
if (!modelId) continue;
// Qoder model ID format may be "qoder/auto" or "auto", need to remove prefix
const cleanModelId = modelId.replace(/^qoder\//, "");
// Qoder model ID format may be "qoder/auto", "qoder-cn/auto" or "auto",
// need to remove the provider prefix before storing.
const cleanModelId = modelId.replace(/^(qoder-cn|qoder)\//, "");
const alreadyExists = customModels.some(
(entry) => entry.providerAlias === providerStorageAlias && entry.id === cleanModelId && (entry.kind || entry.type || "llm") === "llm"
) || Object.values(modelAliases).includes(`${providerStorageAlias}/${cleanModelId}`);
@@ -1245,8 +1246,8 @@ export default function ProviderDetailPage() {
Add Model
</button>
{/* Import Qoder models button — only show for qoder provider */}
{providerId === "qoder" && connections.some((conn) => conn.isActive !== false) && (
{/* Import Qoder models button — only show for qoder/qoder-cn provider */}
{(providerId === "qoder" || providerId === "qoder-cn") && connections.some((conn) => conn.isActive !== false) && (
<button
onClick={handleImportQoderModels}
disabled={importingQoderModels}
@@ -45,6 +45,7 @@ export default function ProviderLimitCard({
codex: "#10A37F",
kiro: "#FF9900",
qoder: "#EC4899",
"qoder-cn": "#EC4899",
claude: "#D97757",
};
return colors[provider?.toLowerCase()] || "#6B7280";
@@ -551,6 +551,7 @@ export function parseQuotaData(provider, data) {
break;
case "qoder":
case "qoder-cn":
// Qoder ships a `user` quota and (optionally) an `organization`
// quota, both with same shape: {total, used, remaining, unit, resetAt}.
// Skip an organization bucket when its total is 0 — most personal
@@ -263,6 +263,7 @@ export async function GET(request, { params }) {
"codebuddy-cn",
"codebuddy-intl",
"qoder",
"qoder-cn",
"grok-cli",
];
let deviceData;
@@ -505,7 +506,7 @@ export async function POST(request, { params }) {
} else if (provider === "kiro") {
// Kiro needs extraData (clientId, clientSecret) from device code response
result = await pollForToken(provider, deviceCode, null, extraData);
} else if (provider === "qoder") {
} else if (provider === "qoder" || provider === "qoder-cn") {
// Qoder needs both the PKCE verifier (codeVerifier) and the machineId
// captured at device-code time (extraData._qoderMachineId) so
// mapTokens can persist it for COSY signing.
+45 -36
View File
@@ -127,6 +127,49 @@ const buildOAuthResolver = ({ refreshFn, fetchFn, parseFn, errorLabel }) => asyn
return { models: [], warning };
};
// Qoder shares one resolver across intl (qoder) and CN (qoder-cn); the
// credentials carry the connection's provider so qoderModels picks the right
// region's catalog endpoint, and the ids keep the provider prefix.
function buildQoderModelsResolver(providerId) {
return {
customResolver: async (connection) => {
const credentials = {
provider: providerId,
accessToken: connection.accessToken,
apiKey: connection.apiKey,
refreshToken: connection.refreshToken,
email: connection.email,
displayName: connection.displayName,
providerSpecificData: connection.providerSpecificData || {},
};
let warning;
try {
const result = await resolveQoderModels(credentials, { forceRefresh: true });
if (result?.models?.length) {
return {
models: result.models.map((m) => ({
// Use the canonical "<providerId>/<key>" id so the dashboard
// surfaces the same identifier the chat router expects.
id: `${providerId}/${m.id}`,
name: m.name,
contextLength: m.contextLength,
isVL: m.isVL,
isReasoning: m.isReasoning,
maxOutputTokens: m.maxOutputTokens,
description: m.description,
})),
};
}
warning = "Qoder returned no models; falling back to static catalog.";
} catch (error) {
warning = `Failed to fetch Qoder models: ${error.message}`;
console.log("Failed to fetch Qoder models dynamically, falling back to static:", error.message);
}
return { models: [], warning };
},
};
}
// Provider models endpoints configuration
const PROVIDER_MODELS_CONFIG = {
claude: {
@@ -403,42 +446,8 @@ const PROVIDER_MODELS_CONFIG = {
return { models: [], warning };
}
},
qoder: {
customResolver: async (connection) => {
const credentials = {
accessToken: connection.accessToken,
apiKey: connection.apiKey,
refreshToken: connection.refreshToken,
email: connection.email,
displayName: connection.displayName,
providerSpecificData: connection.providerSpecificData || {},
};
let warning;
try {
const result = await resolveQoderModels(credentials, { forceRefresh: true });
if (result?.models?.length) {
return {
models: result.models.map((m) => ({
// Use the canonical "qoder/<key>" id so the dashboard
// surfaces the same identifier the chat router expects.
id: `qoder/${m.id}`,
name: m.name,
contextLength: m.contextLength,
isVL: m.isVL,
isReasoning: m.isReasoning,
maxOutputTokens: m.maxOutputTokens,
description: m.description,
})),
};
}
warning = "Qoder returned no models; falling back to static catalog.";
} catch (error) {
warning = `Failed to fetch Qoder models: ${error.message}`;
console.log("Failed to fetch Qoder models dynamically, falling back to static:", error.message);
}
return { models: [], warning };
},
},
qoder: buildQoderModelsResolver("qoder"),
"qoder-cn": buildQoderModelsResolver("qoder-cn"),
"gemini-cli": {
customResolver: buildOAuthResolver({
refreshFn: (conn) => refreshGoogleToken(conn.refreshToken, GEMINI_CONFIG.clientId, GEMINI_CONFIG.clientSecret),
+14 -2
View File
@@ -74,6 +74,14 @@ const OAUTH_TEST_CONFIG = {
authPrefix: "Bearer ",
refreshable: false,
},
"qoder-cn": {
// Same shape as intl qoder, CN host.
url: "https://openapi.qoder.com.cn/api/v1/userinfo",
method: "GET",
authHeader: "Authorization",
authPrefix: "Bearer ",
refreshable: false,
},
kimi: { checkExpiry: true, refreshable: true },
"kimi-coding": { checkExpiry: true, refreshable: true },
cursor: { tokenExists: true },
@@ -781,12 +789,16 @@ async function testApiKeyConnection(connection, effectiveProxy = null) {
}, effectiveProxy);
return { valid: res.ok, error: res.ok ? null : "Invalid API key" };
}
case "qoder": {
case "qoder":
case "qoder-cn": {
// PAT (pt-...) exchange → job token. A successful exchange proves the PAT.
const exchangeUrl = provider === "qoder-cn"
? "https://openapi.qoder.com.cn/api/v1/jobToken/exchange"
: "https://openapi.qoder.sh/api/v1/jobToken/exchange";
const raw = connection.apiKey || "";
const pat = raw.startsWith("pt-") ? raw : `pt-${raw}`;
const exRes = await fetchWithConnectionProxy(
"https://openapi.qoder.sh/api/v1/jobToken/exchange",
exchangeUrl,
{
method: "POST",
headers: {
+3 -2
View File
@@ -582,11 +582,12 @@ export async function POST(request) {
break;
}
case "qoder": {
case "qoder":
case "qoder-cn": {
// PAT (pt-...) needs the job-token exchange before it can sign
// anything — the generic OpenAI-compat probe below can't validate it.
try {
const resolved = await resolveQoderCredentials({ apiKey, providerSpecificData }, null, AbortSignal.timeout(8000));
const resolved = await resolveQoderCredentials({ provider, apiKey, providerSpecificData }, null, AbortSignal.timeout(8000));
const result = await resolveQoderModels(resolved, { forceRefresh: true });
isValid = !!result?.models?.length;
} catch (err) {
+23 -16
View File
@@ -19,6 +19,27 @@ import { updateProviderCredentials } from "@/sse/services/tokenRefresh";
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
import { capabilitiesFromServiceKind, getCapabilitiesForModel, aggregateComboCapabilities } from "open-sse/providers/capabilities.js";
// Qoder shares one live resolver across intl (qoder) and CN (qoder-cn); the
// credentials carry the provider id so qoderModels picks the right region's
// catalog endpoint.
async function resolveQoderLiveModels(conn, provider) {
const result = await resolveQoderModels({
provider,
accessToken: conn.accessToken,
// PAT (pt-...) connections keep the token in apiKey; without it the live
// catalog silently fails and /v1/models falls back to the static list.
apiKey: conn.apiKey,
refreshToken: conn.refreshToken,
email: conn.email,
displayName: conn.displayName,
providerSpecificData: conn.providerSpecificData || {}
});
// Visible + hidden (enable:false) catalog keys — chat routes all of them.
const models = routableQoderModels(result);
if (!models.length) return null;
return { models: models.map((m) => ({ id: m.id, name: m.name })) };
}
// Per-provider live model resolvers. Each receives a connection record and
// returns { models: [{ id, name? }, ...] } | null on failure.
// Adding a provider here makes /v1/models prefer the live catalog for it.
@@ -31,22 +52,8 @@ const LIVE_MODEL_RESOLVERS = {
}, { log: console });
return result?.models?.length ? { models: result.models } : null;
},
qoder: async (conn) => {
const result = await resolveQoderModels({
accessToken: conn.accessToken,
// PAT (pt-...) connections keep the token in apiKey; without it the live
// catalog silently fails and /v1/models falls back to the static list.
apiKey: conn.apiKey,
refreshToken: conn.refreshToken,
email: conn.email,
displayName: conn.displayName,
providerSpecificData: conn.providerSpecificData || {}
});
// Visible + hidden (enable:false) catalog keys — chat routes all of them.
const models = routableQoderModels(result);
if (!models.length) return null;
return { models: models.map((m) => ({ id: m.id, name: m.name })) };
},
qoder: async (conn) => resolveQoderLiveModels(conn, "qoder"),
"qoder-cn": async (conn) => resolveQoderLiveModels(conn, "qoder-cn"),
kimchi: async (conn) => {
const result = await resolveKimchiModels({
accessToken: conn.accessToken,
+4
View File
@@ -35,6 +35,9 @@ export const GEMINI_CONFIG = { ...GOOGLE_OAUTH_CLIENT, ...PROVIDER_OAUTH["gemini
// of attempting to silently rotate.
export const QODER_CONFIG = { ...PROVIDER_OAUTH["qoder"] };
// Qoder CN (qoder.com.cn) — same device flow as intl Qoder, CN endpoints.
export const QODER_CN_CONFIG = { ...PROVIDER_OAUTH["qoder-cn"] };
// iFlow OAuth Configuration (Authorization Code)
export const IFLOW_CONFIG = { ...PROVIDER_OAUTH["iflow"] };
@@ -219,6 +222,7 @@ export const PROVIDERS = {
CODEX: "codex",
GEMINI: "gemini-cli",
QODER: "qoder",
QODER_CN: "qoder-cn",
IFLOW: "iflow",
ANTIGRAVITY: "antigravity",
OPENAI: "openai",
+2
View File
@@ -12,6 +12,7 @@ import geminiCli from "./gemini-cli.js";
import antigravity from "./antigravity.js";
import iflow from "./iflow.js";
import qoder from "./qoder.js";
import qoderCn from "./qoder-cn.js";
import github from "./github.js";
import kiro from "./kiro.js";
import cursor from "./cursor.js";
@@ -37,6 +38,7 @@ const PROVIDERS = {
antigravity,
iflow,
qoder,
"qoder-cn": qoderCn,
github,
kiro,
cursor,
+5
View File
@@ -0,0 +1,5 @@
import { QODER_CN_CONFIG } from "../constants/oauth.js";
import { createQoderProvider } from "./qoder.js";
// Qoder CN (qoder.com.cn) — same device flow as intl Qoder, CN endpoints.
export default createQoderProvider(QODER_CN_CONFIG);
+104 -95
View File
@@ -1,102 +1,111 @@
import { QODER_CONFIG } from "../constants/oauth.js";
const qoder = {
config: QODER_CONFIG,
flowType: "device_code",
// Qoder uses a custom device flow: PKCE + nonce + machine_id are generated
// locally, the user lands on qoder.com/device/selectAccounts in the
// browser, and we poll openapi.qoder.sh until a `dt-...` token appears.
requestDeviceCode: async (config) => {
const { QoderService } = await import("@/lib/oauth/services/qoder");
const flow = new QoderService().initiateDeviceFlow();
// Match the device_code shape the rest of the OAuthModal expects
// (device_code, user_code, verification_uri[_complete], interval).
// The poll endpoint identifies us by nonce+verifier, not by a
// server-issued device_code, so we plumb our own values through:
// device_code = nonce (modal forwards as deviceCode on poll)
// codeVerifier = our PKCE verifier (route forwards as codeVerifier)
return {
device_code: flow.nonce,
user_code: flow.nonce.slice(0, 8).toUpperCase(),
verification_uri: config.loginUrl,
verification_uri_complete: flow.verificationUriComplete,
expires_in: 300,
interval: 2,
codeVerifier: flow.codeVerifier,
_qoderNonce: flow.nonce,
_qoderMachineId: flow.machineId,
};
},
pollToken: async (config, deviceCode, codeVerifier, extraData) => {
const { QoderService } = await import("@/lib/oauth/services/qoder");
const svc = new QoderService();
const nonce = deviceCode || extraData?._qoderNonce;
const verifier = codeVerifier || extraData?._qoderVerifier;
if (!nonce || !verifier) {
/**
* Build a Qoder device-code provider for a region. `config` is the registry
* oauth block (see open-sse/providers/registry/qoder.js / qoder-cn.js), which
* carries the region's login/deviceToken/userInfo URLs. The device flow is
* identical across regions — only the hosts differ.
*/
export function createQoderProvider(config) {
return {
config,
flowType: "device_code",
// Qoder uses a custom device flow: PKCE + nonce + machine_id are generated
// locally, the user lands on qoder.com[-cn]/device/selectAccounts in the
// browser, and we poll the region's deviceToken endpoint until a `dt-...`
// token appears.
requestDeviceCode: async (cfg) => {
const { QoderService } = await import("@/lib/oauth/services/qoder");
const flow = new QoderService(cfg).initiateDeviceFlow();
// Match the device_code shape the rest of the OAuthModal expects
// (device_code, user_code, verification_uri[_complete], interval).
// The poll endpoint identifies us by nonce+verifier, not by a
// server-issued device_code, so we plumb our own values through:
// device_code = nonce (modal forwards as deviceCode on poll)
// codeVerifier = our PKCE verifier (route forwards as codeVerifier)
return {
ok: false,
data: { error: "invalid_request", error_description: "Missing nonce/verifier" },
device_code: flow.nonce,
user_code: flow.nonce.slice(0, 8).toUpperCase(),
verification_uri: cfg.loginUrl,
verification_uri_complete: flow.verificationUriComplete,
expires_in: 300,
interval: 2,
codeVerifier: flow.codeVerifier,
_qoderNonce: flow.nonce,
_qoderMachineId: flow.machineId,
};
}
let result;
try {
result = await svc.pollDeviceToken({ nonce, codeVerifier: verifier });
} catch (err) {
},
pollToken: async (cfg, deviceCode, codeVerifier, extraData) => {
const { QoderService } = await import("@/lib/oauth/services/qoder");
const svc = new QoderService(cfg);
const nonce = deviceCode || extraData?._qoderNonce;
const verifier = codeVerifier || extraData?._qoderVerifier;
if (!nonce || !verifier) {
return {
ok: false,
data: { error: "invalid_request", error_description: "Missing nonce/verifier" },
};
}
let result;
try {
result = await svc.pollDeviceToken({ nonce, codeVerifier: verifier });
} catch (err) {
return {
ok: false,
data: { error: "poll_failed", error_description: err.message },
};
}
if (result.status === "pending") {
return { ok: false, data: { error: "authorization_pending" } };
}
// Best-effort profile lookup so we have a name/email to display.
const userInfo = await svc.fetchUserInfo(result.accessToken);
// expireTime is a Unix-ms timestamp from QoderService.parseExpiry,
// which already falls back to "now + 30 days" when the upstream
// omits expiry. Floor to a sane minimum (1 day) so a stale or
// skewed upstream timestamp doesn't truncate the stored token below
// something useful.
const minSeconds = 24 * 60 * 60;
const remainingSeconds = Math.floor((result.expireTime - Date.now()) / 1000);
const expiresIn = Math.max(minSeconds, remainingSeconds);
return {
ok: false,
data: { error: "poll_failed", error_description: err.message },
ok: true,
data: {
access_token: result.accessToken,
refresh_token: result.refreshToken,
expires_in: expiresIn,
_qoderUserId: result.userId,
_qoderMachineId: extraData?._qoderMachineId || "",
_qoderName: userInfo.name,
_qoderEmail: userInfo.email,
_qoderOrganizationId: userInfo.organizationId,
},
};
}
if (result.status === "pending") {
return { ok: false, data: { error: "authorization_pending" } };
}
// Best-effort profile lookup so we have a name/email to display.
const userInfo = await svc.fetchUserInfo(result.accessToken);
// expireTime is a Unix-ms timestamp from QoderService.parseExpiry,
// which already falls back to "now + 30 days" when the upstream
// omits expiry. Floor to a sane minimum (1 day) so a stale or
// skewed upstream timestamp doesn't truncate the stored token below
// something useful.
const minSeconds = 24 * 60 * 60;
const remainingSeconds = Math.floor((result.expireTime - Date.now()) / 1000);
const expiresIn = Math.max(minSeconds, remainingSeconds);
return {
ok: true,
data: {
access_token: result.accessToken,
refresh_token: result.refreshToken,
expires_in: expiresIn,
_qoderUserId: result.userId,
_qoderMachineId: extraData?._qoderMachineId || "",
_qoderName: userInfo.name,
_qoderEmail: userInfo.email,
_qoderOrganizationId: userInfo.organizationId,
},
};
},
mapTokens: (tokens) => {
const rawEmail = (tokens._qoderEmail || "").trim();
const displayName = (tokens._qoderName || "").trim() || null;
const userId = tokens._qoderUserId || "";
// Dedup in createProviderConnection requires a non-empty email. When
// fetchUserInfo silently fails (returns ""), fall back to a stable
// synthetic identifier derived from userId so re-logins update the
// existing row instead of accumulating "Account N" duplicates.
const email = rawEmail || (userId ? `qoder-user-${userId}` : null);
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || null,
expiresIn: tokens.expires_in,
email,
displayName,
providerSpecificData: {
authMethod: "device",
userId,
machineId: tokens._qoderMachineId || "",
organizationId: tokens._qoderOrganizationId || "",
},
};
},
};
},
mapTokens: (tokens) => {
const rawEmail = (tokens._qoderEmail || "").trim();
const displayName = (tokens._qoderName || "").trim() || null;
const userId = tokens._qoderUserId || "";
// Dedup in createProviderConnection requires a non-empty email. When
// fetchUserInfo silently fails (returns ""), fall back to a stable
// synthetic identifier derived from userId so re-logins update the
// existing row instead of accumulating "Account N" duplicates.
const email = rawEmail || (userId ? `qoder-user-${userId}` : null);
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || null,
expiresIn: tokens.expires_in,
email,
displayName,
providerSpecificData: {
authMethod: "device",
userId,
machineId: tokens._qoderMachineId || "",
organizationId: tokens._qoderOrganizationId || "",
},
};
},
};
}
export default qoder;
export default createQoderProvider(QODER_CONFIG);
+24 -3
View File
@@ -52,6 +52,27 @@ async function fetchWithTimeout(url, init = {}) {
}
export class QoderService {
/**
* Region-aware device flow. Pass an oauth config block (registry oauth →
* PROVIDER_OAUTH) to hit the CN site; without one, the intl endpoints are
* used. Only the hostnames differ between regions — the flow is identical.
*/
constructor(config = {}) {
this.config = config;
}
loginUrl() {
return this.config.loginUrl || QODER_LOGIN_URL;
}
deviceTokenUrl() {
return this.config.deviceTokenUrl || QODER_DEVICE_TOKEN_URL;
}
userInfoUrl() {
return this.config.userInfoUrl || QODER_USERINFO_URL;
}
/**
* Generate a PKCE verifier + S256 challenge pair.
* Uses 32 random bytes (matches qodercli/Veria).
@@ -79,7 +100,7 @@ export class QoderService {
});
return {
verificationUriComplete: `${QODER_LOGIN_URL}?${params.toString()}`,
verificationUriComplete: `${this.loginUrl()}?${params.toString()}`,
codeVerifier: verifier,
nonce,
machineId,
@@ -98,7 +119,7 @@ export class QoderService {
if (!nonce || !codeVerifier) {
throw new Error("pollDeviceToken: missing nonce or code verifier");
}
const url = `${QODER_DEVICE_TOKEN_URL}?nonce=${encodeURIComponent(nonce)}&verifier=${encodeURIComponent(codeVerifier)}&challenge_method=S256`;
const url = `${this.deviceTokenUrl()}?nonce=${encodeURIComponent(nonce)}&verifier=${encodeURIComponent(codeVerifier)}&challenge_method=S256`;
const response = await fetchWithTimeout(url, {
method: "GET",
@@ -155,7 +176,7 @@ export class QoderService {
*/
async fetchUserInfo(accessToken) {
try {
const response = await fetchWithTimeout(QODER_USERINFO_URL, {
const response = await fetchWithTimeout(this.userInfoUrl(), {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
+2 -1
View File
@@ -289,6 +289,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
"codebuddy-cn",
"codebuddy-intl",
"qoder",
"qoder-cn",
"grok-cli",
];
if (deviceCodeProviders.includes(provider)) {
@@ -324,7 +325,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
_authMethod: data._authMethod,
_startUrl: data._startUrl,
}
: provider === "qoder"
: (provider === "qoder" || provider === "qoder-cn")
? {
_qoderNonce: data._qoderNonce,
_qoderMachineId: data._qoderMachineId,