mirror of https://github.com/aliasrobotics/cai.git
fix: stabilize DeepSeek streaming output
This commit is contained in:
parent
1c79507140
commit
c78b489edb
|
|
@ -68,7 +68,6 @@ async def fetch_response_litellm_openai(
|
|||
"""
|
||||
try:
|
||||
if stream:
|
||||
ret = await litellm.acompletion(**kwargs)
|
||||
stream_obj = await litellm.acompletion(**kwargs)
|
||||
return _build_response_obj(model_name, model_settings, tool_choice, parallel_tool_calls), stream_obj
|
||||
else:
|
||||
|
|
@ -102,7 +101,6 @@ async def fetch_response_litellm_openai(
|
|||
kwargs["messages"] = messages
|
||||
|
||||
if stream:
|
||||
ret = await litellm.acompletion(**kwargs)
|
||||
stream_obj = await litellm.acompletion(**kwargs)
|
||||
return _build_response_obj(model_name, model_settings, tool_choice, parallel_tool_calls), stream_obj
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1994,10 +1994,7 @@ class OpenAIChatCompletionsModel(Model):
|
|||
|
||||
# Check if model supports reasoning (Claude or DeepSeek)
|
||||
model_str_lower = str(self.model).lower()
|
||||
if (
|
||||
detect_claude_thinking_in_stream(str(self.model))
|
||||
or "deepseek" in model_str_lower
|
||||
):
|
||||
if detect_claude_thinking_in_stream(str(self.model)):
|
||||
print_claude_reasoning_simple(
|
||||
reasoning_content, self.agent_name, str(self.model)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4208,7 +4208,7 @@ def create_claude_thinking_context(agent_name, counter, model):
|
|||
context = {
|
||||
"thinking_id": thinking_id,
|
||||
"live": live,
|
||||
"panel": panel,
|
||||
"panel": None,
|
||||
"header": header,
|
||||
"thinking_content": thinking_content,
|
||||
"timestamp": timestamp,
|
||||
|
|
@ -4358,10 +4358,29 @@ def finish_claude_thinking_display(context):
|
|||
return False
|
||||
|
||||
|
||||
def _raw_reasoning_display_enabled() -> bool:
|
||||
"""Return whether raw provider reasoning text should be displayed.
|
||||
|
||||
DeepSeek-compatible gateways often stream ``reasoning_content`` in tiny
|
||||
deltas. Rendering those deltas by default floods the terminal and can leak
|
||||
model-internal scratch text. Keep it opt-in for DeepSeek via
|
||||
``CAI_SHOW_REASONING=true`` (or ``CAI_SHOW_THINKING=true`` for older local
|
||||
configs).
|
||||
"""
|
||||
raw = os.getenv("CAI_SHOW_REASONING")
|
||||
if raw is None:
|
||||
raw = os.getenv("CAI_SHOW_THINKING")
|
||||
if raw is None:
|
||||
return False
|
||||
return raw.strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def detect_claude_thinking_in_stream(model_name):
|
||||
"""
|
||||
Detect if a model should show thinking/reasoning display.
|
||||
Applies to Claude and DeepSeek models with reasoning capability.
|
||||
|
||||
Claude keeps the historical default. DeepSeek raw reasoning is opt-in
|
||||
because providers commonly stream it token-by-token as ``reasoning_content``.
|
||||
|
||||
Args:
|
||||
model_name: The model name to check
|
||||
|
|
@ -4389,17 +4408,10 @@ def detect_claude_thinking_in_stream(model_name):
|
|||
or "thinking" in model_str
|
||||
)
|
||||
|
||||
# Check for DeepSeek models with reasoning capability
|
||||
has_deepseek_reasoning = "deepseek" in model_str and (
|
||||
# DeepSeek reasoner models
|
||||
"reasoner" in model_str
|
||||
or
|
||||
# DeepSeek chat models also support reasoning
|
||||
"chat" in model_str
|
||||
or
|
||||
# Generic deepseek models likely support it
|
||||
"/" in model_str # e.g., deepseek/deepseek-chat
|
||||
)
|
||||
# DeepSeek reasoning display is intentionally opt-in. The text is still
|
||||
# accumulated internally by the model stream handler so empty-response
|
||||
# detection and accounting keep working; it is just not printed by default.
|
||||
has_deepseek_reasoning = "deepseek" in model_str and _raw_reasoning_display_enabled()
|
||||
|
||||
return has_claude_reasoning or has_deepseek_reasoning
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import pytest
|
||||
from openai import NOT_GIVEN
|
||||
|
||||
from cai.sdk.agents.model_settings import ModelSettings
|
||||
from cai.sdk.agents.models.chatcompletions.litellm_adapter import (
|
||||
fetch_response_litellm_openai,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_streaming_fetch_opens_one_completion(monkeypatch):
|
||||
calls = []
|
||||
sentinel_stream = object()
|
||||
|
||||
async def fake_acompletion(**kwargs):
|
||||
calls.append(kwargs.copy())
|
||||
return sentinel_stream
|
||||
|
||||
monkeypatch.setattr(
|
||||
"cai.sdk.agents.models.chatcompletions.litellm_adapter.litellm.acompletion",
|
||||
fake_acompletion,
|
||||
)
|
||||
|
||||
response, stream = await fetch_response_litellm_openai(
|
||||
kwargs={"model": "deepseek/deepseek-v4-pro", "messages": [], "stream": True},
|
||||
model_name="deepseek/deepseek-v4-pro",
|
||||
model_settings=ModelSettings(),
|
||||
tool_choice=NOT_GIVEN,
|
||||
stream=True,
|
||||
parallel_tool_calls=False,
|
||||
)
|
||||
|
||||
assert stream is sentinel_stream
|
||||
assert response.model == "deepseek/deepseek-v4-pro"
|
||||
assert len(calls) == 1
|
||||
assert calls[0]["stream"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_litellm_streaming_tool_call_id_retry_opens_one_retry_stream(monkeypatch):
|
||||
calls = []
|
||||
sentinel_stream = object()
|
||||
|
||||
async def fake_acompletion(**kwargs):
|
||||
calls.append(kwargs.copy())
|
||||
if len(calls) == 1:
|
||||
raise Exception("Invalid 'messages': tool_call_id string too long maximum length")
|
||||
return sentinel_stream
|
||||
|
||||
monkeypatch.setattr(
|
||||
"cai.sdk.agents.models.chatcompletions.litellm_adapter.litellm.acompletion",
|
||||
fake_acompletion,
|
||||
)
|
||||
|
||||
long_id = "call_" + "x" * 80
|
||||
kwargs = {
|
||||
"model": "deepseek/deepseek-v4-pro",
|
||||
"messages": [
|
||||
{"role": "tool", "tool_call_id": long_id, "content": "ok"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": long_id,
|
||||
"type": "function",
|
||||
"function": {"name": "probe", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
_response, stream = await fetch_response_litellm_openai(
|
||||
kwargs=kwargs,
|
||||
model_name="deepseek/deepseek-v4-pro",
|
||||
model_settings=ModelSettings(),
|
||||
tool_choice=NOT_GIVEN,
|
||||
stream=True,
|
||||
parallel_tool_calls=False,
|
||||
)
|
||||
|
||||
assert stream is sentinel_stream
|
||||
assert len(calls) == 2
|
||||
assert kwargs["messages"][0]["tool_call_id"] == long_id[:40]
|
||||
assert kwargs["messages"][1]["tool_calls"][0]["id"] == long_id[:40]
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
from cai.util import streaming
|
||||
|
||||
|
||||
def test_create_claude_thinking_context_for_deepseek_does_not_reference_missing_panel(capsys):
|
||||
streaming._CLAUDE_THINKING_PANELS.clear()
|
||||
|
||||
context = streaming.create_claude_thinking_context(
|
||||
"Web App Pentester",
|
||||
1,
|
||||
"deepseek/deepseek-v4-pro",
|
||||
)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert context is not None
|
||||
assert "Error creating DeepSeek thinking context" not in captured.out
|
||||
assert context["model_display"] == "DeepSeek"
|
||||
assert context["accumulated_thinking"] == ""
|
||||
assert context["is_started"] is False
|
||||
|
||||
thinking_id = context["thinking_id"]
|
||||
assert streaming._CLAUDE_THINKING_PANELS[thinking_id] is context
|
||||
streaming._CLAUDE_THINKING_PANELS.clear()
|
||||
|
||||
|
||||
def test_deepseek_reasoning_display_is_opt_in(monkeypatch):
|
||||
monkeypatch.delenv("CAI_SHOW_REASONING", raising=False)
|
||||
monkeypatch.delenv("CAI_SHOW_THINKING", raising=False)
|
||||
|
||||
assert streaming.detect_claude_thinking_in_stream("deepseek/deepseek-v4-pro") is False
|
||||
|
||||
monkeypatch.setenv("CAI_SHOW_REASONING", "true")
|
||||
assert streaming.detect_claude_thinking_in_stream("deepseek/deepseek-v4-pro") is True
|
||||
|
||||
|
||||
def test_claude_reasoning_display_keeps_historical_default(monkeypatch):
|
||||
monkeypatch.delenv("CAI_SHOW_REASONING", raising=False)
|
||||
monkeypatch.delenv("CAI_SHOW_THINKING", raising=False)
|
||||
|
||||
assert streaming.detect_claude_thinking_in_stream("claude-sonnet-4-20250514") is True
|
||||
Loading…
Reference in New Issue