mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
Fix bug strip image
This commit is contained in:
13
open-sse/config/models.js
Normal file
13
open-sse/config/models.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Model metadata registry
|
||||||
|
// Only define models that differ from DEFAULT_MODEL_INFO
|
||||||
|
// Custom entries are merged over default
|
||||||
|
const DEFAULT_MODEL_INFO = {
|
||||||
|
type: ["chat"],
|
||||||
|
contextWindow: 200000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MODEL_INFO = {};
|
||||||
|
|
||||||
|
export function getModelInfo(modelId) {
|
||||||
|
return { ...DEFAULT_MODEL_INFO, ...MODEL_INFO[modelId] };
|
||||||
|
}
|
||||||
@@ -1,191 +1,5 @@
|
|||||||
import { PROVIDERS } from "./providers.js";
|
import { PROVIDERS } from "./providers.js";
|
||||||
|
|
||||||
// Global model capabilities registry
|
|
||||||
// Only define models that have non-default capabilities
|
|
||||||
// Default: { thinking: false, multimodal: { image: false, audio: false, video: false, pdf: false } }
|
|
||||||
export const MODEL_CAPS = {
|
|
||||||
// Claude models — full capabilities
|
|
||||||
"claude-opus-4-6": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-sonnet-4-6": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-opus-4-6-thinking": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-opus-4-5-20251101": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-sonnet-4-5-20250929": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-haiku-4-5-20251001": { multimodal: { image: true } },
|
|
||||||
"claude-sonnet-4-20250514": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-opus-4-20250514": { thinking: true, multimodal: { image: true, pdf: true } },
|
|
||||||
"claude-3-5-sonnet-20241022": { multimodal: { image: true } },
|
|
||||||
// Cursor Claude aliases
|
|
||||||
"claude-4.5-opus-high-thinking": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-4.5-opus-high": { multimodal: { image: true } },
|
|
||||||
"claude-4.5-sonnet-thinking": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-4.5-sonnet": { multimodal: { image: true } },
|
|
||||||
"claude-4.5-haiku": { multimodal: { image: true } },
|
|
||||||
"claude-4.5-opus": { multimodal: { image: true } },
|
|
||||||
"claude-4.6-opus-max": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-4.6-sonnet-medium-thinking": { thinking: true, multimodal: { image: true } },
|
|
||||||
// GitHub Copilot Claude aliases
|
|
||||||
"claude-haiku-4.5": { multimodal: { image: true } },
|
|
||||||
"claude-opus-4.1": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-opus-4.5": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-sonnet-4": { multimodal: { image: true } },
|
|
||||||
"claude-sonnet-4.5": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-sonnet-4.6": { thinking: true, multimodal: { image: true } },
|
|
||||||
"claude-opus-4.6": { thinking: true, multimodal: { image: true } },
|
|
||||||
// Kiro aliases
|
|
||||||
"claude-sonnet-4.5": { thinking: true, multimodal: { image: true } },
|
|
||||||
|
|
||||||
// Gemini models — full multimodal
|
|
||||||
"gemini-3.1-pro-preview": { thinking: true, multimodal: { image: true, audio: true, video: true, pdf: true } },
|
|
||||||
"gemini-3.1-flash-lite-preview": { multimodal: { image: true, audio: true, video: true } },
|
|
||||||
"gemini-3.1-flash-image-preview": { multimodal: { image: true, audio: true, video: true } },
|
|
||||||
"gemini-3-flash-preview": { thinking: true, multimodal: { image: true, audio: true, video: true } },
|
|
||||||
"gemini-3-pro-preview": { thinking: true, multimodal: { image: true, audio: true, video: true, pdf: true } },
|
|
||||||
"gemini-3-flash": { thinking: true, multimodal: { image: true, audio: true, video: true } },
|
|
||||||
"gemini-3.1-pro-high": { thinking: true, multimodal: { image: true, audio: true, video: true, pdf: true } },
|
|
||||||
"gemini-3.1-pro-low": { thinking: true, multimodal: { image: true, audio: true, video: true, pdf: true } },
|
|
||||||
"gemini-2.5-pro": { thinking: true, multimodal: { image: true, audio: true, video: true, pdf: true } },
|
|
||||||
"gemini-2.5-flash": { thinking: true, multimodal: { image: true, audio: true, video: true } },
|
|
||||||
"gemini-2.5-flash-lite": { multimodal: { image: true } },
|
|
||||||
"gemini-2.0-flash": { multimodal: { image: true, audio: true, video: true } },
|
|
||||||
"gemini-2.0-flash-lite": { multimodal: { image: true } },
|
|
||||||
|
|
||||||
// GPT models
|
|
||||||
"gpt-5.4": { multimodal: { image: true } },
|
|
||||||
"gpt-5.4-mini": { multimodal: { image: true } },
|
|
||||||
"gpt-5.3-codex": { thinking: true, multimodal: { image: true } },
|
|
||||||
"gpt-5.3-codex-xhigh": { thinking: true },
|
|
||||||
"gpt-5.3-codex-high": { thinking: true },
|
|
||||||
"gpt-5.3-codex-low": { thinking: true },
|
|
||||||
"gpt-5.3-codex-none": {},
|
|
||||||
"gpt-5.3-codex-spark": {},
|
|
||||||
"gpt-5.2-codex": { thinking: true },
|
|
||||||
"gpt-5.2": { multimodal: { image: true } },
|
|
||||||
"gpt-5.1-codex": { thinking: true },
|
|
||||||
"gpt-5.1-codex-mini": { thinking: true },
|
|
||||||
"gpt-5.1-codex-high": { thinking: true },
|
|
||||||
"gpt-5.1-codex-max": { thinking: true },
|
|
||||||
"gpt-5.1": { multimodal: { image: true } },
|
|
||||||
"gpt-5-codex": { thinking: true },
|
|
||||||
"gpt-5-codex-mini": {},
|
|
||||||
"gpt-5": { multimodal: { image: true } },
|
|
||||||
"gpt-5-mini": { multimodal: { image: true } },
|
|
||||||
"gpt-4o": { multimodal: { image: true, audio: true } },
|
|
||||||
"gpt-4o-mini": { multimodal: { image: true } },
|
|
||||||
"gpt-4-turbo": { multimodal: { image: true } },
|
|
||||||
"gpt-4.1": { multimodal: { image: true } },
|
|
||||||
"gpt-4.1-mini": { multimodal: { image: true } },
|
|
||||||
"gpt-4.1-nano": {},
|
|
||||||
"o3": { thinking: true, multimodal: { image: true } },
|
|
||||||
"o3-mini": { thinking: true },
|
|
||||||
"o3-pro": { thinking: true, multimodal: { image: true } },
|
|
||||||
"o4-mini": { thinking: true, multimodal: { image: true } },
|
|
||||||
"o1": { thinking: true, multimodal: { image: true } },
|
|
||||||
"o1-mini": { thinking: true },
|
|
||||||
|
|
||||||
// DeepSeek models
|
|
||||||
"deepseek-chat": {},
|
|
||||||
"deepseek-reasoner": { thinking: true },
|
|
||||||
"deepseek-r1": { thinking: true },
|
|
||||||
"deepseek-v3": {},
|
|
||||||
"deepseek-v3.1": {},
|
|
||||||
"deepseek-v3.2": {},
|
|
||||||
"deepseek-3.1": {},
|
|
||||||
"deepseek-3.2": {},
|
|
||||||
"deepseek-ai/DeepSeek-R1": { thinking: true },
|
|
||||||
"deepseek-ai/DeepSeek-V3": {},
|
|
||||||
"deepseek-ai/DeepSeek-V3.2": {},
|
|
||||||
"deepseek-ai/DeepSeek-V3.1": {},
|
|
||||||
"deepseek-ai/deepseek-v3.2-maas": {},
|
|
||||||
|
|
||||||
// Qwen models
|
|
||||||
"qwen3-vl-plus": { multimodal: { image: true } },
|
|
||||||
"vision-model": { multimodal: { image: true } },
|
|
||||||
"qwen3-coder-plus": {},
|
|
||||||
"qwen3-coder-flash": {},
|
|
||||||
"qwen3-max": { thinking: true },
|
|
||||||
"qwen3-max-preview": { thinking: true },
|
|
||||||
"qwen3-235b": { thinking: true },
|
|
||||||
"qwen3-235b-a22b-instruct": {},
|
|
||||||
"qwen3-235b-a22b-thinking-2507": { thinking: true },
|
|
||||||
"qwen3-32b": { thinking: true },
|
|
||||||
"qwen3-coder-next": {},
|
|
||||||
"qwen3.5-plus": {},
|
|
||||||
"qwen/qwen3-32b": { thinking: true },
|
|
||||||
"qwen/qwen3-next-80b-a3b-thinking-maas": { thinking: true },
|
|
||||||
"qwen/qwen3-next-80b-a3b-instruct-maas": {},
|
|
||||||
"Qwen/Qwen3-235B-A22B": { thinking: true },
|
|
||||||
"Qwen/Qwen3-235B-A22B-Instruct-2507": {},
|
|
||||||
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {},
|
|
||||||
"Qwen/Qwen3-32B": { thinking: true },
|
|
||||||
"qwen-3-235b-a22b-instruct-2507": {},
|
|
||||||
"qwen-3-32b": { thinking: true },
|
|
||||||
|
|
||||||
// Kimi models
|
|
||||||
"kimi-k2": {},
|
|
||||||
"kimi-k2.5": {},
|
|
||||||
"kimi-k2.5-thinking": { thinking: true },
|
|
||||||
"kimi-latest": {},
|
|
||||||
"moonshotai/Kimi-K2.5": {},
|
|
||||||
"moonshotai/kimi-k2.5": {},
|
|
||||||
|
|
||||||
// GLM models
|
|
||||||
"glm-5.1": {},
|
|
||||||
"glm-5": {},
|
|
||||||
"glm-4.7": {},
|
|
||||||
"glm-4.6v": { multimodal: { image: true } },
|
|
||||||
"glm-4.6": {},
|
|
||||||
"glm-4.5-air": {},
|
|
||||||
"glm-4.7-flash": {},
|
|
||||||
"z-ai/glm4.7": {},
|
|
||||||
"zai-org/GLM-4.7": {},
|
|
||||||
"zai-glm-4.7": {},
|
|
||||||
"zai-org/glm-5-maas": {},
|
|
||||||
|
|
||||||
// Grok models
|
|
||||||
"grok-4": { thinking: true, multimodal: { image: true } },
|
|
||||||
"grok-4-fast-reasoning": { thinking: true },
|
|
||||||
"grok-code-fast-1": {},
|
|
||||||
"grok-3": { multimodal: { image: true } },
|
|
||||||
|
|
||||||
// GPT-OSS (no toolUse)
|
|
||||||
"gpt-oss-120b": {},
|
|
||||||
"gpt-oss-120b-medium": {},
|
|
||||||
"openai/gpt-oss-120b": {},
|
|
||||||
"gpt-oss:120b": {},
|
|
||||||
|
|
||||||
// Llama models
|
|
||||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": {},
|
|
||||||
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { multimodal: { image: true } },
|
|
||||||
"meta-llama/llama-4-maverick-17b-128e-instruct": { multimodal: { image: true } },
|
|
||||||
"meta-llama/Llama-3.3-70B-Instruct": {},
|
|
||||||
"meta-llama/Llama-3.2-3B-Instruct": {},
|
|
||||||
"llama-3.3-70b-versatile": {},
|
|
||||||
"llama-3.3-70b": {},
|
|
||||||
"llama-4-scout-17b-16e-instruct": { multimodal: { image: true } },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default capabilities for unknown models
|
|
||||||
const DEFAULT_CAPS = { thinking: false, multimodal: { image: false, audio: false, video: false, pdf: false } };
|
|
||||||
|
|
||||||
// Merge caps: global as base, provider entry overrides
|
|
||||||
function mergeCaps(global, override) {
|
|
||||||
if (!override) return global;
|
|
||||||
return {
|
|
||||||
thinking: override.thinking ?? global.thinking,
|
|
||||||
multimodal: { ...global.multimodal, ...override.multimodal }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve model capabilities: provider override → global → default
|
|
||||||
export function getModelCaps(alias, modelId) {
|
|
||||||
const entry = PROVIDER_MODELS[alias]?.find(m => m.id === modelId);
|
|
||||||
const global = MODEL_CAPS[modelId] ?? DEFAULT_CAPS;
|
|
||||||
// Extract caps fields from entry (exclude id, name, type, targetFormat)
|
|
||||||
const { id, name, type, targetFormat, ...overrideCaps } = entry || {};
|
|
||||||
const hasOverride = Object.keys(overrideCaps).length > 0;
|
|
||||||
return mergeCaps({ ...DEFAULT_CAPS, ...global }, hasOverride ? overrideCaps : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider models - Single source of truth
|
// Provider models - Single source of truth
|
||||||
// Key = alias (cc, cx, gc, qw, if, ag, gh for OAuth; id for API Key)
|
// Key = alias (cc, cx, gc, qw, if, ag, gh for OAuth; id for API Key)
|
||||||
// Field "provider" for special cases (e.g. AntiGravity models that call different backends)
|
// Field "provider" for special cases (e.g. AntiGravity models that call different backends)
|
||||||
@@ -293,9 +107,9 @@ export const PROVIDER_MODELS = {
|
|||||||
// { id: "claude-opus-4.5", name: "Claude Opus 4.5" },
|
// { id: "claude-opus-4.5", name: "Claude Opus 4.5" },
|
||||||
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
|
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" },
|
||||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||||
{ id: "deepseek-3.2", name: "DeepSeek 3.2" },
|
{ id: "deepseek-3.2", name: "DeepSeek 3.2", strip: ["image", "audio"] },
|
||||||
{ id: "deepseek-3.1", name: "DeepSeek 3.1" },
|
{ id: "deepseek-3.1", name: "DeepSeek 3.1", strip: ["image", "audio"] },
|
||||||
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next" },
|
{ id: "qwen3-coder-next", name: "Qwen3 Coder Next", strip: ["image", "audio"] },
|
||||||
],
|
],
|
||||||
cu: [ // Cursor IDE
|
cu: [ // Cursor IDE
|
||||||
{ id: "default", name: "Auto (Server Picks)" },
|
{ id: "default", name: "Auto (Server Picks)" },
|
||||||
@@ -611,5 +425,9 @@ export function getModelsByProviderId(providerId) {
|
|||||||
return PROVIDER_MODELS[alias] || [];
|
return PROVIDER_MODELS[alias] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export getModelCaps here for convenience (defined above PROVIDER_MODELS)
|
// Get strip list for a model entry (explicit opt-in only)
|
||||||
// getModelCaps is already exported above
|
// Returns array of content types to strip, e.g. ["image", "audio"]
|
||||||
|
export function getModelStrip(alias, modelId) {
|
||||||
|
const entry = PROVIDER_MODELS[alias]?.find(m => m.id === modelId);
|
||||||
|
return entry?.strip || [];
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { COLORS } from "../utils/stream.js";
|
|||||||
import { createStreamController } from "../utils/streamHandler.js";
|
import { createStreamController } from "../utils/streamHandler.js";
|
||||||
import { refreshWithRetry } from "../services/tokenRefresh.js";
|
import { refreshWithRetry } from "../services/tokenRefresh.js";
|
||||||
import { createRequestLogger } from "../utils/requestLogger.js";
|
import { createRequestLogger } from "../utils/requestLogger.js";
|
||||||
import { getModelTargetFormat, getModelCaps, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
|
import { getModelTargetFormat, getModelStrip, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
|
||||||
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
|
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
|
||||||
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
||||||
import { handleBypassRequest } from "../utils/bypassHandler.js";
|
import { handleBypassRequest } from "../utils/bypassHandler.js";
|
||||||
@@ -37,7 +37,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||||
const modelTargetFormat = getModelTargetFormat(alias, model);
|
const modelTargetFormat = getModelTargetFormat(alias, model);
|
||||||
const targetFormat = modelTargetFormat || getTargetFormat(provider);
|
const targetFormat = modelTargetFormat || getTargetFormat(provider);
|
||||||
const modelCaps = getModelCaps(alias, model);
|
const stripList = getModelStrip(alias, model);
|
||||||
|
|
||||||
const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI;
|
const clientRequestedStreaming = body.stream === true || sourceFormat === FORMATS.ANTIGRAVITY || sourceFormat === FORMATS.GEMINI || sourceFormat === FORMATS.GEMINI_CLI;
|
||||||
const providerRequiresStreaming = provider === "openai" || provider === "codex";
|
const providerRequiresStreaming = provider === "openai" || provider === "codex";
|
||||||
@@ -68,7 +68,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
|
|||||||
log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`);
|
log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`);
|
||||||
translatedBody = { ...body, model };
|
translatedBody = { ...body, model };
|
||||||
} else {
|
} else {
|
||||||
translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, modelCaps, connectionId);
|
translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, stripList, connectionId);
|
||||||
if (!translatedBody) {
|
if (!translatedBody) {
|
||||||
trackPendingRequest(model, provider, connectionId, false, true);
|
trackPendingRequest(model, provider, connectionId, false, true);
|
||||||
return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`);
|
return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`);
|
||||||
|
|||||||
@@ -52,37 +52,30 @@ function ensureInitialized() {
|
|||||||
require("./response/ollama-to-openai.js");
|
require("./response/ollama-to-openai.js");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip multimodal content blocks (image/audio/video) from messages if model doesn't support them
|
// Strip specific content types from messages (explicit opt-in via strip[] in PROVIDER_MODELS)
|
||||||
function stripUnsupportedMultimodal(body, multimodal = {}) {
|
function stripContentTypes(body, stripList = []) {
|
||||||
if (!body.messages || !Array.isArray(body.messages)) return;
|
if (!stripList.length || !body.messages || !Array.isArray(body.messages)) return;
|
||||||
|
const imageTypes = new Set(["image_url", "image"]);
|
||||||
|
const audioTypes = new Set(["audio_url", "input_audio"]);
|
||||||
|
const shouldStrip = (type) => {
|
||||||
|
if (imageTypes.has(type)) return stripList.includes("image");
|
||||||
|
if (audioTypes.has(type)) return stripList.includes("audio");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
for (const msg of body.messages) {
|
for (const msg of body.messages) {
|
||||||
if (!Array.isArray(msg.content)) continue;
|
if (!Array.isArray(msg.content)) continue;
|
||||||
msg.content = msg.content.filter(part => {
|
msg.content = msg.content.filter(part => !shouldStrip(part.type));
|
||||||
if (part.type === "image_url" || part.type === "image") return multimodal.image === true;
|
|
||||||
if (part.type === "audio_url" || part.type === "input_audio") return multimodal.audio === true;
|
|
||||||
return true; // keep text, tool_use, tool_result, etc.
|
|
||||||
});
|
|
||||||
// If content array becomes empty after filtering, replace with empty string to avoid API errors
|
|
||||||
if (msg.content.length === 0) msg.content = "";
|
if (msg.content.length === 0) msg.content = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate request: source -> openai -> target
|
// Translate request: source -> openai -> target
|
||||||
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, caps = null, connectionId = null) {
|
export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null, reqLogger = null, stripList = [], connectionId = null) {
|
||||||
ensureInitialized();
|
ensureInitialized();
|
||||||
let result = body;
|
let result = body;
|
||||||
|
|
||||||
// Apply model capability guards before translation
|
// Strip explicit content types (opt-in via strip[] in PROVIDER_MODELS entry)
|
||||||
if (caps) {
|
stripContentTypes(result, stripList);
|
||||||
// Strip multimodal content if model doesn't support it
|
|
||||||
stripUnsupportedMultimodal(result, caps.multimodal || {});
|
|
||||||
|
|
||||||
// Strip thinking config if model doesn't support thinking
|
|
||||||
if (!caps.thinking) {
|
|
||||||
delete result.thinking;
|
|
||||||
delete result.reasoning_effort;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize thinking config: remove if lastMessage is not user
|
// Normalize thinking config: remove if lastMessage is not user
|
||||||
normalizeThinkingConfig(result);
|
normalizeThinkingConfig(result);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "9router-app",
|
"name": "9router-app",
|
||||||
"version": "0.3.79",
|
"version": "0.3.80",
|
||||||
"description": "9Router web dashboard",
|
"description": "9Router web dashboard",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
import { FREE_PROVIDERS } from "@/shared/constants/providers";
|
||||||
import Badge from "./Badge";
|
import Badge from "./Badge";
|
||||||
import Card from "./Card";
|
import Card from "./Card";
|
||||||
import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards";
|
import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards";
|
||||||
@@ -196,18 +197,21 @@ export default function UsageStats() {
|
|||||||
const [period, setPeriod] = useState("7d");
|
const [period, setPeriod] = useState("7d");
|
||||||
|
|
||||||
// Fetch connected providers once, deduplicate by provider type
|
// Fetch connected providers once, deduplicate by provider type
|
||||||
|
// Always include noAuth free providers (e.g. opencode) regardless of connections
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/providers")
|
fetch("/api/providers")
|
||||||
.then((r) => r.ok ? r.json() : null)
|
.then((r) => r.ok ? r.json() : null)
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
if (!d?.connections) return;
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const unique = d.connections.filter((c) => {
|
const unique = (d?.connections || []).filter((c) => {
|
||||||
if (seen.has(c.provider)) return false;
|
if (seen.has(c.provider)) return false;
|
||||||
seen.add(c.provider);
|
seen.add(c.provider);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
setProviders(unique);
|
const noAuthProviders = Object.values(FREE_PROVIDERS)
|
||||||
|
.filter((p) => p.noAuth && !seen.has(p.id))
|
||||||
|
.map((p) => ({ provider: p.id, name: p.name }));
|
||||||
|
setProviders([...unique, ...noAuthProviders]);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
// Import directly from file to avoid pulling in server-side dependencies via index.js
|
// Import directly from file to avoid pulling in server-side dependencies via index.js
|
||||||
export {
|
export {
|
||||||
PROVIDER_MODELS,
|
PROVIDER_MODELS,
|
||||||
MODEL_CAPS,
|
|
||||||
getProviderModels,
|
getProviderModels,
|
||||||
getDefaultModel,
|
getDefaultModel,
|
||||||
isValidModel as isValidModelCore,
|
isValidModel as isValidModelCore,
|
||||||
findModelName,
|
findModelName,
|
||||||
getModelTargetFormat,
|
getModelTargetFormat,
|
||||||
getModelCaps,
|
getModelStrip,
|
||||||
PROVIDER_ID_TO_ALIAS,
|
PROVIDER_ID_TO_ALIAS,
|
||||||
getModelsByProviderId
|
getModelsByProviderId
|
||||||
} from "open-sse/config/providerModels.js";
|
} from "open-sse/config/providerModels.js";
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const APIKEY_PROVIDERS = {
|
|||||||
"alicode-intl": { id: "alicode-intl", alias: "alicode-intl", name: "Alibaba Intl", icon: "cloud", color: "#FF6A00", textIcon: "ALi" },
|
"alicode-intl": { id: "alicode-intl", alias: "alicode-intl", name: "Alibaba Intl", icon: "cloud", color: "#FF6A00", textIcon: "ALi" },
|
||||||
openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com", serviceKinds: ["llm", "embedding", "tts"] },
|
openai: { id: "openai", alias: "openai", name: "OpenAI", icon: "auto_awesome", color: "#10A37F", textIcon: "OA", website: "https://platform.openai.com", serviceKinds: ["llm", "embedding", "tts"] },
|
||||||
anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN", website: "https://console.anthropic.com", serviceKinds: ["llm"] },
|
anthropic: { id: "anthropic", alias: "anthropic", name: "Anthropic", icon: "smart_toy", color: "#D97757", textIcon: "AN", website: "https://console.anthropic.com", serviceKinds: ["llm"] },
|
||||||
gemini: { id: "gemini", alias: "gemini", name: "Gemini", icon: "diamond", color: "#4285F4", textIcon: "GE", website: "https://ai.google.dev", serviceKinds: ["llm", "embedding"] },
|
|
||||||
deepseek: { id: "deepseek", alias: "ds", name: "DeepSeek", icon: "bolt", color: "#4D6BFE", textIcon: "DS", website: "https://deepseek.com" },
|
deepseek: { id: "deepseek", alias: "ds", name: "DeepSeek", icon: "bolt", color: "#4D6BFE", textIcon: "DS", website: "https://deepseek.com" },
|
||||||
groq: { id: "groq", alias: "groq", name: "Groq", icon: "speed", color: "#F55036", textIcon: "GQ", website: "https://groq.com" },
|
groq: { id: "groq", alias: "groq", name: "Groq", icon: "speed", color: "#F55036", textIcon: "GQ", website: "https://groq.com" },
|
||||||
xai: { id: "xai", alias: "xai", name: "xAI (Grok)", icon: "auto_awesome", color: "#1DA1F2", textIcon: "XA", website: "https://x.ai" },
|
xai: { id: "xai", alias: "xai", name: "xAI (Grok)", icon: "auto_awesome", color: "#1DA1F2", textIcon: "XA", website: "https://x.ai" },
|
||||||
|
|||||||
Reference in New Issue
Block a user