feat(clinepass): add ClinePass provider support

Register clinepass provider (OAuth + API-key) using Cline's
OpenAI-compatible API with 10 curated models, live /v1/models
resolver, refreshCline-based token refresh with workos: prefix,
and dashboard OAuth login handler.

Reference: https://github.com/jellydn/pi-clinepass-provider
Closes #2261

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
sternelee
2026-07-03 10:50:32 +07:00
committed by decolua
parent 76752a4396
commit b08751c4ea
8 changed files with 282 additions and 84 deletions

View File

@@ -226,6 +226,7 @@ export class DefaultExecutor extends BaseExecutor {
gemini: () => this.refreshFromGrant(credentials, proxyOptions),
kiro: () => this.refreshKiro(credentials.refreshToken, proxyOptions),
cline: () => this.refreshCline(credentials.refreshToken, proxyOptions),
clinepass: () => this.refreshCline(credentials.refreshToken, proxyOptions),
"kimi-coding": () => this.refreshKimiCoding(credentials.refreshToken, proxyOptions),
kilocode: () => this.refreshKilocode(credentials.refreshToken, proxyOptions)
};
@@ -299,7 +300,11 @@ export class DefaultExecutor extends BaseExecutor {
const data = payload?.data || payload;
const expiresAtIso = data?.expiresAt;
const expiresIn = expiresAtIso ? Math.max(1, Math.floor((new Date(expiresAtIso).getTime() - Date.now()) / 1000)) : undefined;
return { accessToken: data?.accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
let accessToken = data?.accessToken;
if (accessToken && !accessToken.startsWith("workos:")) {
accessToken = `workos:${accessToken}`;
}
return { accessToken, refreshToken: data?.refreshToken || refreshToken, expiresIn };
}
async refreshKimiCoding(refreshToken, proxyOptions = null) {

View File

@@ -0,0 +1,57 @@
export default {
id: "clinepass",
priority: 85,
alias: "clinepass",
uiAlias: "clinepass",
display: {
name: "ClinePass",
icon: "vpn_key",
color: "#5B9BD5",
textIcon: "CP",
website: "https://cline.bot",
notice: {
signupUrl: "https://app.cline.bot",
},
},
category: "oauth",
authModes: ["oauth", "apikey"],
hasOAuth: true,
transport: {
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
headers: {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
},
auth: {
combined: true,
header: "Authorization",
scheme: "bearer",
hooks: [
"clineHeaders",
],
},
},
models: [
{ id: "cline-pass/glm-5.2", name: "GLM-5.2 (ClinePass)" },
{ id: "cline-pass/kimi-k2.7-code", name: "Kimi K2.7 Code (ClinePass)" },
{ id: "cline-pass/kimi-k2.6", name: "Kimi K2.6 (ClinePass)" },
{ id: "cline-pass/deepseek-v4-pro", name: "DeepSeek V4 Pro (ClinePass)" },
{ id: "cline-pass/deepseek-v4-flash", name: "DeepSeek V4 Flash (ClinePass)" },
{ id: "cline-pass/mimo-v2.5", name: "MiMo-V2.5 (ClinePass)" },
{ id: "cline-pass/mimo-v2.5-pro", name: "MiMo-V2.5-Pro (ClinePass)" },
{ id: "cline-pass/minimax-m3", name: "MiniMax M3 (ClinePass)" },
{ id: "cline-pass/qwen3.7-max", name: "Qwen3.7 Max (ClinePass)" },
{ id: "cline-pass/qwen3.7-plus", name: "Qwen3.7 Plus (ClinePass)" },
],
oauth: {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
authorizeUrl: "https://api.cline.bot/api/v1/auth/authorize",
tokenUrl: "https://api.cline.bot/api/v1/auth/token",
refreshUrl: "https://api.cline.bot/api/v1/auth/refresh",
},
thinkingConfig: {
options: ["auto", "on", "off"],
defaultMode: "auto",
},
};

View File

@@ -15,86 +15,87 @@ import p12 from "./cerebras.js";
import p13 from "./chutes.js";
import p14 from "./claude.js";
import p15 from "./cline.js";
import p16 from "./cloudflare-ai.js";
import p17 from "./codebuddy-cn.js";
import p18 from "./codex.js";
import p19 from "./cohere.js";
import p20 from "./comfyui.js";
import p21 from "./commandcode.js";
import p22 from "./coqui.js";
import p23 from "./cursor.js";
import p24 from "./deepgram.js";
import p25 from "./deepseek.js";
import p26 from "./edge-tts.js";
import p27 from "./elevenlabs.js";
import p28 from "./exa.js";
import p29 from "./fal-ai.js";
import p30 from "./firecrawl.js";
import p31 from "./fireworks.js";
import p32 from "./gemini-cli.js";
import p33 from "./gemini.js";
import p34 from "./github.js";
import p35 from "./gitlab.js";
import p36 from "./glm-cn.js";
import p37 from "./glm.js";
import p38 from "./google-pse.js";
import p39 from "./google-tts.js";
import p40 from "./grok-web.js";
import p41 from "./groq.js";
import p42 from "./huggingface.js";
import p43 from "./hyperbolic.js";
import p44 from "./iflow.js";
import p45 from "./inworld.js";
import p46 from "./jina-ai.js";
import p47 from "./jina-reader.js";
import p48 from "./kilocode.js";
import p49 from "./kimchi.js";
import p50 from "./kimi-coding.js";
import p51 from "./kimi.js";
import p52 from "./kiro.js";
import p53 from "./linkup.js";
import p54 from "./local-device.js";
import p55 from "./mimo-free.js";
import p56 from "./minimax-cn.js";
import p57 from "./minimax.js";
import p58 from "./mistral.js";
import p59 from "./mmf.js";
import p60 from "./nanobanana.js";
import p61 from "./nebius.js";
import p62 from "./nvidia.js";
import p63 from "./ollama-local.js";
import p64 from "./ollama.js";
import p65 from "./openai.js";
import p66 from "./opencode-go.js";
import p67 from "./opencode.js";
import p68 from "./openrouter.js";
import p69 from "./perplexity-web.js";
import p70 from "./perplexity.js";
import p71 from "./playht.js";
import p72 from "./qoder.js";
import p73 from "./qwen.js";
import p74 from "./recraft.js";
import p75 from "./runwayml.js";
import p76 from "./sdwebui.js";
import p77 from "./searchapi.js";
import p78 from "./searxng.js";
import p79 from "./serper.js";
import p80 from "./siliconflow.js";
import p81 from "./stability-ai.js";
import p82 from "./tavily.js";
import p83 from "./together.js";
import p84 from "./topaz.js";
import p85 from "./tortoise.js";
import p86 from "./venice.js";
import p87 from "./vercel-ai-gateway.js";
import p88 from "./vertex-partner.js";
import p89 from "./vertex.js";
import p90 from "./volcengine-ark.js";
import p91 from "./voyage-ai.js";
import p92 from "./xai.js";
import p93 from "./xiaomi-mimo.js";
import p94 from "./xiaomi-tokenplan.js";
import p95 from "./youcom.js";
import p16 from "./clinepass.js";
import p17 from "./cloudflare-ai.js";
import p18 from "./codebuddy-cn.js";
import p19 from "./codex.js";
import p20 from "./cohere.js";
import p21 from "./comfyui.js";
import p22 from "./commandcode.js";
import p23 from "./coqui.js";
import p24 from "./cursor.js";
import p25 from "./deepgram.js";
import p26 from "./deepseek.js";
import p27 from "./edge-tts.js";
import p28 from "./elevenlabs.js";
import p29 from "./exa.js";
import p30 from "./fal-ai.js";
import p31 from "./firecrawl.js";
import p32 from "./fireworks.js";
import p33 from "./gemini-cli.js";
import p34 from "./gemini.js";
import p35 from "./github.js";
import p36 from "./gitlab.js";
import p37 from "./glm-cn.js";
import p38 from "./glm.js";
import p39 from "./google-pse.js";
import p40 from "./google-tts.js";
import p41 from "./grok-web.js";
import p42 from "./groq.js";
import p43 from "./huggingface.js";
import p44 from "./hyperbolic.js";
import p45 from "./iflow.js";
import p46 from "./inworld.js";
import p47 from "./jina-ai.js";
import p48 from "./jina-reader.js";
import p49 from "./kilocode.js";
import p50 from "./kimchi.js";
import p51 from "./kimi-coding.js";
import p52 from "./kimi.js";
import p53 from "./kiro.js";
import p54 from "./linkup.js";
import p55 from "./local-device.js";
import p56 from "./mimo-free.js";
import p57 from "./minimax-cn.js";
import p58 from "./minimax.js";
import p59 from "./mistral.js";
import p60 from "./mmf.js";
import p61 from "./nanobanana.js";
import p62 from "./nebius.js";
import p63 from "./nvidia.js";
import p64 from "./ollama-local.js";
import p65 from "./ollama.js";
import p66 from "./openai.js";
import p67 from "./opencode-go.js";
import p68 from "./opencode.js";
import p69 from "./openrouter.js";
import p70 from "./perplexity-web.js";
import p71 from "./perplexity.js";
import p72 from "./playht.js";
import p73 from "./qoder.js";
import p74 from "./qwen.js";
import p75 from "./recraft.js";
import p76 from "./runwayml.js";
import p77 from "./sdwebui.js";
import p78 from "./searchapi.js";
import p79 from "./searxng.js";
import p80 from "./serper.js";
import p81 from "./siliconflow.js";
import p82 from "./stability-ai.js";
import p83 from "./tavily.js";
import p84 from "./together.js";
import p85 from "./topaz.js";
import p86 from "./tortoise.js";
import p87 from "./venice.js";
import p88 from "./vercel-ai-gateway.js";
import p89 from "./vertex-partner.js";
import p90 from "./vertex.js";
import p91 from "./volcengine-ark.js";
import p92 from "./voyage-ai.js";
import p93 from "./xai.js";
import p94 from "./xiaomi-mimo.js";
import p95 from "./xiaomi-tokenplan.js";
import p96 from "./youcom.js";
export default [
p0,
@@ -192,5 +193,6 @@ export default [
p92,
p93,
p94,
p95
p95,
p96
];

View File

@@ -0,0 +1,63 @@
import { buildClineHeaders } from "../shared/clineAuth.js";
const CLINEPASS_MODELS_ENDPOINT = "https://api.cline.bot/api/v1/models";
const FETCH_TIMEOUT_MS = 5000;
/**
* Build request headers for the ClinePass /models endpoint (Cline's upstream API).
* - API keys are sent as plain Bearer tokens.
* - OAuth access tokens must carry the WorkOS `workos:` prefix (handled by buildClineHeaders).
*/
function buildModelListHeaders(token, isApiKey) {
if (isApiKey) {
return {
Accept: "application/json",
Authorization: `Bearer ${token}`,
};
}
return buildClineHeaders(token, { Accept: "application/json" });
}
/**
* Fetch ClinePass live model catalog from Cline's /models endpoint.
*
* @param {object} credentials - Connection credentials ({ accessToken, apiKey })
* @returns {Promise<{ models: { id: string, name: string }[] } | null>}
*/
export async function resolveClinepassModels(credentials) {
const isApiKey = Boolean(credentials?.apiKey);
const token = isApiKey ? credentials.apiKey : credentials?.accessToken;
if (!token) return null;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const headers = buildModelListHeaders(token, isApiKey);
const response = await fetch(CLINEPASS_MODELS_ENDPOINT, {
method: "GET",
headers,
signal: controller.signal,
});
if (!response.ok) return null;
const json = await response.json();
const rawList = Array.isArray(json) ? json : json?.data;
if (!Array.isArray(rawList)) return null;
const models = rawList
.filter((m) => typeof m?.id === "string" && m.id.startsWith("cline-pass/"))
.map((m) => ({
id: m.id,
name: m.name || m.id,
}));
return models.length ? { models } : null;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}

View File

@@ -232,8 +232,8 @@ export async function POST(request, { params }) {
});
}
// Cline uses authorization_code without PKCE. Kimchi returns a browser token.
const noPkceExchangeProviders = ["cline", "kimchi"];
// Cline and ClinePass use authorization_code without PKCE. Kimchi returns a browser token.
const noPkceExchangeProviders = ["cline", "clinepass", "kimchi"];
if (!code || !redirectUri || (!codeVerifier && !noPkceExchangeProviders.includes(provider))) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}

