spacr.qt.ai.providers

Provider abstraction — one class per AI vendor. Each shells out to the vendor’s own coding-agent CLI so authentication piggy-backs on the user’s chat subscription (Claude.ai Pro, ChatGPT Plus/Pro/Team, Google account) — no separate API billing.

  • Anthropic Claude → the claude CLI (“Claude Code”)

  • OpenAI ChatGPT → the codex CLI

  • Google Gemini → the gemini CLI

Each provider:

is_installed()   — is the CLI on PATH?
is_logged_in()   — best-effort check; falls back to "assume yes if
                   installed" (the actual auth error surfaces on
                   the first stream chunk).
stream_chat()    — spawn the CLI subprocess, yield stdout chunks.

Conversation context is carried by concatenating the full message history into each prompt (simplest approach that works uniformly across all three CLIs). For subscription users token count is not a concern.

How each CLI is installed is data, not prose: INSTALL_METHODS holds one row per way of installing it on each operating system, and both the install_hint a screen shows and the command spacr.qt.ai.cli_install runs are read from those rows. The GitHub CLI is described the same way by GitHubCli, which shares the CommandLineTool interface without being a chat provider.

Exceptions

ProviderFailed

A provider CLI exited with a non-zero status, so what it printed is an

Classes

ChatProvider

Abstract base for AI chat providers that shell out to a vendor CLI.

ClaudeCliProvider

Anthropic Claude via the claude (Claude Code) CLI.

CodexCliProvider

OpenAI ChatGPT via the codex CLI.

CommandLineTool

A vendor command-line tool spaCR can find, install and sign in to.

GeminiCliProvider

Google Gemini via the gemini CLI.

GitHubCli

The GitHub CLI, gh: installed and signed in like a provider.

InstallMethod

One way of installing a command-line tool on one operating system.

Functions

configured_providers(→ List[ChatProvider])

Return only providers whose CLI is installed and logged in.

get_provider(→ Optional[ChatProvider])

Look up a registered provider by its short id.

gh_conda_row(→ InstallMethod)

The row that installs the GitHub CLI with conda.

github_cli(→ GitHubCli)

The GitHub CLI, described the way the AI providers are.

install_hint_for(→ str)

The one-line install hint for tool name on platform.

install_methods_for(→ Tuple[InstallMethod, ...])

The ways of installing tool name on platform, in order.

list_providers(→ List[ChatProvider])

Return every registered provider, regardless of install state.

platform_family(→ str)

Name the operating system INSTALL_METHODS is keyed by.

quote_path(→ str)

A path as it is typed in a command on platform.

terminate_all_streams(→ int)

Terminate active provider subprocesses and release their readers.

Module Contents

exception spacr.qt.ai.providers.ProviderFailed(cli: str, exit_status: int, output_tail: str, login_command: str = '')[source]

Bases: RuntimeError

A provider CLI exited with a non-zero status, so what it printed is an error message and not an answer.

The three vendor CLIs report a failure the way any command-line tool does: a line on stdout or stderr, then a non-zero exit. A signed-out claude prints Not logged in · Please run /login and exits 1. In GitHub #117 an expired one printed Failed to authenticate: OAuth session expired and could not be refreshed. Streamed as if it were a reply, that line was shown as spaCR AI’s answer to a crash, and it was filed into GitHub issues as “spaCR AI’s analysis of this error”.

Parameters:
  • cli – the executable that failed, for the message.

  • exit_status – its exit status.

  • output_tail – the last lines it printed, already stripped.

  • login_command – the command that signs in to this provider, or "" when the caller did not say which provider this was.

Variables:
  • exit_status – the exit status, for a caller that needs the number.

  • output_tail – the quoted output, for a caller that needs the text.

Build the message a user reads after [AI error].

class spacr.qt.ai.providers.ChatProvider[source]

Bases: CommandLineTool, abc.ABC

Abstract base for AI chat providers that shell out to a vendor CLI.

Subclasses set the name/label/cli_name/install_hint/ login_command class attributes and implement stream_chat().

