fix(cline,airforce): unwrap {success,data} envelope, add live catalog, and refresh airforce free models
Cline (api.cline.bot) wraps non-stream chat completions in
{"success":true,"data":{...choices...}}, which both the dashboard model-test
ping and the proxy non-stream path read at top level, producing "Provider
returned no completion choices for this model" (#3644). Unwrap the envelope
before usage extraction and response translation; the error envelope
({"success":false,...}) never matches and passes through untouched.
Scoped through `transport.quirks.clineEnvelope` so only cline/clinepass opt
in — no other provider's response body is ever rewritten.
Also adds a live Cline catalog: `fetchClineRawModels()` is shared between
`resolveClineModels()` (full catalog, including free-tier ids such as
z-ai/glm-5.3-flash) and `resolveClinepassModels()` (cline-pass/* only), wired
into /v1/models, the per-provider models route, and the combo selector's
model picker with the static catalog kept as fallback.
Refreshes the dead api-airforce free models (anthropic/claude-3.7-sonnet,
moonshot/kimi-k2.6, google/gemini-2.5-flash) with the live gpt-oss-120b,
gpt-oss-20b and kimi-k2.7-code, plus passthroughModels, forceStream and a
suggested-models filter.
This commit is contained in:
@@ -20,6 +20,54 @@ const PROVIDER_ORDER = [
|
||||
// Providers that need no auth — always show in model selector
|
||||
const NO_AUTH_PROVIDER_IDS = Object.keys(FREE_PROVIDERS).filter(id => FREE_PROVIDERS[id].noAuth);
|
||||
|
||||
// Providers with per-account live catalogs via /api/providers/[id]/models.
|
||||
// Static registry stays as fallback when live fetch fails or is empty.
|
||||
const LIVE_CATALOG_PROVIDERS = ["cursor", "cline", "clinepass"];
|
||||
|
||||
// Fetch a provider's account-scoped catalog for every active connection and merge
|
||||
// the results. Entries collapse by model id on purpose: two connections of the
|
||||
// same provider produce the same picker value (`alias/id`), so keeping the first
|
||||
// avoids duplicate rows. There is no per-connection metadata to preserve beyond
|
||||
// {id,name}. Empty array means "nothing live" so callers keep the static fallback.
|
||||
function useLiveProviderModels(isOpen, connectionIds, label) {
|
||||
const [models, setModels] = useState([]);
|
||||
const idsKey = (connectionIds ?? []).join("|");
|
||||
|
||||
useEffect(() => {
|
||||
const ids = idsKey ? idsKey.split("|") : [];
|
||||
if (!isOpen || ids.length === 0) {
|
||||
setModels([]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
Promise.all(ids.map(async (connectionId) => {
|
||||
const response = await fetch(`/api/providers/${connectionId}/models`, { cache: "no-store" });
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return Array.isArray(data.models) ? data.models : [];
|
||||
}))
|
||||
.then((modelLists) => {
|
||||
if (cancelled) return;
|
||||
const seen = new Set();
|
||||
setModels(modelLists.flat().filter((model) => {
|
||||
if (!model?.id || seen.has(model.id)) return false;
|
||||
seen.add(model.id);
|
||||
return true;
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
// Do not hide the static fallback when the account catalog is unavailable.
|
||||
console.warn(`Unable to load ${label} models for selector:`, error);
|
||||
if (!cancelled) setModels([]);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [isOpen, idsKey, label]);
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
export default function ModelSelectModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -49,48 +97,25 @@ export default function ModelSelectModal({
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
const [customModels, setCustomModels] = useState([]);
|
||||
const [disabledModels, setDisabledModels] = useState({});
|
||||
const [cursorModels, setCursorModels] = useState([]);
|
||||
|
||||
// Cursor exposes the usable catalog per account. Keep the static catalog only
|
||||
// as a fallback, since it quickly becomes stale and different accounts can
|
||||
// have different model entitlements.
|
||||
const cursorConnectionIds = useMemo(
|
||||
() => activeProviders
|
||||
.filter((provider) => provider.provider === "cursor" && provider.id)
|
||||
.map((provider) => provider.id),
|
||||
[activeProviders],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || cursorConnectionIds.length === 0) {
|
||||
setCursorModels([]);
|
||||
return undefined;
|
||||
// Cursor and Cline expose the usable catalog per account, so the static catalog is
|
||||
// kept only as a fallback: it goes stale quickly and entitlements differ per account.
|
||||
// Single map driven by LIVE_CATALOG_PROVIDERS so the constant cannot drift
|
||||
// from the memos below; per-provider arrays stay referentially stable unless
|
||||
// activeProviders itself changes.
|
||||
const liveConnectionIdsByProvider = useMemo(() => {
|
||||
const map = Object.fromEntries(LIVE_CATALOG_PROVIDERS.map((id) => [id, []]));
|
||||
for (const p of activeProviders) {
|
||||
if (p?.id && Object.prototype.hasOwnProperty.call(map, p.provider)) map[p.provider].push(p.id);
|
||||
}
|
||||
return map;
|
||||
}, [activeProviders]);
|
||||
const cursorConnectionIds = liveConnectionIdsByProvider.cursor;
|
||||
const clineConnectionIds = liveConnectionIdsByProvider.cline;
|
||||
const clinepassConnectionIds = liveConnectionIdsByProvider.clinepass;
|
||||
|
||||
let cancelled = false;
|
||||
Promise.all(cursorConnectionIds.map(async (connectionId) => {
|
||||
const response = await fetch(`/api/providers/${connectionId}/models`, { cache: "no-store" });
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return Array.isArray(data.models) ? data.models : [];
|
||||
}))
|
||||
.then((modelLists) => {
|
||||
if (cancelled) return;
|
||||
const seen = new Set();
|
||||
setCursorModels(modelLists.flat().filter((model) => {
|
||||
if (!model?.id || seen.has(model.id)) return false;
|
||||
seen.add(model.id);
|
||||
return true;
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
// Do not hide the static fallback when the account catalog is unavailable.
|
||||
console.warn("Unable to load Cursor models for selector:", error);
|
||||
if (!cancelled) setCursorModels([]);
|
||||
});
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [isOpen, cursorConnectionIds]);
|
||||
const cursorModels = useLiveProviderModels(isOpen, cursorConnectionIds, "Cursor");
|
||||
const clineModels = useLiveProviderModels(isOpen, clineConnectionIds, "Cline");
|
||||
const clinepassModels = useLiveProviderModels(isOpen, clinepassConnectionIds, "ClinePass");
|
||||
|
||||
const fetchCombos = async () => {
|
||||
try {
|
||||
@@ -323,8 +348,9 @@ export default function ModelSelectModal({
|
||||
hasModels: mergedModels.length > 0,
|
||||
};
|
||||
} else {
|
||||
const hardcodedModels = providerId === "cursor" && cursorModels.length > 0
|
||||
? cursorModels
|
||||
const liveModels = providerId === "cursor" ? cursorModels : providerId === "cline" ? clineModels : providerId === "clinepass" ? clinepassModels : [];
|
||||
const hardcodedModels = liveModels.length > 0
|
||||
? liveModels
|
||||
: getModelsByProviderId(providerId);
|
||||
const hardcodedIds = new Set(hardcodedModels.map((m) => m.id));
|
||||
|
||||
@@ -394,7 +420,7 @@ export default function ModelSelectModal({
|
||||
});
|
||||
|
||||
return groups;
|
||||
}, [filteredActiveProviders, modelAliases, allProviders, providerNodes, customModels, disabledModels, kindFilter, activeProviders, cursorModels]);
|
||||
}, [filteredActiveProviders, modelAliases, allProviders, providerNodes, customModels, disabledModels, kindFilter, activeProviders, cursorModels, clineModels, clinepassModels]);
|
||||
|
||||
// Filter combos by search query (and hide combos when kindFilter is set — combos are LLM-only by design)
|
||||
const filteredCombos = useMemo(() => {
|
||||
|
||||
@@ -199,7 +199,7 @@ export const CLI_TOOLS = {
|
||||
id: "cline",
|
||||
name: "Cline",
|
||||
image: "/providers/cline.png",
|
||||
color: "#00D1B2",
|
||||
color: "#5B9BD5",
|
||||
description: "Cline AI Coding Assistant",
|
||||
configType: "custom",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user