Skip to content

Commit 316227a

Browse files
Add MiMo OpenAI-compatible guidance and LiteLLM fallback pricing hook
Agent-Logs-Url: https://github.com/ZhuLinsen/daily_stock_analysis/sessions/d13f1d78-733b-44f8-bde8-9cac1b6d4b6c Co-authored-by: ted-davis227 <254451857+ted-davis227@users.noreply.github.com>
1 parent 0456805 commit 316227a

7 files changed

Lines changed: 98 additions & 8 deletions

File tree

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ GEMINI_API_KEY=
211211
# LLM_MINIMAX_MODELS=MiniMax-M2.7,MiniMax-M2.7-highspeed
212212
# LITELLM_MODEL=openai/MiniMax-M2.7
213213
#
214+
# 小米 MiMo(OpenAI Compatible)
215+
# Base URL 与模型名请以 MiMo 官方文档/控制台为准。
216+
# LLM_CHANNELS=mimo
217+
# LLM_MIMO_PROTOCOL=openai
218+
# LLM_MIMO_BASE_URL=
219+
# LLM_MIMO_API_KEY=sk-xxx
220+
# LLM_MIMO_MODELS=mimo-xxx
221+
# LITELLM_MODEL=openai/mimo-xxx
222+
#
214223
# 火山方舟 / 豆包(OpenAI Compatible,按实际开通地域调整 endpoint)
215224
# 来源:火山方舟在线推理 / Responses API 文档 https://www.volcengine.com/docs/82379/2121998
216225
# LiteLLM 通过 openai/<model> 路由到兼容 Chat Completions 的 Base URL。

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
4040
- [修复] 修正分析报告 API 构建策略点位时数值字段未归一为字符串的问题,避免策略价格触发响应 DTO 类型校验失败。
4141
- [修复] Docker 启动入口自动修复 `data` / `logs` / `reports` 挂载目录权限并降权运行,文档化的 Compose `exec` 手动命令显式使用 `dsa` 用户,避免普通部署需要手动 `chown` / `chmod`
4242
- [修复] Web 首页大盘复盘结果改由主内容滚动区承载,避免 loading 切换到长结果后下方报告区域被截断或无法继续滚动。
43+
- [修复] 为 OpenAI-compatible 未知模型注册兜底计费信息,避免 cost 计算异常影响返回正文,并补充 MiMo 渠道示例配置。
4344

4445
## [3.16.0] - 2026-05-10
4546

