feat:添加模型数据来源
This commit is contained in:
1 parent
1d4d9836da
commit
5f72629835
2 files changed
+466
-29
No files matched your search
+153
-28
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
@@ -447,7 +448,7 @@ func (s *sAdmin) FetchOfficialPricing(ctx context.Context, req *v1.PricingFetchO
|
||||
return nil, common.NewBusinessError(404, "模型不存在")
|
||||
}
|
||||
|
||||
sources := make([]*v1.OfficialPricingSource, 0, 2)
|
||||
sources := make([]*v1.OfficialPricingSource, 0, 4)
|
||||
|
||||
// Source 1: LiteLLM
|
||||
litellmSource := fetchLiteLLMSource(ctx, model.ModelId)
|
||||
@@ -457,6 +458,14 @@ func (s *sAdmin) FetchOfficialPricing(ctx context.Context, req *v1.PricingFetchO
|
||||
modelsDevSource := fetchModelsDevSource(ctx, model.ModelId)
|
||||
sources = append(sources, modelsDevSource)
|
||||
|
||||
// Source 3: BaseLLM
|
||||
baseLLMSource := fetchBaseLLMSource(ctx, model.ModelId)
|
||||
sources = append(sources, baseLLMSource)
|
||||
|
||||
// Source 4: OpenRouter
|
||||
openRouterSource := fetchOpenRouterSource(ctx, model.ModelId)
|
||||
sources = append(sources, openRouterSource)
|
||||
|
||||
return &v1.PricingFetchOfficialRes{
|
||||
ModelName: model.ModelId,
|
||||
Sources: sources,
|
||||
@@ -543,46 +552,162 @@ func fetchModelsDevSource(ctx context.Context, modelName string) *v1.OfficialPri
|
||||
return source
|
||||
}
|
||||
|
||||
func fetchBaseLLMSource(ctx context.Context, modelName string) *v1.OfficialPricingSource {
|
||||
source := &v1.OfficialPricingSource{Source: "basellm"}
|
||||
|
||||
data, err := common.FetchBaseLLMPricing(ctx)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[FetchOfficialPricing] basellm fetch failed for %s: %v", modelName, err)
|
||||
source.Error = err.Error()
|
||||
return source
|
||||
}
|
||||
|
||||
matchedName, entry := common.FindBaseLLMModel(data, modelName)
|
||||
if entry == nil {
|
||||
return source
|
||||
}
|
||||
|
||||
cacheReadPrice := 0.0
|
||||
if entry.PricePerMCacheRead != nil {
|
||||
cacheReadPrice = *entry.PricePerMCacheRead
|
||||
}
|
||||
cacheWritePrice := 0.0
|
||||
if entry.PricePerMCacheWrite != nil {
|
||||
cacheWritePrice = *entry.PricePerMCacheWrite
|
||||
}
|
||||
|
||||
source.Found = true
|
||||
source.Provider = entry.VendorName
|
||||
source.MaxContext = common.ParseBaseLLMContext(entry.Tags)
|
||||
source.Pricing = &v1.OfficialPricingItem{
|
||||
InputPrice: roundTo2(entry.PricePerMInput),
|
||||
OutputPrice: roundTo2(entry.PricePerMOutput),
|
||||
CacheReadPrice: roundTo2(cacheReadPrice),
|
||||
CacheCreationPrice: roundTo2(cacheWritePrice),
|
||||
BillingMode: "token",
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "[FetchOfficialPricing] basellm matched %s -> %s", modelName, matchedName)
|
||||
return source
|
||||
}
|
||||
|
||||
func fetchOpenRouterSource(ctx context.Context, modelName string) *v1.OfficialPricingSource {
|
||||
source := &v1.OfficialPricingSource{Source: "openrouter"}
|
||||
|
||||
data, err := common.FetchOpenRouterModels(ctx)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "[FetchOfficialPricing] openrouter fetch failed for %s: %v", modelName, err)
|
||||
source.Error = err.Error()
|
||||
return source
|
||||
}
|
||||
|
||||
matchedName, entry := common.FindOpenRouterModel(data, modelName)
|
||||
if entry == nil || entry.Pricing == nil {
|
||||
return source
|
||||
}
|
||||
|
||||
inputPrice := common.OpenRouterPricePerM(entry.Pricing.Prompt)
|
||||
outputPrice := common.OpenRouterPricePerM(entry.Pricing.Completion)
|
||||
cacheReadPrice := common.OpenRouterPricePerM(entry.Pricing.InputCacheRead)
|
||||
cacheWritePrice := common.OpenRouterPricePerM(entry.Pricing.InputCacheWrite)
|
||||
|
||||
source.Found = true
|
||||
if idx := strings.Index(entry.ID, "/"); idx >= 0 {
|
||||
source.Provider = entry.ID[:idx]
|
||||
}
|
||||
source.MaxContext = entry.ContextLength
|
||||
if entry.TopProvider != nil && entry.TopProvider.MaxCompletionTokens > 0 {
|
||||
source.MaxOutput = entry.TopProvider.MaxCompletionTokens
|
||||
}
|
||||
source.Pricing = &v1.OfficialPricingItem{
|
||||
InputPrice: roundTo2(inputPrice),
|
||||
OutputPrice: roundTo2(outputPrice),
|
||||
CacheReadPrice: roundTo2(cacheReadPrice),
|
||||
CacheCreationPrice: roundTo2(cacheWritePrice),
|
||||
BillingMode: "token",
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "[FetchOfficialPricing] openrouter matched %s -> %s", modelName, matchedName)
|
||||
return source
|
||||
}
|
||||
|
||||
func roundTo2(v float64) float64 {
|
||||
return math.Round(v*100) / 100
|
||||
}
|
||||
|
||||
// FetchOfficialModelInfo 按模型名称拉取官方模型信息(上下文长度+能力特性)
|
||||
// 按优先级尝试 LiteLLM → OpenRouter → BaseLLM,返回第一个匹配的结果
|
||||
func (s *sAdmin) FetchOfficialModelInfo(ctx context.Context, req *v1.ModelFetchOfficialInfoReq) (*v1.ModelFetchOfficialInfoRes, error) {
|
||||
data, err := common.FetchLiteLLMPricing(ctx)
|
||||
if err != nil {
|
||||
var lastError string
|
||||
|
||||
// Source 1: LiteLLM
|
||||
if data, err := common.FetchLiteLLMPricing(ctx); err != nil {
|
||||
g.Log().Warningf(ctx, "[FetchOfficialModelInfo] litellm fetch failed for %s: %v", req.ModelName, err)
|
||||
return &v1.ModelFetchOfficialInfoRes{Found: false, Error: err.Error()}, nil
|
||||
lastError = err.Error()
|
||||
} else if _, entry := common.FindLiteLLMModel(data, req.ModelName); entry != nil {
|
||||
return &v1.ModelFetchOfficialInfoRes{
|
||||
Found: true,
|
||||
Provider: entry.LitellmProvider,
|
||||
MaxContextTokens: entry.MaxInputTokens,
|
||||
MaxOutputTokens: entry.MaxOutputTokens,
|
||||
Capabilities: map[string]bool{
|
||||
"vision": entry.SupportsVision,
|
||||
"function_calling": entry.SupportsFunctionCalling,
|
||||
"parallel_function_calling": entry.SupportsParallelFuncCalling,
|
||||
"tool_choice": entry.SupportsToolChoice,
|
||||
"response_schema": entry.SupportsResponseSchema,
|
||||
"system_messages": entry.SupportsSystemMessages,
|
||||
"prompt_caching": entry.SupportsPromptCaching,
|
||||
"audio_input": entry.SupportsAudioInput,
|
||||
"audio_output": entry.SupportsAudioOutput,
|
||||
"pdf_input": entry.SupportsPdfInput,
|
||||
"embedding_image": entry.SupportsEmbeddingImage,
|
||||
"reasoning": entry.SupportsReasoning,
|
||||
"web_search": entry.SupportsWebSearch,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, entry := common.FindLiteLLMModel(data, req.ModelName)
|
||||
if entry == nil {
|
||||
return &v1.ModelFetchOfficialInfoRes{Found: false}, nil
|
||||
// Source 2: OpenRouter
|
||||
if data, err := common.FetchOpenRouterModels(ctx); err != nil {
|
||||
g.Log().Warningf(ctx, "[FetchOfficialModelInfo] openrouter fetch failed for %s: %v", req.ModelName, err)
|
||||
lastError = err.Error()
|
||||
} else if _, entry := common.FindOpenRouterModel(data, req.ModelName); entry != nil {
|
||||
maxOutput := 0
|
||||
if entry.TopProvider != nil {
|
||||
maxOutput = entry.TopProvider.MaxCompletionTokens
|
||||
}
|
||||
provider := ""
|
||||
if idx := strings.Index(entry.ID, "/"); idx >= 0 {
|
||||
provider = entry.ID[:idx]
|
||||
}
|
||||
return &v1.ModelFetchOfficialInfoRes{
|
||||
Found: true,
|
||||
Provider: provider,
|
||||
MaxContextTokens: entry.ContextLength,
|
||||
MaxOutputTokens: maxOutput,
|
||||
Capabilities: common.ParseOpenRouterCapabilities(entry),
|
||||
}, nil
|
||||
}
|
||||
|
||||
capabilities := map[string]bool{
|
||||
"vision": entry.SupportsVision,
|
||||
"function_calling": entry.SupportsFunctionCalling,
|
||||
"parallel_function_calling": entry.SupportsParallelFuncCalling,
|
||||
"tool_choice": entry.SupportsToolChoice,
|
||||
"response_schema": entry.SupportsResponseSchema,
|
||||
"system_messages": entry.SupportsSystemMessages,
|
||||
"prompt_caching": entry.SupportsPromptCaching,
|
||||
"audio_input": entry.SupportsAudioInput,
|
||||
"audio_output": entry.SupportsAudioOutput,
|
||||
"pdf_input": entry.SupportsPdfInput,
|
||||
"embedding_image": entry.SupportsEmbeddingImage,
|
||||
"reasoning": entry.SupportsReasoning,
|
||||
"web_search": entry.SupportsWebSearch,
|
||||
// Source 3: BaseLLM
|
||||
if data, err := common.FetchBaseLLMPricing(ctx); err != nil {
|
||||
g.Log().Warningf(ctx, "[FetchOfficialModelInfo] basellm fetch failed for %s: %v", req.ModelName, err)
|
||||
lastError = err.Error()
|
||||
} else if _, entry := common.FindBaseLLMModel(data, req.ModelName); entry != nil {
|
||||
return &v1.ModelFetchOfficialInfoRes{
|
||||
Found: true,
|
||||
Provider: entry.VendorName,
|
||||
MaxContextTokens: common.ParseBaseLLMContext(entry.Tags),
|
||||
Capabilities: common.ParseBaseLLMCapabilities(entry.Tags),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &v1.ModelFetchOfficialInfoRes{
|
||||
Found: true,
|
||||
Provider: entry.LitellmProvider,
|
||||
MaxContextTokens: entry.MaxInputTokens,
|
||||
MaxOutputTokens: entry.MaxOutputTokens,
|
||||
Capabilities: capabilities,
|
||||
}, nil
|
||||
res := &v1.ModelFetchOfficialInfoRes{Found: false}
|
||||
if lastError != "" {
|
||||
res.Error = lastError
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ExportModels exports model list to CSV or Excel.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -111,7 +112,7 @@ func FindLiteLLMModel(data map[string]*LiteLLMModelEntry, modelName string) (str
|
||||
}
|
||||
|
||||
// Try common provider prefixes that LiteLLM uses
|
||||
providers := []string{"openai/", "anthropic/", "vertex_ai/", "bedrock/", "azure/"}
|
||||
providers := []string{"openai/", "anthropic/", "vertex_ai/", "bedrock/", "azure/", "deepseek/", "volcengine/"}
|
||||
for _, prefix := range providers {
|
||||
if entry, ok := data[prefix+modelName]; ok {
|
||||
return prefix + modelName, entry
|
||||
@@ -289,3 +290,314 @@ func FindModelsDevModel(data map[string]*ModelsDevModelEntry, modelName string)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ---------- BaseLLM 数据源(国内模型覆盖好) ----------
|
||||
|
||||
const (
|
||||
baseLLMURL = "https://basellm.github.io/llm-metadata/api/newapi/models.json"
|
||||
baseLLMCacheKey = "basellm_pricing_data"
|
||||
baseLLMCacheTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
// BaseLLMModelEntry represents a single model entry in the BaseLLM data.
|
||||
type BaseLLMModelEntry struct {
|
||||
ModelName string `json:"model_name"`
|
||||
VendorName string `json:"vendor_name"`
|
||||
Tags string `json:"tags"`
|
||||
Status int `json:"status"`
|
||||
PricePerMInput float64 `json:"price_per_m_input"`
|
||||
PricePerMOutput float64 `json:"price_per_m_output"`
|
||||
PricePerMCacheRead *float64 `json:"price_per_m_cache_read"`
|
||||
PricePerMCacheWrite *float64 `json:"price_per_m_cache_write"`
|
||||
}
|
||||
|
||||
var baseLLMCache = gcache.New()
|
||||
|
||||
// FetchBaseLLMPricing fetches and caches the BaseLLM pricing data.
|
||||
func FetchBaseLLMPricing(ctx context.Context) (map[string]*BaseLLMModelEntry, error) {
|
||||
cached, err := baseLLMCache.Get(ctx, baseLLMCacheKey)
|
||||
if err == nil && cached != nil {
|
||||
if data, ok := cached.Val().(map[string]*BaseLLMModelEntry); ok {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "[PricingSource] fetching BaseLLM pricing data from remote...")
|
||||
|
||||
resp, err := g.Client().SetHeaderMap(map[string]string{
|
||||
"User-Agent": "github.com/qianfree/team-api/1.0",
|
||||
}).Get(ctx, baseLLMURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取远程数据失败: %w", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("获取远程数据失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body := resp.ReadAll()
|
||||
if len(body) == 0 {
|
||||
return nil, fmt.Errorf("获取远程数据失败: 响应为空")
|
||||
}
|
||||
|
||||
var raw struct {
|
||||
Data []*BaseLLMModelEntry `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return nil, fmt.Errorf("获取远程数据失败: JSON 解析失败: %w", err)
|
||||
}
|
||||
|
||||
result := make(map[string]*BaseLLMModelEntry, len(raw.Data))
|
||||
for _, entry := range raw.Data {
|
||||
if entry.ModelName == "" || entry.Status != 1 {
|
||||
continue
|
||||
}
|
||||
name := strings.ToLower(entry.ModelName)
|
||||
if _, exists := result[name]; !exists {
|
||||
result[name] = entry
|
||||
}
|
||||
}
|
||||
|
||||
if err := baseLLMCache.Set(ctx, baseLLMCacheKey, result, baseLLMCacheTTL); err != nil {
|
||||
g.Log().Warningf(ctx, "[PricingSource] failed to cache BaseLLM data: %v", err)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "[PricingSource] loaded %d models from BaseLLM", len(result))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FindBaseLLMModel searches for a model in the BaseLLM pricing data.
|
||||
func FindBaseLLMModel(data map[string]*BaseLLMModelEntry, modelName string) (string, *BaseLLMModelEntry) {
|
||||
name := strings.ToLower(modelName)
|
||||
if entry, ok := data[name]; ok {
|
||||
return name, entry
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ParseBaseLLMContext extracts context window size from BaseLLM tags (e.g. "128K" → 128000, "1M" → 1000000).
|
||||
func ParseBaseLLMContext(tags string) int {
|
||||
for _, tag := range strings.Split(tags, ",") {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if len(tag) < 2 {
|
||||
continue
|
||||
}
|
||||
suffix := tag[len(tag)-1:]
|
||||
numStr := tag[:len(tag)-1]
|
||||
switch suffix {
|
||||
case "K":
|
||||
if num, err := strconv.ParseFloat(numStr, 64); err == nil {
|
||||
return int(num * 1000)
|
||||
}
|
||||
case "M":
|
||||
if num, err := strconv.ParseFloat(numStr, 64); err == nil {
|
||||
return int(num * 1_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParseBaseLLMCapabilities extracts capability map from BaseLLM tags.
|
||||
func ParseBaseLLMCapabilities(tags string) map[string]bool {
|
||||
caps := make(map[string]bool)
|
||||
for _, tag := range strings.Split(tags, ",") {
|
||||
switch strings.TrimSpace(tag) {
|
||||
case "Vision":
|
||||
caps["vision"] = true
|
||||
case "Tools":
|
||||
caps["function_calling"] = true
|
||||
caps["tool_choice"] = true
|
||||
case "Reasoning":
|
||||
caps["reasoning"] = true
|
||||
case "Audio":
|
||||
caps["audio_input"] = true
|
||||
caps["audio_output"] = true
|
||||
case "Files":
|
||||
caps["pdf_input"] = true
|
||||
}
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// ---------- OpenRouter 数据源 ----------
|
||||
|
||||
const (
|
||||
openRouterURL = "https://openrouter.ai/api/v1/models"
|
||||
openRouterCacheKey = "openrouter_models_data"
|
||||
openRouterCacheTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
// OpenRouterModelEntry represents a single model entry in the OpenRouter API.
|
||||
type OpenRouterModelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ContextLength int `json:"context_length"`
|
||||
Architecture *OpenRouterArchitecture `json:"architecture"`
|
||||
Pricing *OpenRouterPricing `json:"pricing"`
|
||||
TopProvider *OpenRouterTopProvider `json:"top_provider"`
|
||||
SupportedParams []string `json:"supported_parameters"`
|
||||
}
|
||||
|
||||
// OpenRouterArchitecture describes model input/output modalities.
|
||||
type OpenRouterArchitecture struct {
|
||||
Modality string `json:"modality"`
|
||||
InputModalities []string `json:"input_modalities"`
|
||||
OutputModalities []string `json:"output_modalities"`
|
||||
}
|
||||
|
||||
// OpenRouterPricing holds per-token pricing (USD/token, string format).
|
||||
type OpenRouterPricing struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Completion string `json:"completion"`
|
||||
InputCacheRead string `json:"input_cache_read"`
|
||||
InputCacheWrite string `json:"input_cache_write"`
|
||||
}
|
||||
|
||||
// OpenRouterTopProvider holds provider-level limits.
|
||||
type OpenRouterTopProvider struct {
|
||||
ContextLength int `json:"context_length"`
|
||||
MaxCompletionTokens int `json:"max_completion_tokens"`
|
||||
}
|
||||
|
||||
var openRouterCache = gcache.New()
|
||||
|
||||
// FetchOpenRouterModels fetches and caches the OpenRouter model catalog.
|
||||
func FetchOpenRouterModels(ctx context.Context) (map[string]*OpenRouterModelEntry, error) {
|
||||
cached, err := openRouterCache.Get(ctx, openRouterCacheKey)
|
||||
if err == nil && cached != nil {
|
||||
if data, ok := cached.Val().(map[string]*OpenRouterModelEntry); ok {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Info(ctx, "[PricingSource] fetching OpenRouter model data from remote...")
|
||||
|
||||
resp, err := g.Client().SetHeaderMap(map[string]string{
|
||||
"User-Agent": "github.com/qianfree/team-api/1.0",
|
||||
}).Get(ctx, openRouterURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取远程数据失败: %w", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("获取远程数据失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body := resp.ReadAll()
|
||||
if len(body) == 0 {
|
||||
return nil, fmt.Errorf("获取远程数据失败: 响应为空")
|
||||
}
|
||||
|
||||
var raw struct {
|
||||
Data []*OpenRouterModelEntry `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return nil, fmt.Errorf("获取远程数据失败: JSON 解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 同时以完整 ID 和裸模型名作为 key,方便匹配
|
||||
result := make(map[string]*OpenRouterModelEntry, len(raw.Data)*2)
|
||||
for _, entry := range raw.Data {
|
||||
if entry.ID == "" {
|
||||
continue
|
||||
}
|
||||
result[entry.ID] = entry
|
||||
if idx := strings.LastIndex(entry.ID, "/"); idx >= 0 {
|
||||
bareName := entry.ID[idx+1:]
|
||||
if _, exists := result[bareName]; !exists {
|
||||
result[bareName] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := openRouterCache.Set(ctx, openRouterCacheKey, result, openRouterCacheTTL); err != nil {
|
||||
g.Log().Warningf(ctx, "[PricingSource] failed to cache OpenRouter data: %v", err)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "[PricingSource] loaded %d models from OpenRouter", len(raw.Data))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FindOpenRouterModel searches for a model in the OpenRouter data.
|
||||
func FindOpenRouterModel(data map[string]*OpenRouterModelEntry, modelName string) (string, *OpenRouterModelEntry) {
|
||||
if entry, ok := data[modelName]; ok {
|
||||
return modelName, entry
|
||||
}
|
||||
|
||||
providers := []string{
|
||||
"openai/", "anthropic/", "google/", "meta-llama/", "mistralai/",
|
||||
"deepseek/", "qwen/", "zhipu/", "moonshot/", "minimax/",
|
||||
"baichuan/", "01-ai/", "stepfun/", "volcengine/",
|
||||
}
|
||||
for _, prefix := range providers {
|
||||
if entry, ok := data[prefix+modelName]; ok {
|
||||
return prefix + modelName, entry
|
||||
}
|
||||
}
|
||||
|
||||
for _, prefix := range providers {
|
||||
if strings.HasPrefix(modelName, prefix) {
|
||||
stripped := strings.TrimPrefix(modelName, prefix)
|
||||
if entry, ok := data[stripped]; ok {
|
||||
return stripped, entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ParseOpenRouterCapabilities extracts capability map from OpenRouter model entry.
|
||||
func ParseOpenRouterCapabilities(entry *OpenRouterModelEntry) map[string]bool {
|
||||
caps := make(map[string]bool)
|
||||
|
||||
paramSet := make(map[string]bool, len(entry.SupportedParams))
|
||||
for _, p := range entry.SupportedParams {
|
||||
paramSet[p] = true
|
||||
}
|
||||
if paramSet["tools"] || paramSet["tool_choice"] {
|
||||
caps["function_calling"] = true
|
||||
caps["tool_choice"] = true
|
||||
}
|
||||
if paramSet["reasoning"] || paramSet["include_reasoning"] {
|
||||
caps["reasoning"] = true
|
||||
}
|
||||
if paramSet["response_format"] || paramSet["structured_outputs"] {
|
||||
caps["response_schema"] = true
|
||||
}
|
||||
|
||||
if entry.Architecture != nil {
|
||||
for _, m := range entry.Architecture.InputModalities {
|
||||
switch m {
|
||||
case "image":
|
||||
caps["vision"] = true
|
||||
case "audio":
|
||||
caps["audio_input"] = true
|
||||
case "file":
|
||||
caps["pdf_input"] = true
|
||||
}
|
||||
}
|
||||
for _, m := range entry.Architecture.OutputModalities {
|
||||
if m == "audio" {
|
||||
caps["audio_output"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
// OpenRouterPricePerM converts an OpenRouter per-token price string to USD/1M tokens.
|
||||
func OpenRouterPricePerM(perToken string) float64 {
|
||||
if perToken == "" {
|
||||
return 0
|
||||
}
|
||||
v, err := strconv.ParseFloat(perToken, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return v * 1_000_000
|
||||
}
|
||||
Reference in new issue
Block a user