Parcourir la source

fix(provider): 按真实模型设 context_window — 治 qwen-max 400 输入超限

切到 qwen-max 后 dashscope 返 400 "Range of input length should be [1, 30720]"。
根因:OpenAICompatProvider 没按模型设 context_window,沿用 base.py 默认 200000;
qwen-max 真实输入上限只有 30720 token → ConversationLam 的 max_history
(min(cw//2,80000))放行 80000 token 历史 → 累积超 30720 → 400。(系统提示仅
1457 字符,非元凶;是历史/观察累积。)

修:
- openai_compat_provider 加 _MODEL_CONTEXT_WINDOWS(qwen-max=30720/qwen-plus=131072/
  deepseek=65536/gpt-4o=128000/...),__init__ 按模型名前缀设 self.config.context_window。
- compiler 构造 ConversationLam 时把字符硬上限随 cw 收紧:max_input_chars=cw*0.9
  (dashscope 限的是输入 token、与输出分开;CJK 最密~1 char/token,*0.9 即便 1:1 也稳),
  夹在 [8000,200000];可经 model.maxInputChars 覆盖。qwen-max → max_history 15360、
  max_input_chars 27648,双重兜底。

回归 TestModelContextWindow(3)+ lambdagent 593 全绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kenny67nju il y a 2 mois
Parent
commit
787126d501

+ 9 - 0
lambdagent/src/lambdagent/fromconfig/compiler.py

@@ -613,6 +613,14 @@ def _compile_lam(cfg: Dict, name_suffix: str = "", overrides: Dict = None) -> "T
             model_name = model_cfg.get("name", "") or provider.default_model
             temperature = model_cfg.get("temperature", 0.0)
             max_tokens = model_cfg.get("maxTokens", 4096)
+            # 字符硬上限随 context_window 收紧(治 qwen-max 30720:200000 字符默认远超)。
+            # dashscope 限的是**输入** token(与输出分开),所以不扣 max_tokens。CJK 最密
+            # 可达 ~1 char/token,故用 cw×0.9 作字符上限——即便 1:1 也稳在限内;典型 CJK
+            # (~1.5 char/token)/英文(~4)更宽松。夹在 [8000, 200000]。
+            _cw = provider.context_window
+            max_input_chars = model_cfg.get(
+                "maxInputChars", max(8000, min(200000, int(_cw * 0.9))),
+            )
             return ConversationLam(
                 name=agent_name,
                 provider=provider,
@@ -621,6 +629,7 @@ def _compile_lam(cfg: Dict, name_suffix: str = "", overrides: Dict = None) -> "T
                 model=model_name,
                 temperature=temperature,
                 max_tokens=max_tokens,
+                max_input_chars=max_input_chars,
             )
     except Exception as e:
         # Fall through to legacy Lam — but loudly: silent fallback hides

+ 27 - 0
lambdagent/src/lambdagent/providers/openai_compat_provider.py

@@ -49,6 +49,28 @@ class OpenAICompatProvider(LLMProvider):
         "zhipu":     ("https://open.bigmodel.cn/api/paas/v4",              "glm-4",             "ZHIPU_API_KEY"),
     }
 
+    # 各模型真实输入上限(token)。base.py 默认 200000 对 qwen-max 是错的——qwen-max
+    # 实际只有 30720(dashscope 报 "Range of input length should be [1, 30720]")。
+    # 不设对的话 ConversationLam 的 max_history(min(cw//2,80000))会放行 80000 token
+    # 历史 → 累积超限 → 400。按模型名前缀匹配,未知则保持 config 原值。
+    _MODEL_CONTEXT_WINDOWS = {
+        "qwen-max": 30720, "qwen-plus": 131072, "qwen-turbo": 1000000,
+        "qwen-long": 10000000, "qwen2.5": 32768, "qwen2": 32768, "qwen3": 32768,
+        "deepseek": 65536, "moonshot-v1-8k": 8192, "moonshot-v1-32k": 32768,
+        "moonshot-v1-128k": 128000, "gpt-4o": 128000, "gpt-4-turbo": 128000,
+        "gpt-4": 8192, "glm-4": 128000,
+    }
+
+    @classmethod
+    def _resolve_context_window(cls, model: str):
+        m = (model or "").lower()
+        # 最长前缀优先(qwen-max-longcontext 命中 qwen-max;但更具体的先匹配)
+        best = None
+        for key, cw in cls._MODEL_CONTEXT_WINDOWS.items():
+            if m.startswith(key) and (best is None or len(key) > len(best[0])):
+                best = (key, cw)
+        return best[1] if best else None
+
     def __init__(self, config: ProviderConfig, provider_name: str = "openai",
                  api_key: str = "", base_url: str = "", model: str = ""):
         super().__init__(config)
@@ -68,6 +90,11 @@ class OpenAICompatProvider(LLMProvider):
         if model:
             self.config.model = model
 
+        # 按真实模型设 context_window(治 qwen-max 被当 200000 → 历史超 30720 → 400)。
+        _cw = self._resolve_context_window(self.config.model)
+        if _cw:
+            self.config.context_window = _cw
+
         # L03: Token-usage accumulator — mirrors ClaudeCodeProvider.get_usage()
         # so agentpaas cost tracking works for any provider.
         self._usage_input  = 0

+ 21 - 0
lambdagent/tests/test_providers.py

@@ -576,3 +576,24 @@ class TestAuthErrorTranslation(unittest.TestCase):
                 p._call_new([{"role": "user", "content": "hi"}])
         self.assertIn("登录", str(ei.exception))
         self.assertFalse(ei.exception.retryable)
+
+
+class TestModelContextWindow(unittest.TestCase):
+    """provider 必须按真实模型设 context_window — 治 qwen-max 被当 200000 → 历史超
+    30720 → dashscope 400 'Range of input length should be [1, 30720]'。"""
+
+    def test_qwen_max_is_30720(self):
+        from lambdagent.providers.openai_compat_provider import OpenAICompatProvider as P
+        self.assertEqual(P._resolve_context_window("qwen-max"), 30720)
+        self.assertEqual(P._resolve_context_window("qwen-max-latest"), 30720)
+
+    def test_other_models(self):
+        from lambdagent.providers.openai_compat_provider import OpenAICompatProvider as P
+        self.assertEqual(P._resolve_context_window("qwen-plus"), 131072)
+        self.assertEqual(P._resolve_context_window("deepseek-chat"), 65536)
+        self.assertIsNone(P._resolve_context_window("totally-unknown"))
+
+    def test_provider_sets_context_window(self):
+        from lambdagent.providers import create_provider
+        p = create_provider("dashscope")  # default qwen-max
+        self.assertEqual(p.context_window, 30720)