Metadata-Version: 2.4
Name: ccs-llmconnector
Version: 1.6.0
Summary: Lightweight wrapper around different LLM provider Python SDK Responses APIs.
Author: CCS
License: MIT
Project-URL: Homepage, https://cleancodesolutions.de
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai>=1.66.0
Requires-Dist: google-genai
Requires-Dist: anthropic
Requires-Dist: xai-sdk
Dynamic: license-file

# ccs-llmconnector

`ccs-llmconnector` is a thin Python wrapper around leading large-language-model SDKs,
including the OpenAI Responses API, Google's Gemini SDK, Anthropic's Claude
Messages API, xAI's Grok chat API, and the DeepSeek API. It exposes a minimal interface that
forwards the most common options such as API key, prompt, optional reasoning
effort hints, token limits, and image inputs, and includes helpers to enumerate
the models available to your account with each provider.

## Installation

```bash
# from PyPI (normalized project name)
pip install ccs-llmconnector

# or from source (this repository)
pip install .
```

### Requirements

- Python 3.10+
- Provider SDKs installed automatically with the package:
  - `openai`
  - `google-genai`
  - `anthropic`
  - `xai-sdk`

## Components

- `OpenAIResponsesClient` - direct wrapper around the OpenAI Responses API, ideal when your project only targets OpenAI models. Includes a model discovery helper.
- `GeminiClient` - thin wrapper around the Google Gemini SDK. Includes a model discovery helper.
- `AnthropicClient` - lightweight wrapper around the Anthropic Claude Messages API. Includes a model discovery helper.
- `GrokClient` - wrapper around the xAI Grok chat API. Includes a model discovery helper.
- `DeepSeekClient` - wrapper around the DeepSeek Chat Completions API (OpenAI-compatible, through the `openai` SDK). Includes a model discovery helper.
- `LLMClient` - provider router that delegates to registered clients for OpenAI, Gemini, Anthropic, Grok (alias: `xai`), and DeepSeek without changing call sites.

## Common Options

All clients expose the same optional controls:

- `messages`: list of `{role, content}` entries (e.g., `system`, `user`, `assistant`). If both `prompt` and `messages` are provided, `prompt` is appended as the last user message.
- `temperature`: optional sampling temperature.
- `top_p`: optional nucleus sampling value.
- `request_id`: free-form request identifier for tracing/logging.
- `timeout_s`: optional timeout in seconds (best-effort depending on provider).
- `max_retries` and `retry_backoff_s`: retry count and exponential backoff base delay.

Async counterparts are available as `async_generate_response`, `async_generate_image`, and `async_list_models`.

### Reasoning text

`generate_response_with_usage` returns an `LLMResponse`. Its `reasoning_text` carries the model's
reasoning when the provider returns it, separate from `text`:

| Provider | What comes back | How to request it |
|----------|-----------------|-------------------|
| Anthropic | summarized thinking | `include_reasoning=True` (sends `thinking.display: "summarized"`) |
| OpenAI | reasoning summary | `include_reasoning=True` (sends `reasoning.summary: "auto"`; retried without it if the organization is not allowed summaries) |
| Gemini | thought summaries | `include_reasoning=True` (sends `include_thoughts`) |
| DeepSeek | the full chain of thought (`reasoning_content`) | always, while thinking is on |

`LLMClient` forwards `include_reasoning` only to providers that take it. Summaries are written by
the provider and are not a complete record of the model's reasoning.

### Prompt caching

`generate_response_with_usage(..., prompt_cache=True)` caches the stable start of a request, the
system prompt, in whatever way the provider offers. Requests that repeat it pay the cached-input price
for that part:

