omk-ai 0.96.2 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +239 -119
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +349 -272
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +4 -1
- package/dist/models.js.map +1 -1
- package/dist/utils/oauth/google-oauth-shared.d.ts.map +1 -1
- package/dist/utils/oauth/google-oauth-shared.js.map +1 -1
- package/dist/utils/oauth/qwen.d.ts +1 -1
- package/dist/utils/oauth/qwen.d.ts.map +1 -1
- package/dist/utils/oauth/qwen.js +50 -19
- package/dist/utils/oauth/qwen.js.map +1 -1
- package/package.json +1 -1
package/dist/models.js
CHANGED
|
@@ -5,7 +5,10 @@ const modelRegistry = new Map();
|
|
|
5
5
|
for (const [provider, models] of Object.entries(MODELS)) {
|
|
6
6
|
const providerModels = new Map();
|
|
7
7
|
for (const [id, model] of Object.entries(models)) {
|
|
8
|
-
const
|
|
8
|
+
const generated = model;
|
|
9
|
+
const registered = generated.id.toLowerCase().includes("gpt-5.6")
|
|
10
|
+
? { ...generated, contextWindow: 1_000_000 }
|
|
11
|
+
: generated;
|
|
9
12
|
providerModels.set(id, provider === "xai" ? applyGrokThinking(registered) : registered);
|
|
10
13
|
}
|
|
11
14
|
modelRegistry.set(provider, providerModels);
|
package/dist/models.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAGjE,MAAM,aAAa,GAAyC,IAAI,GAAG,EAAE,CAAC;AAEtE,iDAAiD;AACjD,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACzD,MAAM,cAAc,GAAG,IAAI,GAAG,EAAsB,CAAC;IACrD,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,MAAM,
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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","sourcesContent":["import { MODELS } from \"./models.generated.ts\";\nimport { applyGrokThinking } from \"./providers/grok-thinking.ts\";\nimport type { Api, KnownProvider, Model, ModelThinkingLevel, Usage } from \"./types.ts\";\n\nconst modelRegistry: Map<string, Map<string, Model<Api>>> = new Map();\n\n// Initialize registry from MODELS on module load\nfor (const [provider, models] of Object.entries(MODELS)) {\n\tconst providerModels = new Map<string, Model<Api>>();\n\tfor (const [id, model] of Object.entries(models)) {\n\t\tconst generated = model as Model<Api>;\n\t\tconst registered = generated.id.toLowerCase().includes(\"gpt-5.6\")\n\t\t\t? { ...generated, contextWindow: 1_000_000 }\n\t\t\t: generated;\n\t\tproviderModels.set(id, provider === \"xai\" ? applyGrokThinking(registered) : registered);\n\t}\n\tmodelRegistry.set(provider, providerModels);\n}\n\ntype ModelApi<\n\tTProvider extends KnownProvider,\n\tTModelId extends keyof (typeof MODELS)[TProvider],\n> = (typeof MODELS)[TProvider][TModelId] extends { api: infer TApi } ? (TApi extends Api ? TApi : never) : never;\n\nexport function getModel<TProvider extends KnownProvider, TModelId extends keyof (typeof MODELS)[TProvider]>(\n\tprovider: TProvider,\n\tmodelId: TModelId,\n): Model<ModelApi<TProvider, TModelId>> {\n\tconst providerModels = modelRegistry.get(provider);\n\treturn providerModels?.get(modelId as string) as Model<ModelApi<TProvider, TModelId>>;\n}\n\nexport function getProviders(): KnownProvider[] {\n\treturn Array.from(modelRegistry.keys()) as KnownProvider[];\n}\n\nexport function getModels<TProvider extends KnownProvider>(\n\tprovider: TProvider,\n): Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[] {\n\tconst models = modelRegistry.get(provider);\n\treturn models ? (Array.from(models.values()) as Model<ModelApi<TProvider, keyof (typeof MODELS)[TProvider]>>[]) : [];\n}\n\nexport function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage[\"cost\"] {\n\tusage.cost.input = (model.cost.input / 1000000) * usage.input;\n\tusage.cost.output = (model.cost.output / 1000000) * usage.output;\n\tusage.cost.cacheRead = (model.cost.cacheRead / 1000000) * usage.cacheRead;\n\tusage.cost.cacheWrite = (model.cost.cacheWrite / 1000000) * usage.cacheWrite;\n\tusage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;\n\treturn usage.cost;\n}\n\nconst EXTENDED_THINKING_LEVELS: ModelThinkingLevel[] = [\n\t\"off\",\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\n\nexport function getSupportedThinkingLevels<TApi extends Api>(model: Model<TApi>): ModelThinkingLevel[] {\n\tif (!model.reasoning) return [\"off\"];\n\n\treturn EXTENDED_THINKING_LEVELS.filter((level) => {\n\t\tconst mapped = model.thinkingLevelMap?.[level];\n\t\tif (mapped === null) return false;\n\t\t// xhigh, max, and ultra are top-tier levels only exposed when a model explicitly maps them.\n\t\tif (level === \"xhigh\" || level === \"max\" || level === \"ultra\") return mapped !== undefined;\n\t\treturn true;\n\t});\n}\n\nexport function clampThinkingLevel<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tlevel: ModelThinkingLevel,\n): ModelThinkingLevel {\n\tconst availableLevels = getSupportedThinkingLevels(model);\n\tif (availableLevels.includes(level)) return level;\n\n\tconst requestedIndex = EXTENDED_THINKING_LEVELS.indexOf(level);\n\tif (requestedIndex === -1) return availableLevels[0] ?? \"off\";\n\n\tfor (let i = requestedIndex; i < EXTENDED_THINKING_LEVELS.length; i++) {\n\t\tconst candidate = EXTENDED_THINKING_LEVELS[i];\n\t\tif (availableLevels.includes(candidate)) return candidate;\n\t}\n\tfor (let i = requestedIndex - 1; i >= 0; i--) {\n\t\tconst candidate = EXTENDED_THINKING_LEVELS[i];\n\t\tif (availableLevels.includes(candidate)) return candidate;\n\t}\n\treturn availableLevels[0] ?? \"off\";\n}\n\n/**\n * Check if two models are equal by comparing both their id and provider.\n * Returns false if either model is null or undefined.\n */\nexport function modelsAreEqual<TApi extends Api>(\n\ta: Model<TApi> | null | undefined,\n\tb: Model<TApi> | null | undefined,\n): boolean {\n\tif (!a || !b) return false;\n\treturn a.id === b.id && a.provider === b.provider;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-oauth-shared.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"google-oauth-shared.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExE,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAClG;AAgHD,4CAA4C;AAC5C,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,mBAAmB,EAC9B,MAAM,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAyE3B;AAED,oCAAoC;AACpC,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,UAAU,GAAG,cAAc,GAAG,UAAU,CAAC,EAC7E,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,gBAAgB,CAAC,CA6B3B","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Originally adapted from the oh-my-pi project for Node.js (no Bun dependencies);\n * that tree is no longer vendored here (ADR-OMP-010).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-oauth-shared.js","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Adapted from vendor/oh-my-pi for Node.js (no Bun dependencies).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"google-oauth-shared.js","sourceRoot":"","sources":["../../../src/utils/oauth/google-oauth-shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared Google OAuth flow for Gemini CLI and Antigravity.\n *\n * Both use the same authorization-code flow with a local callback server;\n * only client credentials, scopes, and project-discovery logic differ.\n * Originally adapted from the oh-my-pi project for Node.js (no Bun dependencies);\n * that tree is no longer vendored here (ADR-OMP-010).\n */\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks } from \"./types.ts\";\n\nexport interface GoogleOAuthFlowConfig {\n\tclientId: string;\n\tclientSecret: string;\n\tauthUrl: string;\n\ttokenUrl: string;\n\tscopes: string[];\n\tcallbackPort: number;\n\tcallbackPath: string;\n\tdiscoverProject: (accessToken: string, onProgress?: (message: string) => void) => Promise<string>;\n}\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Google OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nasync function getUserEmail(accessToken: string): Promise<string | undefined> {\n\ttry {\n\t\tconst response = await fetch(\"https://www.googleapis.com/oauth2/v1/userinfo?alt=json\", {\n\t\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t\t});\n\t\tif (response.ok) {\n\t\t\tconst data = (await response.json()) as { email?: string };\n\t\t\treturn data.email;\n\t\t}\n\t} catch {\n\t\t// Ignore errors, email is optional\n\t}\n\treturn undefined;\n}\n\nfunction startCallbackServer(\n\tport: number,\n\tcallbackPath: string,\n): Promise<{\n\tserver: Server;\n\tredirectUri: string;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n\tcancelWait: () => void;\n}> {\n\tconst hostname = \"127.0.0.1\";\n\tconst redirectUri = `http://${hostname}:${port}${callbackPath}`;\n\n\treturn getNodeApis().then(({ createServer }) => {\n\t\treturn new Promise((resolveServer) => {\n\t\t\tlet settled = false;\n\n\t\t\tconst server = createServer((req, res) => {\n\t\t\t\tif (!req.url?.startsWith(callbackPath)) {\n\t\t\t\t\tres.writeHead(404).end();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst url = new URL(req.url, `http://${req.headers.host || \"localhost\"}`);\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error || !code) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml);\n\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tsetImmediate(() => {\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver,\n\t\t\t\t\t\t\tredirectUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve({ code, state: state || \"\" }),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tserver.listen(port, hostname, () => {\n\t\t\t\t// Server ready\n\t\t\t});\n\n\t\t\tserver.on(\"error\", () => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\t// Try random port fallback\n\t\t\t\t\tconst fallback = createServer(server.listeners(\"request\")[0] as any);\n\t\t\t\t\tfallback.listen(0, hostname, () => {\n\t\t\t\t\t\tconst addr = fallback.address();\n\t\t\t\t\t\tconst actualPort = typeof addr === \"object\" && addr ? addr.port : port;\n\t\t\t\t\t\tconst fallbackUri = `http://${hostname}:${actualPort}${callbackPath}`;\n\t\t\t\t\t\tresolveServer({\n\t\t\t\t\t\t\tserver: fallback,\n\t\t\t\t\t\t\tredirectUri: fallbackUri,\n\t\t\t\t\t\t\twaitForCode: () => Promise.resolve(null),\n\t\t\t\t\t\t\tcancelWait: () => {},\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t});\n}\n\n/** Run the full Google OAuth login flow. */\nexport async function runGoogleOAuthLogin(\n\tcallbacks: OAuthLoginCallbacks,\n\tconfig: GoogleOAuthFlowConfig,\n): Promise<OAuthCredentials> {\n\tconst state = crypto.randomUUID();\n\tconst server = await startCallbackServer(config.callbackPort, config.callbackPath);\n\n\tconst authParams = new URLSearchParams({\n\t\tclient_id: config.clientId,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: server.redirectUri,\n\t\tscope: config.scopes.join(\" \"),\n\t\tstate,\n\t\taccess_type: \"offline\",\n\t\tprompt: \"consent\",\n\t});\n\n\tconst authUrl = `${config.authUrl}?${authParams.toString()}`;\n\n\tcallbacks.onAuth({ url: authUrl, instructions: \"Complete the sign-in in your browser.\" });\n\tcallbacks.onProgress?.(\"Waiting for browser authentication...\");\n\n\tlet result: { code: string; state: string } | null;\n\ttry {\n\t\tresult = await server.waitForCode();\n\t} finally {\n\t\tserver.cancelWait();\n\t\tserver.server.close();\n\t}\n\n\tif (!result) {\n\t\tthrow new Error(\"Login cancelled or timed out\");\n\t}\n\n\tcallbacks.onProgress?.(\"Exchanging authorization code for tokens...\");\n\n\tconst tokenResponse = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\tcode: result.code,\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tredirect_uri: server.redirectUri,\n\t\t}),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\tif (!tokenData.refresh_token) {\n\t\tthrow new Error(\"No refresh token received. Please try again.\");\n\t}\n\n\tcallbacks.onProgress?.(\"Getting user info...\");\n\tconst email = await getUserEmail(tokenData.access_token);\n\n\tcallbacks.onProgress?.(\"Discovering project...\");\n\tconst projectId = await config.discoverProject(tokenData.access_token, callbacks.onProgress);\n\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t\temail,\n\t};\n}\n\n/** Refresh a Google OAuth token. */\nexport async function refreshGoogleToken(\n\tconfig: Pick<GoogleOAuthFlowConfig, \"clientId\" | \"clientSecret\" | \"tokenUrl\">,\n\trefreshToken: string,\n\tprojectId: string,\n): Promise<OAuthCredentials> {\n\tconst response = await fetch(config.tokenUrl, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n\t\tbody: new URLSearchParams({\n\t\t\tclient_id: config.clientId,\n\t\t\tclient_secret: config.clientSecret,\n\t\t\trefresh_token: refreshToken,\n\t\t\tgrant_type: \"refresh_token\",\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Google token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\texpires_in: number;\n\t\trefresh_token?: string;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token || refreshToken,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t\tprojectId,\n\t};\n}\n"]}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.ts";
|
|
10
10
|
export declare const QWEN_OAUTH_PROVIDER_ID = "qwen-oauth";
|
|
11
|
-
/** Normalize `resource_url
|
|
11
|
+
/** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */
|
|
12
12
|
export declare function normalizeQwenBaseUrl(resourceUrl: string | undefined): string;
|
|
13
13
|
/** Run the Qwen device-authorization login flow. */
|
|
14
14
|
export declare function loginQwen(options: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qwen.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEhG,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAiCnD,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAO5E;AAoGD,oDAAoD;AACpD,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACxC,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAU5B;AAED,kCAAkC;AAClC,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAYtF;AAmBD,eAAO,MAAM,iBAAiB,EAAE,sBAgC/B,CAAC","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize `resource_url` into an absolute base URL ending in `/v1`. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\tconst withProtocol = /^https?:\\/\\//i.test(resourceUrl) ? resourceUrl : `https://${resourceUrl}`;\n\tconst trimmed = withProtocol.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(\"/v1\") ? trimmed : `${trimmed}/v1`;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(`Qwen token response missing access_token: ${JSON.stringify(json)}`);\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen device code request failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(`Invalid Qwen device code response: ${JSON.stringify(json)}`);\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tmessage: `Qwen token response missing access_token: ${JSON.stringify(json)}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tmessage: `Qwen token poll failed (${response.status}): ${text || response.statusText}`,\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: device.verification_uri_complete || device.verification_uri || DEFAULT_VERIFICATION_URI,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen token refresh failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"qwen.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEhG,eAAO,MAAM,sBAAsB,eAAe,CAAC;AAkCnD,gGAAgG;AAChG,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CA4B5E;AAkHD,oDAAoD;AACpD,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACxC,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAC;IAClD,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAU5B;AAED,kCAAkC;AAClC,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAWtF;AAmBD,eAAO,MAAM,iBAAiB,EAAE,sBAgC/B,CAAC","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\nconst QWEN_RESOURCE_HOST_SUFFIXES = [\"qwen.ai\", \"aliyuncs.com\"] as const;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\ttry {\n\t\tconst raw = resourceUrl.trim();\n\t\tconst url = new URL(raw.includes(\"://\") ? raw : `https://${raw}`);\n\t\tconst hostname = url.hostname.toLowerCase();\n\t\tconst approvedHost = QWEN_RESOURCE_HOST_SUFFIXES.some(\n\t\t\t(suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),\n\t\t);\n\t\tif (\n\t\t\turl.protocol !== \"https:\" ||\n\t\t\t!approvedHost ||\n\t\t\turl.username !== \"\" ||\n\t\t\turl.password !== \"\" ||\n\t\t\t(url.port !== \"\" && url.port !== \"443\") ||\n\t\t\turl.search !== \"\" ||\n\t\t\turl.hash !== \"\"\n\t\t) {\n\t\t\tthrow new Error(\"unapproved origin\");\n\t\t}\n\t\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\t\turl.pathname = path.endsWith(\"/v1\") ? path : `${path}/v1`;\n\t\treturn url.toString();\n\t} catch {\n\t\tthrow new Error(\"Qwen resource_url must use an approved HTTPS origin\");\n\t}\n}\n\nfunction normalizeVerificationUri(...candidates: Array<string | undefined>): string {\n\tfor (const candidate of candidates) {\n\t\tif (!candidate) continue;\n\t\ttry {\n\t\t\tconst url = new URL(candidate);\n\t\t\tif (\n\t\t\t\turl.protocol === \"https:\" &&\n\t\t\t\turl.hostname.toLowerCase() === \"chat.qwen.ai\" &&\n\t\t\t\turl.username === \"\" &&\n\t\t\t\turl.password === \"\"\n\t\t\t) {\n\t\t\t\treturn url.toString();\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try the next fixed-origin candidate.\n\t\t}\n\t}\n\treturn DEFAULT_VERIFICATION_URI;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(\"Qwen token response missing access_token\");\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tnormalizeQwenBaseUrl(json.resource_url);\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen device code request failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(\"Invalid Qwen device code response\");\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn { status: \"failed\", message: \"Qwen token response missing access_token\" };\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn { status: \"failed\", message: `Qwen token poll failed (${response.status})` };\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: normalizeVerificationUri(device.verification_uri_complete, device.verification_uri),\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen token refresh failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
|
package/dist/utils/oauth/qwen.js
CHANGED
|
@@ -17,21 +17,59 @@ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
|
17
17
|
const DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
18
18
|
const DEFAULT_VERIFICATION_URI = "https://chat.qwen.ai";
|
|
19
19
|
const DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;
|
|
20
|
+
const QWEN_RESOURCE_HOST_SUFFIXES = ["qwen.ai", "aliyuncs.com"];
|
|
20
21
|
function formEncode(data) {
|
|
21
22
|
return new URLSearchParams(data).toString();
|
|
22
23
|
}
|
|
23
|
-
/** Normalize `resource_url
|
|
24
|
+
/** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */
|
|
24
25
|
export function normalizeQwenBaseUrl(resourceUrl) {
|
|
25
26
|
if (!resourceUrl || resourceUrl.trim().length === 0) {
|
|
26
27
|
return DEFAULT_BASE_URL;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
try {
|
|
30
|
+
const raw = resourceUrl.trim();
|
|
31
|
+
const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
|
|
32
|
+
const hostname = url.hostname.toLowerCase();
|
|
33
|
+
const approvedHost = QWEN_RESOURCE_HOST_SUFFIXES.some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`));
|
|
34
|
+
if (url.protocol !== "https:" ||
|
|
35
|
+
!approvedHost ||
|
|
36
|
+
url.username !== "" ||
|
|
37
|
+
url.password !== "" ||
|
|
38
|
+
(url.port !== "" && url.port !== "443") ||
|
|
39
|
+
url.search !== "" ||
|
|
40
|
+
url.hash !== "") {
|
|
41
|
+
throw new Error("unapproved origin");
|
|
42
|
+
}
|
|
43
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
44
|
+
url.pathname = path.endsWith("/v1") ? path : `${path}/v1`;
|
|
45
|
+
return url.toString();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("Qwen resource_url must use an approved HTTPS origin");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function normalizeVerificationUri(...candidates) {
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
if (!candidate)
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(candidate);
|
|
57
|
+
if (url.protocol === "https:" &&
|
|
58
|
+
url.hostname.toLowerCase() === "chat.qwen.ai" &&
|
|
59
|
+
url.username === "" &&
|
|
60
|
+
url.password === "") {
|
|
61
|
+
return url.toString();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Try the next fixed-origin candidate.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return DEFAULT_VERIFICATION_URI;
|
|
31
69
|
}
|
|
32
70
|
function credentialsFromToken(json, fallbackRefresh) {
|
|
33
71
|
if (!json.access_token) {
|
|
34
|
-
throw new Error(
|
|
72
|
+
throw new Error("Qwen token response missing access_token");
|
|
35
73
|
}
|
|
36
74
|
const refresh = json.refresh_token ?? fallbackRefresh;
|
|
37
75
|
if (!refresh) {
|
|
@@ -44,6 +82,7 @@ function credentialsFromToken(json, fallbackRefresh) {
|
|
|
44
82
|
expires: Date.now() + expiresInSeconds * 1000 - 30_000,
|
|
45
83
|
};
|
|
46
84
|
if (typeof json.resource_url === "string" && json.resource_url.length > 0) {
|
|
85
|
+
normalizeQwenBaseUrl(json.resource_url);
|
|
47
86
|
credentials.resource_url = json.resource_url;
|
|
48
87
|
}
|
|
49
88
|
return credentials;
|
|
@@ -61,12 +100,11 @@ async function requestDeviceCode(challenge, signal) {
|
|
|
61
100
|
signal,
|
|
62
101
|
});
|
|
63
102
|
if (!response.ok) {
|
|
64
|
-
|
|
65
|
-
throw new Error(`Qwen device code request failed (${response.status}): ${text || response.statusText}`);
|
|
103
|
+
throw new Error(`Qwen device code request failed (${response.status})`);
|
|
66
104
|
}
|
|
67
105
|
const json = (await response.json());
|
|
68
106
|
if (!json?.device_code || !json.user_code) {
|
|
69
|
-
throw new Error(
|
|
107
|
+
throw new Error("Invalid Qwen device code response");
|
|
70
108
|
}
|
|
71
109
|
return json;
|
|
72
110
|
}
|
|
@@ -90,10 +128,7 @@ async function pollForToken(deviceCode, verifier, intervalSeconds, signal) {
|
|
|
90
128
|
if (response.ok) {
|
|
91
129
|
const json = (await response.json());
|
|
92
130
|
if (!json.access_token) {
|
|
93
|
-
return {
|
|
94
|
-
status: "failed",
|
|
95
|
-
message: `Qwen token response missing access_token: ${JSON.stringify(json)}`,
|
|
96
|
-
};
|
|
131
|
+
return { status: "failed", message: "Qwen token response missing access_token" };
|
|
97
132
|
}
|
|
98
133
|
return { status: "complete", value: credentialsFromToken(json) };
|
|
99
134
|
}
|
|
@@ -111,10 +146,7 @@ async function pollForToken(deviceCode, verifier, intervalSeconds, signal) {
|
|
|
111
146
|
if (errorCode === "slow_down") {
|
|
112
147
|
return { status: "slow_down" };
|
|
113
148
|
}
|
|
114
|
-
return {
|
|
115
|
-
status: "failed",
|
|
116
|
-
message: `Qwen token poll failed (${response.status}): ${text || response.statusText}`,
|
|
117
|
-
};
|
|
149
|
+
return { status: "failed", message: `Qwen token poll failed (${response.status})` };
|
|
118
150
|
},
|
|
119
151
|
});
|
|
120
152
|
}
|
|
@@ -124,7 +156,7 @@ export async function loginQwen(options) {
|
|
|
124
156
|
const device = await requestDeviceCode(challenge, options.signal);
|
|
125
157
|
options.onDeviceCode({
|
|
126
158
|
userCode: device.user_code ?? "",
|
|
127
|
-
verificationUri: device.verification_uri_complete
|
|
159
|
+
verificationUri: normalizeVerificationUri(device.verification_uri_complete, device.verification_uri),
|
|
128
160
|
intervalSeconds: device.interval,
|
|
129
161
|
expiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,
|
|
130
162
|
});
|
|
@@ -138,8 +170,7 @@ export async function refreshQwenToken(refreshToken) {
|
|
|
138
170
|
body: formEncode({ grant_type: "refresh_token", client_id: CLIENT_ID, refresh_token: refreshToken }),
|
|
139
171
|
});
|
|
140
172
|
if (!response.ok) {
|
|
141
|
-
|
|
142
|
-
throw new Error(`Qwen token refresh failed (${response.status}): ${text || response.statusText}`);
|
|
173
|
+
throw new Error(`Qwen token refresh failed (${response.status})`);
|
|
143
174
|
}
|
|
144
175
|
const json = (await response.json());
|
|
145
176
|
return credentialsFromToken(json, refreshToken);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qwen.js","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize `resource_url` into an absolute base URL ending in `/v1`. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\tconst withProtocol = /^https?:\\/\\//i.test(resourceUrl) ? resourceUrl : `https://${resourceUrl}`;\n\tconst trimmed = withProtocol.replace(/\\/+$/, \"\");\n\treturn trimmed.endsWith(\"/v1\") ? trimmed : `${trimmed}/v1`;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(`Qwen token response missing access_token: ${JSON.stringify(json)}`);\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen device code request failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(`Invalid Qwen device code response: ${JSON.stringify(json)}`);\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\tmessage: `Qwen token response missing access_token: ${JSON.stringify(json)}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tmessage: `Qwen token poll failed (${response.status}): ${text || response.statusText}`,\n\t\t\t};\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: device.verification_uri_complete || device.verification_uri || DEFAULT_VERIFICATION_URI,\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tconst text = await response.text().catch(() => \"\");\n\t\tthrow new Error(`Qwen token refresh failed (${response.status}): ${text || response.statusText}`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"qwen.js","sourceRoot":"","sources":["../../../src/utils/oauth/qwen.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Qwen OAuth (Qwen Code subscription).\n *\n * RFC 8628 device-authorization flow with PKCE. The client id, endpoints and\n * scopes mirror the open-source qwen-code CLI. After login the token response\n * carries a `resource_url` pointing at an OpenAI-compatible chat endpoint, so\n * Qwen models are exposed through the standard `openai-completions` API.\n */\n\nimport type { Api, Model } from \"../../types.ts\";\nimport { pollOAuthDeviceCodeFlow } from \"./device-code.ts\";\nimport { generatePKCE } from \"./pkce.ts\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.ts\";\n\nexport const QWEN_OAUTH_PROVIDER_ID = \"qwen-oauth\";\n\nconst DEVICE_CODE_URL = \"https://chat.qwen.ai/api/v1/oauth2/device/code\";\nconst TOKEN_URL = \"https://chat.qwen.ai/api/v1/oauth2/token\";\nconst CLIENT_ID = \"f0304373b74a44d2b584a3fb70ca9e56\";\nconst SCOPE = \"openid profile email model.completion\";\nconst DEVICE_GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\nconst DEFAULT_BASE_URL = \"https://dashscope.aliyuncs.com/compatible-mode/v1\";\nconst DEFAULT_VERIFICATION_URI = \"https://chat.qwen.ai\";\nconst DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;\nconst QWEN_RESOURCE_HOST_SUFFIXES = [\"qwen.ai\", \"aliyuncs.com\"] as const;\n\ntype QwenDeviceAuthResponse = {\n\tdevice_code?: string;\n\tuser_code?: string;\n\tverification_uri?: string;\n\tverification_uri_complete?: string;\n\texpires_in?: number;\n\tinterval?: number;\n};\n\ntype QwenTokenResponse = {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\tresource_url?: string;\n\terror?: string;\n\terror_description?: string;\n};\n\nfunction formEncode(data: Record<string, string>): string {\n\treturn new URLSearchParams(data).toString();\n}\n\n/** Normalize an OAuth `resource_url`, rejecting origins that could receive the bearer token. */\nexport function normalizeQwenBaseUrl(resourceUrl: string | undefined): string {\n\tif (!resourceUrl || resourceUrl.trim().length === 0) {\n\t\treturn DEFAULT_BASE_URL;\n\t}\n\ttry {\n\t\tconst raw = resourceUrl.trim();\n\t\tconst url = new URL(raw.includes(\"://\") ? raw : `https://${raw}`);\n\t\tconst hostname = url.hostname.toLowerCase();\n\t\tconst approvedHost = QWEN_RESOURCE_HOST_SUFFIXES.some(\n\t\t\t(suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`),\n\t\t);\n\t\tif (\n\t\t\turl.protocol !== \"https:\" ||\n\t\t\t!approvedHost ||\n\t\t\turl.username !== \"\" ||\n\t\t\turl.password !== \"\" ||\n\t\t\t(url.port !== \"\" && url.port !== \"443\") ||\n\t\t\turl.search !== \"\" ||\n\t\t\turl.hash !== \"\"\n\t\t) {\n\t\t\tthrow new Error(\"unapproved origin\");\n\t\t}\n\t\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\t\turl.pathname = path.endsWith(\"/v1\") ? path : `${path}/v1`;\n\t\treturn url.toString();\n\t} catch {\n\t\tthrow new Error(\"Qwen resource_url must use an approved HTTPS origin\");\n\t}\n}\n\nfunction normalizeVerificationUri(...candidates: Array<string | undefined>): string {\n\tfor (const candidate of candidates) {\n\t\tif (!candidate) continue;\n\t\ttry {\n\t\t\tconst url = new URL(candidate);\n\t\t\tif (\n\t\t\t\turl.protocol === \"https:\" &&\n\t\t\t\turl.hostname.toLowerCase() === \"chat.qwen.ai\" &&\n\t\t\t\turl.username === \"\" &&\n\t\t\t\turl.password === \"\"\n\t\t\t) {\n\t\t\t\treturn url.toString();\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try the next fixed-origin candidate.\n\t\t}\n\t}\n\treturn DEFAULT_VERIFICATION_URI;\n}\n\nfunction credentialsFromToken(json: QwenTokenResponse, fallbackRefresh?: string): OAuthCredentials {\n\tif (!json.access_token) {\n\t\tthrow new Error(\"Qwen token response missing access_token\");\n\t}\n\tconst refresh = json.refresh_token ?? fallbackRefresh;\n\tif (!refresh) {\n\t\tthrow new Error(\"Qwen token response missing refresh_token\");\n\t}\n\tconst expiresInSeconds = typeof json.expires_in === \"number\" ? json.expires_in : 3600;\n\tconst credentials: OAuthCredentials = {\n\t\taccess: json.access_token,\n\t\trefresh,\n\t\texpires: Date.now() + expiresInSeconds * 1000 - 30_000,\n\t};\n\tif (typeof json.resource_url === \"string\" && json.resource_url.length > 0) {\n\t\tnormalizeQwenBaseUrl(json.resource_url);\n\t\tcredentials.resource_url = json.resource_url;\n\t}\n\treturn credentials;\n}\n\nasync function requestDeviceCode(challenge: string, signal?: AbortSignal): Promise<QwenDeviceAuthResponse> {\n\tconst response = await fetch(DEVICE_CODE_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tscope: SCOPE,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t}),\n\t\tsignal,\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen device code request failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenDeviceAuthResponse;\n\tif (!json?.device_code || !json.user_code) {\n\t\tthrow new Error(\"Invalid Qwen device code response\");\n\t}\n\treturn json;\n}\n\nasync function pollForToken(\n\tdeviceCode: string,\n\tverifier: string,\n\tintervalSeconds: number | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<OAuthCredentials> {\n\treturn pollOAuthDeviceCodeFlow<OAuthCredentials>({\n\t\tintervalSeconds,\n\t\texpiresInSeconds: DEVICE_CODE_TIMEOUT_SECONDS,\n\t\tsignal,\n\t\tpoll: async () => {\n\t\t\tconst response = await fetch(TOKEN_URL, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\t\t\tbody: formEncode({\n\t\t\t\t\tgrant_type: DEVICE_GRANT_TYPE,\n\t\t\t\t\tclient_id: CLIENT_ID,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tcode_verifier: verifier,\n\t\t\t\t}),\n\t\t\t\tsignal,\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst json = (await response.json()) as QwenTokenResponse;\n\t\t\t\tif (!json.access_token) {\n\t\t\t\t\treturn { status: \"failed\", message: \"Qwen token response missing access_token\" };\n\t\t\t\t}\n\t\t\t\treturn { status: \"complete\", value: credentialsFromToken(json) };\n\t\t\t}\n\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tlet errorCode: string | undefined;\n\t\t\ttry {\n\t\t\t\terrorCode = (JSON.parse(text) as QwenTokenResponse).error;\n\t\t\t} catch {\n\t\t\t\t// non-JSON error body\n\t\t\t}\n\t\t\tif (errorCode === \"authorization_pending\") {\n\t\t\t\treturn { status: \"pending\" };\n\t\t\t}\n\t\t\tif (errorCode === \"slow_down\") {\n\t\t\t\treturn { status: \"slow_down\" };\n\t\t\t}\n\t\t\treturn { status: \"failed\", message: `Qwen token poll failed (${response.status})` };\n\t\t},\n\t});\n}\n\n/** Run the Qwen device-authorization login flow. */\nexport async function loginQwen(options: {\n\tonDeviceCode: OAuthLoginCallbacks[\"onDeviceCode\"];\n\tsignal?: AbortSignal;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst device = await requestDeviceCode(challenge, options.signal);\n\toptions.onDeviceCode({\n\t\tuserCode: device.user_code ?? \"\",\n\t\tverificationUri: normalizeVerificationUri(device.verification_uri_complete, device.verification_uri),\n\t\tintervalSeconds: device.interval,\n\t\texpiresInSeconds: device.expires_in ?? DEVICE_CODE_TIMEOUT_SECONDS,\n\t});\n\treturn pollForToken(device.device_code ?? \"\", verifier, device.interval, options.signal);\n}\n\n/** Refresh a Qwen OAuth token. */\nexport async function refreshQwenToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/x-www-form-urlencoded\", Accept: \"application/json\" },\n\t\tbody: formEncode({ grant_type: \"refresh_token\", client_id: CLIENT_ID, refresh_token: refreshToken }),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`Qwen token refresh failed (${response.status})`);\n\t}\n\tconst json = (await response.json()) as QwenTokenResponse;\n\treturn credentialsFromToken(json, refreshToken);\n}\n\nfunction qwenModels(baseUrl: string): Model<\"openai-completions\">[] {\n\tconst shared = {\n\t\tapi: \"openai-completions\" as const,\n\t\tprovider: QWEN_OAUTH_PROVIDER_ID,\n\t\tbaseUrl,\n\t\treasoning: false,\n\t\tinput: [\"text\"] as (\"text\" | \"image\")[],\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\tcontextWindow: 1_048_576,\n\t\tmaxTokens: 65_536,\n\t};\n\treturn [\n\t\t{ ...shared, id: \"qwen3-coder-plus\", name: \"Qwen3 Coder Plus\" },\n\t\t{ ...shared, id: \"qwen3-coder-flash\", name: \"Qwen3 Coder Flash\" },\n\t];\n}\n\nexport const qwenOAuthProvider: OAuthProviderInterface = {\n\tid: QWEN_OAUTH_PROVIDER_ID,\n\tname: \"Qwen (Qwen Code Subscription)\",\n\tusesCallbackServer: false,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginQwen({ onDeviceCode: callbacks.onDeviceCode, signal: callbacks.signal });\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tconst refreshed = await refreshQwenToken(credentials.refresh);\n\t\t// The refresh response may omit resource_url; keep the previously resolved endpoint.\n\t\tif (refreshed.resource_url === undefined && typeof credentials.resource_url === \"string\") {\n\t\t\trefreshed.resource_url = credentials.resource_url;\n\t\t}\n\t\treturn refreshed;\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n\n\tmodifyModels(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[] {\n\t\tconst resourceUrl = typeof credentials.resource_url === \"string\" ? credentials.resource_url : undefined;\n\t\tconst baseUrl = normalizeQwenBaseUrl(resourceUrl);\n\t\t// Non-destructive: keep any user-configured qwen-oauth models, only add missing defaults.\n\t\tconst existingIds = new Set(\n\t\t\tmodels.filter((model) => model.provider === QWEN_OAUTH_PROVIDER_ID).map((model) => model.id),\n\t\t);\n\t\tconst additions = qwenModels(baseUrl).filter((model) => !existingIds.has(model.id));\n\t\treturn [...models, ...additions];\n\t},\n};\n"]}
|