diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 63771a366cb86..08d0a431b9924 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -4746,6 +4746,13 @@ def run_conversation( provider=agent.provider, api_mode=agent.api_mode, ) + # Persist an explicit provider-reported limit before + # compression/retry. The next request can be rate + # limited, omit usage, or the process can restart; none + # of those should discard metadata the provider already + # confirmed. Keep the probe flags as a best-effort + # post-success retry if this write cannot complete. + save_context_length(agent.model, agent.base_url, new_ctx) # Context probing flags — only set on built-in # compressor (plugin engines manage their own). This # value came from the provider, so it is safe to cache. diff --git a/agent/model_metadata.py b/agent/model_metadata.py index cec891dbb7889..9fc6150fdc0b0 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -634,12 +634,17 @@ def _is_known_provider_base_url(base_url: str) -> bool: def _endpoint_scoped_context_length(model: str, base_url: str) -> Optional[int]: - """Return metadata confirmed only for the Kimi Coding endpoint. + """Return context metadata confirmed for one provider endpoint. Kimi Coding serves K3 under the bare slug ``k3``, but users may also configure or select the public-facing aliases ``kimi-k3`` and ``kimi-k3-cot``. Only canonical ``https://api.kimi.com/coding`` endpoints (legacy Moonshot keys do not serve K3) get the 1 Mi context window. + + NVIDIA NIM serves ``deepseek-ai/deepseek-v4-pro`` with a 262,144-token + window even though DeepSeek's native endpoint serves the V4 family with a + 1M window. Keep the lower limit scoped to NVIDIA instead of weakening the + global model-family metadata. """ normalized = _normalize_base_url(base_url) try: @@ -659,6 +664,18 @@ def _endpoint_scoped_context_length(model: str, base_url: str) -> Optional[int]: and model.strip().lower() in {"k3", "kimi-k3", "kimi-k3-cot"} ): return 1_048_576 + if ( + parsed.scheme.lower() == "https" + and (parsed.hostname or "").lower() == "integrate.api.nvidia.com" + and port in (None, 443) + and parsed.username is None + and parsed.password is None + and parsed.path.rstrip("/") == "/v1" + and not parsed.query + and not parsed.fragment + and model.strip().lower() == "deepseek-ai/deepseek-v4-pro" + ): + return 262_144 return None diff --git a/tests/agent/test_model_metadata.py b/tests/agent/test_model_metadata.py index bbb8f9c80ad61..c4442ebe0abda 100644 --- a/tests/agent/test_model_metadata.py +++ b/tests/agent/test_model_metadata.py @@ -134,6 +134,42 @@ class TestEstimateRequestTokensRough: # ========================================================================= class TestDefaultContextLengths: + def test_nvidia_deepseek_v4_pro_context_is_endpoint_scoped(self): + """NVIDIA's 262K NIM window must not lower DeepSeek V4 globally.""" + with patch("agent.model_metadata.get_cached_context_length", return_value=None), \ + patch("agent.model_metadata.fetch_model_metadata", return_value={}), \ + patch("agent.model_metadata.fetch_endpoint_model_metadata", return_value={}), \ + patch("agent.model_metadata._query_ollama_api_show", return_value=None), \ + patch("agent.models_dev.lookup_models_dev_context", return_value=None): + accepted_urls = ( + "https://integrate.api.nvidia.com/v1", + "https://INTEGRATE.API.NVIDIA.COM/v1/", + "https://integrate.api.nvidia.com:443/v1", + ) + rejected_urls = ( + "http://integrate.api.nvidia.com/v1", + "https://integrate.api.nvidia.com:8443/v1", + "https://integrate.api.nvidia.com/v1/other", + "https://integrate.api.nvidia.com/v1?route=other", + "https://example.invalid/v1", + "https://api.deepseek.com/v1", + "https://openrouter.ai/api/v1", + ) + + for base_url in accepted_urls: + assert get_model_context_length( + "deepseek-ai/deepseek-v4-pro", + provider="nvidia", + base_url=base_url, + ) == 262_144 + + for base_url in rejected_urls: + assert get_model_context_length( + "deepseek-ai/deepseek-v4-pro", + provider="nvidia", + base_url=base_url, + ) == 1_000_000 + def test_k3_context_is_scoped_to_confirmed_coding_endpoint(self): """The bare ``k3`` slug's 1 Mi context must not leak to unverified endpoints. @@ -1076,4 +1112,3 @@ class TestMoAContextLength: assert compressor.context_length == configured_context assert compressor.threshold_tokens == configured_context // 2 endpoint_probe.assert_not_called() - diff --git a/tests/run_agent/test_413_compression.py b/tests/run_agent/test_413_compression.py index 47962a7dd6476..294fc6714beca 100644 --- a/tests/run_agent/test_413_compression.py +++ b/tests/run_agent/test_413_compression.py @@ -327,6 +327,62 @@ class TestHTTP413Compression: assert result["final_response"] == "Recovered after compression" + def test_provider_context_limit_is_cached_before_retry_succeeds(self, agent): + """A confirmed limit survives when the recovery response omits usage.""" + err_400 = Exception( + "Error code: 400 - {'error': {'message': " + "\"This model's maximum context length is 262144 tokens. " + "However, your messages resulted in 271877 tokens.\", 'code': 400}}" + ) + err_400.status_code = 400 + # NVIDIA-compatible endpoints can omit usage. Before the fix, caching + # happened only in the successful-response usage block, so this lost + # the provider-confirmed limit across a restart. + ok_resp = _mock_response( + content="Recovered without usage metadata", + finish_reason="stop", + usage=None, + ) + agent.model = "deepseek-ai/deepseek-v4-pro" + agent.provider = "nvidia" + agent.base_url = "https://integrate.api.nvidia.com/v1" + agent.context_compressor.update_model( + model=agent.model, + context_length=1_000_000, + base_url=agent.base_url, + api_key=agent.api_key, + provider=agent.provider, + api_mode=agent.api_mode, + ) + agent.client.chat.completions.create.side_effect = [err_400, ok_resp] + + with ( + patch.object(agent, "_compress_context") as mock_compress, + patch.object(agent, "_persist_session"), + patch.object(agent, "_save_trajectory"), + patch.object(agent, "_cleanup_task_resources"), + patch("agent.conversation_loop.save_context_length") as mock_save, + ): + mock_compress.return_value = ( + [{"role": "user", "content": "compressed summary"}], + "compressed prompt", + ) + result = agent.run_conversation( + "continue", + conversation_history=[ + {"role": "user", "content": "previous question"}, + {"role": "assistant", "content": "previous answer"}, + ], + ) + + assert result["completed"] is True + mock_save.assert_called_once_with( + "deepseek-ai/deepseek-v4-pro", + "https://integrate.api.nvidia.com/v1", + 262_144, + ) + + def test_context_length_retry_rebuilds_request_after_compression(self, agent): """Retry must send the compressed transcript, not the stale oversized payload.""" err_400 = Exception( @@ -1132,5 +1188,3 @@ class TestOverflowWithCompactionDisabled: assert result.get("failed") is True assert result.get("compaction_disabled") is True assert "auto-compaction is disabled" in result["error"] - -