fix(cline): stop workos:-prefixing ClinePass API keys and add clinepass token refresh

Cline/ClinePass requests failed with HTTP 401 ("Please make sure you are using
the latest version of Cline and re-authenticate your Cline account", #3230 /
#2333 / #3644). `getClineAccessToken()` unconditionally prefixed every token
with `workos:`, which is correct for Cline OAuth access tokens (WorkOS JWTs)
but wrong for ClinePass API keys — those are opaque strings (e.g. `clp_…`)
that the API accepts only verbatim, so the `workos:`-prefixed value was
rejected.

Only prefix tokens that look like a WorkOS JWT (`eyJ…`); API keys and other
opaque tokens pass through untouched, and an existing `workos:` prefix is
never doubled.

Also register `clinepass` in the token-refresh handlers. ClinePass shares
Cline's WorkOS auth endpoints, but without the entry expired ClinePass OAuth
tokens were never rotated, so every request kept 401ing. Finally, list
`apikey` first in the ClinePass `authModes` (ClinePass is meant to be used
with an API key from app.cline.bot/settings/api-keys), and add an "Import
from /models" button that pulls the live Cline catalog into custom models.
This commit is contained in:
izzzzzi
2026-09-10 22:48:06 +07:00
committed by decolua
parent 45ec1d30bb
commit f6e7cabe60
5 changed files with 116 additions and 2 deletions

View File

@@ -81,6 +81,7 @@ export default function ProviderDetailPage() {
const [oneByOneSummary, setOneByOneSummary] = useState(null);
const stopOneByOneRef = useRef(false);
const [importingQoderModels, setImportingQoderModels] = useState(false);
const [importingClineModels, setImportingClineModels] = useState(false);
const { copied, copy } = useCopyToClipboard();
const AG_RISK_STORAGE_KEY = "ag_risk_confirmed";
@@ -612,6 +613,53 @@ export default function ProviderDetailPage() {
setImportingQoderModels(false);
}
};
// Fetch the live Cline /models catalog and add every model not yet present.
// Cline and ClinePass share the same catalog endpoint (api.cline.bot/api/v1/models).
const handleImportClineModels = async () => {
if (importingClineModels) return;
const activeConnection = connections.find((conn) => conn.isActive !== false);
if (!activeConnection) {
alert(translate("Please add an active Cline connection first"));
return;
}
setImportingClineModels(true);
try {
const res = await fetch(`/api/providers/${activeConnection.id}/models`);
const data = await res.json();
if (!res.ok) {
alert(data.error || translate("Failed to fetch models"));
return;
}
const models = data.models || [];
if (models.length === 0) {
alert(translate("No models returned"));
return;
}
let importedCount = 0;
for (const model of models) {
const modelId = model.id || model.name;
if (!modelId) continue;
const alreadyExists = customModels.some(
(entry) => entry.providerAlias === providerStorageAlias && entry.id === modelId && (entry.kind || entry.type || "llm") === "llm"
) || Object.values(modelAliases).includes(`${providerStorageAlias}/${modelId}`);
if (alreadyExists) {
continue;
}
await handleAddCustomModel(modelId, "llm", providerStorageAlias);
importedCount += 1;
}
if (importedCount === 0) {
alert(translate("All models already exist, no new models added"));
} else {
alert(translate("Successfully added") + ` ${importedCount} ` + translate("models"));
}
} catch (error) {
console.log("Error importing Cline models:", error);
alert(translate("Error fetching models") + ": " + error.message);
} finally {
setImportingClineModels(false);
}
};
const handleRunOneByOneTest = async () => {
if (oneByOneRunning || connections.length === 0) return;
@@ -1187,6 +1235,20 @@ export default function ProviderDetailPage() {
</button>
)}
{/* Import Cline /models catalog button — only show for cline and clinepass providers */}
{(providerId === "cline" || providerId === "clinepass") && connections.some((conn) => conn.isActive !== false) && (
<button
onClick={handleImportClineModels}
disabled={importingClineModels}
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed border-blue-500/40 px-3 py-2 text-xs text-blue-600 dark:text-blue-400 transition-colors hover:border-blue-500 hover:bg-blue-500/5 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
>
<span className="material-symbols-outlined text-sm" style={importingClineModels ? { animation: "spin 1s linear infinite" } : undefined}>
{importingClineModels ? "progress_activity" : "download"}
</span>
{importingClineModels ? translate("Fetching...") : translate("Import from /models")}
</button>
)}
{/* Suggested models from provider API — show only models not yet added */}
{suggestedModels.length > 0 && (() => {
const addedFullModels = new Set([