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:
@@ -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) {
|
||||
|
||||
57
open-sse/providers/registry/clinepass.js
Normal file
57
open-sse/providers/registry/clinepass.js
Normal 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",
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
];
|
||||
|
||||
63
open-sse/services/clinepassModels.js
Normal file
63
open-sse/services/clinepassModels.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user