Variables:
  • name – short id (“claude” / “codex” / “gemini”).

  • label – human-readable label shown in the UI.

  • cli_name – executable expected on PATH.

  • install_hint – shell one-liner suggested for installation, built from install_methods.

  • login_command – shell one-liner the user runs to authenticate.

Create the provider with no child process running.

The running process is tracked so that cancelling a stream can actually terminate it – otherwise iterating the child’s stdout blocks indefinitely and the worker thread never exits.

cancel_stream() → None[source]

Kill the running subprocess (if any).

This is the ONLY reliable way to unblock a stream that’s stuck waiting on stdout — flipping a Python flag would only unblock between chunks, which may never come.

is_configured() → bool[source]

Return True when the CLI is both installed and logged in.

is_logged_in() → bool[source]

Best-effort — override per provider if a cheap check exists.

Default: assume yes when installed. The real auth error will surface as a normal subprocess failure on the first send.

source_of_key() → str[source]

Compat string for the old KeysDialog — now describes the CLI’s install/login state.

abstract stream_chat(messages: List[Dict], system: str = '', model: str | None = None) → Iterator[str][source]

Yield text chunks streaming from the CLI subprocess.

Parameters:

messages – conversation history as {role, content} dicts, oldest first; each implementation formats it into the CLI prompt.

class spacr.qt.ai.providers.ClaudeCliProvider[source]

Bases: ChatProvider

Anthropic Claude via the claude (Claude Code) CLI.

Create the provider with no child process running.

The running process is tracked so that cancelling a stream can actually terminate it – otherwise iterating the child’s stdout blocks indefinitely and the worker thread never exits.

stream_chat(messages: List[Dict], system: str = '', model: str | None = None) → Iterator[str][source]

Stream a chat completion from the claude CLI.

Parameters:
  • messages – conversation history as {role, content} dicts.

  • system – optional system prompt appended via --append-system-prompt.

  • model – optional model override passed via --model. When None the current response-speed setting supplies one.

Returns:

iterator yielding stdout text chunks.

class spacr.qt.ai.providers.CodexCliProvider[source]

Bases: ChatProvider

OpenAI ChatGPT via the codex CLI.

Create the provider with no child process running.

The running process is tracked so that cancelling a stream can actually terminate it – otherwise iterating the child’s stdout blocks indefinitely and the worker thread never exits.

stream_chat(messages: List[Dict], system: str = '', model: str | None = None) → Iterator[str][source]

Stream a chat completion from the codex CLI.

Parameters:
  • messages – conversation history as {role, content} dicts.

  • system – optional system prompt folded into the prompt body.

  • model – optional model override passed via --model. When None the current response-speed setting supplies one.

Returns:

iterator yielding stdout text chunks.

class spacr.qt.ai.providers.CommandLineTool[source]

A vendor command-line tool spaCR can find, install and sign in to.

The AI providers and the GitHub CLI share this, so “is it there?” and “is it signed in?” are asked one way for all four.

Variables:
  • name – short id, and the key of INSTALL_METHODS.

  • label – human-readable label shown in the UI.

  • cli_name – executable expected on PATH.

  • install_methods – the ways of installing it on this system, tried in order.

  • install_hint – the one-line hint built from install_methods.

  • login_command – the command a user runs to sign in.

  • status_command – a command that exits 0 when the tool is signed in, or () when the tool has none.

check_signed_in() → bool | None[source]

Ask the tool itself whether it is signed in.

Runs status_command with its output discarded – for the GitHub CLI that output is the token – so call it off the GUI thread.

Returns:

True when the command exits 0, False when it exits otherwise, cannot be started or runs past STATUS_TIMEOUT_S, and None when the tool has no status command to ask.

is_installed() → bool[source]

Return True when the tool’s executable is on PATH.

class spacr.qt.ai.providers.GeminiCliProvider[source]

Bases: ChatProvider

Google Gemini via the gemini CLI.

Create the provider with no child process running.

The running process is tracked so that cancelling a stream can actually terminate it – otherwise iterating the child’s stdout blocks indefinitely and the worker thread never exits.

stream_chat(messages: List[Dict], system: str = '', model: str | None = None) → Iterator[str][source]

Stream a chat completion from the gemini CLI.

