2026-03-20 15:43:14 +00:00

149 lines
4.7 KiB
TypeScript

import { buildQwenPortalProvider, QWEN_PORTAL_BASE_URL } from "./provider-catalog.js";
import {
buildOauthProviderAuthResult,
definePluginEntry,
ensureAuthProfileStore,
listProfilesForProvider,
QWEN_OAUTH_MARKER,
refreshQwenPortalCredentials,
type ProviderAuthContext,
type ProviderCatalogContext,
} from "./runtime-api.js";
const PROVIDER_ID = "qwen-portal";
const PROVIDER_LABEL = "Qwen";
const DEFAULT_MODEL = "qwen-portal/coder-model";
const DEFAULT_BASE_URL = QWEN_PORTAL_BASE_URL;
function normalizeBaseUrl(value: string | undefined): string {
const raw = value?.trim() || DEFAULT_BASE_URL;
const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`;
return withProtocol.endsWith("/v1") ? withProtocol : `${withProtocol.replace(/\/+$/, "")}/v1`;
}
function buildProviderCatalog(params: { baseUrl: string; apiKey: string }) {
return {
...buildQwenPortalProvider(),
baseUrl: params.baseUrl,
apiKey: params.apiKey,
};
}
function resolveCatalog(ctx: ProviderCatalogContext) {
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
const envApiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const hasProfiles = listProfilesForProvider(authStore, PROVIDER_ID).length > 0;
const explicitApiKey =
typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined;
const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? QWEN_OAUTH_MARKER : undefined);
if (!apiKey) {
return null;
}
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl : undefined;
return {
provider: buildProviderCatalog({
baseUrl: normalizeBaseUrl(explicitBaseUrl),
apiKey,
}),
};
}
export default definePluginEntry({
id: "qwen-portal-auth",
name: "Qwen OAuth",
description: "OAuth flow for Qwen (free-tier) models",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: PROVIDER_LABEL,
docsPath: "/providers/qwen",
aliases: ["qwen"],
envVars: ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"],
catalog: {
run: async (ctx: ProviderCatalogContext) => resolveCatalog(ctx),
},
auth: [
{
id: "device",
label: "Qwen OAuth",
hint: "Device code login",
kind: "device_code",
run: async (ctx: ProviderAuthContext) => {
const progress = ctx.prompter.progress("Starting Qwen OAuth…");
try {
const { loginQwenPortalOAuth } = await import("./oauth.runtime.js");
const result = await loginQwenPortalOAuth({
openUrl: ctx.openUrl,
note: ctx.prompter.note,
progress,
});
progress.stop("Qwen OAuth complete");
const baseUrl = normalizeBaseUrl(result.resourceUrl);
return buildOauthProviderAuthResult({
providerId: PROVIDER_ID,
defaultModel: DEFAULT_MODEL,
access: result.access,
refresh: result.refresh,
expires: result.expires,
configPatch: {
models: {
providers: {
[PROVIDER_ID]: {
baseUrl,
models: [],
},
},
},
agents: {
defaults: {
models: {
"qwen-portal/coder-model": { alias: "qwen" },
"qwen-portal/vision-model": {},
},
},
},
},
notes: [
"Qwen OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.",
`Base URL defaults to ${DEFAULT_BASE_URL}. Override models.providers.${PROVIDER_ID}.baseUrl if needed.`,
],
});
} catch (err) {
progress.stop("Qwen OAuth failed");
await ctx.prompter.note(
"If OAuth fails, verify your Qwen account has portal access and try again.",
"Qwen OAuth",
);
throw err;
}
},
},
],
wizard: {
setup: {
choiceId: "qwen-portal",
choiceLabel: "Qwen OAuth",
choiceHint: "Device code login",
methodId: "device",
},
},
refreshOAuth: async (cred) => ({
...cred,
...(await refreshQwenPortalCredentials(cred)),
type: "oauth",
provider: PROVIDER_ID,
email: cred.email,
}),
});
},
});