Imports:
  - Types:
      - require_vars
      - MissingEnvironmentVariablesError
    Usages:
      - environment
    From: swax/config

Usages:
  conventions: .goga/usages/conventions.md
  anthropic: .goga/usages/cooks/anthropic.md
  openai: .goga/usages/cooks/openai.md

Annotations: |
  Provider-agnostic LLM transport. Adapters wrap the Anthropic and OpenAI SDKs behind
  a single protocol; the factory selects one by SWAX_LLM_PROTOCOL.

  Adapters receive the SDK client and the model identifier via constructor injection —
  for testability via mock at import point. The model is read from SWAX_LLM_MODEL by
  `build_llm_client` and forwarded to the chosen adapter.
  SWAX_LLM_TOKEN never appears in logs. Relative imports inside the cell.
  `require_vars` is the entry point for credential validation; `MissingEnvironmentVariablesError`
  propagates to the caller.

  Use `conventions` for code writing rules and testing.
  Use `anthropic` for the Anthropic SDK client and error mapping.
  Use `openai` for the OpenAI SDK client and error mapping.
  Use `environment` for `require_vars` (lazy credential validation).

---

"LLMClient()":
  location: llm_client.py
  annotations: |
    Structural protocol for LLM transports. Adapters satisfy it implicitly — no inheritance required.

    The protocol is domain-agnostic: methods accept pre-built system/user strings and return
    raw response text. Multi-step orchestration (dependency inference, schema refinement) lives
    in the consumer, not here.

  methods:
    "ask(system: str, user: str) -> response: str": |
      Single-turn transport: sends one system + one user message, returns raw response text.

      `system`: system prompt (output of build_graph_system_prompt).
      `user`: user payload (output of build_graph_user_prompt or build_refine_user_prompt).
      `response`: concatenated text content returned by the provider.

      Requirements:
      - Returns the concatenated text content of the response.
      - SDK errors are mapped to LLMCallError / LLMRateLimitedError.
    "ask_multi_turn(system: str, messages: list[dict[str, str]]) -> response: str": |
      Multi-turn transport: sends a system prompt plus an ordered message history,
      returns raw response text.

      `system`: system prompt reused across turns.
      `messages`: ordered list of {"role": ..., "content": ...} dicts for the refinement pass.
      `response`: concatenated text content returned by the provider.

      Requirements:
      - Message order is preserved — the SDK relies on it for context continuity.
      - SDK errors are mapped to LLMCallError / LLMRateLimitedError.

"LLMClient::AnthropicAdapter(client: Anthropic, model: str)":
  location: anthropic_adapter.py
  annotations: |
    LLMClient backed by the Anthropic SDK.

    `client`: injected Anthropic SDK client, constructed by build_anthropic_client.
    `model`: model identifier sent on every request, supplied by build_llm_client
      from SWAX_LLM_MODEL.

    Requirements:
    - Method signatures match the LLMClient protocol exactly — provider switching
      requires no consumer code change.

  properties:
    "client -> Anthropic": |
      The injected Anthropic SDK client instance.
    "model -> str": |
      The injected model identifier sent on every request.
  methods:
    "ask(system: str, user: str) -> response: str": |
      Anthropic-backed single-turn call.

      Algorithm:
      1. Send a single user message together with the system prompt at the injected model.
      2. Concatenate text content blocks of the response.
      3. Map rate-limit errors to LLMRateLimitedError and other API errors to LLMCallError.

    "ask_multi_turn(system: str, messages: list[dict[str, str]]) -> response: str": |
      Anthropic-backed multi-turn call.

      Algorithm:
      1. Send the message history together with the system prompt at the injected model.
      2. Concatenate text content blocks of the response.
      3. Map errors as in ask.

