| import { PROVIDERS } from "../config/providers.js"; |
| import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js"; |
|
|
| const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-"; |
| const OPENAI_COMPATIBLE_DEFAULTS = { |
| baseUrl: "https://api.openai.com/v1", |
| }; |
|
|
| const ANTHROPIC_COMPATIBLE_PREFIX = "anthropic-compatible-"; |
| const ANTHROPIC_COMPATIBLE_DEFAULTS = { |
| baseUrl: "https://api.anthropic.com/v1", |
| }; |
|
|
| function isOpenAICompatible(provider) { |
| return typeof provider === "string" && provider.startsWith(OPENAI_COMPATIBLE_PREFIX); |
| } |
|
|
| function isAnthropicCompatible(provider) { |
| return typeof provider === "string" && provider.startsWith(ANTHROPIC_COMPATIBLE_PREFIX); |
| } |
|
|
| function getOpenAICompatibleType(provider) { |
| if (!isOpenAICompatible(provider)) return "chat"; |
| return provider.includes("responses") ? "responses" : "chat"; |
| } |
|
|
| function buildOpenAICompatibleUrl(baseUrl, apiType) { |
| const normalized = baseUrl.replace(/\/$/, ""); |
| const path = apiType === "responses" ? "/responses" : "/chat/completions"; |
| return `${normalized}${path}`; |
| } |
|
|
| function buildAnthropicCompatibleUrl(baseUrl) { |
| const normalized = baseUrl.replace(/\/$/, ""); |
| return `${normalized}/messages`; |
| } |
|
|
| function buildQwenBaseUrl(resourceUrl, fallbackBaseUrl) { |
| const fallback = (fallbackBaseUrl || "").replace(/\/chat\/completions$/, ""); |
| const raw = typeof resourceUrl === "string" ? resourceUrl.trim() : ""; |
| if (!raw) return fallback; |
| if (raw.startsWith("http://") || raw.startsWith("https://")) { |
| return raw.replace(/\/$/, ""); |
| } |
| return `https://${raw.replace(/\/$/, "")}/v1`; |
| } |
|
|
| |
| export function detectFormat(body) { |
| |
| |
| if (body.input && (Array.isArray(body.input) || typeof body.input === "string") && !body.messages) { |
| return "openai-responses"; |
| } |
|
|
| |
| if (body.request?.contents && body.userAgent === "antigravity") { |
| return "antigravity"; |
| } |
|
|
| |
| if (body.contents && Array.isArray(body.contents)) { |
| return "gemini"; |
| } |
|
|
| |
| |
| if ( |
| body.stream_options || |
| body.response_format || |
| body.logprobs !== undefined || |
| body.top_logprobs !== undefined || |
| body.n !== undefined || |
| body.presence_penalty !== undefined || |
| body.frequency_penalty !== undefined || |
| body.logit_bias || |
| body.user |
| ) { |
| return "openai"; |
| } |
|
|
| |
| |
| if (body.messages && Array.isArray(body.messages)) { |
| const firstMsg = body.messages[0]; |
| |
| |
| if (firstMsg?.content && Array.isArray(firstMsg.content)) { |
| const firstContent = firstMsg.content[0]; |
| |
| |
| |
| if (firstContent?.type === "text" && !body.model?.includes("/")) { |
| |
| |
| if (body.system || body.anthropic_version) { |
| return "claude"; |
| } |
| |
| const hasClaudeImage = firstMsg.content.some(c => |
| c.type === "image" && c.source?.type === "base64" |
| ); |
| const hasOpenAIImage = firstMsg.content.some(c => |
| c.type === "image_url" && c.image_url?.url |
| ); |
| if (hasClaudeImage) return "claude"; |
| if (hasOpenAIImage) return "openai"; |
| |
| |
| const hasClaudeTool = firstMsg.content.some(c => |
| c.type === "tool_use" || c.type === "tool_result" |
| ); |
| if (hasClaudeTool) return "claude"; |
| } |
| } |
| |
| |
| |
| if (body.system !== undefined || body.anthropic_version) { |
| return "claude"; |
| } |
| } |
|
|
| |
| return "openai"; |
| } |
|
|
| |
| export function getProviderConfig(provider) { |
| if (isOpenAICompatible(provider)) { |
| const apiType = getOpenAICompatibleType(provider); |
| return { |
| ...PROVIDERS.openai, |
| format: apiType === "responses" ? "openai-responses" : "openai", |
| baseUrl: OPENAI_COMPATIBLE_DEFAULTS.baseUrl, |
| }; |
| } |
| if (isAnthropicCompatible(provider)) { |
| return { |
| ...PROVIDERS.anthropic, |
| format: "claude", |
| baseUrl: ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl, |
| }; |
| } |
| return PROVIDERS[provider] || PROVIDERS.openai; |
| } |
|
|
| |
| export function getProviderFallbackCount(provider) { |
| const config = getProviderConfig(provider); |
| return config.baseUrls?.length || 1; |
| } |
|
|
| |
| export function buildProviderUrl(provider, model, stream = true, options = {}) { |
| if (isOpenAICompatible(provider)) { |
| const apiType = getOpenAICompatibleType(provider); |
| const baseUrl = options?.baseUrl || OPENAI_COMPATIBLE_DEFAULTS.baseUrl; |
| return buildOpenAICompatibleUrl(baseUrl, apiType); |
| } |
| if (isAnthropicCompatible(provider)) { |
| const baseUrl = options?.baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl; |
| return buildAnthropicCompatibleUrl(baseUrl); |
| } |
| const config = getProviderConfig(provider); |
|
|
| switch (provider) { |
| case "claude": |
| return `${config.baseUrl}?beta=true`; |
|
|
| case "gemini": { |
| const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; |
| return `${config.baseUrl}/${model}:${action}`; |
| } |
|
|
| case "gemini-cli": { |
| const action = stream ? "streamGenerateContent?alt=sse" : "generateContent"; |
| return `${config.baseUrl}:${action}`; |
| } |
|
|
| case "antigravity": { |
| |
| const urlIndex = options?.baseUrlIndex || 0; |
| const baseUrl = config.baseUrls[urlIndex] || config.baseUrls[0]; |
| const path = stream ? "/v1internal:streamGenerateContent?alt=sse" : "/v1internal:generateContent"; |
| return `${baseUrl}${path}`; |
| } |
|
|
| case "codex": |
| return config.baseUrl; |
|
|
| case "qwen": { |
| const baseUrl = buildQwenBaseUrl(options?.qwenResourceUrl, config.baseUrl); |
| return `${baseUrl}/chat/completions`; |
| } |
|
|
| case "github": |
| return config.baseUrl; |
|
|
| case "glm": |
| case "kimi": |
| case "minimax": |
| |
| return `${config.baseUrl}?beta=true`; |
|
|
| default: |
| return config.baseUrl; |
| } |
| } |
|
|
| |
| export function buildProviderHeaders(provider, credentials, stream = true, body = null) { |
| const config = getProviderConfig(provider); |
| const headers = { |
| "Content-Type": "application/json", |
| ...config.headers |
| }; |
|
|
| |
| |
| if (isAnthropicCompatible(provider)) { |
| if (credentials.apiKey) { |
| headers["x-api-key"] = credentials.apiKey; |
| |
| |
| } else if (credentials.accessToken) { |
| headers["Authorization"] = `Bearer ${credentials.accessToken}`; |
| } |
| |
| if (!headers["anthropic-version"]) { |
| headers["anthropic-version"] = "2023-06-01"; |
| } |
| } else { |
| switch (provider) { |
| case "gemini": |
| if (credentials.apiKey) { |
| headers["x-goog-api-key"] = credentials.apiKey; |
| } else if (credentials.accessToken) { |
| headers["Authorization"] = `Bearer ${credentials.accessToken}`; |
| } |
| break; |
| |
| case "antigravity": |
| case "gemini-cli": |
| |
| headers["Authorization"] = `Bearer ${credentials.accessToken}`; |
| break; |
| |
| case "claude": |
| |
| if (credentials.apiKey) { |
| headers["x-api-key"] = credentials.apiKey; |
| } else if (credentials.accessToken) { |
| headers["Authorization"] = `Bearer ${credentials.accessToken}`; |
| } |
| break; |
| |
| case "github": { |
| |
| |
| const githubToken = credentials.copilotToken || credentials.accessToken; |
| |
| headers["Authorization"] = `Bearer ${githubToken}`; |
| headers["Content-Type"] = "application/json"; |
| headers["copilot-integration-id"] = "vscode-chat"; |
| headers["editor-version"] = "vscode/1.107.1"; |
| headers["editor-plugin-version"] = "copilot-chat/0.26.7"; |
| headers["user-agent"] = "GitHubCopilotChat/0.26.7"; |
| headers["openai-intent"] = "conversation-panel"; |
| headers["x-github-api-version"] = "2025-04-01"; |
| |
| headers["x-request-id"] = crypto.randomUUID ? crypto.randomUUID() : |
| 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| const r = Math.random() * 16 | 0; |
| const v = c == 'x' ? r : (r & 0x3 | 0x8); |
| return v.toString(16); |
| }); |
| headers["x-vscode-user-agent-library-version"] = "electron-fetch"; |
| headers["X-Initiator"] = "user"; |
| headers["Accept"] = "application/json"; |
| break; |
| } |
| |
| case "codex": |
| case "qwen": |
| case "openai": |
| case "openrouter": |
| headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; |
| break; |
|
|
| case "cline": |
| Object.assign(headers, buildClineHeaders(credentials.apiKey || credentials.accessToken)); |
| break; |
| |
| case "glm": |
| case "kimi": |
| case "minimax": |
| |
| headers["x-api-key"] = credentials.apiKey; |
| break; |
|
|
| case "vertex": |
| case "vertex-partner": |
| |
| |
| break; |
| |
| default: |
| headers["Authorization"] = `Bearer ${credentials.apiKey || credentials.accessToken}`; |
| break; |
| } |
| } |
|
|
| |
| if (stream) { |
| headers["Accept"] = "text/event-stream"; |
| } |
|
|
| return headers; |
| } |
|
|
| |
| export function getTargetFormat(provider) { |
| if (isOpenAICompatible(provider)) { |
| return getOpenAICompatibleType(provider) === "responses" ? "openai-responses" : "openai"; |
| } |
| if (isAnthropicCompatible(provider)) { |
| return "claude"; |
| } |
| const config = getProviderConfig(provider); |
| return config.format || "openai"; |
| } |
|
|
| |
| export function isLastMessageFromUser(body) { |
| const messages = body.messages || body.contents; |
| if (!messages?.length) return true; |
| const lastMsg = messages[messages.length - 1]; |
| return lastMsg?.role === "user"; |
| } |
|
|
| |
| export function hasThinkingConfig(body) { |
| return !!(body.reasoning_effort || body.thinking?.type === "enabled"); |
| } |
|
|
| |
| |
| |
| export function normalizeThinkingConfig(body) { |
| if (!isLastMessageFromUser(body)) { |
| delete body.reasoning_effort; |
| delete body.thinking; |
| } |
| return body; |
| } |
|
|