Add provider for-tools command and max_tokens support
- Add 'cmdforge providers for-tools <tool>...' to list providers used by tools - Add --warm flag to pre-load local models (Ollama) for faster first inference - Warm larger models first to claim contiguous GPU memory - Skip cloud providers (claude, opencode, gemini) during warm-up - Add max_tokens field to PromptStep for controlling output length - Pass max_tokens to call_provider with provider-specific flags - Add comprehensive tests for provider functionality Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
+120
-1
@@ -12,7 +12,7 @@ from cmdforge.providers import (
|
||||
load_providers, save_providers, get_provider,
|
||||
add_provider, delete_provider,
|
||||
call_provider, mock_provider,
|
||||
DEFAULT_PROVIDERS
|
||||
DEFAULT_PROVIDERS, strip_ansi
|
||||
)
|
||||
|
||||
|
||||
@@ -507,3 +507,122 @@ class TestProviderFallback:
|
||||
|
||||
assert result.success is True
|
||||
assert "[MOCK]" in result.text
|
||||
|
||||
|
||||
class TestStripAnsi:
|
||||
"""Tests for ANSI escape code stripping."""
|
||||
|
||||
def test_strip_basic_color_codes(self):
|
||||
"""Strip basic color codes like \\x1b[0m."""
|
||||
text = "\x1b[31mred text\x1b[0m"
|
||||
assert strip_ansi(text) == "red text"
|
||||
|
||||
def test_strip_cursor_movements(self):
|
||||
"""Strip cursor movement codes."""
|
||||
text = "\x1b[2Jhello\x1b[H"
|
||||
assert strip_ansi(text) == "hello"
|
||||
|
||||
def test_strip_private_mode_sequences(self):
|
||||
"""Strip private mode sequences like \\x1b[?25l (hide cursor)."""
|
||||
text = "\x1b[?2026h\x1b[?25lspinner\x1b[?25h\x1b[?2026l"
|
||||
assert strip_ansi(text) == "spinner"
|
||||
|
||||
def test_strip_ollama_spinner(self):
|
||||
"""Strip Ollama spinner output."""
|
||||
text = "\x1b[?2026h\x1b[?25l⠋ \x1b[?25h\x1b[?2026l\x1b[?2026h\x1b[?25l⠙ \x1b[?25h"
|
||||
result = strip_ansi(text)
|
||||
# Should only have spinner characters left, not escape codes
|
||||
assert "\x1b" not in result
|
||||
assert "⠋" in result or "⠙" in result
|
||||
|
||||
def test_strip_carriage_return(self):
|
||||
"""Strip carriage returns."""
|
||||
text = "line1\rline2"
|
||||
assert strip_ansi(text) == "line1line2"
|
||||
|
||||
def test_preserve_normal_text(self):
|
||||
"""Normal text should be preserved."""
|
||||
text = "Hello, World! 123 [test] {data}"
|
||||
assert strip_ansi(text) == text
|
||||
|
||||
def test_strip_osc_sequences(self):
|
||||
"""Strip OSC (Operating System Command) sequences."""
|
||||
text = "\x1b]0;Window Title\x07content"
|
||||
assert strip_ansi(text) == "content"
|
||||
|
||||
|
||||
class TestMaxTokens:
|
||||
"""Tests for max_tokens parameter."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_providers_file(self, tmp_path):
|
||||
providers_file = tmp_path / ".cmdforge" / "providers.yaml"
|
||||
with patch('cmdforge.providers.PROVIDERS_FILE', providers_file):
|
||||
yield providers_file
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('shutil.which')
|
||||
def test_max_tokens_appended_for_claude(self, mock_which, mock_run, temp_providers_file):
|
||||
"""max_tokens should append --max-tokens for claude providers."""
|
||||
mock_which.return_value = "/usr/bin/claude"
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="response", stderr="")
|
||||
save_providers([Provider("claude-haiku", "claude -p --model haiku")])
|
||||
|
||||
call_provider("claude-haiku", "Test prompt", max_tokens=4096)
|
||||
|
||||
# Verify the command included --max-tokens
|
||||
call_args = mock_run.call_args
|
||||
cmd = call_args[0][0]
|
||||
assert "--max-tokens 4096" in cmd
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('shutil.which')
|
||||
def test_max_tokens_appended_for_gemini(self, mock_which, mock_run, temp_providers_file):
|
||||
"""max_tokens should append --max-output-tokens for gemini providers."""
|
||||
mock_which.return_value = "/usr/bin/gemini"
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="response", stderr="")
|
||||
save_providers([Provider("gemini", "gemini --model gemini-2.5-pro")])
|
||||
|
||||
call_provider("gemini", "Test prompt", max_tokens=8192)
|
||||
|
||||
call_args = mock_run.call_args
|
||||
cmd = call_args[0][0]
|
||||
assert "--max-output-tokens 8192" in cmd
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('shutil.which')
|
||||
def test_max_tokens_not_appended_when_none(self, mock_which, mock_run, temp_providers_file):
|
||||
"""No flag should be added when max_tokens is None."""
|
||||
mock_which.return_value = "/usr/bin/claude"
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="response", stderr="")
|
||||
save_providers([Provider("claude", "claude -p")])
|
||||
|
||||
call_provider("claude", "Test prompt", max_tokens=None)
|
||||
|
||||
call_args = mock_run.call_args
|
||||
cmd = call_args[0][0]
|
||||
assert "--max-tokens" not in cmd
|
||||
|
||||
@patch('subprocess.run')
|
||||
@patch('shutil.which')
|
||||
def test_max_tokens_passed_through_fallback(self, mock_which, mock_run, temp_providers_file):
|
||||
"""max_tokens should be passed to fallback provider."""
|
||||
mock_which.return_value = "/usr/bin/claude"
|
||||
# First call fails, second succeeds
|
||||
mock_run.side_effect = [
|
||||
MagicMock(returncode=1, stdout="", stderr="Error"),
|
||||
MagicMock(returncode=0, stdout="fallback response", stderr="")
|
||||
]
|
||||
save_providers([
|
||||
Provider("primary", "claude -p --model opus", fallback="fallback"),
|
||||
Provider("fallback", "claude -p --model haiku")
|
||||
])
|
||||
|
||||
result = call_provider("primary", "Test prompt", max_tokens=4096)
|
||||
|
||||
assert result.success is True
|
||||
# Both calls should have had max_tokens
|
||||
calls = mock_run.call_args_list
|
||||
assert "--max-tokens 4096" in calls[0][0][0]
|
||||
assert "--max-tokens 4096" in calls[1][0][0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user