| Provider | What `prompt_cache=True` does | Notes |
|----------|-------------------------------|-------|
| Anthropic | `cache_control` breakpoint on the system prompt; requests that already contain assistant turns also cache the conversation (automatic caching) | 5-minute cache, refreshed by every hit; writes cost 1.25× input, reads 0.1× (0.05× on Opus 5.5); minimum 512–4096 tokens by model |
| OpenAI | explicit cache breakpoint after the system prompt plus `prompt_cache_key` | breakpoints need gpt-5.6 or later; other models reject them once and then only get the key. OpenAI caches automatically from 1024 tokens |
| Gemini | a context cache for the system prompt, managed by the client: created on the second request with the same model and system prompt, reused, replaced before it expires (`cache_ttl_s`, default 1 hour) and deleted by `close()` | from about 1024 tokens; shorter prompts rely on Gemini's implicit caching. A failed request with the cache is repeated without |
| Grok | `x-grok-conv-id` routing header, so requests with the same prefix reach the server holding their cache | xAI caches automatically |
| DeepSeek | nothing to send | DeepSeek caches every prefix automatically |

`cache_key` overrides the routing key (OpenAI, Grok) or the grouping of managed caches (Gemini); the
default is a hash of model and system prompt. The usage reports cache hits as `cached_input_tokens` and
cache writes as `cache_write_tokens` (both part of `input_tokens`). A Gemini context cache is created
by a separate call: the response that created one carries it as `created_cache` (with its
`token_count`), billed once at the input price plus storage per hour while it lives.

```python
from llmconnector import LLMClient

with LLMClient() as client:            # close() deletes the context caches it created
    for question in questions:
        response = client.generate_response_with_usage(
            provider="anthropic",
            api_key="sk-ant-api-key",
            model="claude-opus-5-5",
            messages=[{"role": "system", "content": long_rules}, {"role": "user", "content": question}],
            prompt_cache=True,
        )
        print(response.usage.cached_input_tokens, response.usage.cache_write_tokens)
```

Async code uses `async with LLMClient() as client:` or `await client.aclose()`.

## GeminiClient

### Usage

Use the `GeminiClient` when you want direct access to the Google Gemini SDK without
going through the provider router.

> Requires the `google-genai` Python package (installed automatically with `llmconnector`).

```python
from llmconnector import GeminiClient

client = GeminiClient()

text_response = client.generate_response(
    api_key="your-gemini-api-key",
    prompt="Summarize the key benefits of unit testing.",
    model="gemini-2.5-flash",
    max_tokens=2000,
)

# System messages are automatically extracted and passed as Gemini's
# system_instruction config parameter (Gemini does not support a "system"
# role in contents).
chat_response = client.generate_response(
    api_key="your-gemini-api-key",
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "Explain dependency injection in one paragraph."},
    ],
    model="gemini-2.5-flash",
)

vision_response = client.generate_response(
    api_key="your-gemini-api-key",
    prompt="Describe the main action in this image.",
    model="gemini-2.5-flash",
    images=[
        "/absolute/path/to/local-image.png",
        "https://example.com/sample.jpg",
    ],
)
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `api_key` | `str` | Yes | GEMINI or GOOGLE API key used for authentication. |
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `images` is supplied. |
| `model` | `str` | Yes | Target model identifier, e.g. `gemini-2.5-flash`. |
| `max_tokens` | `int` | No | Defaults to `32000`. Passed to the SDK as `max_output_tokens`. |
| `messages` | `Optional[Sequence[dict]]` | No | Chat-style messages (`role`, `content`). Messages with `role: "system"` are extracted and passed as Gemini's `system_instruction`. |
| `reasoning_effort` | `Optional[str]` | No | Set to `"none"`, `"minimal"`, `"low"`, `"medium"`, or `"high"` to configure Gemini thinking (for supported models). |
| `temperature` | `Optional[float]` | No | Sampling temperature forwarded to Gemini GenerateContentConfig. |
| `top_p` | `Optional[float]` | No | Nucleus sampling value forwarded to Gemini GenerateContentConfig. |
| `images` | `Optional[Sequence[str \| Path]]` | No | Image references (local paths, URLs, or data URLs) read and forwarded to the Gemini SDK. |
| `cached_content` | `Optional[str]` | No | Name of a prompt cache from `create_prompt_cache`. The cache holds the system instruction, so no system messages may be passed with it. |
| `include_reasoning` | `bool` | No | `generate_response_with_usage` only: return thought summaries as `reasoning_text`. |
| `prompt_cache`, `cache_key`, `cache_ttl_s` | | No | `generate_response_with_usage` only: managed context cache for the system prompt, see [Prompt caching](#prompt-caching). |

references are automatically converted into the appropriate `types.Part` instances,
allowing you to mix text and visuals in a single request.

`generate_response_with_usage` also reports `reasoning_tokens` (Gemini thinking tokens, billed as
output) and `cached_input_tokens` (the part of `input_tokens` served from a prompt cache). With
`include_reasoning=True` it asks for thought summaries and returns them as `reasoning_text`.

### Explicit prompt caches

`prompt_cache=True` (see [Prompt caching](#prompt-caching)) manages Gemini context caches for you.
To control them yourself, create a cache once and pass its name as `cached_content`. Cached input
tokens are billed at the model's cached-input price plus a small storage fee per hour until the cache
expires or is deleted. Gemini rejects caches below a model-specific minimum size.

```python
from llmconnector import LLMClient

