diff --git a/.gitignore b/.gitignore index edd8c086306..272be28c22b 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,8 @@ gitbook/README.md open-sse.old/ .graphifyignore graphify-out/* + + +dist/* +.omc +.claude \ No newline at end of file diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js index ca3061d6be2..4c82c4787bd 100644 --- a/open-sse/executors/default.js +++ b/open-sse/executors/default.js @@ -7,6 +7,7 @@ import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { injectReasoningContent } from "../utils/reasoningContentInjector.js"; import { stripUnsupportedParams } from "../translator/concerns/paramSupport.js"; +import { getCapabilitiesForModel } from "../providers/capabilities.js"; // Auth header descriptors — derived from registry transport.auth, fallback to hardcoded defaults. const BEARER = { combined: true, header: "Authorization", scheme: "bearer" }; @@ -92,7 +93,34 @@ export class DefaultExecutor extends BaseExecutor { stripUnsupportedParams(this.provider, model, transformed); } - return injectReasoningContent({ provider: this.provider, model, body: transformed }); + const withReasoning = injectReasoningContent({ provider: this.provider, model, body: transformed }); + return this.ensureThinkingBudget(withReasoning, model); + } + + // ClinePass / OpenRouter-style thinking models burn all of max_tokens on reasoning + // when the budget is too small, leaving content empty (finish_reason: "length"). + // Bump max_tokens to a safe minimum only when reasoning is enabled and budget undersized. + ensureThinkingBudget(body, model) { + if (!body || this.provider !== "clinepass") return body; + const caps = getCapabilitiesForModel(this.provider, model); + if (!caps?.reasoning) return body; + + const reasoningEnabled = body.extra_body?.thinking?.type === "enabled" + || (typeof body.reasoning_effort === "string" && body.reasoning_effort !== "none" && body.reasoning_effort !== "off") + || body.reasoning_effort === true; + if (!reasoningEnabled) return body; + + const MIN_TOKENS = 4096; + const cap = typeof caps.maxOutput === "number" && caps.maxOutput > 0 ? caps.maxOutput : MIN_TOKENS; + const target = Math.min(MIN_TOKENS, cap); + const current = body.max_tokens ?? body.max_completion_tokens; + + if (typeof current !== "number" || current <= 0) { + body.max_tokens = target; + } else if (current < MIN_TOKENS && current < cap) { + body.max_tokens = MIN_TOKENS; + } + return body; } // Fallback json_schema → json_object for openai-compatible providers without native Structured Output. diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js index 190cbb44452..a7348e34d2e 100644 --- a/open-sse/handlers/chatCore.js +++ b/open-sse/handlers/chatCore.js @@ -320,7 +320,23 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred // True non-streaming response if (!stream) { - const result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, toolNameMap, trackDone, appendLog }); + let result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, toolNameMap, trackDone, appendLog }); + + // ClinePass sometimes returns {success:false, error:"empty response content"} transiently + if (provider === "clinepass" && !result?.success && /empty/i.test(result?.error || "")) { + log?.warn?.("RETRY", `clinepass returned empty content, retrying once after 2s`); + await new Promise(r => setTimeout(r, 2000)); + try { + const retry = await executor.execute({ model, body: translatedBody, stream: false, credentials, signal: streamController.signal, log, proxyOptions }); + if (retry?.response?.ok) { + providerResponse = retry.response; + result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, toolNameMap, trackDone, appendLog }); + } + } catch (retryErr) { + log?.warn?.("RETRY", `clinepass retry failed: ${retryErr.message}`); + } + } + streamController.handleComplete(); return result; } diff --git a/open-sse/handlers/chatCore/nonStreamingHandler.js b/open-sse/handlers/chatCore/nonStreamingHandler.js index 3996f94f942..ebb858a25fb 100644 --- a/open-sse/handlers/chatCore/nonStreamingHandler.js +++ b/open-sse/handlers/chatCore/nonStreamingHandler.js @@ -5,6 +5,7 @@ import { ollamaBodyToOpenAI } from "../../translator/response/ollama-to-openai.j import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTracking.js"; import { createErrorResult } from "../../utils/error.js"; import { HTTP_STATUS } from "../../config/runtimeConfig.js"; +import { unwrapClinepassEnvelope } from "../../utils/clinepassEnvelope.js"; import { parseSSEToOpenAIResponse } from "./sseToJsonHandler.js"; import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats } from "./requestDetail.js"; import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js"; @@ -230,6 +231,16 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m }); } + // Unwrap ClinePass {success, data} envelope before decloak/translation + { + const { body: unwrapped, error: envError } = unwrapClinepassEnvelope(responseBody, provider); + if (envError) { + appendLog({ status: `FAILED ${HTTP_STATUS.BAD_GATEWAY}` }); + return createErrorResult(HTTP_STATUS.BAD_GATEWAY, envError.message); + } + responseBody = unwrapped; + } + // Decloak tool_use names once on raw Claude body, before any translation (INPUT side) responseBody = decloakToolNames(responseBody, toolNameMap); diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index 6d5c7815371..51ca7200ee2 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -120,6 +120,13 @@ export const PROVIDER_CAPABILITIES = { "deepseek-v4-flash": { vision: true, reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, "deepseek-v3-2-volc": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 96000, maxOutput: 32000 }, }, + // ClinePass proxies through Vercel's OpenAI Chat Completions API, which only + // accepts reasoning.effort in {none,minimal,low,medium,high,xhigh}. Force + // "openai" so thinkingUnified.js emits valid Vercel enum values. + "clinepass": { + "deepseek-v4-pro": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, + "deepseek-v4-flash": { reasoning: true, thinkingFormat: "openai", thinkingCanDisable: false, contextWindow: 1000000, maxOutput: 50000 }, + }, }; /** diff --git a/open-sse/providers/registry/clinepass.js b/open-sse/providers/registry/clinepass.js new file mode 100644 index 00000000000..596d0b97c67 --- /dev/null +++ b/open-sse/providers/registry/clinepass.js @@ -0,0 +1,46 @@ +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: { + apiKeyUrl: "https://app.cline.bot/settings/api-keys", + }, + }, + category: "apikey", + transport: { + baseUrl: "https://api.cline.bot/api/v1/chat/completions", + thinkingFormat: "openai", + headers: { + "HTTP-Referer": "https://cline.bot", + "X-Title": "Cline", + }, + auth: { + combined: true, + header: "Authorization", + scheme: "bearer", + }, + }, + models: [ + { id: "glm-5.2", name: "GLM-5.2 (ClinePass)", upstreamModelId: "cline-pass/glm-5.2" }, + { id: "kimi-k2.7-code", name: "Kimi K2.7 Code (ClinePass)", upstreamModelId: "cline-pass/kimi-k2.7-code" }, + { id: "kimi-k2.6", name: "Kimi K2.6 (ClinePass)", upstreamModelId: "cline-pass/kimi-k2.6" }, + { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro (ClinePass)", upstreamModelId: "cline-pass/deepseek-v4-pro" }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash (ClinePass)", upstreamModelId: "cline-pass/deepseek-v4-flash" }, + { id: "mimo-v2.5", name: "MiMo-V2.5 (ClinePass)", upstreamModelId: "cline-pass/mimo-v2.5" }, + { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro (ClinePass)", upstreamModelId: "cline-pass/mimo-v2.5-pro" }, + { id: "minimax-m3", name: "MiniMax M3 (ClinePass)", upstreamModelId: "cline-pass/minimax-m3" }, + { id: "qwen3.7-max", name: "Qwen3.7 Max (ClinePass)", upstreamModelId: "cline-pass/qwen3.7-max" }, + { id: "qwen3.7-plus", name: "Qwen3.7 Plus (ClinePass)", upstreamModelId: "cline-pass/qwen3.7-plus" }, + ], + thinkingConfig: { + options: ["auto", "on", "off"], + defaultMode: "auto", + }, +}; diff --git a/open-sse/providers/registry/index.js b/open-sse/providers/registry/index.js index 64edcf329e7..6f0f68266e0 100644 --- a/open-sse/providers/registry/index.js +++ b/open-sse/providers/registry/index.js @@ -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 ]; diff --git a/open-sse/services/clinepassModels.js b/open-sse/services/clinepassModels.js new file mode 100644 index 00000000000..3a742540379 --- /dev/null +++ b/open-sse/services/clinepassModels.js @@ -0,0 +1,52 @@ +import { buildClineHeaders } from "../shared/clineAuth.js"; + +const CLINEPASS_MODELS_ENDPOINT = "https://api.cline.bot/api/v1/models"; +const FETCH_TIMEOUT_MS = 5000; + +function buildModelListHeaders(token, isApiKey) { + if (isApiKey) { + return { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }; + } + return buildClineHeaders(token, { Accept: "application/json" }); +} + +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.slice("cline-pass/".length), + name: m.name || m.id.slice("cline-pass/".length), + })); + + return models.length ? { models } : null; + } catch { + return null; + } finally { + clearTimeout(timer); + } +} diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index 1cf44384499..c3487c15ad6 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -168,7 +168,9 @@ function applyFormat(fmt, body, cfg, caps) { case "openai": { if (none && canDisable) { body.reasoning_effort = "none"; break; } const level = toLevel(eff); - if (level) body.reasoning_effort = level; + // Vercel / OpenAI Chat Completions only accept "none|minimal|low|medium|high|xhigh" + // for reasoning.effort. Internal "max" maps to "xhigh". Drop unknown levels. + if (level) body.reasoning_effort = level === "max" ? "xhigh" : level; break; } case "claude-adaptive": { diff --git a/open-sse/utils/clinepassEnvelope.js b/open-sse/utils/clinepassEnvelope.js new file mode 100644 index 00000000000..d90962b8cfa --- /dev/null +++ b/open-sse/utils/clinepassEnvelope.js @@ -0,0 +1,21 @@ +// ClinePass upstream wraps non-streaming JSON responses in a {success, data} envelope +// (errors use {success: false, error}). Detect and unwrap; pass through untouched otherwise. + +export function unwrapClinepassEnvelope(body, provider) { + if (provider !== "clinepass") return { body, error: null }; + if (!body || typeof body !== "object" || Array.isArray(body)) return { body, error: null }; + if (!("success" in body)) return { body, error: null }; + + if (body.success === false) { + const message = typeof body.error === "string" + ? body.error + : body.error?.message || body.message || "Upstream error"; + return { body: null, error: { message, status: body.statusCode || null } }; + } + + if (body.success === true && "data" in body && body.data !== null && typeof body.data === "object") { + return { body: body.data, error: null }; + } + + return { body, error: null }; +} diff --git a/open-sse/utils/error.js b/open-sse/utils/error.js index 315723e303d..8d6be4166dc 100644 --- a/open-sse/utils/error.js +++ b/open-sse/utils/error.js @@ -1,4 +1,5 @@ import { ERROR_TYPES, DEFAULT_ERROR_MESSAGES } from "../config/errorConfig.js"; +import { unwrapClinepassEnvelope } from "./clinepassEnvelope.js"; /** * Build OpenAI-compatible error response body @@ -77,7 +78,12 @@ export async function parseUpstreamError(response, executor = null) { let message = ""; try { const json = JSON.parse(bodyText); - message = json.error?.message || json.message || json.error || bodyText; + const { error: envError } = unwrapClinepassEnvelope(json, executor?.getProvider?.() || executor?.provider); + if (envError) { + message = envError.message; + } else { + message = json.error?.message || json.message || json.error || bodyText; + } } catch { message = bodyText; } diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js index 8b1c720bca2..326473043c0 100644 --- a/src/app/api/v1/models/route.js +++ b/src/app/api/v1/models/route.js @@ -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; } }; diff --git a/src/lib/oauth/providers.js b/src/lib/oauth/providers.js index bb543b85183..f5f1052aef4 100644 --- a/src/lib/oauth/providers.js +++ b/src/lib/oauth/providers.js @@ -1116,6 +1116,9 @@ const PROVIDERS = { providerSpecificData: { firstName: tokens.firstName, lastName: tokens.lastName }, }), }, + clinepass: { + flowType: "apikey", + }, // GitLab Duo - Authorization Code Flow with PKCE // Supports two login modes via loginMode metadata: "oauth" (default) or "pat" gitlab: {