Parameters:
  • messages – conversation history as {role, content} dicts.

  • system – optional system prompt folded into the prompt body.

  • model – optional model override passed via -m. When None the current response-speed setting supplies one.

Returns:

iterator yielding stdout text chunks.

class spacr.qt.ai.providers.GitHubCli[source]

Bases: CommandLineTool

The GitHub CLI, gh: installed and signed in like a provider.

spaCR reads a token from it (spacr.qt.ai.github_auth) to file an issue without a browser round-trip. Signing in is gh auth login; gh auth token exits 0 exactly when there is a token to read.

is_logged_in() → bool[source]

Return True when gh auth token has a token to give.

Runs a process, so call it off the GUI thread.

class spacr.qt.ai.providers.InstallMethod[source]

Bases: NamedTuple

One way of installing a command-line tool on one operating system.

Variables:
  • needs – executables that must already be on PATH for this way to work, such as ("npm",).

  • command – the command as a user would type it.

  • runner – how spacr.qt.ai.cli_install starts it: "exec" runs the command’s first word as a program with the rest as its arguments, "shell" hands the command to bash -o pipefail -c (for a pipe such as curl ... | bash), and "cmd" runs it as cmd /c "<command>" on Windows.

property shown: str[source]

The command as it is shown on screen and copied.

Returns:

cmd /c "<command>" for the "cmd" runner, because that line runs whole in both Windows shells; otherwise the command itself.

spacr.qt.ai.providers.configured_providers() → List[ChatProvider][source]

Return only providers whose CLI is installed and logged in.

spacr.qt.ai.providers.get_provider(name: str) → ChatProvider | None[source]

Look up a registered provider by its short id.

Parameters:

name – provider id ("claude", "codex", "gemini").

Returns:

the matching provider, or None if no such id.

spacr.qt.ai.providers.gh_conda_row(prefix: str, platform: str) → InstallMethod[source]

The row that installs the GitHub CLI with conda.

Parameters:
  • prefix – spaCR’s own environment, sys.prefix.

  • platform – a sys.platform value, for quoting prefix.

Returns:

when prefix is a conda environment, a command that names it with --prefix and adds gh with --freeze-installed, so the command on screen says which environment changes and conda does not update the packages spaCR runs on; otherwise conda’s plain command, which installs into whichever environment conda treats as active.

spacr.qt.ai.providers.github_cli() → GitHubCli[source]

The GitHub CLI, described the way the AI providers are.

spacr.qt.ai.providers.install_hint_for(name: str, platform: str) → str[source]

The one-line install hint for tool name on platform.

Built from the same rows the Install button runs, so the two cannot drift apart. On macOS and Linux the alternatives are joined by three spaces and then # or, which a POSIX shell reads as a comment; on Windows only the first row is shown, because cmd has no such comment and would hand the words after # to the installer as arguments.

Parameters:
  • name – a key of INSTALL_METHODS.

  • platform – a sys.platform value.

Returns:

the hint, or "" when the tool has no rows.

spacr.qt.ai.providers.install_methods_for(name: str, platform: str) → Tuple[InstallMethod, ...][source]

The ways of installing tool name on platform, in order.

Parameters:
  • name – a key of INSTALL_METHODS, such as "claude".

  • platform – a sys.platform value.

Returns:

the rows, or () for a tool spaCR cannot install.

spacr.qt.ai.providers.list_providers() → List[ChatProvider][source]

Return every registered provider, regardless of install state.

spacr.qt.ai.providers.platform_family(platform: str) → str[source]

Name the operating system INSTALL_METHODS is keyed by.

Parameters:

platform – a sys.platform value.

Returns:

"win32" for any Windows value, "darwin" for macOS, and "linux" for everything else.

spacr.qt.ai.providers.quote_path(path: str, platform: str) → str[source]

A path as it is typed in a command on platform.

Parameters:
  • path – the path.

  • platform – a sys.platform value.

Returns:

on Windows the path in double quotes when it holds a space, else unchanged; elsewhere quoted for a POSIX shell.

spacr.qt.ai.providers.terminate_all_streams() → int[source]

Terminate active provider subprocesses and release their readers.

Returns:

Number of subprocesses for which termination was requested.