client = LLMClient()
cache = client.create_prompt_cache(
    provider="gemini",
    api_key="your-gemini-api-key",
    model="gemini-2.5-flash",
    system=long_system_prompt,
    ttl_s=3600,
)
response = client.generate_response_with_usage(
    provider="gemini",
    api_key="your-gemini-api-key",
    model="gemini-2.5-flash",          # must be the cache's model
    messages=[{"role": "user", "content": "Next question"}],  # no system message
    cached_content=cache.name,
)
print(response.usage.cached_input_tokens)
client.delete_prompt_cache(provider="gemini", api_key="your-gemini-api-key", name=cache.name)
```

Async variants: `async_create_prompt_cache` and `async_delete_prompt_cache`. Providers without
explicit prompt caches raise `ValueError` for these calls and for `cached_content`.

### Image Generation

Use `generate_image` to create images using Gemini's image generation models (e.g., `gemini-3-pro-image-preview`).

```python
image_bytes = client.generate_image(
    api_key="your-gemini-api-key",
    prompt="Generate an infographic of the current weather in Tokyo.",
    model="gemini-3-pro-image-preview",
    image_size="2K",  # Optional, defaults to "2K"
    aspect_ratio="16:9",  # Optional, e.g. "16:9", "4:3"
)

with open("weather_tokyo.png", "wb") as f:
    f.write(image_bytes)
```

You can also provide an input image for editing tasks:

```python
image_bytes = client.generate_image(
    api_key="your-gemini-api-key",
    prompt="Make the background a sunset.",
    model="gemini-3-pro-image-preview",
    image="/path/to/original.png",
)
```

### Listing models

Use `list_models` to enumerate the Gemini models available to your account:

```python
from llmconnector import GeminiClient

client = GeminiClient()
for model in client.list_models(api_key="your-gemini-api-key"):
    print(model["id"], model["display_name"])
```

## AnthropicClient

### Usage

Use the `AnthropicClient` when you want direct access to Anthropic's Claude Messages API.

> Requires the `anthropic` Python package (installed automatically with `llmconnector`).

```python
from llmconnector import AnthropicClient

client = AnthropicClient()

text_response = client.generate_response(
    api_key="sk-ant-api-key",
    prompt="Summarize the key benefits of unit testing.",
    model="claude-opus-5-5",
    max_tokens=16000,
    reasoning_effort="medium",
)

vision_response = client.generate_response(
    api_key="sk-ant-api-key",
    prompt="Describe the main action in this image.",
    model="claude-opus-5-5",
    images=[
        "/absolute/path/to/local-image.png",
        "https://example.com/sample.jpg",
    ],
)

