sandboxy 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sandboxy/cli/main.py CHANGED
@@ -1594,5 +1594,376 @@ def mcp_list_servers() -> None:
1594
1594
  click.echo("More servers: https://github.com/modelcontextprotocol/servers")
1595
1595
 
1596
1596
 
1597
+ # =============================================================================
1598
+ # PROVIDERS COMMAND GROUP
1599
+ # =============================================================================
1600
+
1601
+
1602
+ @main.group()
1603
+ def providers() -> None:
1604
+ """Manage local model providers (Ollama, LM Studio, vLLM, etc.)."""
1605
+ pass
1606
+
1607
+
1608
+ @providers.command("list")
1609
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
1610
+ def providers_list(as_json: bool) -> None:
1611
+ """List all configured local providers.
1612
+
1613
+ Shows provider name, type, URL, connection status, and model count.
1614
+
1615
+ Examples:
1616
+ sandboxy providers list
1617
+ sandboxy providers list --json
1618
+ """
1619
+ import asyncio
1620
+
1621
+ from sandboxy.providers.config import load_providers_config
1622
+ from sandboxy.providers.local import LocalProvider
1623
+
1624
+ config = load_providers_config()
1625
+
1626
+ if not config.providers:
1627
+ if as_json:
1628
+ click.echo(json.dumps({"providers": []}))
1629
+ else:
1630
+ click.echo("No local providers configured.")
1631
+ click.echo("")
1632
+ click.echo("Add a provider with:")
1633
+ click.echo(" sandboxy providers add ollama --url http://localhost:11434/v1")
1634
+ return
1635
+
1636
+ async def get_statuses():
1637
+ results = []
1638
+ for pconfig in config.providers:
1639
+ provider = LocalProvider(pconfig)
1640
+ try:
1641
+ status = await provider.test_connection()
1642
+ results.append(
1643
+ {
1644
+ "name": pconfig.name,
1645
+ "type": pconfig.type,
1646
+ "base_url": pconfig.base_url,
1647
+ "enabled": pconfig.enabled,
1648
+ "status": status.status.value,
1649
+ "model_count": len(status.available_models),
1650
+ }
1651
+ )
1652
+ except Exception as e:
1653
+ results.append(
1654
+ {
1655
+ "name": pconfig.name,
1656
+ "type": pconfig.type,
1657
+ "base_url": pconfig.base_url,
1658
+ "enabled": pconfig.enabled,
1659
+ "status": "error",
1660
+ "model_count": 0,
1661
+ "error": str(e),
1662
+ }
1663
+ )
1664
+ finally:
1665
+ await provider.close()
1666
+ return results
1667
+
1668
+ statuses = asyncio.run(get_statuses())
1669
+
1670
+ if as_json:
1671
+ click.echo(json.dumps({"providers": statuses}, indent=2))
1672
+ return
1673
+
1674
+ # Table output
1675
+ click.echo(f"{'NAME':<15} {'TYPE':<18} {'URL':<35} {'STATUS':<12} {'MODELS':<6}")
1676
+ for s in statuses:
1677
+ status_display = s["status"]
1678
+ if s["status"] == "connected":
1679
+ status_display = click.style("connected", fg="green")
1680
+ elif s["status"] in ("disconnected", "error"):
1681
+ status_display = click.style(s["status"], fg="red")
1682
+
1683
+ click.echo(
1684
+ f"{s['name']:<15} {s['type']:<18} {s['base_url']:<35} {status_display:<12} {s['model_count']:<6}"
1685
+ )
1686
+
1687
+
1688
+ @providers.command("add")
1689
+ @click.argument("name")
1690
+ @click.option(
1691
+ "--type",
1692
+ "provider_type",
1693
+ type=click.Choice(["ollama", "lmstudio", "vllm", "openai-compatible"]),
1694
+ default="openai-compatible",
1695
+ help="Provider type",
1696
+ )
1697
+ @click.option("--url", required=True, help="Base URL for the provider API")
1698
+ @click.option("--api-key", help="Optional API key for authentication")
1699
+ @click.option("--model", "models", multiple=True, help="Manually specify model (can be repeated)")
1700
+ @click.option("--no-test", is_flag=True, help="Skip connection test")
1701
+ def providers_add(
1702
+ name: str,
1703
+ provider_type: str,
1704
+ url: str,
1705
+ api_key: str | None,
1706
+ models: tuple[str, ...],
1707
+ no_test: bool,
1708
+ ) -> None:
1709
+ """Add a new local model provider.
1710
+
1711
+ Examples:
1712
+ sandboxy providers add ollama --url http://localhost:11434/v1
1713
+ sandboxy providers add my-vllm --type vllm --url http://gpu:8000/v1 --api-key $KEY
1714
+ sandboxy providers add custom --url http://localhost:8080/v1 --model llama3 --model mistral
1715
+ """
1716
+ import asyncio
1717
+
1718
+ from sandboxy.providers.config import (
1719
+ LocalProviderConfig,
1720
+ load_providers_config,
1721
+ save_providers_config,
1722
+ )
1723
+ from sandboxy.providers.local import LocalProvider
1724
+ from sandboxy.providers.registry import reload_local_providers
1725
+
1726
+ # Load existing config
1727
+ config = load_providers_config()
1728
+
1729
+ # Check for duplicate name
1730
+ if config.get_provider(name):
1731
+ click.echo(f"Error: Provider '{name}' already exists", err=True)
1732
+ sys.exit(1)
1733
+
1734
+ # Create provider config
1735
+ try:
1736
+ provider_config = LocalProviderConfig(
1737
+ name=name,
1738
+ type=provider_type,
1739
+ base_url=url,
1740
+ api_key=api_key,
1741
+ models=list(models),
1742
+ )
1743
+ except ValueError as e:
1744
+ click.echo(f"Error: {e}", err=True)
1745
+ sys.exit(1)
1746
+
1747
+ # Test connection unless skipped
1748
+ discovered_models: list[str] = []
1749
+ if not no_test:
1750
+ click.echo(f"Testing connection to {url}...")
1751
+
1752
+ async def test():
1753
+ provider = LocalProvider(provider_config)
1754
+ try:
1755
+ status = await provider.test_connection()
1756
+ return status
1757
+ finally:
1758
+ await provider.close()
1759
+
1760
+ try:
1761
+ status = asyncio.run(test())
1762
+ if status.status.value == "connected":
1763
+ click.echo(click.style("✓ Connected", fg="green"))
1764
+ discovered_models = status.available_models
1765
+ if discovered_models:
1766
+ click.echo(f"✓ Found {len(discovered_models)} models")
1767
+ else:
1768
+ click.echo(click.style(f"✗ Connection failed: {status.error_message}", fg="red"))
1769
+ click.echo("")
1770
+ click.echo("Provider will be added but may not work until server is running.")
1771
+ click.echo("Use --no-test to skip this check.")
1772
+ sys.exit(2)
1773
+ except Exception as e:
1774
+ click.echo(click.style(f"✗ Connection failed: {e}", fg="red"))
1775
+ sys.exit(2)
1776
+
1777
+ # Add and save
1778
+ config.add_provider(provider_config)
1779
+ save_providers_config(config)
1780
+
1781
+ # Reload providers in registry
1782
+ reload_local_providers()
1783
+
1784
+ click.echo(f"Added provider '{name}'")
1785
+ if discovered_models:
1786
+ model_list = ", ".join(discovered_models[:5])
1787
+ if len(discovered_models) > 5:
1788
+ model_list += f", ... ({len(discovered_models) - 5} more)"
1789
+ click.echo(f"Found {len(discovered_models)} models: {model_list}")
1790
+ elif models:
1791
+ click.echo(f"Configured {len(models)} model(s): {', '.join(models)}")
1792
+
1793
+
1794
+ @providers.command("remove")
1795
+ @click.argument("name")
1796
+ def providers_remove(name: str) -> None:
1797
+ """Remove a configured provider.
1798
+
1799
+ Examples:
1800
+ sandboxy providers remove ollama
1801
+ """
1802
+ from sandboxy.providers.config import load_providers_config, save_providers_config
1803
+ from sandboxy.providers.registry import reload_local_providers
1804
+
1805
+ config = load_providers_config()
1806
+
1807
+ if not config.remove_provider(name):
1808
+ click.echo(f"Error: Provider '{name}' not found", err=True)
1809
+ sys.exit(1)
1810
+
1811
+ save_providers_config(config)
1812
+ reload_local_providers()
1813
+
1814
+ click.echo(f"Removed provider '{name}'")
1815
+
1816
+
1817
+ @providers.command("test")
1818
+ @click.argument("name")
1819
+ def providers_test(name: str) -> None:
1820
+ """Test connection to a provider.
1821
+
1822
+ Examples:
1823
+ sandboxy providers test ollama
1824
+ """
1825
+ import asyncio
1826
+
1827
+ from sandboxy.providers.config import load_providers_config
1828
+ from sandboxy.providers.local import LocalProvider
1829
+
1830
+ config = load_providers_config()
1831
+ provider_config = config.get_provider(name)
1832
+
1833
+ if not provider_config:
1834
+ click.echo(f"Error: Provider '{name}' not found", err=True)
1835
+ sys.exit(1)
1836
+
1837
+ click.echo(f"Testing connection to {name} ({provider_config.base_url})...")
1838
+
1839
+ async def test():
1840
+ provider = LocalProvider(provider_config)
1841
+ try:
1842
+ return await provider.test_connection()
1843
+ finally:
1844
+ await provider.close()
1845
+
1846
+ try:
1847
+ status = asyncio.run(test())
1848
+
1849
+ if status.status.value == "connected":
1850
+ click.echo(click.style(f"✓ Connected in {status.latency_ms}ms", fg="green"))
1851
+ if status.available_models:
1852
+ click.echo(
1853
+ f"✓ Found {len(status.available_models)} models: {', '.join(status.available_models)}"
1854
+ )
1855
+ else:
1856
+ click.echo(click.style(f"✗ Connection failed: {status.error_message}", fg="red"))
1857
+
1858
+ # Provide helpful suggestions based on provider type
1859
+ if provider_config.type == "ollama":
1860
+ click.echo("")
1861
+ click.echo(" Suggestion: Ensure Ollama is running with: ollama serve")
1862
+ elif provider_config.type == "vllm":
1863
+ click.echo("")
1864
+ click.echo(
1865
+ " Suggestion: Start vLLM with: python -m vllm.entrypoints.openai.api_server"
1866
+ )
1867
+ elif provider_config.type == "lmstudio":
1868
+ click.echo("")
1869
+ click.echo(" Suggestion: Start the server in LM Studio and load a model")
1870
+
1871
+ sys.exit(1)
1872
+
1873
+ except Exception as e:
1874
+ click.echo(click.style(f"✗ Error: {e}", fg="red"))
1875
+ sys.exit(1)
1876
+
1877
+
1878
+ @providers.command("models")
1879
+ @click.argument("name", required=False)
1880
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
1881
+ def providers_models(name: str | None, as_json: bool) -> None:
1882
+ """List models from configured providers.
1883
+
1884
+ If NAME is provided, shows models only from that provider.
1885
+ Otherwise, shows models from all providers.
1886
+
1887
+ Examples:
1888
+ sandboxy providers models
1889
+ sandboxy providers models ollama
1890
+ sandboxy providers models --json
1891
+ """
1892
+ import asyncio
1893
+
1894
+ from sandboxy.providers.config import load_providers_config
1895
+ from sandboxy.providers.local import LocalProvider
1896
+
1897
+ config = load_providers_config()
1898
+
1899
+ if name:
1900
+ provider_config = config.get_provider(name)
1901
+ if not provider_config:
1902
+ click.echo(f"Error: Provider '{name}' not found", err=True)
1903
+ sys.exit(1)
1904
+ providers_to_check = [provider_config]
1905
+ else:
1906
+ providers_to_check = [p for p in config.providers if p.enabled]
1907
+
1908
+ if not providers_to_check:
1909
+ if as_json:
1910
+ click.echo(json.dumps({"models": []}))
1911
+ else:
1912
+ click.echo("No providers configured.")
1913
+ return
1914
+
1915
+ async def get_models():
1916
+ all_models = []
1917
+ for pconfig in providers_to_check:
1918
+ provider = LocalProvider(pconfig)
1919
+ try:
1920
+ models = await provider.refresh_models()
1921
+ for m in models:
1922
+ all_models.append(
1923
+ {
1924
+ "provider": pconfig.name,
1925
+ "id": m.id,
1926
+ "name": m.name,
1927
+ "context_length": m.context_length,
1928
+ "supports_tools": m.supports_tools,
1929
+ }
1930
+ )
1931
+ except Exception:
1932
+ # Provider unreachable, use configured models if any
1933
+ for model_id in pconfig.models:
1934
+ all_models.append(
1935
+ {
1936
+ "provider": pconfig.name,
1937
+ "id": model_id,
1938
+ "name": model_id,
1939
+ "context_length": 0,
1940
+ "supports_tools": False,
1941
+ }
1942
+ )
1943
+ finally:
1944
+ await provider.close()
1945
+ return all_models
1946
+
1947
+ models = asyncio.run(get_models())
1948
+
1949
+ if as_json:
1950
+ output = {"models": models}
1951
+ if name:
1952
+ output["provider"] = name
1953
+ click.echo(json.dumps(output, indent=2))
1954
+ return
1955
+
1956
+ if not models:
1957
+ click.echo("No models found. Is the provider running?")
1958
+ return
1959
+
1960
+ # Table output
1961
+ click.echo(f"{'PROVIDER':<15} {'MODEL':<40} {'TOOLS':<6} {'CONTEXT':<10}")
1962
+ for m in models:
1963
+ tools = "✓" if m["supports_tools"] else "✗"
1964
+ ctx = str(m["context_length"]) if m["context_length"] > 0 else "?"
1965
+ click.echo(f"{m['provider']:<15} {m['id']:<40} {tools:<6} {ctx:<10}")
1966
+
1967
+
1597
1968
  if __name__ == "__main__":
