Enhance configuration and model capabilities
This commit is contained in:
@@ -28,6 +28,8 @@ const nextConfig = {
|
||||
experimental: {
|
||||
// #1529/#1572: LLM clients can send long context or base64 image payloads through /v1 rewrites.
|
||||
proxyClientMaxBodySize,
|
||||
// Cache fetch responses across HMR refreshes for faster dev reloads.
|
||||
serverComponentsHmrCache: true,
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Ignore fs/path modules in browser bundle
|
||||
|
||||
@@ -158,7 +158,8 @@ export const PATTERN_CAPABILITIES = [
|
||||
{ pattern: "*deepseek-v4*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 } },
|
||||
{ pattern: "*reasoner*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
|
||||
{ pattern: "*deepseek-r*", caps: { reasoning: true, thinkingFormat: "deepseek", thinkingCanDisable: false, contextWindow: 128000 } },
|
||||
{ pattern: "*deepseek*", caps: { contextWindow: 128000 } },
|
||||
{ pattern: "*deepseek-chat*", caps: { contextWindow: 128000 } },
|
||||
{ pattern: "*deepseek*", caps: { reasoning: true, thinkingFormat: "deepseek", contextWindow: 128000 } },
|
||||
|
||||
// ── MiniMax (M3 = adaptive; M2.x cannot disable) ─────────────────
|
||||
{ pattern: "*minimax*image*", caps: { imageOutput: true } },
|
||||
|
||||
@@ -31,8 +31,13 @@ export default {
|
||||
clientSecret: "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",
|
||||
},
|
||||
models: [
|
||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
||||
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro Preview" },
|
||||
{ id: "gemini-3-pro-preview", name: "Gemini 3 Pro Preview" },
|
||||
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash Preview" },
|
||||
{ id: "gemini-3.1-flash-lite-preview", name: "Gemini 3.1 Flash Lite Preview" },
|
||||
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro" },
|
||||
{ id: "gemini-2.5-flash", name: "Gemini 2.5 Flash" },
|
||||
{ id: "gemini-2.5-flash-lite", name: "Gemini 2.5 Flash Lite" },
|
||||
],
|
||||
oauth: {
|
||||
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
|
||||
@@ -41,12 +41,6 @@ export default {
|
||||
},
|
||||
},
|
||||
models: [
|
||||
{ id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },
|
||||
{ id: "gpt-4", name: "GPT-4" },
|
||||
{ id: "gpt-4o", name: "GPT-4o" },
|
||||
{ id: "gpt-4o-mini", name: "GPT-4o mini" },
|
||||
{ id: "gpt-4.1", name: "GPT-4.1" },
|
||||
{ id: "gpt-5-mini", name: "GPT-5 Mini" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
@@ -54,7 +48,6 @@ export default {
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
|
||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" },
|
||||
{ id: "claude-sonnet-4", name: "Claude Sonnet 4" },
|
||||
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
|
||||
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
|
||||
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
|
||||
|
||||
@@ -21,6 +21,7 @@ export default {
|
||||
},
|
||||
},
|
||||
models: [
|
||||
{ id: "glm-5.2", name: "GLM 5.2" },
|
||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||
{ id: "glm-5", name: "GLM 5" },
|
||||
{ id: "glm-4.7", name: "GLM-4.7" },
|
||||
|
||||
@@ -33,6 +33,7 @@ export default {
|
||||
},
|
||||
},
|
||||
models: [
|
||||
{ id: "glm-5.2", name: "GLM 5.2" },
|
||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||
{ id: "glm-5", name: "GLM 5" },
|
||||
{ id: "glm-4.7", name: "GLM 4.7" },
|
||||
|
||||
@@ -39,15 +39,16 @@ export function reorderByCapabilities(models, required) {
|
||||
*/
|
||||
const comboRotationState = new Map();
|
||||
|
||||
// Last array item whose role is "user" (current turn), or the last item when no
|
||||
// role is present. History media (older turns) must not pin the combo to a vision
|
||||
// model — those get stripped + placeholdered downstream instead.
|
||||
function lastUserItem(arr) {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return null;
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (!arr[i]?.role || arr[i].role === "user") return arr[i];
|
||||
}
|
||||
return arr[arr.length - 1];
|
||||
// Trailing run of items after the last assistant/model turn = the current user
|
||||
// turn. It may span several messages (e.g. text + image split across blocks),
|
||||
// so we return all of them. History media (older turns) must not pin the combo
|
||||
// to a vision model — those get stripped + placeholdered downstream instead.
|
||||
function trailingUserItems(arr) {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return [];
|
||||
const isAssistant = (r) => r === "assistant" || r === "model";
|
||||
let i = arr.length - 1;
|
||||
while (i >= 0 && !isAssistant(arr[i]?.role)) i--;
|
||||
return arr.slice(i + 1);
|
||||
}
|
||||
|
||||
// Detect which capabilities a request needs. Modalities (vision/pdf) are scanned
|
||||
@@ -72,14 +73,11 @@ export function detectRequiredCapabilities(body) {
|
||||
if (Array.isArray(content)) for (const b of content) scanBlock(b);
|
||||
};
|
||||
|
||||
// Modalities: current user turn only (last item across each known shape).
|
||||
const lastMsg = lastUserItem(body.messages); // openai / claude
|
||||
if (lastMsg) scanContent(lastMsg.content);
|
||||
const lastInput = lastUserItem(body.input); // responses
|
||||
if (lastInput) scanContent(lastInput.content);
|
||||
const contents = body.contents || body.request?.contents; // gemini / antigravity
|
||||
const lastContent = lastUserItem(contents);
|
||||
if (lastContent) scanContent(lastContent.parts);
|
||||
// Modalities: current user turn only (trailing user run across each known shape).
|
||||
for (const m of trailingUserItems(body.messages)) scanContent(m.content); // openai / claude
|
||||
for (const it of trailingUserItems(body.input)) scanContent(it.content); // responses
|
||||
const contents = body.contents || body.request?.contents; // gemini / antigravity
|
||||
for (const c of trailingUserItems(contents)) scanContent(c.parts);
|
||||
|
||||
// search: temporarily disabled in auto-switch (feature not wired yet).
|
||||
|
||||
|
||||
@@ -40,6 +40,15 @@ export function parseSuffix(model) {
|
||||
export function extractThinking(body) {
|
||||
if (!body || typeof body !== "object") return null;
|
||||
|
||||
// Claude output_config.effort (explicit) — priority over adaptive thinking
|
||||
const oc = body.output_config?.effort;
|
||||
if (typeof oc === "string" && oc) {
|
||||
const e = oc.toLowerCase();
|
||||
if (e === "none" || e === "off") return { mode: "none" };
|
||||
if (e === "auto") return { mode: "auto" };
|
||||
return { mode: "level", level: e };
|
||||
}
|
||||
|
||||
// Claude shape
|
||||
const t = body.thinking;
|
||||
if (t && typeof t === "object") {
|
||||
@@ -154,7 +163,7 @@ 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;
|
||||
if (level) body.reasoning_effort = level === "xhigh" || level === "max" ? "high" : level;
|
||||
break;
|
||||
}
|
||||
case "claude-adaptive": {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { adjustMaxTokens } from "./maxTokens.js";
|
||||
import { applyCloaking } from "../../utils/claudeCloaking.js";
|
||||
import { resolveSessionId } from "../../utils/sessionManager.js";
|
||||
import { PROVIDERS } from "../../providers/index.js";
|
||||
import { getCapabilitiesForModel } from "../../providers/capabilities.js";
|
||||
import { DEFAULT_MAX_TOKENS } from "../../config/runtimeConfig.js";
|
||||
|
||||
// Check if message has valid non-empty content
|
||||
export function hasValidContent(msg) {
|
||||
@@ -130,12 +132,18 @@ export function normalizeClaudePassthrough(body, model = "") {
|
||||
// - Add thinking block for Anthropic endpoint (provider === "claude")
|
||||
// - Fix tool_use/tool_result ordering
|
||||
// - Apply cloaking (billing header + fake user ID) for OAuth tokens
|
||||
export function prepareClaudeRequest(body, provider = null, apiKey = null, connectionId = null) {
|
||||
export function prepareClaudeRequest(body, provider = null, apiKey = null, connectionId = null, rawHeaders = null, sessionId = null) {
|
||||
// quirk: MiniMax's Claude-compatible endpoint rejects Anthropic's output_config (400 invalid params)
|
||||
if (PROVIDERS[provider]?.quirks?.dropOutputConfig) {
|
||||
delete body.output_config;
|
||||
}
|
||||
|
||||
// Clamp max_tokens to the model output ceiling (never above DEFAULT_MAX_TOKENS)
|
||||
if (body.max_tokens) {
|
||||
const ceiling = Math.min(getCapabilitiesForModel(provider, body.model).maxOutput, DEFAULT_MAX_TOKENS);
|
||||
if (body.max_tokens > ceiling) body.max_tokens = ceiling;
|
||||
}
|
||||
|
||||
// 1. System: remove all cache_control, add only to last block with ttl 1h
|
||||
if (body.system && Array.isArray(body.system)) {
|
||||
body.system = body.system.map((block, i) => {
|
||||
@@ -252,8 +260,8 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
|
||||
// Apply cloaking for OAuth tokens (billing header + fake user ID)
|
||||
// session_id in user_id must match X-Claude-Code-Session-Id for fingerprint consistency
|
||||
if ((provider === "claude" || provider?.startsWith("anthropic-compatible")) && apiKey) {
|
||||
const sessionId = resolveSessionId({ body, connectionId, scope: "claude" });
|
||||
body = applyCloaking(body, apiKey, sessionId);
|
||||
const sid = sessionId || resolveSessionId({ headers: rawHeaders, body, connectionId, scope: "claude" });
|
||||
body = applyCloaking(body, apiKey, sid);
|
||||
}
|
||||
|
||||
return body;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DEFAULT_MAX_TOKENS, DEFAULT_MIN_TOKENS } from "../../config/runtimeConf
|
||||
export function adjustMaxTokens(body) {
|
||||
let maxTokens = body.max_tokens || DEFAULT_MAX_TOKENS;
|
||||
|
||||
// Auto-increase for tool calling to prevent truncated arguments
|
||||
// Auto-increase for tool calling to prevent truncated arguments (min never above max)
|
||||
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
||||
if (maxTokens < DEFAULT_MIN_TOKENS) {
|
||||
maxTokens = DEFAULT_MIN_TOKENS;
|
||||
@@ -22,6 +22,9 @@ export function adjustMaxTokens(body) {
|
||||
maxTokens = body.thinking.budget_tokens + 1024;
|
||||
}
|
||||
|
||||
// Never exceed the global ceiling
|
||||
if (maxTokens > DEFAULT_MAX_TOKENS) maxTokens = DEFAULT_MAX_TOKENS;
|
||||
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cloakClaudeTools } from "../utils/claudeCloaking.js";
|
||||
import { filterToOpenAIFormat } from "./formats/openai.js";
|
||||
import { normalizeThinkingConfig } from "../services/provider.js";
|
||||
import { applyThinking, captureThinking } from "./concerns/thinkingUnified.js";
|
||||
import { captureSessionId } from "../utils/sessionManager.js";
|
||||
import { AntigravityExecutor } from "../executors/antigravity.js";
|
||||
import { PROVIDERS } from "../providers/index.js";
|
||||
|
||||
@@ -68,6 +69,11 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream
|
||||
// format conversion strips/renames the fields. Applied after translation.
|
||||
const thinkingIntent = captureThinking(result);
|
||||
|
||||
// Capture session id from the original body (envelope still intact, e.g. antigravity request.sessionId)
|
||||
const clientSessionId = captureSessionId(result, credentials, connectionId, targetFormat);
|
||||
// Expose to downstream translators (gemini-cli/antigravity envelopes) that run after envelope is stripped
|
||||
if (credentials) credentials._clientSessionId = clientSessionId;
|
||||
|
||||
// If same format, skip translation steps
|
||||
if (sourceFormat !== targetFormat) {
|
||||
// Step 1: source -> openai (if source is not openai)
|
||||
@@ -101,7 +107,7 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream
|
||||
// Final step: prepare request for Claude format endpoints
|
||||
if (targetFormat === FORMATS.CLAUDE) {
|
||||
const apiKey = credentials?.accessToken || credentials?.apiKey || null;
|
||||
result = prepareClaudeRequest(result, provider, apiKey, connectionId);
|
||||
result = prepareClaudeRequest(result, provider, apiKey, connectionId, credentials?.rawHeaders, clientSessionId);
|
||||
}
|
||||
|
||||
// Claude cloaking: rename client tools with _cc suffix (anti-ban)
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
generateProjectId,
|
||||
cleanJSONSchemaForAntigravity
|
||||
} from "../formats/gemini.js";
|
||||
import { deriveSessionId } from "../../utils/sessionManager.js";
|
||||
import { deriveSessionId, toNumericSessionId } from "../../utils/sessionManager.js";
|
||||
import { ROLE, GEMINI_ROLE, OPENAI_BLOCK, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
|
||||
// Sanitize function names for Gemini API.
|
||||
@@ -259,7 +259,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
userAgent: isAntigravity ? "antigravity" : "gemini-cli",
|
||||
requestId: isAntigravity ? `agent-${generateUUID()}` : generateRequestId(),
|
||||
request: {
|
||||
sessionId: isAntigravity ? deriveSessionId(credentials?.email || credentials?.connectionId) : generateSessionId(),
|
||||
sessionId: toNumericSessionId(credentials?._clientSessionId) || (isAntigravity ? deriveSessionId(credentials?.email || credentials?.connectionId) : generateSessionId()),
|
||||
contents: geminiCLI.contents,
|
||||
systemInstruction: geminiCLI.systemInstruction,
|
||||
generationConfig: geminiCLI.generationConfig,
|
||||
@@ -309,7 +309,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
||||
requestId: `agent-${generateUUID()}`,
|
||||
requestType: "agent",
|
||||
request: {
|
||||
sessionId: deriveSessionId(credentials?.email || credentials?.connectionId),
|
||||
sessionId: toNumericSessionId(credentials?._clientSessionId) || deriveSessionId(credentials?.email || credentials?.connectionId),
|
||||
contents: [],
|
||||
generationConfig: {
|
||||
temperature: claudeRequest.temperature || 1,
|
||||
|
||||
@@ -13,11 +13,18 @@ function generateBillingHeader(payload) {
|
||||
return `x-anthropic-billing-header: cc_version=${CLAUDE_VERSION}.${buildHash}; cc_entrypoint=${CC_ENTRYPOINT}; cch=${cch};`;
|
||||
}
|
||||
|
||||
// Derive a deterministic UUID-v4-shaped string from a seed (stable per account)
|
||||
function deriveUuid(seed) {
|
||||
const h = createHash("sha256").update(seed).digest("hex");
|
||||
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${((parseInt(h[16], 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
||||
}
|
||||
|
||||
// Generate fake user ID in Claude Code 2.1.92+ JSON format:
|
||||
// {"device_id":"<64hex>","account_uuid":"<uuid>","session_id":"<uuid>"}
|
||||
function generateFakeUserID(sessionId) {
|
||||
const deviceId = randomBytes(32).toString("hex");
|
||||
const accountUuid = randomUUID();
|
||||
// device_id/account_uuid derive from apiKey (stable per account), session_id per-conversation
|
||||
function generateFakeUserID(sessionId, apiKey) {
|
||||
const deviceId = apiKey ? createHash("sha256").update(`device:${apiKey}`).digest("hex") : randomBytes(32).toString("hex");
|
||||
const accountUuid = apiKey ? deriveUuid(`account:${apiKey}`) : randomUUID();
|
||||
const sessionUuid = sessionId || randomUUID();
|
||||
return `{"device_id":"${deviceId}","account_uuid":"${accountUuid}","session_id":"${sessionUuid}"}`;
|
||||
}
|
||||
@@ -151,7 +158,7 @@ export function applyCloaking(body, apiKey, sessionId) {
|
||||
// Inject fake user ID into metadata (session_id must match X-Claude-Code-Session-Id)
|
||||
const existingUserId = result.metadata?.user_id;
|
||||
if (!existingUserId) {
|
||||
result.metadata = { ...result.metadata, user_id: generateFakeUserID(sessionId) };
|
||||
result.metadata = { ...result.metadata, user_id: generateFakeUserID(sessionId, apiKey) };
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -89,7 +89,7 @@ const ASSISTANT_CAP_LEN = 200;
|
||||
const MAX_ASSISTANT_SESSIONS = 5000;
|
||||
|
||||
// Client headers/body fields that carry an upstream session id (priority order)
|
||||
const SESSION_HEADER_KEYS = ["x-session-id", "session_id", "x-amp-thread-id", "x-client-request-id"];
|
||||
const SESSION_HEADER_KEYS = ["x-session-id", "session-id", "session_id", "x-amp-thread-id", "x-client-request-id"];
|
||||
const CLAUDE_CODE_SESSION_RE = /_session_([a-f0-9-]+)$/;
|
||||
|
||||
function sha16(text) {
|
||||
@@ -122,9 +122,20 @@ function headerValue(headers, key) {
|
||||
}
|
||||
|
||||
// Read client-provided session id from headers/body (no generation)
|
||||
// Antigravity envelope carries session in request.sessionId; requestId embeds conversation uuid
|
||||
const ANTIGRAVITY_CONV_RE = /^[a-z]+\/([0-9a-f-]{36})\//i;
|
||||
function extractAntigravitySession(body) {
|
||||
const sid = body?.request?.sessionId;
|
||||
if (sid != null && sid !== "") return normalizeSessionId(String(sid));
|
||||
const m = typeof body?.requestId === "string" ? body.requestId.match(ANTIGRAVITY_CONV_RE) : null;
|
||||
return m ? normalizeSessionId(m[1]) : null;
|
||||
}
|
||||
|
||||
function extractClientSessionId(headers, body) {
|
||||
const claude = extractClaudeCodeSession(body?.metadata?.user_id);
|
||||
if (claude) return `claude:${claude}`;
|
||||
const antigravity = extractAntigravitySession(body);
|
||||
if (antigravity) return `antigravity:${antigravity}`;
|
||||
for (const key of SESSION_HEADER_KEYS) {
|
||||
const v = headerValue(headers, key);
|
||||
if (v) return v;
|
||||
@@ -194,6 +205,22 @@ export function resolveSessionId({ headers, body, connectionId, workspaceId, sco
|
||||
return deriveSessionId(connectionId);
|
||||
}
|
||||
|
||||
// Capture session id from request body + credentials (envelope still intact here)
|
||||
export function captureSessionId(body, credentials, connectionId, scope = "") {
|
||||
return resolveSessionId({ headers: credentials?.rawHeaders, body, connectionId, scope });
|
||||
}
|
||||
|
||||
// Convert any session id to Antigravity numeric format "-<int64>" (matches real AG / CLIProxyAPI).
|
||||
// Already-numeric ids (native AG sessionId) pass through unchanged.
|
||||
export function toNumericSessionId(sessionId) {
|
||||
const v = normalizeSessionId(sessionId);
|
||||
if (!v) return null;
|
||||
if (/^-?\d+$/.test(v)) return v;
|
||||
const h = crypto.createHash("sha256").update(v).digest();
|
||||
const n = h.readBigUInt64BE(0) & 0x7fffffffffffffffn;
|
||||
return `-${n.toString()}`;
|
||||
}
|
||||
|
||||
// Cleanup expired assistant-session entries
|
||||
const assistantCleanup = setInterval(() => {
|
||||
const now = Date.now();
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function ProviderDetailPage() {
|
||||
const [headerImgError, setHeaderImgError] = useState(false);
|
||||
const [modelTestResults, setModelTestResults] = useState({});
|
||||
const [modelsTestError, setModelsTestError] = useState("");
|
||||
const [testingModelId, setTestingModelId] = useState(null);
|
||||
const [testingModelIds, setTestingModelIds] = useState(() => new Set());
|
||||
const [showAddCustomModel, setShowAddCustomModel] = useState(false);
|
||||
const [selectedConnectionIds, setSelectedConnectionIds] = useState([]);
|
||||
const [bulkProxyPoolId, setBulkProxyPoolId] = useState("__none__");
|
||||
@@ -876,8 +876,8 @@ export default function ProviderDetailPage() {
|
||||
);
|
||||
|
||||
const handleTestModel = async (modelId) => {
|
||||
if (testingModelId) return;
|
||||
setTestingModelId(modelId);
|
||||
if (testingModelIds.has(modelId)) return;
|
||||
setTestingModelIds((prev) => new Set(prev).add(modelId));
|
||||
try {
|
||||
const res = await fetch("/api/models/test", {
|
||||
method: "POST",
|
||||
@@ -891,7 +891,7 @@ export default function ProviderDetailPage() {
|
||||
setModelTestResults((prev) => ({ ...prev, [modelId]: "error" }));
|
||||
setModelsTestError("Network error");
|
||||
} finally {
|
||||
setTestingModelId(null);
|
||||
setTestingModelIds((prev) => { const n = new Set(prev); n.delete(modelId); return n; });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -952,7 +952,7 @@ export default function ProviderDetailPage() {
|
||||
onDeleteAlias={() => handleDeleteAlias(model.alias)}
|
||||
testStatus={modelTestResults[model.id]}
|
||||
onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
|
||||
isTesting={testingModelId === model.id}
|
||||
isTesting={testingModelIds.has(model.id)}
|
||||
isCustom
|
||||
isFree={false}
|
||||
caps={getCaps(`${providerId}/${model.id}`)}
|
||||
@@ -977,7 +977,7 @@ export default function ProviderDetailPage() {
|
||||
onDeleteAlias={() => handleDeleteAlias(existingAlias)}
|
||||
testStatus={modelTestResults[model.id]}
|
||||
onTest={connections.length > 0 || isFreeNoAuth ? () => handleTestModel(model.id) : undefined}
|
||||
isTesting={testingModelId === model.id}
|
||||
isTesting={testingModelIds.has(model.id)}
|
||||
isFree={model.isFree}
|
||||
onDisable={() => handleDisableModel(model.id)}
|
||||
caps={getCaps(`${providerId}/${model.id}`)}
|
||||
|
||||
@@ -281,15 +281,18 @@ export default function ProvidersPage() {
|
||||
Object.entries(OAUTH_PROVIDERS).filter(([, info]) => !info.hidden && matchSearch(info.name)),
|
||||
"oauth",
|
||||
);
|
||||
const freeEntries = Object.entries(FREE_PROVIDERS).filter(
|
||||
([, info]) => !info.hidden && matchSearch(info.name),
|
||||
);
|
||||
const freeEntries = Object.entries(FREE_PROVIDERS)
|
||||
.filter(([, info]) => !info.hidden && matchSearch(info.name))
|
||||
.sort(([, a], [, b]) => (b.noAuth ? 1 : 0) - (a.noAuth ? 1 : 0));
|
||||
const freeTierEntries = sortByPriority(
|
||||
Object.entries(FREE_TIER_PROVIDERS).filter(
|
||||
([, info]) => !info.hidden && matchSearch(info.name),
|
||||
([, info]) =>
|
||||
!info.hidden &&
|
||||
matchSearch(info.name) &&
|
||||
(info.serviceKinds ?? ["llm"]).includes("llm"),
|
||||
),
|
||||
"freeTier",
|
||||
);
|
||||
).sort(([, a], [, b]) => (b.noAuth ? 1 : 0) - (a.noAuth ? 1 : 0));
|
||||
// API Key: connected providers first, then alphabetical by name
|
||||
const apikeyEntries = Object.entries(APIKEY_PROVIDERS)
|
||||
.filter(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getCapabilitiesForModel } from "open-sse/providers/capabilities.js";
|
||||
|
||||
// Fetch model capabilities once and expose a lookup by fullModel ("provider/model") or bare model id.
|
||||
export function useModelCaps() {
|
||||
@@ -32,7 +33,11 @@ export function useModelCaps() {
|
||||
if (!key) return null;
|
||||
if (byFull[key]) return byFull[key];
|
||||
const bare = key.includes("/") ? key.slice(key.indexOf("/") + 1) : key;
|
||||
return byId[bare] || null;
|
||||
if (byId[bare]) return byId[bare];
|
||||
// Fallback: compute caps for dynamic models (passthrough/custom/suggested) not in static list
|
||||
const provider = key.includes("/") ? key.slice(0, key.indexOf("/")) : null;
|
||||
const c = getCapabilitiesForModel(provider, bare);
|
||||
return { vision: c.vision, search: c.search, reasoning: c.reasoning };
|
||||
};
|
||||
|
||||
return { getCaps };
|
||||
|
||||
Reference in New Issue
Block a user