Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/ui_example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function App() {
<MenuItem value={'coder'}>代码补全模型</MenuItem>
<MenuItem value={'embedding'}>向量模型</MenuItem>
<MenuItem value={'audio'}>音频模型</MenuItem>
<MenuItem value={'reranker'}>重排序模型</MenuItem>
<MenuItem value={'rerank'}>重排序模型</MenuItem>
<MenuItem value={'analysis'}>分析模型</MenuItem>
<MenuItem value={'analysis-vl'}>图像分析模型</MenuItem>
</Select>
Expand Down
12 changes: 9 additions & 3 deletions ui/ModelModal/src/components/ModelTagFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
isFunctionCallingModel,
isCodeModel,
isEmbeddingModel,
isRerankModel
isRerankModel,
isAnalysisModel
} from '../utils/model';

interface ModelTagFilterProps {
Expand Down Expand Up @@ -42,7 +43,8 @@ const ModelTagFilter: React.FC<ModelTagFilterProps> = ({
{ key: 'function_calling', label: '工具调用', color: 'success' as const },
{ key: 'vision', label: '视觉', color: 'secondary' as const },
{ key: 'embedding', label: '向量', color: 'error' as const },
{ key: 'rerank', label: '重排', color: 'default' as const }
{ key: 'rerank', label: '重排', color: 'default' as const },
{ key: 'analysis', label: '分析', color: 'info' as const }
];

// 根据模型计算每个标签的可用性
Expand Down Expand Up @@ -71,6 +73,8 @@ const ModelTagFilter: React.FC<ModelTagFilterProps> = ({
return isEmbeddingModel(model.model, model.provider);
case 'rerank':
return isRerankModel(model.model);
case 'analysis':
return isAnalysisModel(model.model, model.provider);
default:
return false;
}
Expand Down Expand Up @@ -105,6 +109,8 @@ const ModelTagFilter: React.FC<ModelTagFilterProps> = ({
return isEmbeddingModel(model.model, model.provider);
case 'rerank':
return isRerankModel(model.model);
case 'analysis':
return isAnalysisModel(model.model, model.provider);
default:
return false;
}
Expand Down Expand Up @@ -185,4 +191,4 @@ const ModelTagFilter: React.FC<ModelTagFilterProps> = ({
);
};

export default ModelTagFilter;
export default ModelTagFilter;
9 changes: 9 additions & 0 deletions ui/ModelModal/src/components/ModelTagsWithLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isRerankModel,
isVisionModel,
isWebSearchModel,
isAnalysisModel,
} from '../utils/model';
import { FC, memo, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { styled } from '@mui/material';
Expand All @@ -18,6 +19,7 @@ import {
ToolsCallingTag,
VisionTag,
WebSearchTag,
AnalysisTag,
} from './Tags/ModelCapabilities';

interface ModelTagsProps {
Expand Down Expand Up @@ -111,6 +113,13 @@ const ModelTagsWithLabel: FC<ModelTagsProps> = ({
showLabel={shouldShowLabel}
/>
)}
{isAnalysisModel(model_id, provider) && (
<AnalysisTag
size={size}
showTooltip={showTooltip}
showLabel={shouldShowLabel}
/>
)}
{isEmbeddingModel(model_id, provider) && <EmbeddingTag size={size} />}
{isRerankModel(model_id) && <RerankerTag size={size} />}
</Container>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useTranslation } from 'react-i18next'

import CustomTag, { CustomTagProps } from '../CustomTag'

type Props = {
size?: number
showTooltip?: boolean
showLabel?: boolean
} & Omit<CustomTagProps, 'size' | 'tooltip' | 'icon' | 'color' | 'children'>

export const AnalysisTag = ({ size, showTooltip, showLabel, ...restProps }: Props) => {
const { t } = useTranslation()
return (
<CustomTag
size={size}
color="#2db7f5"
icon="分析"
tooltip={showTooltip ? '分析' : undefined}
{...restProps}>
{showLabel ? '分析' : ''}
</CustomTag>
)
}
3 changes: 2 additions & 1 deletion ui/ModelModal/src/components/Tags/ModelCapabilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import { RerankerTag } from './RerankerTag'
import { ToolsCallingTag } from './ToolsCallingTag'
import { VisionTag } from './VisionTag'
import { WebSearchTag } from './WebSearchTag'
import { AnalysisTag } from './AnalysisTag'

export { CodeTag, EmbeddingTag, ReasoningTag, RerankerTag, ToolsCallingTag, VisionTag, WebSearchTag }
export { CodeTag, EmbeddingTag, ReasoningTag, RerankerTag, ToolsCallingTag, VisionTag, WebSearchTag, AnalysisTag }
29 changes: 28 additions & 1 deletion ui/ModelModal/src/utils/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,33 @@ export function isWebSearchModel(model_id: string, provider: string): boolean {
return false
}

export function isAnalysisModel(model_id: string, provider: string): boolean {
if (!model_id || isEmbeddingModel(model_id, provider) || isRerankModel(model_id)) {
return false
}

const modelId = getLowerBaseModelName(model_id, '/')

const bMatch = modelId.match(/(\d+(?:\.\d+)?)\s*(?:b|bn)\b/i)
if (bMatch) {
const n = parseFloat(bMatch[1])
if (!isNaN(n) && n <= 7) {
return true
}
return false
}

const mMatch = modelId.match(/(\d+(?:\.\d+)?)\s*m\b/i)
if (mMatch) {
const n = parseFloat(mMatch[1])
if (!isNaN(n) && n > 0 && n <= 7000) {
return true
}
}

return false
}

export function getModelLogo(modelId: string) {
if (!modelId) {
return undefined
Expand Down Expand Up @@ -456,4 +483,4 @@ export const getModelGroup = (id: string, provider?: string): string => {
}

return str
}
}
181 changes: 181 additions & 0 deletions usecase/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"

Expand Down Expand Up @@ -314,3 +315,183 @@ func (m *ModelKit) geminiImageCheck(ctx context.Context, req *domain.CheckModelR
}
return result.Text(), nil
}

func filterModelsByType(models []domain.ModelListItem, req *domain.ModelListReq) []domain.ModelListItem {
raw := strings.ToLower(req.Type)
p := strings.ToLower(req.Provider)
switch raw {
// 分析模型 排除 嵌入模型和重排模型
case "analysis":
filtered := make([]domain.ModelListItem, 0, len(models))
for _, it := range models {
if !isEmbeddingModel(it.Model, p) && !isRerankModel(it.Model) {
filtered = append(filtered, it)
}
}
return filtered
// 分析模型-视觉模型 仅包含 视觉模型
case "analysis-vl":
filtered := make([]domain.ModelListItem, 0, len(models))
for _, it := range models {
if isVisionModel(it.Model, p) {
filtered = append(filtered, it)
}
}
return filtered
// 聊天模型 排除 嵌入模型和重排模型
case "chat", "llm":
filtered := make([]domain.ModelListItem, 0, len(models))
for _, it := range models {
if !isEmbeddingModel(it.Model, p) && !isRerankModel(it.Model) {
filtered = append(filtered, it)
}
}
return filtered
// 嵌入模型 仅包含 嵌入模型
case "embedding":
filtered := make([]domain.ModelListItem, 0, len(models))
for _, it := range models {
if isEmbeddingModel(it.Model, p) {
filtered = append(filtered, it)
}
}
return filtered
// 重排模型 仅包含 重排模型
case "reranker", "rerank":
filtered := make([]domain.ModelListItem, 0, len(models))
for _, it := range models {
if isRerankModel(it.Model) {
filtered = append(filtered, it)
}
}
return filtered
case "code", "coder":
filtered := make([]domain.ModelListItem, 0, len(models))
for _, it := range models {
if isCodeModel(it.Model, p) {
filtered = append(filtered, it)
}
}
return filtered
default:
return models
}
}

func getLowerBaseModelName(id string) string {
parts := strings.Split(id, "/")
return strings.ToLower(parts[len(parts)-1])
}

func isRerankModel(modelID string) bool {
if modelID == "" {
return false
}
mid := getLowerBaseModelName(modelID)
re := regexp.MustCompile(`(?i)(?:rerank|re-rank|re-ranker|re-ranking|retrieval|retriever)`)
return re.MatchString(mid)
}

func isEmbeddingModel(modelID, provider string) bool {
if modelID == "" {
return false
}
if isRerankModel(modelID) {
return false
}
mid := getLowerBaseModelName(modelID)
if provider == "anthropic" {
return false
}
if provider == "doubao" || strings.Contains(mid, "doubao") {
re := regexp.MustCompile(`(?i)(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings|voyage-)`)
return re.MatchString(mid)
}
re := regexp.MustCompile(`(?i)(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings|voyage-)`)
return re.MatchString(mid)
}

func isCodeModel(modelID, provider string) bool {
if modelID == "" {
return false
}
if isEmbeddingModel(modelID, provider) || isRerankModel(modelID) {
return false
}
mid := getLowerBaseModelName(modelID)
re := regexp.MustCompile(`(?i)(?:^o3$|.*(code|claude\s+sonnet|claude\s+opus|gpt-4\.1|gpt-4o|gpt-5|gemini[\s-]+2\.5|o4-mini|kimi-k2).*)`)
return re.MatchString(mid)
}

var visionModels = []string{
`chatgpt-4o(?:-[\w-]+)?`,
`claude-3`,
`claude-opus-4`,
`claude-sonnet-4`,
`deepseek-vl(?:[\w-]+)?`,
`doubao-seed-1[.-]6(?:-[\w-]+)?`,
`gemini-1\.5`,
`gemini-2\.0`,
`gemini-2\.5`,
`gemini-exp`,
`gemma-3(?:-[\w-]+)`,
`gemma3(?:[-:\w]+)?`,
`glm-4(?:\.\d+)?v(?:-[\w-]+)?`,
`gpt-4(?:-[\w-]+)`,
`gpt-4.1(?:-[\w-]+)?`,
`gpt-4.5(?:-[\w-]+)`,
`gpt-4o(?:-[\w-]+)?`,
`gpt-5(?:-[\w-]+)?`,
`grok-4(?:-[\w-]+)?`,
`grok-vision-beta`,
`internvl2`,
`kimi-latest`,
`kimi-thinking-preview`,
`kimi-vl-a3b-thinking(?:-[\w-]+)?`,
`llama-4(?:-[\w-]+)?`,
`llama-guard-4(?:-[\w-]+)?`,
`llava`,
`minicpm`,
`moondream`,
`o1(?:-[\w-]+)?`,
`o3(?:-[\w-]+)?`,
`o4(?:-[\w-]+)?`,
`pixtral`,
`qvq`,
`qwen-vl`,
`qwen2-vl`,
`qwen2.5-omni`,
`qwen2.5-vl`,
`step-1o(?:.*vision)?`,
`step-1v(?:-[\w-]+)?`,
`vision`,
}

var notVisionModels = []string{
`AIDC-AI/Marco-o1`,
`gpt-4-\d+-preview`,
`gpt-4-turbo-preview`,
`gpt-4-32k`,
`gpt-4-\d+`,
`o1-mini`,
`o3-mini`,
`o1-preview`,
}

func isVisionModel(modelID, provider string) bool {
if modelID == "" {
return false
}
if isEmbeddingModel(modelID, provider) || isRerankModel(modelID) {
return false
}
mid := getLowerBaseModelName(modelID)
not := strings.Join(notVisionModels, "|")
yes := strings.Join(visionModels, "|")
yesRe := regexp.MustCompile(`(?i)\b(?:` + yes + `)\b`)
if !yesRe.MatchString(mid) {
return false
}
notRe := regexp.MustCompile(`(?i)\b(?:` + not + `)\b`)
return !notRe.MatchString(mid)
}
Loading
Loading