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:
@@ -1,4 +1,6 @@
|
||||
import { getApiKeys } from "@/lib/localDb";
|
||||
import { resolveProviderId } from "@/shared/constants/providers.js";
|
||||
import { unwrapClineEnvelope } from "open-sse/shared/clineEnvelope.js";
|
||||
import { UPDATER_CONFIG } from "@/shared/constants/config";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
|
||||
@@ -151,6 +153,11 @@ export async function pingModelByKind(model, kind, baseUrl = `http://127.0.0.1:$
|
||||
let parsed = null;
|
||||
try { parsed = rawText ? JSON.parse(rawText) : null; } catch {}
|
||||
|
||||
// Unwrap before the choices checks below. No-op for providers that do not
|
||||
// opt in via transport.quirks.clineEnvelope.
|
||||
const providerId = resolveProviderId(String(model).split("/")[0]);
|
||||
parsed = unwrapClineEnvelope(parsed, providerId);
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = parsed?.error?.message || parsed?.msg || parsed?.message || parsed?.error || rawText;
|
||||
return { ok: false, latencyMs, error: `HTTP ${res.status}${detail ? `: ${String(detail).slice(0, 240)}` : ""}`, status: res.status };
|
||||
|
||||
@@ -11,6 +11,7 @@ import { resolveQoderModels } from "open-sse/services/qoderModels.js";
|
||||
import { resolveGrokCliModels } from "open-sse/services/grokCliModels.js";
|
||||
import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy";
|
||||
import { resolveCursorModels } from "open-sse/services/cursorModels.js";
|
||||
import { resolveClineModels, resolveClinepassModels } from "open-sse/services/clinepassModels.js";
|
||||
|
||||
const GEMINI_CLI_MODELS_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
||||
|
||||
@@ -287,6 +288,37 @@ const PROVIDER_MODELS_CONFIG = {
|
||||
},
|
||||
},
|
||||
|
||||
// Cline/ClinePass share api.cline.bot/api/v1/models. The service layer already
|
||||
// handles Bearer-vs-`workos:` auth and swallows failures into null, so these follow
|
||||
// the cursor direct pattern (no refreshFn) and only differ in filtering:
|
||||
// cline returns the whole catalog verbatim, clinepass keeps cline-pass/* only.
|
||||
cline: {
|
||||
customResolver: async (connection) => {
|
||||
const result = await resolveClineModels({
|
||||
accessToken: connection.accessToken,
|
||||
apiKey: connection.apiKey,
|
||||
});
|
||||
if (result?.models?.length) return { models: result.models };
|
||||
return {
|
||||
models: getStaticProviderModels("cline"),
|
||||
warning: "Cline returned no live models; falling back to static catalog.",
|
||||
};
|
||||
},
|
||||
},
|
||||
clinepass: {
|
||||
customResolver: async (connection) => {
|
||||
const result = await resolveClinepassModels({
|
||||
accessToken: connection.accessToken,
|
||||
apiKey: connection.apiKey,
|
||||
});
|
||||
if (result?.models?.length) return { models: result.models };
|
||||
return {
|
||||
models: getStaticProviderModels("clinepass"),
|
||||
warning: "ClinePass returned no live models; falling back to static catalog.",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Custom resolvers (non-OpenAI-shaped APIs / token-refresh flows)
|
||||
kiro: {
|
||||
customResolver: async (connection) => {
|
||||
|
||||
@@ -26,4 +26,10 @@ export const FILTERS = {
|
||||
(Array.isArray(models) ? models : [])
|
||||
.filter((m) => m.id?.startsWith("mimo") || m.name?.toLowerCase().includes("mimo"))
|
||||
.map((m) => ({ id: m.id, name: m.name || m.id })),
|
||||
|
||||
"airforce-free": (models) =>
|
||||
(Array.isArray(models) ? models : [])
|
||||
.filter((m) => (m.tier === "free" || m.id?.endsWith(":free")) && m.supports_chat === true && (!m.media_type || m.media_type === "chat" || m.media_type === "text"))
|
||||
.map((m) => ({ id: m.id, name: m.name || m.id, contextLength: m.context_length }))
|
||||
.sort((a, b) => String(a.id).localeCompare(String(b.id))),
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { resolveKiroModels } from "open-sse/services/kiroModels.js";
|
||||
import { resolveKimchiModels } from "open-sse/services/kimchiModels.js";
|
||||
import { resolveQoderModels, routableQoderModels } from "open-sse/services/qoderModels.js";
|
||||
import { resolveCopilotModels } from "open-sse/services/copilotModels.js";
|
||||
import { resolveClinepassModels } from "open-sse/services/clinepassModels.js";
|
||||
import { resolveClinepassModels, resolveClineModels } from "open-sse/services/clinepassModels.js";
|
||||
import { resolveGrokCliModels } from "open-sse/services/grokCliModels.js";
|
||||
import { resolveCursorModels } from "open-sse/services/cursorModels.js";
|
||||
import { resolveZedModels } from "open-sse/shared/zedAuth.js";
|
||||
@@ -79,6 +79,13 @@ const LIVE_MODEL_RESOLVERS = {
|
||||
});
|
||||
return result?.models?.length ? { models: result.models } : null;
|
||||
},
|
||||
cline: async (conn) => {
|
||||
const result = await resolveClineModels({
|
||||
accessToken: conn.accessToken,
|
||||
apiKey: conn.apiKey,
|
||||
});
|
||||
return result?.models?.length ? { models: result.models } : null;
|
||||
},
|
||||
"grok-cli": async (conn) => {
|
||||
const proxy = await resolveConnectionProxyConfig(conn.providerSpecificData || {});
|
||||
const result = await resolveGrokCliModels({
|
||||
|
||||
Reference in New Issue
Block a user