View File

@@ -11,6 +11,7 @@ import { resolveKiroModels } from "open-sse/services/kiroModels.js";
import { resolveKimchiModels } from "open-sse/services/kimchiModels.js";
import { resolveQoderModels } from "open-sse/services/qoderModels.js";
import { resolveCopilotModels } from "open-sse/services/copilotModels.js";
import { resolveClinepassModels } from "open-sse/services/clinepassModels.js";
import { updateProviderCredentials } from "@/sse/services/tokenRefresh";
import { capabilitiesFromServiceKind } from "open-sse/providers/capabilities.js";
@@ -63,6 +64,13 @@ const LIVE_MODEL_RESOLVERS = {
},
});
return result?.models?.length ? { models: result.models } : null;
},
clinepass: async (conn) => {
const result = await resolveClinepassModels({
accessToken: conn.accessToken,
apiKey: conn.apiKey,
});
return result?.models?.length ? { models: result.models } : null;
}
};

View File

@@ -102,6 +102,9 @@ export const KILOCODE_CONFIG = { ...PROVIDER_OAUTH["kilocode"] };
// Cline OAuth Configuration (Local Callback Flow via app.cline.bot)
export const CLINE_CONFIG = { ...PROVIDER_OAUTH["cline"] };
// ClinePass OAuth Configuration (shares Cline's OAuth endpoints)
export const CLINEPASS_CONFIG = { ...PROVIDER_OAUTH["clinepass"] };
// GitLab Duo OAuth Configuration (Authorization Code Flow with PKCE)
export const GITLAB_CONFIG = { ...PROVIDER_OAUTH["gitlab"] };
@@ -130,6 +133,7 @@ export const PROVIDERS = {
KIMI_CODING: "kimi-coding",
KILOCODE: "kilocode",
CLINE: "cline",
CLINEPASS: "clinepass",
GITLAB: "gitlab",
CODEBUDDY: "codebuddy-cn",
KIMCHI: "kimchi",

View File

@@ -23,6 +23,7 @@ import {
KIMI_CODING_CONFIG,
KILOCODE_CONFIG,
CLINE_CONFIG,
CLINEPASS_CONFIG,
GITLAB_CONFIG,
CODEBUDDY_CONFIG,
KIMCHI_CONFIG,
@@ -1116,6 +1117,64 @@ const PROVIDERS = {
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
}),
},
clinepass: {
config: CLINEPASS_CONFIG,
flowType: "authorization_code",
buildAuthUrl: (config, redirectUri) => {
const params = new URLSearchParams({
client_type: "extension",
callback_url: redirectUri,
redirect_uri: redirectUri,
});
return `${config.authorizeUrl}?${params.toString()}`;
},
exchangeToken: async (config, code, redirectUri) => {
try {
// Cline encodes token data as base64 in the code param
let base64 = code;
const padding = 4 - (base64.length % 4);
if (padding !== 4) base64 += "=".repeat(padding);
const decoded = Buffer.from(base64, "base64").toString("utf-8");
const lastBrace = decoded.lastIndexOf("}");
if (lastBrace === -1) throw new Error("No JSON found in decoded code");
const tokenData = JSON.parse(decoded.substring(0, lastBrace + 1));
return {
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
email: tokenData.email,
firstName: tokenData.firstName,
lastName: tokenData.lastName,
expires_at: tokenData.expiresAt,
};
} catch (e) {
const response = await fetch(config.tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ grant_type: "authorization_code", code, client_type: "extension", redirect_uri: redirectUri }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`ClinePass token exchange failed: ${error}`);
}
const data = await response.json();
return {
access_token: data.data?.accessToken || data.accessToken,
refresh_token: data.data?.refreshToken || data.refreshToken,
email: data.data?.userInfo?.email || "",
expires_at: data.data?.expiresAt || data.expiresAt,
};
}
},
mapTokens: (tokens) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_at
? Math.floor((new Date(tokens.expires_at).getTime() - Date.now()) / 1000)
: 3600,
email: tokens.email,
providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName },
}),
},
// GitLab Duo - Authorization Code Flow with PKCE
// Supports two login modes via loginMode metadata: "oauth" (default) or "pat"
gitlab: {