with_usage = client.generate_response_with_usage(
    api_key="sk-ant-api-key",
    prompt="Plan the next move.",
    model="claude-opus-5-5",
    max_tokens=16000,
    reasoning_effort="high",
    include_reasoning=True,          # summarized thinking in with_usage.reasoning_text
)
print(with_usage.usage.reasoning_tokens)
```

Requests are streamed internally (the SDK refuses non-streaming calls that may take longer than 10
minutes, and long thinking would otherwise risk HTTP timeouts); the result is the complete message.

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `api_key` | `str` | Yes | Anthropic API key used for authentication. |
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `images` is supplied. |
| `model` | `str` | Yes | Target model identifier, e.g. `claude-opus-5-5`. |
| `max_tokens` | `int` | No | Defaults to `32000`. Passed to the SDK as `max_tokens`; thinking counts against it. |
| `messages` | `Optional[Sequence[dict]]` | No | Chat-style messages (`role`, `content`). Messages with `role: "system"` are sent as Anthropic's top-level `system` parameter. |
| `reasoning_effort` | `Optional[str]` | No | `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` or `"max"`, see below. `None` keeps the model's defaults. |
| `temperature` | `Optional[float]` | No | Sampling temperature. Current Claude models reject non-default values. |
| `top_p` | `Optional[float]` | No | Nucleus sampling value. Current Claude models reject non-default values. |
| `images` | `Optional[Sequence[str \| Path]]` | No | Image references (local paths, URLs, or data URLs) read and converted to base64 blocks. |
| `include_reasoning` | `bool` | No | `generate_response_with_usage` only: return summarized thinking as `reasoning_text`. |
| `prompt_cache` | `bool` | No | `generate_response_with_usage` only: cache breakpoint on the system prompt, see [Prompt caching](#prompt-caching). |

`reasoning_effort` by model generation:

- Claude 4.6 and later (and model names the client does not recognize): adaptive thinking
  (`thinking: {type: "adaptive"}`) with `output_config.effort`; `"minimal"` becomes `"low"`.
  Several of these models cannot switch thinking off, so `"none"` sends only `effort: "low"`.
- Older models (Claude 3.x up to 4.5): extended thinking with `budget_tokens` (1024 for low, 4096
  for medium, 16384 for high, 32768 for xhigh/max), capped at `max_tokens - 1024`; `"none"` leaves
  thinking off.

`generate_response` returns the generated model output as a plain string.
`generate_response_with_usage` returns an `LLMResponse` with token usage: cache reads and writes
count as input, `cached_input_tokens` are the cache reads, `reasoning_tokens` the thinking tokens.
Optional image references are automatically transformed into Anthropic `image` blocks so you can
mix text and visual inputs in a single request.

### Listing models

Use `list_models` to enumerate the Anthropic models available to your account:

```python
from llmconnector import AnthropicClient

client = AnthropicClient()
for model in client.list_models(api_key="sk-ant-api-key"):
    print(model["id"], model["display_name"])
```

## GrokClient

### Usage

Use the `GrokClient` when you want direct access to xAI's Grok chat API.

> Requires the `xai-sdk` Python package (installed automatically with `llmconnector`).
> Note: `xai-sdk` targets Python 3.10 and newer.

```python
from llmconnector import GrokClient

client = GrokClient()

text_response = client.generate_response(
    api_key="xai-api-key",
    prompt="Summarize the key benefits of unit testing.",
    model="grok-3",
    max_tokens=2000,
)