"LLMClient::OpenAIAdapter(client: OpenAI, model: str)":
  location: openai_adapter.py
  annotations: |
    LLMClient backed by the OpenAI SDK.

    `client`: injected OpenAI SDK client, constructed by build_openai_client.
    `model`: model identifier sent on every request, supplied by build_llm_client
      from SWAX_LLM_MODEL.

    Requirements:
    - Method signatures match the LLMClient protocol exactly — provider switching
      requires no consumer code change.

  properties:
    "client -> OpenAI": |
      The injected OpenAI SDK client instance.
    "model -> str": |
      The injected model identifier sent on every request.
  methods:
    "ask(system: str, user: str) -> response: str": |
      OpenAI-backed single-turn call.

      Algorithm:
      1. Submit a system message followed by the user message at the injected model.
      2. Return the first choice's message content (empty string when absent).
      3. Map rate-limit errors to LLMRateLimitedError and other API errors to LLMCallError.

    "ask_multi_turn(system: str, messages: list[dict[str, str]]) -> response: str": |
      OpenAI-backed multi-turn call.

      Algorithm:
      1. Prepend the system prompt to the message list.
      2. Submit the combined messages at the injected model.
      3. Return the first choice's message content (empty string when absent).
      4. Map errors as in ask.

"LLMCallError(reason: str)":
  location: errors.py
  annotations: |
    Raised by adapters on a generic LLM API error (non-rate-limit).

    `reason`: original error message from the SDK.

"LLMRateLimitedError(reason: str)":
  location: errors.py
  annotations: |
    Raised by adapters when the LLM API returns a rate-limit error.

    `reason`: original error message from the SDK.

"LLMResponseParseError(reason: str, excerpt: str)":
  location: errors.py
  annotations: |
    Raised by LLM consumers when defensive JSON parsing of an LLM response fails.

    `reason`: short diagnostic reason (e.g. "JSONDecodeError", "not a dict", "shape mismatch").

    `excerpt`: raw payload excerpt (first 200 chars) for diagnostics; never contains credentials.

    Constraints:
    - Raised by the consumer (e.g. run_discover helper), not by the adapters themselves.

"UnsupportedLLMProtocolError(protocol: str)":
  location: errors.py
  annotations: |
    Raised by build_llm_client when SWAX_LLM_PROTOCOL is neither anthropic nor openai.

    `protocol`: offending value submitted by the user.

"build_anthropic_client() -> client: Anthropic":
  location: build_anthropic_client.py
  annotations: |
    Constructs an Anthropic SDK client from SWAX_LLM_* environment variables.

    `client`: ready-to-use Anthropic instance for AnthropicAdapter.

    Algorithm:
    1. Call `require_vars` to fail fast on missing credentials.
    2. Read the LLM token and base URL from the environment.
    3. Construct and return the SDK client.

    Requirements:
    - Credentials are read from the environment, never hardcoded.
    - Base URL is assumed already validated (no /v1).

"build_openai_client() -> client: OpenAI":
  location: build_openai_client.py
  annotations: |
    Constructs an OpenAI SDK client from SWAX_LLM_* environment variables.

    `client`: ready-to-use OpenAI instance for OpenAIAdapter.

    Algorithm:
    1. Call `require_vars` to fail fast on missing credentials.
    2. Read the LLM token and base URL from the environment.
    3. Construct and return the SDK client.

    Requirements:
    - Credentials are read from the environment, never hardcoded.
    - Base URL is assumed already validated (no /v1) — the SDK adds the version segment itself.

"build_llm_client() -> client: LLMClient":
  location: build_llm_client.py
  annotations: |
    Factory selecting the adapter based on SWAX_LLM_PROTOCOL.

    `client`: an LLMClient-shaped adapter wrapping the chosen SDK client and pinned
      to the model named by SWAX_LLM_MODEL.

    Algorithm:
    1. Read SWAX_LLM_PROTOCOL from the environment.
    2. Construct the matching SDK client via build_anthropic_client / build_openai_client
       (which call require_vars first, so a missing SWAX_LLM_MODEL surfaces as
       MissingEnvironmentVariablesError before it is read here).
    3. Read SWAX_LLM_MODEL from the environment and forward it to the adapter.
    4. On an unknown protocol value, raise UnsupportedLLMProtocolError.

    Requirements:
    - Switching providers requires no code change in consumers.
    - Selecting a model requires no code change in consumers.
    - Protocol value is validated upstream.

---

Author: Goga
CreatedAt: 25/06/26
Description: |
  Provider-agnostic LLM transport: protocol, Anthropic/OpenAI adapters, factory by SWAX_LLM_PROTOCOL.
