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:
rob
2026-04-04 17:50:26 -03:00
co-authored by Claude Opus 4.5
parent 468a999b80
commit b673ea019c
6 changed files with 280 additions and 14 deletions
+120 -1
View File
@@ -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]