vision_response = client.generate_response(
    api_key="xai-api-key",
    prompt="Describe the main action in this image.",
    model="grok-2-vision",
    images=[
        "/absolute/path/to/local-image.png",
        "https://example.com/sample.jpg",
    ],
)
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `api_key` | `str` | Yes | xAI API key used for authentication. |
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `images` is supplied. |
| `model` | `str` | Yes | Target model identifier, e.g. `grok-3`. |
| `max_tokens` | `int` | No | Defaults to `32000`. Passed to the Grok API as `max_tokens`. |
| `reasoning_effort` | `Optional[str]` | No | Hint for reasoning-focused models (`"none"`, `"low"`, `"medium"`, or `"high"`). |
| `temperature` | `Optional[float]` | No | Sampling temperature forwarded to the Grok request. |
| `top_p` | `Optional[float]` | No | Nucleus sampling value forwarded to the Grok request. |
| `images` | `Optional[Sequence[str \| Path]]` | No | Image references (local paths converted to data URLs, or remote URLs passed through). |
| `prompt_cache`, `cache_key` | | No | `generate_response_with_usage` only: `x-grok-conv-id` routing header, see [Prompt caching](#prompt-caching). |

`generate_response_with_usage` returns an `LLMResponse` with token usage (including cached prompt tokens)
and the model's reasoning as `reasoning_text`. Unlike `generate_response`, it never returns the
reasoning in place of a missing answer.

### Image Generation

Use `generate_image` with xAI image generation models such as `grok-imagine-image`.

```python
image_bytes = client.generate_image(
    api_key="xai-api-key",
    prompt="Generate a clean product mockup on a white background.",
    model="grok-imagine-image",
    image_size="2K",
    aspect_ratio="16:9",
)
```

### Listing models

Use `list_models` to enumerate the Grok language models available to your account:

```python
from llmconnector import GrokClient

client = GrokClient()
for model in client.list_models(api_key="xai-api-key"):
    print(model["id"], model["display_name"])
```

## OpenAIResponsesClient

### Usage

Use the `OpenAIResponsesClient` when you want direct access to the OpenAI Responses API.

> Requires the `openai` Python package. It is declared as a dependency of `llmconnector`, but you can also install it manually with `pip install openai`.

```python
from llmconnector import OpenAIResponsesClient

client = OpenAIResponsesClient()

text_response = client.generate_response(
    api_key="sk-your-api-key",                 # required OpenAI API key
    prompt="Summarize the key benefits of unit testing.",  # plain-text prompt
    model="gpt-4o",                            # any Responses API compatible model
    reasoning_effort="medium",                 # optional: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"
    max_tokens=2000,                           # optional: caps the full response length
)

response_with_usage = client.generate_response_with_usage(
    api_key="sk-your-api-key",
    prompt="Summarize the key benefits of unit testing.",
    model="gpt-4o",
    reasoning_effort="medium",
    max_tokens=2000,
)
print(response_with_usage.text)
print(response_with_usage.usage.total_tokens if response_with_usage.usage else None)

vision_response = client.generate_response(
    api_key="sk-your-api-key",
    prompt="Describe the main action in this image.",
    model="gpt-4o-mini",
    images=[
        "/absolute/path/to/local-image.png",   # local file paths are converted to data URLs
        "https://example.com/sample.jpg",      # remote URLs are passed through directly
    ],
)
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `api_key` | `str` | Yes | OpenAI API key used for authentication. |
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `images` is supplied. |
| `model` | `str` | Yes | Target model identifier, e.g. `gpt-4o`. |
| `max_tokens` | `int` | No | Defaults to `32000`. Passed to the Responses API as `max_output_tokens`. |
| `reasoning_effort` | `Optional[str]` | No | For models that support reasoning hints (`"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`). Supported values vary by model. |
| `temperature` | `Optional[float]` | No | Sampling temperature forwarded to the Responses API. |
| `top_p` | `Optional[float]` | No | Nucleus sampling value forwarded to the Responses API. |
| `images` | `Optional[Sequence[str \| Path]]` | No | List of image URLs or local paths converted to data URLs. |
| `include_reasoning` | `bool` | No | `generate_response_with_usage` only: request a reasoning summary and return it as `reasoning_text`. |
| `prompt_cache`, `cache_key` | | No | `generate_response_with_usage` only: cache breakpoint after the system prompt and `prompt_cache_key`, see [Prompt caching](#prompt-caching). |

`generate_response` returns the generated model output as a plain string.
`generate_response_with_usage` returns an `LLMResponse` containing `text` plus token counts when the provider reports them (`input_tokens`, `output_tokens`, `total_tokens`, and OpenAI reasoning and cached input tokens when available).
Reasoning summaries can require a verified organization; if OpenAI rejects the summary, the
request is sent again without it and `reasoning_text` stays `None`.

> The wrapper accepts `prompt` as plain text and translates it into the structured
> input format expected by the Responses API.

### Image Generation

Use `generate_image` with OpenAI image models. If `image` is supplied, the wrapper calls the image edit endpoint; otherwise it calls image generation.

```python
image_bytes = client.generate_image(
    api_key="sk-your-api-key",
    prompt="Generate a simple icon of a relay connector.",
    model="gpt-image-1",
    image_size="1024x1024",
)
```

### Listing models

Use `list_models` to enumerate the OpenAI models available to your account:

```python
from llmconnector import OpenAIResponsesClient

client = OpenAIResponsesClient()
for model in client.list_models(api_key="sk-your-api-key"):
    print(model["id"], model["display_name"])
```

## DeepSeekClient

### Usage

Use the `DeepSeekClient` for DeepSeek models (for example `deepseek-flash` or `deepseek-v4-pro`). It
talks to DeepSeek's OpenAI-compatible Chat Completions API at `https://api.deepseek.com` through
the `openai` SDK; pass another `base_url` to the constructor if needed.

```python
from llmconnector import DeepSeekClient

client = DeepSeekClient()

response = client.generate_response_with_usage(
    api_key="your-deepseek-api-key",
    prompt="Which is greater, 9.11 or 9.8?",
    model="deepseek-flash",
    reasoning_effort="high",
    max_tokens=16000,
)
print(response.text)
print(response.reasoning_text)                # full chain of thought (reasoning_content)
print(response.usage.cached_input_tokens)     # prompt_cache_hit_tokens
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `api_key` | `str` | Yes | DeepSeek API key. |
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `messages` is supplied. |
| `model` | `str` | Yes | Target model identifier, e.g. `deepseek-flash`. |
| `max_tokens` | `int` | No | Defaults to `32000`. |
| `messages` | `Optional[Sequence[dict]]` | No | Chat-style messages; `system`/`developer` become DeepSeek system messages. |
| `reasoning_effort` | `Optional[str]` | No | `"none"` switches thinking off. `"minimal"`/`"low"` map to DeepSeek's `low`, `"medium"`/`"high"`/`"xhigh"` to `high`, `"max"` to `max`. `None` keeps DeepSeek's default (thinking on, `high`). |
| `temperature` | `Optional[float]` | No | Forwarded; has no effect while the model thinks. |
| `top_p` | `Optional[float]` | No | Forwarded. |
| `images` | `Optional[Sequence[str \| Path]]` | No | Not supported; raises `ValueError`. |

DeepSeek caches prompt prefixes automatically; cache hits are reported as `cached_input_tokens`.

## LLMClient

### Usage

`LLMClient` routes requests to whichever provider has been registered; OpenAI, Gemini, Anthropic, Grok (alias: `xai`), and DeepSeek are configured by default. The client also exposes `list_models` to surface the identifiers available for the selected provider.

```python
from llmconnector import LLMClient

llm_client = LLMClient()

response_via_router = llm_client.generate_response(
    provider="openai",                         # selects the OpenAI wrapper
    api_key="sk-your-api-key",
    prompt="List three advantages of integration testing.",
    model="gpt-4o",
    max_tokens=1500,
)

# async usage
# response_via_router = await llm_client.async_generate_response(
#     provider="openai",
#     api_key="sk-your-api-key",
#     messages=[{"role": "system", "content": "You are concise."}],
#     prompt="Summarize the plan.",
#     model="gpt-4o-mini",
# )

gemini_response = llm_client.generate_response(
    provider="gemini",                         # google-genai is installed with llmconnector
    api_key="your-gemini-api-key",
    prompt="Outline best practices for prompt engineering.",
    model="gemini-2.5-flash",
    max_tokens=1500,
)

anthropic_response = llm_client.generate_response(
    provider="anthropic",
    api_key="sk-ant-api-key",
    prompt="Summarize when to rely on retrieval-augmented generation.",
    model="claude-opus-5-5",
    max_tokens=1500,
)

deepseek_response = llm_client.generate_response_with_usage(
    provider="deepseek",
    api_key="your-deepseek-api-key",
    prompt="Plan the next move.",
    model="deepseek-flash",
    reasoning_effort="low",
    include_reasoning=True,                    # ignored here: DeepSeek always returns its reasoning
)

# Additional providers can be registered at runtime:
# llm_client.register_provider("custom", CustomProviderClient())
# llm_client.generate_response(provider="custom", ...)

# Image generation (currently only supported by Gemini)
image_bytes = llm_client.generate_image(
    provider="gemini",
    api_key="your-gemini-api-key",
    prompt="A futuristic city",
    model="gemini-3-pro-image-preview",
    aspect_ratio="16:9",
)
```

### Listing models

```python
from llmconnector import LLMClient

llm_client = LLMClient()
for model in llm_client.list_models(provider="openai", api_key="sk-your-api-key"):
    print(model["id"], model["display_name"])
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `provider` | `str` | Yes | Registered provider key (default registry includes `'openai'`, `'gemini'`, `'anthropic'`, `'grok'`/`'xai'`). |
| `api_key` | `str` | Yes | Provider-specific API key. |
| `prompt` | `Optional[str]` | Conditional | Plain-text prompt. Required unless `images` is supplied. |
| `messages` | `Optional[Sequence[dict]]` | No | Chat-style messages (`role`, `content`). |
| `model` | `str` | Yes | Provider-specific model identifier. |
| `max_tokens` | `int` | No | Defaults to `32000`. |
| `reasoning_effort` | `Optional[str]` | No | Reasoning hint forwarded when supported (e.g. OpenAI, Grok, Gemini). |
| `temperature` | `Optional[float]` | No | Sampling temperature forwarded to the selected provider when supported. |
| `top_p` | `Optional[float]` | No | Nucleus sampling value forwarded to the selected provider when supported. |
| `images` | `Optional[Sequence[str \| Path]]` | No | Image references forwarded to the provider implementation. |
| `request_id` | `Optional[str]` | No | Request identifier for tracing/logging. |
| `timeout_s` | `Optional[float]` | No | Timeout in seconds (best-effort). |
| `max_retries` | `Optional[int]` | No | Retry count for transient failures. |
| `retry_backoff_s` | `Optional[float]` | No | Base delay (seconds) for exponential backoff. |

Use `LLMClient.register_provider(name, client)` to add additional providers that implement
`generate_response` with the same signature.

## CLI

The package provides a simple CLI entry point named `client_cli` (see `pyproject.toml`).
It reads API keys from environment variables and supports generating responses and
listing models.

- API key environment variables:
  - OpenAI: `OPENAI_API_KEY`
  - Gemini: `GEMINI_API_KEY` (fallback: `GOOGLE_API_KEY`)
  - Anthropic: `ANTHROPIC_API_KEY`
  - Grok/xAI: `GROK_API_KEY` or `XAI_API_KEY` (either works)
  - DeepSeek: `DEEPSEEK_API_KEY`

Examples:

```bash
# Generate a response
client_cli respond --provider openai --model gpt-4o --prompt "Hello!"

# Generate with retry/timeout controls
client_cli respond --provider openai --model gpt-4o --prompt "Hello!" --timeout-s 30 --max-retries 2

# Generate with explicit sampling controls
client_cli respond --provider openai --model gpt-4o --prompt "Hello!" --temperature 0.8 --top-p 0.9

# Generate with reasoning effort; response text is printed to stdout and token usage to stderr
client_cli respond --provider openai --model gpt-5 --prompt "Solve this carefully." --reasoning-effort high

# List models for one provider (human-readable)
client_cli models --provider gemini

# List models for one provider (JSON)
client_cli models --provider anthropic --json

# List models for all registered providers
client_cli all-models
```

## Development

The project uses a standard `pyproject.toml` based packaging layout with sources
stored under `src/`. Install the project in editable mode and run your preferred tooling:

```bash
pip install -e .
```

Requires Python 3.10+.