1598
1969
  main()
@@ -163,6 +163,7 @@ class MLflowExporter:
163
163
  scenario_id: str,
164
164
  agent_name: str = "default",
165
165
  dataset_case: str | None = None,
166
+ run_name: str | None = None,
166
167
  ) -> str | None:
167
168
  """Export run result to MLflow (creates a new run).
168
169
 
@@ -176,6 +177,7 @@ class MLflowExporter:
176
177
  scenario_id: Unique scenario identifier
177
178
  agent_name: Agent configuration name
178
179
  dataset_case: Optional dataset case identifier
180
+ run_name: Optional run name (defaults to "scenario_name - agent_name")
179
181
 
180
182
  Returns:
181
183
  MLflow run ID on success, None on failure
@@ -196,8 +198,12 @@ class MLflowExporter:
196
198
  if not self._setup_tracking():
197
199
  return None
198
200
 
201
+ # Generate run name if not provided
202
+ if run_name is None:
203
+ run_name = f"{scenario_name} - {agent_name}"
204
+
199
205
  # Start run and log everything
200
- with mlflow.start_run() as run:
206
+ with mlflow.start_run(run_name=run_name) as run:
201
207
  run_id = run.info.run_id
202
208
 
203
209
  self._log_parameters(result, scenario_name, scenario_id)
@@ -4,6 +4,7 @@ Supports multiple LLM providers through a unified interface:
4
4
  - OpenRouter (400+ models via single API)
5
5
  - OpenAI (direct)
6
6
  - Anthropic (direct)
7
+ - Local providers (Ollama, LM Studio, vLLM, OpenAI-compatible)
7
8
 
8
9
  Usage:
9
10
  from sandboxy.providers import get_provider, ProviderRegistry
@@ -12,23 +13,56 @@ Usage:
12
13
  provider = get_provider("openai/gpt-4o")
13
14
  response = await provider.complete("openai/gpt-4o", messages)
14
15
 
15
- # Or use the registry
16
+ # Or use the registry for local models
16
17
  registry = ProviderRegistry()
17
- provider = registry.get_provider_for_model("anthropic/claude-3-opus")
18
+ provider = registry.get_provider_for_model("ollama/llama3")
18
19
  """
