diff --git a/python/README.md b/python/README.md index 497c92d9..3f542fe9 100644 --- a/python/README.md +++ b/python/README.md @@ -79,12 +79,10 @@ async with await client.create_session({"model": "gpt-5"}) as session: ### CopilotClient ```python -client = CopilotClient({ - "cli_path": "copilot", # Optional: path to CLI executable - "cli_url": None, # Optional: URL of existing server (e.g., "localhost:8080") - "log_level": "info", # Optional: log level (default: "info") - "auto_start": True, # Optional: auto-start server (default: True) -}) +from copilot import CopilotClient, SubprocessConfig + +# Spawn a local CLI process (default) +client = CopilotClient() # uses bundled CLI, stdio transport await client.start() session = await client.create_session({"model": "gpt-5"}) @@ -101,17 +99,39 @@ await session.disconnect() await client.stop() ``` -**CopilotClient Options:** +```python +from copilot import CopilotClient, ExternalServerConfig -- `cli_path` (str): Path to CLI executable (default: "copilot" or `COPILOT_CLI_PATH` env var) -- `cli_url` (str): URL of existing CLI server (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). When provided, the client will not spawn a CLI process. -- `cwd` (str): Working directory for CLI process -- `port` (int): Server port for TCP mode (default: 0 for random) +# Connect to an existing CLI server +client = CopilotClient(ExternalServerConfig(url="localhost:3000")) +``` + +**CopilotClient Constructor:** + +```python +CopilotClient( + config=None, # SubprocessConfig | ExternalServerConfig | None + *, + auto_start=True, # auto-start server on first use + on_list_models=None, # custom handler for list_models() +) +``` + +**SubprocessConfig** — spawn a local CLI process: + +- `cli_path` (str | None): Path to CLI executable (default: bundled binary) +- `cli_args` (list[str]): Extra arguments for the CLI executable +- `cwd` (str | None): Working directory for CLI process (default: current dir) - `use_stdio` (bool): Use stdio transport instead of TCP (default: True) +- `port` (int): Server port for TCP mode (default: 0 for random) - `log_level` (str): Log level (default: "info") -- `auto_start` (bool): Auto-start server on first use (default: True) -- `github_token` (str): GitHub token for authentication. When provided, takes priority over other auth methods. -- `use_logged_in_user` (bool): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). Cannot be used with `cli_url`. +- `env` (dict | None): Environment variables for the CLI process +- `github_token` (str | None): GitHub token for authentication. When provided, takes priority over other auth methods. +- `use_logged_in_user` (bool | None): Whether to use logged-in user for authentication (default: True, but False when `github_token` is provided). + +**ExternalServerConfig** — connect to an existing CLI server: + +- `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`). **SessionConfig Options (for `create_session`):** diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index f5f7ed0b..99c14b33 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -11,6 +11,7 @@ AzureProviderOptions, ConnectionState, CustomAgentConfig, + ExternalServerConfig, GetAuthStatusResponse, GetStatusResponse, MCPLocalServerConfig, @@ -33,6 +34,7 @@ SessionListFilter, SessionMetadata, StopError, + SubprocessConfig, Tool, ToolHandler, ToolInvocation, @@ -47,6 +49,7 @@ "CopilotSession", "ConnectionState", "CustomAgentConfig", + "ExternalServerConfig", "GetAuthStatusResponse", "GetStatusResponse", "MCPLocalServerConfig", @@ -69,6 +72,7 @@ "SessionListFilter", "SessionMetadata", "StopError", + "SubprocessConfig", "Tool", "ToolHandler", "ToolInvocation", diff --git a/python/copilot/client.py b/python/copilot/client.py index 815faf0e..fd8b62bd 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -16,11 +16,12 @@ import inspect import os import re +import shutil import subprocess import sys import threading import uuid -from collections.abc import Callable +from collections.abc import Awaitable, Callable from pathlib import Path from typing import Any, cast @@ -31,8 +32,8 @@ from .session import CopilotSession from .types import ( ConnectionState, - CopilotClientOptions, CustomAgentConfig, + ExternalServerConfig, GetAuthStatusResponse, GetStatusResponse, ModelInfo, @@ -46,6 +47,7 @@ SessionListFilter, SessionMetadata, StopError, + SubprocessConfig, ToolInvocation, ToolResult, ) @@ -90,9 +92,6 @@ class CopilotClient: The client supports both stdio (default) and TCP transport modes for communication with the CLI server. - Attributes: - options: The configuration options for the client. - Example: >>> # Create a client with default options (spawns CLI server) >>> client = CopilotClient() @@ -111,100 +110,72 @@ class CopilotClient: >>> await client.stop() >>> # Or connect to an existing server - >>> client = CopilotClient({"cli_url": "localhost:3000"}) + >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) """ - def __init__(self, options: CopilotClientOptions | None = None): + def __init__( + self, + config: SubprocessConfig | ExternalServerConfig | None = None, + *, + auto_start: bool = True, + on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, + ): """ Initialize a new CopilotClient. Args: - options: Optional configuration options for the client. If not provided, - default options are used (spawns CLI server using stdio). - - Raises: - ValueError: If mutually exclusive options are provided (e.g., cli_url - with use_stdio or cli_path). + config: Connection configuration. Pass a :class:`SubprocessConfig` to + spawn a local CLI process, or an :class:`ExternalServerConfig` to + connect to an existing server. Defaults to ``SubprocessConfig()``. + auto_start: Automatically start the connection on first use + (default: ``True``). + on_list_models: Custom handler for :meth:`list_models`. When provided, + the handler is called instead of querying the CLI server. Example: - >>> # Default options - spawns CLI server using stdio + >>> # Default — spawns CLI server using stdio >>> client = CopilotClient() >>> >>> # Connect to an existing server - >>> client = CopilotClient({"cli_url": "localhost:3000"}) + >>> client = CopilotClient(ExternalServerConfig(url="localhost:3000")) >>> >>> # Custom CLI path with specific log level - >>> client = CopilotClient({ - ... "cli_path": "/usr/local/bin/copilot", - ... "log_level": "debug" - ... }) + >>> client = CopilotClient(SubprocessConfig( + ... cli_path="/usr/local/bin/copilot", + ... log_level="debug", + ... )) """ - opts = options or {} - - # Validate mutually exclusive options - if opts.get("cli_url") and (opts.get("use_stdio") or opts.get("cli_path")): - raise ValueError("cli_url is mutually exclusive with use_stdio and cli_path") + if config is None: + config = SubprocessConfig() - # Validate auth options with external server - if opts.get("cli_url") and ( - opts.get("github_token") or opts.get("use_logged_in_user") is not None - ): - raise ValueError( - "github_token and use_logged_in_user cannot be used with cli_url " - "(external server manages its own auth)" - ) + self._config: SubprocessConfig | ExternalServerConfig = config + self._auto_start = auto_start + self._on_list_models = on_list_models - # Parse cli_url if provided + # Resolve connection-mode-specific state self._actual_host: str = "localhost" - self._is_external_server: bool = False - if opts.get("cli_url"): - self._actual_host, actual_port = self._parse_cli_url(opts["cli_url"]) + self._is_external_server: bool = isinstance(config, ExternalServerConfig) + + if isinstance(config, ExternalServerConfig): + self._actual_host, actual_port = self._parse_cli_url(config.url) self._actual_port: int | None = actual_port - self._is_external_server = True else: self._actual_port = None - # Determine CLI path: explicit option > bundled binary - # Not needed when connecting to external server via cli_url - if opts.get("cli_url"): - default_cli_path = "" # Not used for external server - elif opts.get("cli_path"): - default_cli_path = opts["cli_path"] - else: - bundled_path = _get_bundled_cli_path() - if bundled_path: - default_cli_path = bundled_path - else: - raise RuntimeError( - "Copilot CLI not found. The bundled CLI binary is not available. " - "Ensure you installed a platform-specific wheel, or provide cli_path." - ) + # Resolve CLI path: explicit > bundled binary + if config.cli_path is None: + bundled_path = _get_bundled_cli_path() + if bundled_path: + config.cli_path = bundled_path + else: + raise RuntimeError( + "Copilot CLI not found. The bundled CLI binary is not available. " + "Ensure you installed a platform-specific wheel, or provide cli_path." + ) - # Default use_logged_in_user to False when github_token is provided - github_token = opts.get("github_token") - use_logged_in_user = opts.get("use_logged_in_user") - if use_logged_in_user is None: - use_logged_in_user = False if github_token else True - - self.options: CopilotClientOptions = { - "cli_path": default_cli_path, - "cwd": opts.get("cwd", os.getcwd()), - "port": opts.get("port", 0), - "use_stdio": False if opts.get("cli_url") else opts.get("use_stdio", True), - "log_level": opts.get("log_level", "info"), - "auto_start": opts.get("auto_start", True), - "use_logged_in_user": use_logged_in_user, - } - if opts.get("cli_args"): - self.options["cli_args"] = opts["cli_args"] - if opts.get("cli_url"): - self.options["cli_url"] = opts["cli_url"] - if opts.get("env"): - self.options["env"] = opts["env"] - if github_token: - self.options["github_token"] = github_token - - self._on_list_models = opts.get("on_list_models") + # Resolve use_logged_in_user default + if config.use_logged_in_user is None: + config.use_logged_in_user = not bool(config.github_token) self._process: subprocess.Popen | None = None self._client: JsonRpcClient | None = None @@ -286,8 +257,9 @@ async def start(self) -> None: """ Start the CLI server and establish a connection. - If connecting to an external server (via cli_url), only establishes the - connection. Otherwise, spawns the CLI server process and then connects. + If connecting to an external server (via :class:`ExternalServerConfig`), + only establishes the connection. Otherwise, spawns the CLI server process + and then connects. This method is called automatically when creating a session if ``auto_start`` is True (default). @@ -296,7 +268,7 @@ async def start(self) -> None: RuntimeError: If the server fails to start or the connection fails. Example: - >>> client = CopilotClient({"auto_start": False}) + >>> client = CopilotClient(auto_start=False) >>> await client.start() >>> # Now ready to create sessions """ @@ -480,7 +452,7 @@ async def create_session(self, config: SessionConfig) -> CopilotSession: ... }) """ if not self._client: - if self.options["auto_start"]: + if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") @@ -672,7 +644,7 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> ... }) """ if not self._client: - if self.options["auto_start"]: + if self._auto_start: await self.start() else: raise RuntimeError("Client not connected. Call start() first.") @@ -1289,25 +1261,30 @@ async def _start_cli_server(self) -> None: Raises: RuntimeError: If the server fails to start or times out. """ - cli_path = self.options["cli_path"] + assert isinstance(self._config, SubprocessConfig) + cfg = self._config + + cli_path = cfg.cli_path + assert cli_path is not None # resolved in __init__ # Verify CLI exists if not os.path.exists(cli_path): - raise RuntimeError(f"Copilot CLI not found at {cli_path}") + original_path = cli_path + if (cli_path := shutil.which(cli_path)) is None: + raise RuntimeError(f"Copilot CLI not found at {original_path}") # Start with user-provided cli_args, then add SDK-managed args - cli_args = self.options.get("cli_args") or [] - args = list(cli_args) + [ + args = list(cfg.cli_args) + [ "--headless", "--no-auto-update", "--log-level", - self.options["log_level"], + cfg.log_level, ] # Add auth-related flags - if self.options.get("github_token"): + if cfg.github_token: args.extend(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]) - if not self.options.get("use_logged_in_user", True): + if not cfg.use_logged_in_user: args.append("--no-auto-login") # If cli_path is a .js file, run it with node @@ -1318,21 +1295,22 @@ async def _start_cli_server(self) -> None: args = [cli_path] + args # Get environment variables - env = self.options.get("env") - if env is None: + if cfg.env is None: env = dict(os.environ) else: - env = dict(env) + env = dict(cfg.env) # Set auth token in environment if provided - if self.options.get("github_token"): - env["COPILOT_SDK_AUTH_TOKEN"] = self.options["github_token"] + if cfg.github_token: + env["COPILOT_SDK_AUTH_TOKEN"] = cfg.github_token # On Windows, hide the console window to avoid distracting users in GUI apps creationflags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 + cwd = cfg.cwd or os.getcwd() + # Choose transport mode - if self.options["use_stdio"]: + if cfg.use_stdio: args.append("--stdio") # Use regular Popen with pipes (buffering=0 for unbuffered) self._process = subprocess.Popen( @@ -1341,25 +1319,25 @@ async def _start_cli_server(self) -> None: stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, - cwd=self.options["cwd"], + cwd=cwd, env=env, creationflags=creationflags, ) else: - if self.options["port"] > 0: - args.extend(["--port", str(self.options["port"])]) + if cfg.port > 0: + args.extend(["--port", str(cfg.port)]) self._process = subprocess.Popen( args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - cwd=self.options["cwd"], + cwd=cwd, env=env, creationflags=creationflags, ) # For stdio mode, we're ready immediately - if self.options["use_stdio"]: + if cfg.use_stdio: return # For TCP mode, wait for port announcement @@ -1394,7 +1372,8 @@ async def _connect_to_server(self) -> None: Raises: RuntimeError: If the connection fails. """ - if self.options["use_stdio"]: + use_stdio = isinstance(self._config, SubprocessConfig) and self._config.use_stdio + if use_stdio: await self._connect_via_stdio() else: await self._connect_via_tcp() diff --git a/python/copilot/types.py b/python/copilot/types.py index 6ac66f76..41989189 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable -from dataclasses import dataclass +from dataclasses import KW_ONLY, dataclass, field from typing import Any, Literal, NotRequired, TypedDict # Import generated SessionEvent types @@ -69,38 +69,69 @@ class SelectionAttachment(TypedDict): Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment -# Options for creating a CopilotClient -class CopilotClientOptions(TypedDict, total=False): - """Options for creating a CopilotClient""" +# Configuration for CopilotClient connection modes - cli_path: str # Path to the Copilot CLI executable (default: "copilot") - # Extra arguments to pass to the CLI executable (inserted before SDK-managed args) - cli_args: list[str] - # Working directory for the CLI process (default: current process's cwd) - cwd: str - port: int # Port for the CLI server (TCP mode only, default: 0) - use_stdio: bool # Use stdio transport instead of TCP (default: True) - cli_url: str # URL of an existing Copilot CLI server to connect to over TCP - # Format: "host:port" or "http://host:port" or just "port" (defaults to localhost) - # Examples: "localhost:8080", "http://127.0.0.1:9000", "8080" - # Mutually exclusive with cli_path, use_stdio - log_level: LogLevel # Log level - auto_start: bool # Auto-start the CLI server on first use (default: True) - env: dict[str, str] # Environment variables for the CLI process - # GitHub token to use for authentication. - # When provided, the token is passed to the CLI server via environment variable. - # This takes priority over other authentication methods. - github_token: str - # Whether to use the logged-in user for authentication. - # When True, the CLI server will attempt to use stored OAuth tokens or gh CLI auth. - # When False, only explicit tokens (github_token or environment variables) are used. - # Default: True (but defaults to False when github_token is provided) - use_logged_in_user: bool - # Custom handler for listing available models. - # When provided, client.list_models() calls this handler instead of - # querying the CLI server. Useful in BYOK mode to return models - # available from your custom provider. - on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] + +@dataclass +class SubprocessConfig: + """Config for spawning a local Copilot CLI subprocess. + + Example: + >>> config = SubprocessConfig(github_token="ghp_...") + >>> client = CopilotClient(config) + + >>> # Custom CLI path with TCP transport + >>> config = SubprocessConfig( + ... cli_path="/usr/local/bin/copilot", + ... use_stdio=False, + ... log_level="debug", + ... ) + """ + + cli_path: str | None = None + """Path to the Copilot CLI executable. ``None`` uses the bundled binary.""" + + cli_args: list[str] = field(default_factory=list) + """Extra arguments passed to the CLI executable (inserted before SDK-managed args).""" + + _: KW_ONLY + + cwd: str | None = None + """Working directory for the CLI process. ``None`` uses the current directory.""" + + use_stdio: bool = True + """Use stdio transport (``True``, default) or TCP (``False``).""" + + port: int = 0 + """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random.""" + + log_level: LogLevel = "info" + """Log level for the CLI process.""" + + env: dict[str, str] | None = None + """Environment variables for the CLI process. ``None`` inherits the current env.""" + + github_token: str | None = None + """GitHub token for authentication. Takes priority over other auth methods.""" + + use_logged_in_user: bool | None = None + """Use the logged-in user for authentication. + + ``None`` (default) resolves to ``True`` unless ``github_token`` is set. + """ + + +@dataclass +class ExternalServerConfig: + """Config for connecting to an existing Copilot CLI server over TCP. + + Example: + >>> config = ExternalServerConfig(url="localhost:3000") + >>> client = CopilotClient(config) + """ + + url: str + """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``.""" ToolResultType = Literal["success", "failure", "rejected", "denied"] diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index cee6814f..6eb07f64 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from copilot.generated.rpc import SessionAgentSelectParams from .testharness import CLI_PATH, E2ETestContext @@ -14,7 +14,7 @@ class TestAgentSelectionRpc: @pytest.mark.asyncio async def test_should_list_available_custom_agents(self): """Test listing available custom agents via RPC.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -54,7 +54,7 @@ async def test_should_list_available_custom_agents(self): @pytest.mark.asyncio async def test_should_return_null_when_no_agent_is_selected(self): """Test getCurrent returns null when no agent is selected.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -83,7 +83,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): @pytest.mark.asyncio async def test_should_select_and_get_current_agent(self): """Test selecting an agent and verifying getCurrent returns it.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -122,7 +122,7 @@ async def test_should_select_and_get_current_agent(self): @pytest.mark.asyncio async def test_should_deselect_current_agent(self): """Test deselecting the current agent.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -156,7 +156,7 @@ async def test_should_deselect_current_agent(self): @pytest.mark.asyncio async def test_should_return_empty_list_when_no_custom_agents_configured(self): """Test listing agents returns empty when none configured.""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py index 1395a388..d7ec39dc 100644 --- a/python/e2e/test_client.py +++ b/python/e2e/test_client.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler, StopError +from copilot import CopilotClient, PermissionHandler, StopError, SubprocessConfig from .testharness import CLI_PATH @@ -10,7 +10,7 @@ class TestClient: @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_stdio(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -27,7 +27,7 @@ async def test_should_start_and_connect_to_server_using_stdio(self): @pytest.mark.asyncio async def test_should_start_and_connect_to_server_using_tcp(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": False}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=False)) try: await client.start() @@ -46,7 +46,7 @@ async def test_should_start_and_connect_to_server_using_tcp(self): async def test_should_raise_exception_group_on_failed_cleanup(self): import asyncio - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) try: await client.create_session({"on_permission_request": PermissionHandler.approve_all}) @@ -70,7 +70,7 @@ async def test_should_raise_exception_group_on_failed_cleanup(self): @pytest.mark.asyncio async def test_should_force_stop_without_cleanup(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.create_session({"on_permission_request": PermissionHandler.approve_all}) await client.force_stop() @@ -78,7 +78,7 @@ async def test_should_force_stop_without_cleanup(self): @pytest.mark.asyncio async def test_should_get_status_with_version_and_protocol_info(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -96,7 +96,7 @@ async def test_should_get_status_with_version_and_protocol_info(self): @pytest.mark.asyncio async def test_should_get_auth_status(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -114,7 +114,7 @@ async def test_should_get_auth_status(self): @pytest.mark.asyncio async def test_should_list_models_when_authenticated(self): - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -142,7 +142,7 @@ async def test_should_list_models_when_authenticated(self): @pytest.mark.asyncio async def test_should_cache_models_list(self): """Test that list_models caches results to avoid rate limiting""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -187,11 +187,11 @@ async def test_should_cache_models_list(self): async def test_should_report_error_with_stderr_when_cli_fails_to_start(self): """Test that CLI startup errors include stderr output in the error message.""" client = CopilotClient( - { - "cli_path": CLI_PATH, - "cli_args": ["--nonexistent-flag-for-testing"], - "use_stdio": True, - } + SubprocessConfig( + cli_path=CLI_PATH, + cli_args=["--nonexistent-flag-for-testing"], + use_stdio=True, + ) ) try: diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index caf58cd5..5131ad2b 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -15,8 +15,10 @@ from copilot import ( CopilotClient, + ExternalServerConfig, PermissionHandler, PermissionRequestResult, + SubprocessConfig, ToolInvocation, define_tool, ) @@ -54,15 +56,15 @@ async def setup(self): ) # Client 1 uses TCP mode so a second client can connect to the same server - opts: dict = { - "cli_path": self.cli_path, - "cwd": self.work_dir, - "env": self.get_env(), - "use_stdio": False, - } - if github_token: - opts["github_token"] = github_token - self._client1 = CopilotClient(opts) + self._client1 = CopilotClient( + SubprocessConfig( + cli_path=self.cli_path, + cwd=self.work_dir, + env=self.get_env(), + use_stdio=False, + github_token=github_token, + ) + ) # Trigger connection by creating and disconnecting an init session init_session = await self._client1.create_session( @@ -74,7 +76,7 @@ async def setup(self): actual_port = self._client1.actual_port assert actual_port is not None, "Client 1 should have an actual port after connecting" - self._client2 = CopilotClient({"cli_url": f"localhost:{actual_port}"}) + self._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}")) async def teardown(self, test_failed: bool = False): if self._client2: @@ -443,7 +445,7 @@ def ephemeral_tool(params: InputParams, invocation: ToolInvocation) -> str: # Recreate client2 for future tests (but don't rejoin the session) actual_port = mctx.client1.actual_port - mctx._client2 = CopilotClient({"cli_url": f"localhost:{actual_port}"}) + mctx._client2 = CopilotClient(ExternalServerConfig(url=f"localhost:{actual_port}")) # Now only stable_tool should be available await session1.send( diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py index 1b455d63..0db2b4fe 100644 --- a/python/e2e/test_rpc.py +++ b/python/e2e/test_rpc.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from copilot.generated.rpc import PingParams from .testharness import CLI_PATH, E2ETestContext @@ -14,7 +14,7 @@ class TestRpc: @pytest.mark.asyncio async def test_should_call_rpc_ping_with_typed_params(self): """Test calling rpc.ping with typed params and result""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -30,7 +30,7 @@ async def test_should_call_rpc_ping_with_typed_params(self): @pytest.mark.asyncio async def test_should_call_rpc_models_list(self): """Test calling rpc.models.list with typed result""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -53,7 +53,7 @@ async def test_should_call_rpc_models_list(self): @pytest.mark.asyncio async def test_should_call_rpc_account_get_quota(self): """Test calling rpc.account.getQuota when authenticated""" - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -112,7 +112,7 @@ async def test_get_and_set_session_mode(self): """Test getting and setting session mode""" from copilot.generated.rpc import Mode, SessionModeSetParams - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -148,7 +148,7 @@ async def test_read_update_and_delete_plan(self): """Test reading, updating, and deleting plan""" from copilot.generated.rpc import SessionPlanUpdateParams - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() @@ -191,7 +191,7 @@ async def test_create_list_and_read_workspace_files(self): SessionWorkspaceReadFileParams, ) - client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: await client.start() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 79fb661d..a779fd07 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -4,7 +4,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from copilot.types import Tool, ToolResult from .testharness import E2ETestContext, get_final_assistant_message, get_next_event_of_type @@ -194,12 +194,12 @@ async def test_should_resume_a_session_using_a_new_client(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - { - "cli_path": ctx.cli_path, - "cwd": ctx.work_dir, - "env": ctx.get_env(), - "github_token": github_token, - } + SubprocessConfig( + cli_path=ctx.cli_path, + cwd=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) ) try: diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py index d347015a..f05b3b35 100644 --- a/python/e2e/test_streaming_fidelity.py +++ b/python/e2e/test_streaming_fidelity.py @@ -4,7 +4,7 @@ import pytest -from copilot import CopilotClient, PermissionHandler +from copilot import CopilotClient, PermissionHandler, SubprocessConfig from .testharness import E2ETestContext @@ -77,12 +77,12 @@ async def test_should_produce_deltas_after_session_resume(self, ctx: E2ETestCont "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) new_client = CopilotClient( - { - "cli_path": ctx.cli_path, - "cwd": ctx.work_dir, - "env": ctx.get_env(), - "github_token": github_token, - } + SubprocessConfig( + cli_path=ctx.cli_path, + cwd=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) ) try: diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index c0308891..27dce38a 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -10,7 +10,7 @@ import tempfile from pathlib import Path -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig from .proxy import CapiProxy @@ -64,12 +64,12 @@ async def setup(self): "fake-token-for-e2e-tests" if os.environ.get("GITHUB_ACTIONS") == "true" else None ) self._client = CopilotClient( - { - "cli_path": self.cli_path, - "cwd": self.work_dir, - "env": self.get_env(), - "github_token": github_token, - } + SubprocessConfig( + cli_path=self.cli_path, + cwd=self.work_dir, + env=self.get_env(), + github_token=github_token, + ) ) async def teardown(self, test_failed: bool = False): diff --git a/python/test_client.py b/python/test_client.py index 62ae7b18..9b7e8eb0 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,7 +6,14 @@ import pytest -from copilot import CopilotClient, PermissionHandler, PermissionRequestResult, define_tool +from copilot import ( + CopilotClient, + ExternalServerConfig, + PermissionHandler, + PermissionRequestResult, + SubprocessConfig, + define_tool, +) from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports from e2e.testharness import CLI_PATH @@ -14,7 +21,7 @@ class TestPermissionHandlerRequired: @pytest.mark.asyncio async def test_create_session_raises_without_permission_handler(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: with pytest.raises(ValueError, match="on_permission_request.*is required"): @@ -24,7 +31,7 @@ async def test_create_session_raises_without_permission_handler(self): @pytest.mark.asyncio async def test_v2_permission_adapter_rejects_no_result(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(CLI_PATH)) await client.start() try: session = await client.create_session( @@ -46,7 +53,7 @@ async def test_v2_permission_adapter_rejects_no_result(self): @pytest.mark.asyncio async def test_resume_session_raises_without_permission_handler(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: session = await client.create_session( @@ -60,123 +67,106 @@ async def test_resume_session_raises_without_permission_handler(self): class TestURLParsing: def test_parse_port_only_url(self): - client = CopilotClient({"cli_url": "8080", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="8080")) assert client._actual_port == 8080 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_host_port_url(self): - client = CopilotClient({"cli_url": "127.0.0.1:9000", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="127.0.0.1:9000")) assert client._actual_port == 9000 assert client._actual_host == "127.0.0.1" assert client._is_external_server def test_parse_http_url(self): - client = CopilotClient({"cli_url": "http://localhost:7000", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="http://localhost:7000")) assert client._actual_port == 7000 assert client._actual_host == "localhost" assert client._is_external_server def test_parse_https_url(self): - client = CopilotClient({"cli_url": "https://example.com:443", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="https://example.com:443")) assert client._actual_port == 443 assert client._actual_host == "example.com" assert client._is_external_server def test_invalid_url_format(self): with pytest.raises(ValueError, match="Invalid cli_url format"): - CopilotClient({"cli_url": "invalid-url", "log_level": "error"}) + CopilotClient(ExternalServerConfig(url="invalid-url")) def test_invalid_port_too_high(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient({"cli_url": "localhost:99999", "log_level": "error"}) + CopilotClient(ExternalServerConfig(url="localhost:99999")) def test_invalid_port_zero(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient({"cli_url": "localhost:0", "log_level": "error"}) + CopilotClient(ExternalServerConfig(url="localhost:0")) def test_invalid_port_negative(self): with pytest.raises(ValueError, match="Invalid port in cli_url"): - CopilotClient({"cli_url": "localhost:-1", "log_level": "error"}) - - def test_cli_url_with_use_stdio(self): - with pytest.raises(ValueError, match="cli_url is mutually exclusive"): - CopilotClient({"cli_url": "localhost:8080", "use_stdio": True, "log_level": "error"}) - - def test_cli_url_with_cli_path(self): - with pytest.raises(ValueError, match="cli_url is mutually exclusive"): - CopilotClient( - {"cli_url": "localhost:8080", "cli_path": "/path/to/cli", "log_level": "error"} - ) - - def test_use_stdio_false_when_cli_url(self): - client = CopilotClient({"cli_url": "8080", "log_level": "error"}) - assert not client.options["use_stdio"] + CopilotClient(ExternalServerConfig(url="localhost:-1")) def test_is_external_server_true(self): - client = CopilotClient({"cli_url": "localhost:8080", "log_level": "error"}) + client = CopilotClient(ExternalServerConfig(url="localhost:8080")) assert client._is_external_server class TestAuthOptions: def test_accepts_github_token(self): client = CopilotClient( - {"cli_path": CLI_PATH, "github_token": "gho_test_token", "log_level": "error"} + SubprocessConfig( + cli_path=CLI_PATH, + github_token="gho_test_token", + log_level="error", + ) ) - assert client.options.get("github_token") == "gho_test_token" + assert isinstance(client._config, SubprocessConfig) + assert client._config.github_token == "gho_test_token" def test_default_use_logged_in_user_true_without_token(self): - client = CopilotClient({"cli_path": CLI_PATH, "log_level": "error"}) - assert client.options.get("use_logged_in_user") is True + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, log_level="error")) + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is True def test_default_use_logged_in_user_false_with_token(self): client = CopilotClient( - {"cli_path": CLI_PATH, "github_token": "gho_test_token", "log_level": "error"} + SubprocessConfig( + cli_path=CLI_PATH, + github_token="gho_test_token", + log_level="error", + ) ) - assert client.options.get("use_logged_in_user") is False + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is False def test_explicit_use_logged_in_user_true_with_token(self): client = CopilotClient( - { - "cli_path": CLI_PATH, - "github_token": "gho_test_token", - "use_logged_in_user": True, - "log_level": "error", - } + SubprocessConfig( + cli_path=CLI_PATH, + github_token="gho_test_token", + use_logged_in_user=True, + log_level="error", + ) ) - assert client.options.get("use_logged_in_user") is True + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is True def test_explicit_use_logged_in_user_false_without_token(self): client = CopilotClient( - {"cli_path": CLI_PATH, "use_logged_in_user": False, "log_level": "error"} - ) - assert client.options.get("use_logged_in_user") is False - - def test_github_token_with_cli_url_raises(self): - with pytest.raises( - ValueError, match="github_token and use_logged_in_user cannot be used with cli_url" - ): - CopilotClient( - { - "cli_url": "localhost:8080", - "github_token": "gho_test_token", - "log_level": "error", - } - ) - - def test_use_logged_in_user_with_cli_url_raises(self): - with pytest.raises( - ValueError, match="github_token and use_logged_in_user cannot be used with cli_url" - ): - CopilotClient( - {"cli_url": "localhost:8080", "use_logged_in_user": False, "log_level": "error"} + SubprocessConfig( + cli_path=CLI_PATH, + use_logged_in_user=False, + log_level="error", ) + ) + assert isinstance(client._config, SubprocessConfig) + assert client._config.use_logged_in_user is False class TestOverridesBuiltInTool: @pytest.mark.asyncio async def test_overrides_built_in_tool_sent_in_tool_definition(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -205,7 +195,7 @@ def grep(params) -> str: @pytest.mark.asyncio async def test_resume_session_sends_overrides_built_in_tool(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -258,7 +248,10 @@ def handler(): handler_calls.append(1) return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) await client.start() try: models = await client.list_models() @@ -287,7 +280,10 @@ def handler(): handler_calls.append(1) return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) await client.start() try: await client.list_models() @@ -313,7 +309,10 @@ async def test_list_models_async_handler(self): async def handler(): return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) await client.start() try: models = await client.list_models() @@ -341,7 +340,10 @@ def handler(): handler_calls.append(1) return custom_models - client = CopilotClient({"cli_path": CLI_PATH, "on_list_models": handler}) + client = CopilotClient( + SubprocessConfig(cli_path=CLI_PATH), + on_list_models=handler, + ) models = await client.list_models() assert len(handler_calls) == 1 assert models == custom_models @@ -350,7 +352,7 @@ def handler(): class TestSessionConfigForwarding: @pytest.mark.asyncio async def test_create_session_forwards_client_name(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -371,7 +373,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_client_name(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -400,7 +402,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_create_session_forwards_agent(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -425,7 +427,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_resume_session_forwards_agent(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: @@ -457,7 +459,7 @@ async def mock_request(method, params): @pytest.mark.asyncio async def test_set_model_sends_correct_rpc(self): - client = CopilotClient({"cli_path": CLI_PATH}) + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) await client.start() try: diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py index e50a33c1..5b82d592 100644 --- a/test/scenarios/auth/byok-anthropic/python/main.py +++ b/test/scenarios/auth/byok-anthropic/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") @@ -13,10 +13,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py index 89f37178..b6dcc869 100644 --- a/test/scenarios/auth/byok-azure/python/main.py +++ b/test/scenarios/auth/byok-azure/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT") AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY") @@ -14,10 +14,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py index b86c76ba..38546268 100644 --- a/test/scenarios/auth/byok-ollama/python/main.py +++ b/test/scenarios/auth/byok-ollama/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1") OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b") @@ -12,10 +12,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py index b501bb10..455288f6 100644 --- a/test/scenarios/auth/byok-openai/python/main.py +++ b/test/scenarios/auth/byok-openai/python/main.py @@ -1,7 +1,7 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5") @@ -13,10 +13,9 @@ async def main(): - opts = {} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py index 4886fe07..8295c73d 100644 --- a/test/scenarios/auth/gh-app/python/main.py +++ b/test/scenarios/auth/gh-app/python/main.py @@ -4,7 +4,7 @@ import time import urllib.request -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig DEVICE_CODE_URL = "https://github.com/login/device/code" @@ -78,10 +78,10 @@ async def main(): display_name = f" ({user.get('name')})" if user.get("name") else "" print(f"Authenticated as: {user.get('login')}{display_name}") - opts = {"github_token": token} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=token, + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py index 29563149..e4c45dea 100644 --- a/test/scenarios/bundling/app-backend-to-server/python/main.py +++ b/test/scenarios/bundling/app-backend-to-server/python/main.py @@ -5,7 +5,7 @@ import urllib.request from flask import Flask, request, jsonify -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig app = Flask(__name__) @@ -13,7 +13,7 @@ async def ask_copilot(prompt: str) -> str: - client = CopilotClient({"cli_url": CLI_URL}) + client = CopilotClient(ExternalServerConfig(url=CLI_URL)) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py index c407d4fe..bbf6cf20 100644 --- a/test/scenarios/bundling/app-direct-server/python/main.py +++ b/test/scenarios/bundling/app-direct-server/python/main.py @@ -1,12 +1,12 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py index c407d4fe..bbf6cf20 100644 --- a/test/scenarios/bundling/container-proxy/python/main.py +++ b/test/scenarios/bundling/container-proxy/python/main.py @@ -1,12 +1,12 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index d1441361..26a2cd17 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index 8df61b9d..5f7bc916 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig hook_log: list[str] = [] @@ -40,10 +40,10 @@ async def on_error_occurred(input_data, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 9674da91..2ff25380 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig # Track which tools requested permission permission_log: list[str] = [] @@ -16,10 +16,10 @@ async def auto_approve_tool(input_data, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index dc8d9fa9..683f11d8 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig input_log: list[str] = [] @@ -20,10 +20,10 @@ async def handle_user_input(request, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index dadc0e7b..45063b29 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 0b243caf..a8cf1edc 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index c7e21e8b..31df91c8 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -1,15 +1,15 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely.""" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index b38452a8..38675f14 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 5e396c8c..b4f5caff 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,15 +1,15 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.""" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index ebca8990..07babc21 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,16 +1,16 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig PIRATE_PROMPT = "You are a pirate. Always say Arrr!" ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session1, session2 = await asyncio.gather( diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index 23749d06..0bd69d81 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({ diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 7eb5e0ca..df5eb33e 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: # 1. Create a session diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 94569de1..aff9d24d 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index 0b5f073d..5d83380d 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index f092fb9a..daf7c726 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: # MCP server config — demonstrates the configuration pattern. diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index a3824bab..b4fc620a 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig SYSTEM_PROMPT = """You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -9,10 +9,10 @@ async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 3e06650b..396e3365 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -2,14 +2,14 @@ import os from pathlib import Path -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 1fdfacc7..9a6e1054 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,15 +1,15 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.""" async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 1f1099f0..89bd41e4 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -from copilot import CopilotClient, PermissionHandler, define_tool +from copilot import CopilotClient, PermissionHandler, SubprocessConfig, define_tool class GrepParams(BaseModel): @@ -16,10 +16,10 @@ def custom_grep(params: GrepParams) -> str: async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 9a51e7ef..e8317c71 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -1,6 +1,6 @@ import asyncio import os -from copilot import CopilotClient, define_tool +from copilot import CopilotClient, SubprocessConfig, define_tool from pydantic import BaseModel, Field # In-memory virtual filesystem @@ -46,10 +46,10 @@ async def auto_approve_tool(input_data, invocation): async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session( diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py index 1b82b109..bb60aabf 100644 --- a/test/scenarios/transport/reconnect/python/main.py +++ b/test/scenarios/transport/reconnect/python/main.py @@ -1,13 +1,13 @@ import asyncio import os import sys -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: # First session diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index d1441361..26a2cd17 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,13 +1,13 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, SubprocessConfig async def main(): - opts = {"github_token": os.environ.get("GITHUB_TOKEN")} - if os.environ.get("COPILOT_CLI_PATH"): - opts["cli_path"] = os.environ["COPILOT_CLI_PATH"] - client = CopilotClient(opts) + client = CopilotClient(SubprocessConfig( + github_token=os.environ.get("GITHUB_TOKEN"), + cli_path=os.environ.get("COPILOT_CLI_PATH"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"}) diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py index c407d4fe..bbf6cf20 100644 --- a/test/scenarios/transport/tcp/python/main.py +++ b/test/scenarios/transport/tcp/python/main.py @@ -1,12 +1,12 @@ import asyncio import os -from copilot import CopilotClient +from copilot import CopilotClient, ExternalServerConfig async def main(): - client = CopilotClient({ - "cli_url": os.environ.get("COPILOT_CLI_URL", "localhost:3000"), - }) + client = CopilotClient(ExternalServerConfig( + url=os.environ.get("COPILOT_CLI_URL", "localhost:3000"), + )) try: session = await client.create_session({"model": "claude-haiku-4.5"})