diff --git a/src/together/lib/cli/utils/_preparse_tokens.py b/src/together/lib/cli/utils/_preparse_tokens.py index 4c2910800..293f899c0 100644 --- a/src/together/lib/cli/utils/_preparse_tokens.py +++ b/src/together/lib/cli/utils/_preparse_tokens.py @@ -97,17 +97,18 @@ def _long_option_names_in_tokens(tokens: list[str]) -> list[str]: return names -def _legacy_command_before_first_option(tokens: list[str]) -> tuple[str, bool]: +def _unknown_command_fallback(tokens: list[str]) -> tuple[str, bool]: """Fallback when cyclopts cannot resolve a command chain (unknown invocations).""" - parts: list[str] = [] - for token in tokens: - if token.startswith("--"): - break - parts.append(token) - is_beta_command = bool(parts and parts[0] == "beta") - if is_beta_command: - parts = parts[1:] - return (" ".join(parts), is_beta_command) + is_beta_command = bool(tokens and tokens[0] == "beta") + command_index = 1 if is_beta_command else 0 + if command_index >= len(tokens) or tokens[command_index].startswith("-"): + return ("", is_beta_command) + + # Once command resolution fails, every later token may be an argument value. + # Keep only the unknown subcommand, including when a caller supplied a quoted + # invocation as one token. + command = tokens[command_index].split(maxsplit=1)[0] + return (command, is_beta_command) # First subcommand token only (alias -> primary name) for stable telemetry. @@ -149,7 +150,7 @@ def preparse_tokens(app: App, tokens: list[str]) -> tuple[str, list[str], bool, argv = list(tokens) argv = _expand_implicit_retrieve_tokens(app, *argv) chain, apps, rest_after_chain = app.parse_commands(argv, include_parent_meta=False) - legacy_cmd, legacy_beta = _legacy_command_before_first_option(argv) + fallback_cmd, fallback_beta = _unknown_command_fallback(argv) if chain: is_beta_command = chain[0] == "beta" @@ -157,10 +158,10 @@ def preparse_tokens(app: App, tokens: list[str]) -> tuple[str, list[str], bool, parsed_command = " ".join(chain_tail) # ``beta`` alone matches first; remaining tokens are not nested beta subcommands (invalid path). if chain == ("beta",) and rest_after_chain: - parsed_command = legacy_cmd + parsed_command = fallback_cmd else: - parsed_command = legacy_cmd - is_beta_command = legacy_beta + parsed_command = fallback_cmd + is_beta_command = fallback_beta # App.default handlers (e.g. beta endpoints retrieve) don't rewrite argv; still tag telemetry. parsed_command = _telemetry_command_for_default(parsed_command, apps, rest_after_chain) diff --git a/tests/cli/test_command_telemetry.py b/tests/cli/test_command_telemetry.py index 83e990112..a2e5c893b 100644 --- a/tests/cli/test_command_telemetry.py +++ b/tests/cli/test_command_telemetry.py @@ -195,6 +195,22 @@ def test_unknown_option_equals_value_telemetry_strips_value( assert "hunter2secret" not in failed["error"] +@pytest.mark.usefixtures("isolated_cli_config") +def test_unknown_command_telemetry_omits_argument_values( + track_cli_capture: list[tuple[CliTrackingEvents, dict[str, Any]]], + cli_runner: CliRunner, +) -> None: + r = cli_runner.invoke(["chat", "-m", "private-model", "-p", "private prompt"]) + assert r.exit_code == 1 + assert _event_kinds(track_cli_capture) == [ + CliTrackingEvents.CommandStarted.value, + CliTrackingEvents.CommandFailed.value, + ] + for _, payload in track_cli_capture: + assert payload["command"] == "chat" + assert "private" not in repr(payload) + + @pytest.mark.usefixtures("isolated_cli_config") def test_command_keyboard_interrupt_emits_started_then_user_aborted( track_cli_capture: list[tuple[CliTrackingEvents, dict[str, Any]]], diff --git a/tests/unit/test_cli_telemetry.py b/tests/unit/test_cli_telemetry.py index bd7dab89e..2d447c4b8 100644 --- a/tests/unit/test_cli_telemetry.py +++ b/tests/unit/test_cli_telemetry.py @@ -445,6 +445,29 @@ def test_parse_command_and_flags_positionals_are_argument_names_not_command_toke assert is_beta is True +@pytest.mark.parametrize( + ("tokens", "expected_command", "expected_beta"), + [ + (["chat", "-m", "private-model", "-p", "private prompt"], "chat", False), + (["chat", "private-model", "private prompt"], "chat", False), + (["chat -m private-model -p private-prompt"], "chat", False), + (["beta", "unknown", "private-value"], "unknown", True), + ], +) +def test_parse_unknown_command_does_not_treat_argument_values_as_command_tokens( + tokens: list[str], + expected_command: str, + expected_beta: bool, +) -> None: + from together.lib.cli import app + from together.lib.cli.utils._preparse_tokens import preparse_tokens + + cmd, _, is_beta, _ = preparse_tokens(app, tokens) + assert cmd == expected_command + assert is_beta is expected_beta + assert "private" not in cmd + + def test_telemetry_status_no_api_key(monkeypatch: pytest.MonkeyPatch, cli_runner: CliRunner) -> None: monkeypatch.delenv("TOGETHER_API_KEY", raising=False) monkeypatch.delenv("TOGETHER_TELEMETRY_DISABLED", raising=False)