Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions studio/backend/core/inference/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ class GenStreamError(str):
"""A stream chunk carrying a real backend/generation error, not model text.

Subclasses str so existing display/logging consumers are unaffected, while
callers that must abort a distributed run on error (raise_on_streamed_error)
can distinguish a real error from model output whose visible text starts with
"Error:" by checking isinstance(chunk, GenStreamError).
callers can distinguish a real error from model output whose visible text
starts with "Error:" by checking isinstance(chunk, GenStreamError).
"""

__slots__ = ()
Expand Down
2 changes: 1 addition & 1 deletion unsloth_cli/_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def collect_stream(stream, show_thinking: bool) -> str:
def raise_on_streamed_error(stream):
# Match real backend errors by type (GenStreamError), not the "Error:" text
# prefix, so a completion whose text opens with "Error:" is not misread as a
# failure that aborts a distributed run.
# backend failure.
try:
ensure_studio_backend_path()
from core.inference.orchestrator import GenStreamError
Expand Down
2 changes: 1 addition & 1 deletion unsloth_cli/commands/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def generate(backend = None, use_adapter = None):
enable_thinking = show_thinking,
use_adapter = use_adapter,
)
return raise_on_streamed_error(stream) if is_mlx_distributed else stream
return raise_on_streamed_error(stream)

if should_print:
console.print()
Expand Down
5 changes: 1 addition & 4 deletions unsloth_cli/commands/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,12 @@ def inference(
repetition_penalty = repetition_penalty,
enable_thinking = think,
)
if is_mlx_distributed:
stream = raise_on_streamed_error(stream)
stream = raise_on_streamed_error(stream)
Comment thread
Lyxot marked this conversation as resolved.
if rank == 0:
typer.echo("Assistant:")
try:
stream_to_stdout(stream, show_thinking = think)
except RuntimeError as exc:
if not is_mlx_distributed:
raise
typer.echo(f"Error: {exc}", err = True)
raise typer.Exit(code = 1)
else:
Expand Down
98 changes: 98 additions & 0 deletions unsloth_cli/tests/test_inference_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,104 @@ def fake_load(model, **kwargs):
assert set(closed) == {"tuned", "base"}


@pytest.mark.parametrize(
("chunk_kind", "expected_exit"),
[
("answer", 0),
("model_text_error", 0),
("real_error", 1),
],
)
def test_inference_local_handles_stream(monkeypatch, chunk_kind, expected_exit):
from unsloth_cli.commands import inference as infermod
from unsloth_cli._inference import ensure_studio_backend_path

ensure_studio_backend_path()
from core.inference.orchestrator import GenStreamError

chunks = {
"answer": ["answer"],
"model_text_error": ["Error: printed by the model, not a backend failure"],
"real_error": [GenStreamError("Error: generation failed")],
}[chunk_kind]
closed = []

class _FakeBackend:
def stream(self, messages, **kwargs):
return iter(chunks)

def close(self):
closed.append(True)

monkeypatch.setattr(
infermod,
"connect_studio_server",
lambda *_a, **_k: (_ for _ in ()).throw(AssertionError("server disabled")),
)
monkeypatch.setattr(infermod, "load_chat_backend", lambda *a, **k: _FakeBackend())

result = CliRunner().invoke(
_inference_app(),
["fake-model", "hello", "--no-server"],
)

assert result.exit_code == expected_exit, result.output
assert closed == [True]
if chunk_kind == "real_error":
assert result.stdout == "Assistant:\n"
assert result.stderr == "Error: generation failed\n"
else:
assert chunks[0] in result.output


@pytest.mark.parametrize("chunk_kind", ["answer", "model_text_error", "real_error"])
def test_chat_local_handles_stream(monkeypatch, chunk_kind):
from unsloth_cli._inference import ensure_studio_backend_path

ensure_studio_backend_path()
from core.inference.orchestrator import GenStreamError

first_chunk = {
"answer": "answer",
"model_text_error": "Error: printed by the model, not a backend failure",
"real_error": GenStreamError("Error: generation failed"),
}[chunk_kind]
calls, closed = [], []

class _FakeChatBackend:
def stream(self, messages, **kwargs):
calls.append([dict(message) for message in messages])
return iter([first_chunk if len(calls) == 1 else "second answer"])

def close(self):
closed.append(True)

monkeypatch.setattr(chatmod, "resolve_model_config", lambda *a, **k: _FakeConfig())
monkeypatch.setattr(chatmod, "connect_studio_server", lambda *a, **k: None)
monkeypatch.setattr(chatmod, "load_chat_backend", lambda *a, **k: _FakeChatBackend())
monkeypatch.setattr(chatmod, "_compare_needs_second_model", lambda: False)

result = CliRunner().invoke(
_chat_app(),
["fake-model"],
input = "first\nsecond\n/exit\n",
)

assert result.exit_code == 0, result.output
assert closed == [True]
if chunk_kind == "real_error":
assert calls[1] == [{"role": "user", "content": "second"}]
assert "(error: generation failed)" in result.output
assert "Error: generation failed" not in result.output
else:
assert calls[1] == [
{"role": "user", "content": "first"},
{"role": "assistant", "content": first_chunk},
{"role": "user", "content": "second"},
]
assert first_chunk in result.output


@pytest.mark.parametrize(
("chunk_kind", "expected_exit"),
[
Expand Down
Loading