19
20
 
20
21
  from sandboxy.providers.base import (
21
22
  BaseProvider,
23
+ ModelInfo,
22
24
  ModelResponse,
23
25
  ProviderError,
24
26
  )
25
- from sandboxy.providers.registry import ProviderRegistry, get_provider, get_registry
27
+ from sandboxy.providers.config import (
28
+ LocalModelInfo,
29
+ LocalProviderConfig,
30
+ ProvidersConfigFile,
31
+ ProviderStatus,
32
+ ProviderStatusEnum,
33
+ get_enabled_providers,
34
+ load_providers_config,
35
+ save_providers_config,
36
+ )
37
+ from sandboxy.providers.local import LocalProvider, LocalProviderConnectionError
38
+ from sandboxy.providers.registry import (
39
+ ProviderRegistry,
40
+ get_provider,
41
+ get_registry,
42
+ reload_local_providers,
43
+ )
26
44
 
27
45
  __all__ = [
46
+ # Base types
28
47
  "BaseProvider",
48
+ "ModelInfo",
29
49
  "ModelResponse",
30
50
  "ProviderError",
51
+ # Registry
31
52
  "ProviderRegistry",
32
53
  "get_provider",
33
54
  "get_registry",
55
+ "reload_local_providers",
56
+ # Local provider
57
+ "LocalProvider",
58
+ "LocalProviderConnectionError",
59
+ "LocalProviderConfig",
60
+ "LocalModelInfo",
61
+ "ProvidersConfigFile",
62
+ "ProviderStatus",
63
+ "ProviderStatusEnum",
64
+ # Config functions
65
+ "load_providers_config",
66
+ "save_providers_config",
67
+ "get_enabled_providers",
34
68
  ]