File size: 5,642 Bytes
fa1140b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""anuma.ai ModelRegistry:模型目录(探测自 GET portal.anuma.ai/api/v1/curated-models)。

字段:id(catalog id,即上游 curated-models 的 id 如 ``kimi/kimi-k3``)/
name / owner(provider)/ upstream_id(Fireworks 格式 ``accounts/fireworks/models/<短名>``)。

⚠️ 列表为 2026-08-01 探测快照;上游增删模型时用 scripts/probe_catalog.py 重新生成。
"""
from __future__ import annotations

import re
from typing import Any

from app.upstream.base import ModelRegistry

__all__ = [
    "DEFAULT_MODEL", "MODEL_CATALOG", "IMAGE_MODEL_CATALOG",
    "normalize_image_model", "DefaultModelRegistry",
]

DEFAULT_MODEL = "openai/gpt-5.6-luna"  # 上游未识别 model 时的回退模型(实测所见)

# 生图模型(2026-08-08 从 chat.anuma.ai HAR 抓取):/v1/images 路由用。
# key = AnumaMediaMCP-anuma_create_image 工具 model 参数值;name = 对外展示名。
# 前缀 * 为 GPT-5.6 Luna 生图实测可用模型;tier alias(auto/anuma-*)由上游按订阅分层。
IMAGE_MODEL_CATALOG: list[dict[str, Any]] = [
    {"id": "gpt-image-2", "name": "GPT Image 2"},
    {"id": "flux-2-pro", "name": "Flux 2 Pro"},
    {"id": "flux-2-klein-4b", "name": "Flux 2 Klein 4B"},
    {"id": "grok-imagine", "name": "Grok Imagine"},
    {"id": "nano-banana", "name": "Nano Banana"},
    {"id": "nano-banana-pro", "name": "Nano Banana Pro"},
    {"id": "nano-banana-2", "name": "Nano Banana 2"},
    {"id": "anuma-flash", "name": "Anuma Flash (tier alias)"},
    {"id": "anuma-pro", "name": "Anuma Pro (tier alias)"},
    {"id": "anuma-flash-private", "name": "Anuma Flash Private (tier alias)"},
    {"id": "anuma-pro-private", "name": "Anuma Pro Private (tier alias)"},
]

_IMAGE_BY_KEY: dict[str, dict[str, Any]] = {}
for _im in IMAGE_MODEL_CATALOG:
    _IMAGE_BY_KEY[_im["id"]] = _im
    _IMAGE_BY_KEY[_im["name"].lower()] = _im


def normalize_image_model(model: str | None) -> str | None:
    """客户端传入的 gen 图片模型 → 上游 tool 参数值;空/未知 → None(由客户端容错)。"""
    if not model:
        return None
    if model in _IMAGE_BY_KEY:
        return _IMAGE_BY_KEY[model]["id"]
    low = model.lower()
    if low in _IMAGE_BY_KEY:
        return _IMAGE_BY_KEY[low]["id"]
    norm = re.sub(r"[^a-z0-9]", "", low)
    for m in IMAGE_MODEL_CATALOG:
        if re.sub(r"[^a-z0-9]", "", m["id"]) == norm:
            return m["id"]
    return None

# 探测自 /api/v1/curated-models(active=true 的 text/vision 模型为主)
MODEL_CATALOG: list[dict[str, Any]] = [
    {"id": "inclusionai/ling-2.6-flash", "name": "Ling 2.6 Flash", "owner": "InclusionAI", "upstream_id": "accounts/fireworks/models/ling-2.6-flash"},
    {"id": "kimi/kimi-k3", "name": "Kimi K3", "owner": "Kimi", "upstream_id": "accounts/fireworks/models/kimi-k3"},
    {"id": "kimi/kimi-k2.7-code", "name": "Kimi K2.7 Code", "owner": "Kimi", "upstream_id": "accounts/fireworks/models/kimi-k2.7-code"},
    {"id": "kimi/kimi-k2.6", "name": "Kimi 2.6", "owner": "Kimi", "upstream_id": "accounts/fireworks/models/kimi-k2.6"},
    {"id": "openai/gpt-5.6-sol", "name": "GPT 5.6 Sol", "owner": "OpenAI", "upstream_id": "accounts/fireworks/models/gpt-5.6-sol"},
    {"id": "openai/gpt-5.6-terra", "name": "GPT 5.6 Terra", "owner": "OpenAI", "upstream_id": "accounts/fireworks/models/gpt-5.6-terra"},
    {"id": "openai/gpt-5.6-luna", "name": "GPT 5.6 Luna", "owner": "OpenAI", "upstream_id": "accounts/fireworks/models/gpt-5.6-luna"},
    {"id": "anthropic/claude-sonnet-5", "name": "Claude Sonnet 5", "owner": "Anthropic", "upstream_id": "accounts/fireworks/models/claude-sonnet-5"},
    {"id": "anthropic/claude-opus-5", "name": "Claude Opus 5", "owner": "Anthropic", "upstream_id": "accounts/fireworks/models/claude-opus-5"},
    {"id": "openai/gpt-5.5", "name": "GPT 5.5", "owner": "OpenAI", "upstream_id": "accounts/fireworks/models/gpt-5.5"},
    {"id": "qwen/qwen-3.7-plus", "name": "Qwen 3.7 Plus", "owner": "Qwen", "upstream_id": "accounts/fireworks/models/qwen-3.7-plus"},
    {"id": "qwen/qwen-3.6-plus", "name": "Qwen 3.6 Plus", "owner": "Qwen", "upstream_id": "accounts/fireworks/models/qwen-3.6-plus"},
    {"id": "glm/glm-5.2", "name": "GLM 5.2", "owner": "Zhipu", "upstream_id": "accounts/fireworks/models/glm-5.2"},
    {"id": "minimax/minimax-m3", "name": "MiniMax M3", "owner": "MiniMax", "upstream_id": "accounts/fireworks/models/minimax-m3"},
    {"id": "minimax/minimax-m2.7", "name": "Minimax 2.7", "owner": "MiniMax", "upstream_id": "accounts/fireworks/models/minimax-m2.7"},
]

_BY_KEY: dict[str, dict[str, Any]] = {}
for _m in MODEL_CATALOG:
    _BY_KEY[_m["id"]] = _m
    _BY_KEY[_m["name"].lower()] = _m


class DefaultModelRegistry(ModelRegistry):
    def catalog(self) -> list[dict[str, Any]]:
        return list(MODEL_CATALOG)

    def normalize(self, model: str | None) -> str:
        """客户端传入的 model → catalog id;空或未知 → 默认。匹配:精确 id > 显示名(小写) > 模糊。"""
        if not model:
            return DEFAULT_MODEL
        if model in _BY_KEY:
            return _BY_KEY[model]["id"]
        low = model.lower()
        if low in _BY_KEY:
            return _BY_KEY[low]["id"]
        norm = re.sub(r"[^a-z0-9]", "", low)
        for m in MODEL_CATALOG:
            if (re.sub(r"[^a-z0-9]", "", m["id"]) == norm
                    or re.sub(r"[^a-z0-9]", "", m["name"].lower()) == norm):
                return m["id"]
        return DEFAULT_MODEL

    def upstream_id_for(self, model_id: str) -> str | None:
        m = _BY_KEY.get(model_id)
        return m["upstream_id"] if m else None