spacr.qt.ai.providers
=====================

.. py:module:: spacr.qt.ai.providers

.. autoapi-nested-parse::

   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.







Module Contents
---------------

.. py:class:: ChatProvider

   Bases: :py:obj:`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 :meth:`stream_chat`.

   :ivar name: short id ("claude" / "codex" / "gemini").
   :ivar label: human-readable label shown in the UI.
   :ivar cli_name: executable expected on ``PATH``.
   :ivar install_hint: shell one-liner suggested for installation.
   :ivar login_command: shell one-liner the user runs to authenticate.


   .. py:attribute:: name
      :type:  str
      :value: ''



   .. py:attribute:: label
      :type:  str
      :value: ''



   .. py:attribute:: cli_name
      :type:  str
      :value: ''



   .. py:attribute:: install_hint
      :type:  str
      :value: ''



   .. py:attribute:: login_command
      :type:  str
      :value: ''



   .. py:method:: is_installed() -> bool

      Return True when the provider's CLI executable is on ``PATH``.



   .. py:method:: is_logged_in() -> bool

      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.



   .. py:method:: is_configured() -> bool

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



   .. py:method:: source_of_key() -> str

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



   .. py:method:: cancel_stream() -> None

      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.



   .. py:method:: stream_chat(messages: List[Dict], system: str = '', model: Optional[str] = None) -> Iterator[str]
      :abstractmethod:


      Yield text chunks streaming from the CLI subprocess.



.. py:class:: ClaudeCliProvider

   Bases: :py:obj:`ChatProvider`


   Anthropic Claude via the ``claude`` (Claude Code) CLI.


   .. py:attribute:: name
      :value: 'claude'



   .. py:attribute:: label
      :value: 'Claude (via Claude Code)'



   .. py:attribute:: cli_name
      :value: 'claude'



   .. py:attribute:: install_hint
      :value: 'curl -fsSL https://claude.ai/install.sh | bash   # or npm install -g @anthropic-ai/claude-code'



   .. py:attribute:: login_command
      :value: 'claude setup-token'



   .. py:method:: stream_chat(messages: List[Dict], system: str = '', model: Optional[str] = None) -> Iterator[str]

      Stream a chat completion from the ``claude`` CLI.

      :param messages: conversation history as ``{role, content}`` dicts.
      :param system: optional system prompt appended via
          ``--append-system-prompt``.
      :param model: optional model override passed via ``--model``.
      :returns: iterator yielding stdout text chunks.



.. py:class:: CodexCliProvider

   Bases: :py:obj:`ChatProvider`


   OpenAI ChatGPT via the ``codex`` CLI.


   .. py:attribute:: name
      :value: 'codex'



   .. py:attribute:: label
      :value: 'ChatGPT (via Codex CLI)'



   .. py:attribute:: cli_name
      :value: 'codex'



   .. py:attribute:: install_hint
      :value: 'npm install -g @openai/codex   # or brew install codex'



   .. py:attribute:: login_command
      :value: 'codex login'



   .. py:method:: stream_chat(messages: List[Dict], system: str = '', model: Optional[str] = None) -> Iterator[str]

      Stream a chat completion from the ``codex`` CLI.

      :param messages: conversation history as ``{role, content}`` dicts.
      :param system: optional system prompt folded into the prompt body.
      :param model: optional model override passed via ``--model``.
      :returns: iterator yielding stdout text chunks.



.. py:class:: GeminiCliProvider

   Bases: :py:obj:`ChatProvider`


   Google Gemini via the ``gemini`` CLI.


   .. py:attribute:: name
      :value: 'gemini'



   .. py:attribute:: label
      :value: 'Gemini (via Gemini CLI)'



   .. py:attribute:: cli_name
      :value: 'gemini'



   .. py:attribute:: install_hint
      :value: 'npm install -g @google/gemini-cli   # or brew install gemini-cli'



   .. py:attribute:: login_command
      :value: 'gemini'



   .. py:method:: stream_chat(messages: List[Dict], system: str = '', model: Optional[str] = None) -> Iterator[str]

      Stream a chat completion from the ``gemini`` CLI.

      :param messages: conversation history as ``{role, content}`` dicts.
      :param system: optional system prompt folded into the prompt body.
      :param model: optional model override passed via ``-m``.
      :returns: iterator yielding stdout text chunks.



.. py:function:: list_providers() -> List[ChatProvider]

   Return every registered provider, regardless of install state.


.. py:function:: configured_providers() -> List[ChatProvider]

   Return only providers whose CLI is installed and logged in.


.. py:function:: get_provider(name: str) -> Optional[ChatProvider]

   Look up a registered provider by its short id.

   :param name: provider id (``"claude"``, ``"codex"``, ``"gemini"``).
   :returns: the matching provider, or ``None`` if no such id.