docs/llm-providers.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ OpenAI-compatible Base URL 只填到服务商兼容入口,不额外拼接 `/ch
7070
| 通义千问 / DashScope | `dashscope` | `openai` | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `qwen3.6-plus,qwen3.6-flash` |
7171
| 智谱 GLM | `zhipu` | `openai` | `https://open.bigmodel.cn/api/paas/v4` | `glm-5.1,glm-4.7-flash` |
7272
| MiniMax | `minimax` | `openai` | `https://api.minimax.io/v1` | `MiniMax-M2.7,MiniMax-M2.7-highspeed` |
73+
| 小米 MiMo | `mimo` | `openai` | 官方控制台提供 | 官方文档/控制台为准 |
7374
| 火山方舟 / 豆包 | `volcengine` | `openai` | `https://ark.cn-beijing.volces.com/api/v3` | `doubao-seed-1-6-251015,doubao-seed-1-6-thinking-251015` |
7475
| 硅基流动 / SiliconFlow | `siliconflow` | `openai` | `https://api.siliconflow.cn/v1` | `deepseek-ai/DeepSeek-V3.2,Qwen/Qwen3-235B-A22B-Thinking-2507` |
7576
| OpenRouter | `openrouter` | `openai` | `https://openrouter.ai/api/v1` | `~anthropic/claude-sonnet-latest,~openai/gpt-latest` |
@@ -88,6 +89,7 @@ OpenAI-compatible Base URL 只填到服务商兼容入口,不额外拼接 `/ch
8889
| 通义千问 / DashScope | [文本生成](https://help.aliyun.com/zh/model-studio/text-generation-model/) | 百炼推荐 `qwen3.6-plus`,确认效果后可用 `qwen3.6-flash` 降低成本。 |
8990
| 智谱 GLM | [模型概览](https://docs.bigmodel.cn/cn/guide/start/model-overview)[GLM-5.1](https://docs.bigmodel.cn/cn/guide/models/text/glm-5.1) | `glm-5.1` 是当前旗舰;`glm-4.7-flash` 作为轻量/免费模型示例。 |
9091
| MiniMax | [OpenAI API 兼容](https://platform.minimax.io/docs/api-reference/text-chat)[获取模型列表](https://platform.minimax.io/docs/api-reference/models/openai/list-models) | 官方 OpenAI-compatible Base URL 为 `https://api.minimax.io/v1`,并列出 `MiniMax-M2.7``MiniMax-M2.7-highspeed`。中国区 Coding 工具场景可能使用 `.com`/Anthropic 专用入口,以控制台为准。 |
92+
| 小米 MiMo | 官方文档 / 控制台 | 当前按 OpenAI-compatible 方式接入,Base URL、模型名与权限以 MiMo 官方文档/控制台为准。 |
9193
| 火山方舟 / 豆包 | [在线推理(常规)](https://www.volcengine.com/docs/82379/2121998)[模型列表](https://www.volcengine.com/docs/82379/1949118) | 官方示例使用 `https://ark.cn-beijing.volces.com/api/v3``doubao-seed-1-6-251015`;如使用 Coding Plan,请改用其专用 Base URL 和模型名,不要套用本表的在线推理模板。 |
9294
| SiliconFlow | [模型列表](https://docs.siliconflow.cn/quickstart/models)[获取模型列表 API](https://docs.siliconflow.cn/cn/api-reference/models/get-model-list) | 平台模型实时更新且 `/models` 需要 API Key;模板只给常见新模型示例,保存前建议在 Web 设置页点击「获取模型」确认账号可见性。 |
9395
| OpenRouter | [Models API](https://openrouter.ai/docs/api/api-reference/models/get-models) | OpenRouter 支持 `~anthropic/claude-sonnet-latest``~openai/gpt-latest` 等 latest router alias;2026-05-03 的一次手动 live smoke 以 Claude Sonnet latest 作为默认示例通过,GPT latest 保留为可按账号权限切换的备选。 |

src/agent/llm_adapter.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import time
1212
import uuid
1313
from dataclasses import dataclass, field
14-
from typing import Any, Dict, List, Optional
14+
from typing import Any, Dict, Iterable, List, Optional, Tuple
1515

1616
import litellm
1717
from litellm import Router
@@ -101,6 +101,28 @@ class LLMResponse:
101101
},
102102
}
103103

104+
_FALLBACK_MODEL_PRICING: Dict[str, Any] = {
105+
"supports_function_calling": True,
106+
"supports_vision": False,
107+
"supports_audio_input": False,
108+
"supports_audio_output": False,
109+
"context_window": 100000,
110+
"max_tokens": 10000,
111+
"input_cost_per_token": 0.0,
112+
"output_cost_per_token": 0.0,
113+
}
114+
_FALLBACK_MODEL_PRICING_REGISTERED: set[str] = set()
115+
116+
117+
def _split_provider_model(model: str) -> Tuple[str, str]:
118+
normalized = (model or "").strip()
119+
if not normalized:
120+
return "", ""
121+
if "/" in normalized:
122+
provider, remainder = normalized.split("/", 1)
123+
return provider.lower(), remainder.strip()
124+
return "openai", normalized
125+
104126

105127
def _model_matches(model: str, entries: List[str]) -> bool:
106128
"""Check if model name matches any entry (exact or prefix with version suffix)."""
@@ -176,7 +198,6 @@ def _register_custom_model_pricing() -> None:
176198
logger.debug(f"Registered custom pricing for {model_name}")
177199
except Exception as e:
178200
logger.debug(f"Model {model_name} may already be registered or pricing error: {e}")
179-
180201
def _has_channel_config(self) -> bool:
181202
"""Check if multi-channel config (channels / YAML) is active."""
182203
return bool(self._config.llm_model_list) and not all(
@@ -566,3 +587,30 @@ def _parse_litellm_response(self, response: Any, model: str) -> LLMResponse:
566587
model=model,
567588
raw=response,
568589
)
590+
591+
592+
def register_fallback_model_pricing(models: Iterable[str]) -> None:
593+
"""Register zero-cost pricing for unknown OpenAI-compatible models."""
594+
if not models:
595+
return
596+
LLMToolAdapter._register_custom_model_pricing()
597+
register = getattr(litellm, "register_model", None)
598+
if not callable(register):
599+
return
600+
cost_map = getattr(litellm, "model_cost", {})
601+
if not isinstance(cost_map, dict):
602+
cost_map = {}
603+
for model in models:
604+
provider, wire_model = _split_provider_model(str(model))
605+
if provider != "openai":
606+
continue
607+
if not wire_model or wire_model.startswith("__legacy_"):
608+
continue
609+
if wire_model in cost_map or wire_model in _FALLBACK_MODEL_PRICING_REGISTERED:
610+
continue
611+
try:
612+
register({wire_model: dict(_FALLBACK_MODEL_PRICING)})
613+
_FALLBACK_MODEL_PRICING_REGISTERED.add(wire_model)
614+
logger.debug("Registered fallback pricing for %s", wire_model)
615+
except Exception as exc:
616+
logger.debug("Fallback pricing registration skipped for %s: %s", wire_model, exc)

src/analyzer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from json_repair import repair_json
2323
from litellm import Router
2424

25-
from src.agent.llm_adapter import get_thinking_extra_body
25+
from src.agent.llm_adapter import get_thinking_extra_body, register_fallback_model_pricing
2626
from src.agent.skills.defaults import CORE_TRADING_SKILL_POLICY_ZH
2727
from src.config import (
2828
Config,
@@ -31,6 +31,7 @@
3131
get_config,
3232
get_configured_llm_models,
3333
normalize_litellm_temperature,
34+
resolve_litellm_wire_model,
3435
resolve_news_window_days,
3536
)
3637
from src.storage import persist_llm_usage
@@ -1884,6 +1885,8 @@ def _dispatch_litellm_completion(
18841885
router_model_names: set[str],
18851886
) -> Any:
18861887
"""Dispatch a LiteLLM completion through router or direct fallback."""
1888+
wire_model = resolve_litellm_wire_model(model, config.llm_model_list)
1889+
register_fallback_model_pricing([wire_model])
18871890
effective_kwargs = dict(call_kwargs)
18881891
if use_channel_router and self._router and model in router_model_names:
18891892
return self._router.completion(**effective_kwargs)

src/services/system_config_service.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -724,12 +724,10 @@ def test_llm_channel(
724724

725725
try:
726726
import litellm
727-
from src.agent.llm_adapter import LLMToolAdapter
727+
from src.agent.llm_adapter import register_fallback_model_pricing
728728

729-
# Register custom model pricing for MiniMax models not in LiteLLM's built-in list
730-
# This must be done before litellm.completion() to prevent cost calculation errors
731-
# Reuses the registration logic from LLMToolAdapter to avoid code duplication
732-
LLMToolAdapter._register_custom_model_pricing()
729+
# Register fallback pricing for OpenAI-compatible models to prevent cost calculation errors
730+
register_fallback_model_pricing([resolved_model])
733731

734732
started_at = time.perf_counter()
735733
response = litellm.completion(**call_kwargs)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
"""Tests for fallback LiteLLM pricing registration."""
3+
4+
import unittest
5+
from unittest.mock import patch
6+
7+
try:
8+
import litellm # noqa: F401
9+
except ModuleNotFoundError:
10+
from tests.litellm_stub import ensure_litellm_stub
11+
12+
ensure_litellm_stub()
13+
14+
from src.agent import llm_adapter
15+
16+
17+
class LiteLLMFallbackPricingTestCase(unittest.TestCase):
18+
def test_register_fallback_pricing_registers_openai_model(self) -> None:
19+
registered = []
20+
21+
def _register(payload):
22+
registered.append(payload)
23+
24+
with patch.object(llm_adapter.litellm, "register_model", side_effect=_register, create=True):
25+
with patch.object(llm_adapter.litellm, "model_cost", {}, create=True):
26+
llm_adapter._FALLBACK_MODEL_PRICING_REGISTERED.clear()
27+
llm_adapter.register_fallback_model_pricing(["openai/mimo-alpha"])
28+
29+
self.assertTrue(any("mimo-alpha" in payload for payload in registered))

0 commit comments

Comments
 (0)