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
claudeCLI (“Claude Code”)OpenAI ChatGPT → the
codexCLIGoogle Gemini → the
geminiCLI
A fourth, HostedProvider, needs no CLI and no account: it asks
spaCR’s relay, which answers with a free hosted model, and it is the default
when none of the three is installed and signed in (default_provider()).
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¶
spaCR's relay gave no advice: its free daily limit, or no connection. |
|
A provider CLI exited with a non-zero status, so what it printed is an |
Classes¶
Abstract base for AI chat providers that shell out to a vendor CLI. |
|
Anthropic Claude via the |
|
OpenAI ChatGPT via the |
|
A vendor command-line tool spaCR can find, install and sign in to. |
|
Google Gemini via the |
|
The GitHub CLI, |
|
spaCR's hosted assistant: advice from spaCR's relay, no account needed. |
|
One way of installing a command-line tool on one operating system. |
Functions¶
|
Save a provider choice and use it at once in every open console. |
|
Return the providers that can answer now, spaCR AI (free) first. |
|
Return the provider the AI switch uses. |
|
One line naming what a denied tool call would have done. |
|
Return the name of the provider Preferences shows as chosen. |
|
Have the active AI explain a failure once, for the console and the issue. |
|
Look up a provider by its short id. |
|
The row that installs the GitHub CLI with conda. |
|
The GitHub CLI, described the way the AI providers are. |
|
One line saying how many free hosted-AI questions are left today. |
|
The hint for connecting a CLI that uses the user's own provider account. |
|
The free allowance of spaCR AI, as Preferences states it. |
|
Return spaCR's hosted provider. |
|
The sentence shown when the hosted provider gives no advice. |
|
The one-line install hint for tool |
|
The ways of installing tool |
|
Return every vendor CLI provider, regardless of install state. |
|
The time the free allowances reset, in this computer's local time. |
|
Name the operating system |
|
The key a fresh question's answer is saved under on this computer. |
|
A path as it is typed in a command on |
|
Terminate active provider subprocesses and release their readers. |
|
The |
|
What the usage bar under the AI chat box shows for a provider. |
|
Return the report with the AI summary its provider allows. |
|
Have the user's own AI summarise an anonymised report, or return |
Module Contents¶
- exception spacr.qt.ai.providers.HostedProviderRefused(message: str, reason: str = 'error')[source]¶
Bases:
ProviderFailedspaCR’s relay gave no advice: its free daily limit, or no connection.
A
ProviderFailed, so the console writes the message as it stands after “[AI error]”.- Parameters:
message – the sentence the user reads.
reason –
"quota","network"or"error", fromspacr.crashreport.RelayRefused, kept on the exception asreason.
Keep the reason; the message is the whole text shown.
- exception spacr.qt.ai.providers.ProviderFailed(cli: str, exit_status: int, output_tail: str, login_command: str = '')[source]¶
Bases:
RuntimeErrorA 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
claudeprintsNot logged in · Please run /loginand exits 1. In GitHub #117 an expired one printedFailed 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.ABCAbstract base for AI chat providers that shell out to a vendor CLI.
Subclasses set the
name/label/cli_name/install_hint/login_commandclass attributes and implementstream_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).
Terminating the process is what unblocks a stream that is waiting on stdout; a Python flag is only checked between chunks, and the next chunk may never arrive.
- 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.
- class spacr.qt.ai.providers.ClaudeCliProvider[source]¶
Bases:
ChatProviderAnthropic 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.
- allow_once(denials: List[Dict], session_id: str) List[str][source]¶
Let the next
stream_chat()resumesession_idwith these tool calls allowed, for that one reply only.- Parameters:
denials – entries of
last_permission_denialsthe user approved.session_id – the session they were denied in.
- Returns:
the
--allowedToolsrules that will be passed.
- stream_chat(messages: List[Dict], system: str = '', model: str | None = None) Iterator[str][source]¶
Stream a chat completion from the
claudeCLI.- 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:
ChatProviderOpenAI ChatGPT via the
codexCLI.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
codexCLI.- 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_commandwith its output discarded – for the GitHub CLI that output is the token – so call it off the GUI thread.- Returns:
Truewhen the command exits 0,Falsewhen it exits otherwise, cannot be started or runs pastSTATUS_TIMEOUT_S, andNonewhen the tool has no status command to ask.
- class spacr.qt.ai.providers.GeminiCliProvider[source]¶
Bases:
ChatProviderGoogle Gemini via the
geminiCLI.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
geminiCLI.- 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:
CommandLineToolThe 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 isgh auth login;gh auth tokenexits 0 exactly when there is a token to read.
- class spacr.qt.ai.providers.HostedProvider[source]¶
Bases:
ChatProviderspaCR’s hosted assistant: advice from spaCR’s relay, no account needed.
It sends only what
_hosted_request()keeps, the anonymised error summary and the question, to the relay’s/diagnoseroute throughspacr.crashreport.diagnose_remotely(). The answer is generated advice and is labelled as such. It is the provider used when no vendor CLI is installed and signed in (default_provider()), and it is unavailable when the network is forbidden (spacr.crashreport.network_allowed()).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]¶
Ask the relay and yield its advice, then a line saying what it is.
- Parameters:
messages – conversation history as
{role, content}dicts; only the anonymised summary and the newest question leave this computer.system – ignored; the relay holds spaCR’s own prompt.
model – ignored; the relay chooses its free model.
- Returns:
iterator yielding the answer.
- Raises:
HostedProviderRefused – with a message that says the free daily limit is reached, or that the relay could not be reached.
- class spacr.qt.ai.providers.InstallMethod[source]¶
Bases:
NamedTupleOne way of installing a command-line tool on one operating system.
- Variables:
needs – executables that must already be on
PATHfor this way to work, such as("npm",).command – the command as a user would type it.
runner – how
spacr.qt.ai.cli_installstarts it:"exec"runs the command’s first word as a program with the rest as its arguments,"shell"hands the command tobash -o pipefail -c(for a pipe such ascurl ... | bash), and"cmd"runs it ascmd /c "<command>"on Windows.
- spacr.qt.ai.providers.apply_provider_choice(name: str) bool[source]¶
Save a provider choice and use it at once in every open console.
A vendor CLI that is not installed and signed in is saved but not applied; the consoles keep spaCR AI (free) until it is set up.
- Parameters:
name – a name from
PROVIDER_CHOICES.- Returns:
whether the chosen provider can answer now.
- spacr.qt.ai.providers.configured_providers() List[ChatProvider][source]¶
Return the providers that can answer now, spaCR AI (free) first.
The hosted provider leads whenever the network is allowed, since it is the default; a vendor CLI follows when it is installed and logged in.
- spacr.qt.ai.providers.default_provider(preferred: str = '') ChatProvider | None[source]¶
Return the provider the AI switch uses.
- Parameters:
preferred – the provider Preferences names, or
"".- Returns:
preferredwhen it can answer; otherwise spaCR AI (free); otherwise, with the network forbidden, the first vendor CLI that is installed and logged in;Nonewhen nothing can answer.
- spacr.qt.ai.providers.describe_tool_call(denial: Dict) str[source]¶
One line naming what a denied tool call would have done.
- Parameters:
denial – one
permission_denialsentry.- Returns:
the command for a shell call, otherwise the tool’s name and its path, address or pattern.
- spacr.qt.ai.providers.effective_provider_name(preferred: str = '') str[source]¶
Return the name of the provider Preferences shows as chosen.
- Parameters:
preferred – the stored preference;
""(never chosen) means spaCR AI (free).- Returns:
a name from
PROVIDER_CHOICES.
- spacr.qt.ai.providers.explain_failure(summary: Dict, provider: ChatProvider | None, timeout: float | None = None) Dict[source]¶
Have the active AI explain a failure once, for the console and the issue.
spaCR AI (free) asks the relay’s
/diagnosewith the anonymised summary andspacr.crashreport.EXPLAIN_QUESTION; a vendor CLI answersspacr.crashreport.ai_summary_prompt()here (write_report_summary()). The same text is shown in the console and sent as the report’sai_summary, so a failure costs one AI call. Blocks, so call it off the GUI thread.- Parameters:
summary – a summary from
spacr.crashreport.public_summary().provider – the active provider, or
None.timeout – seconds a vendor CLI may take; defaults to
spacr.crashreport.AI_SUMMARY_TIMEOUT_S.
- Returns:
{"text": explanation or "", "tier": "best"|"light"|"", "refused": "" or "quota"|"network"|"error"|"none"}, plus"saved": "local"when the answer was saved on this computer (no AI call, no network) or"relay"when the relay reused one.
An error explained before is not explained again: the answer saved on this computer (
spacr.crashreport.saved_explanation()) comes first, for every provider, and each new answer is saved.
- spacr.qt.ai.providers.get_provider(name: str) ChatProvider | None[source]¶
Look up a provider by its short id.
- Parameters:
name – provider id (
"claude","codex","gemini"orHOSTED_PROVIDER_NAME).- Returns:
the matching provider, or
Noneif 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.platformvalue, for quotingprefix.
- Returns:
when
prefixis a conda environment, a command that names it with--prefixand addsghwith--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.hosted_allowance_text(allowance: Dict | None) str[source]¶
One line saying how many free hosted-AI questions are left today.
- Parameters:
allowance – what
spacr.crashreport.fetch_allowance()returns, orNonewhen it is not known.- Returns:
the translated line, or
""when nothing is known.
- spacr.qt.ai.providers.hosted_cli_hint() str[source]¶
The hint for connecting a CLI that uses the user’s own provider account.
- Returns:
the translated sentence.
- spacr.qt.ai.providers.hosted_daily_allowance_text() str[source]¶
The free allowance of spaCR AI, as Preferences states it.
- Returns:
the translated sentence.
- spacr.qt.ai.providers.hosted_provider() HostedProvider[source]¶
Return spaCR’s hosted provider.
- spacr.qt.ai.providers.hosted_refusal_message(reason: str, scope: str = '', resets_at=None) str[source]¶
The sentence shown when the hosted provider gives no advice.
- Parameters:
reason –
"quota","network"or anything else.scope – for a quota refusal,
"install"(this install’s daily questions are used),"global"(spaCR’s shared free capacity is) or"ip".resets_at – when the limit resets; defaults to the next 00:00 UTC.
- Returns:
the translated sentence; a quota refusal also carries
hosted_cli_hint().
- spacr.qt.ai.providers.install_hint_for(name: str, platform: str) str[source]¶
The one-line install hint for tool
nameonplatform.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, becausecmdhas no such comment and would hand the words after#to the installer as arguments.- Parameters:
name – a key of
INSTALL_METHODS.platform – a
sys.platformvalue.
- 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
nameonplatform, in order.- Parameters:
name – a key of
INSTALL_METHODS, such as"claude".platform – a
sys.platformvalue.
- Returns:
the rows, or
()for a tool spaCR cannot install.
- spacr.qt.ai.providers.list_providers() List[ChatProvider][source]¶
Return every vendor CLI provider, regardless of install state.
The hosted provider is not one of them: it has nothing to install or sign in to.
hosted_provider()returns it.
- spacr.qt.ai.providers.local_reset_time(resets_at=None) str[source]¶
The time the free allowances reset, in this computer’s local time.
- Parameters:
resets_at – an aware datetime or an ISO-8601 string; defaults to the next 00:00 UTC.
- Returns:
HH:MM, followed by the weekday when it is not today.
- spacr.qt.ai.providers.platform_family(platform: str) str[source]¶
Name the operating system
INSTALL_METHODSis keyed by.- Parameters:
platform – a
sys.platformvalue.- Returns:
"win32"for any Windows value,"darwin"for macOS, and"linux"for everything else.
- spacr.qt.ai.providers.question_cache_key(messages: List[Dict]) str[source]¶
The key a fresh question’s answer is saved under on this computer.
Only the first question of a conversation is saved, and only when the question and whatever was pasted with it (console context, code, traceback) stay within
spacr.crashreport.QUESTION_CACHE_CONTEXT_MAXcharacters. The key isspacr.crashreport.question_key()of the question and the active app; it is the same for every provider.- Parameters:
messages – conversation history as
{role, content}dicts.- Returns:
the key, or
""when the answer must not be saved.
- 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.platformvalue.
- 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.
- spacr.qt.ai.providers.tool_rule(denial: Dict) str[source]¶
The
--allowedToolsrule that allows exactly one denied tool call.A shell command is allowed as written,
Bash(gh auth status); a web fetch for its host; any other tool by its name.- Parameters:
denial – one
permission_denialsentry.- Returns:
the rule, or
""when the entry names no tool.
- spacr.qt.ai.providers.usage_bar(provider_name: str | None, allowance: Dict | None = None) Tuple[int | None, str][source]¶
What the usage bar under the AI chat box shows for a provider.
For spaCR AI (free) the bar is the user’s share of today’s shared free capacity,
neurons_left / share_neurons, from the relay’s dynamic allowance. For a vendor CLI there is no reliable usage source (CLI_USAGE_TEXT), so the bar is hidden and only a line is shown.- Parameters:
provider_name – the selected provider’s name, or
None.allowance – what
spacr.crashreport.fetch_allowance()returned, for spaCR AI.
- Returns:
(percent or None to hide the bar, translated text).
- spacr.qt.ai.providers.with_ai_summary(summary: Dict, provider: ChatProvider | None) Dict[source]¶
Return the report with the AI summary its provider allows.
spaCR AI (free) asks the relay to write it (
summarize: true, charged to the user’s light-model allowance). A vendor CLI writes it here (write_report_summary()) and it travels asai_summary. With no provider, or when the CLI gives nothing, the report is unchanged.- Parameters:
summary – a summary from
spacr.crashreport.public_summary().provider – the active provider, or
None.
- Returns:
a new summary dict.
- spacr.qt.ai.providers.write_report_summary(summary: Dict, provider: ChatProvider | None, timeout: float | None = None) str[source]¶
Have the user’s own AI summarise an anonymised report, or return
"".The provider is given
spacr.crashreport.ai_summary_prompt()and nothing else. It runs on a thread of its own and is stopped aftertimeoutseconds; a failure, a timeout or an empty answer gives"", and the report is then filed without a summary. Blocks, so call it off the GUI thread.- Parameters:
summary – a summary from
spacr.crashreport.public_summary().provider – a vendor CLI provider, or
None.timeout – seconds to wait; defaults to
spacr.crashreport.AI_SUMMARY_TIMEOUT_S.
- Returns:
the summary text, scrubbed and cut to
spacr.crashreport.MAX_AI_SUMMARY_CHARS.