# Reno AI SDK

Official Python client for the Reno API. Simple, fast, and built with production use in mind.

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Authentication](#authentication)
- [Making Requests](#making-requests)
  - [ask()](#ask)
  - [chat()](#chat)
  - [Streaming](#streaming)
  - [Conversations](#conversations)
- [Models](#models)
- [Response Objects](#response-objects)
- [Error Handling](#error-handling)
- [Error Codes Reference](#error-codes-reference)
- [Configuration](#configuration)
- [Running Tests](#running-tests)

---

## Installation

```bash
pip install renoai
```

Requires Python 3.8 or higher.

## Quick Start

```python
from renoai import Reno

client = Reno(api_key="reno_sk_xxx")

answer = client.ask("What is machine learning?")
print(answer)
```

## Authentication

Every request requires an API key. You can pass it directly or load it from an environment variable (recommended for production):

```python
import os
from renoai import Reno

client = Reno(api_key=os.environ["RENO_API_KEY"])
```

API keys follow the format `reno_sk_...`. Keep them secret and never commit them to version control.

---

## Making Requests

### ask()

The simplest way to get a response. Sends a single message and returns the reply as a plain string.

```python
answer = client.ask("Explain quantum computing in simple terms.")
print(answer)
```

With an optional system prompt:

```python
answer = client.ask(
    "What is the boiling point of water?",
    system="You are a science teacher. Keep answers under 2 sentences.",
    temperature=0.3,
    max_tokens=100,
)
print(answer)
```

**Parameters**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `prompt` | `str` | required | The user's question or instruction |
| `system` | `str` | `None` | Optional system message |
| `model` | `str` | `gemma2:2b-instruct` | Model to use |
| `temperature` | `float` | `0.7` | Sampling temperature, 0 to 2 |
| `max_tokens` | `int` | `None` | Maximum tokens to generate |

### chat()

Full control over the conversation with a list of messages. Returns a `Completion` object.

```python
response = client.chat([
    {"role": "system",    "content": "You are a helpful assistant."},
    {"role": "user",      "content": "What is Python?"},
    {"role": "assistant", "content": "Python is a high-level programming language."},
    {"role": "user",      "content": "What is it mainly used for?"},
])

print(response.text)
print(response.usage.total_tokens)
```

**Parameters**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `messages` | `list` | required | List of `{"role": ..., "content": ...}` dicts |
| `model` | `str` | `gemma2:2b-instruct` | Model to use |
| `temperature` | `float` | `0.7` | Sampling temperature, 0 to 2 |
| `max_tokens` | `int` | `None` | Maximum tokens to generate |
| `stream` | `bool` | `False` | Enable streaming mode |

Valid roles are `user`, `assistant`, and `system`.

### Streaming

Stream tokens as they are generated instead of waiting for the full response.

**Using `stream_text()`** — yields plain strings, the easiest option:

```python
for token in client.stream_text("Write me a short poem about the ocean."):
    print(token, end="", flush=True)
print()
```

**Using `chat()` with `stream=True`** — yields `StreamChunk` objects for full control:

```python
chunks = client.chat(
    [{"role": "user", "content": "Tell me a story."}],
    stream=True,
)

full_text = ""
for chunk in chunks:
    if chunk.delta:
        full_text += chunk.delta
        print(chunk.delta, end="", flush=True)
    if chunk.is_final:
        print(f"\n\nFinished. Reason: {chunk.finish_reason}")
```

### Conversations

`Conversation` manages message history automatically so you can focus on the dialogue.

```python
from renoai import Reno, Conversation

client = Reno(api_key="reno_sk_xxx")
conv = Conversation(client, system="You are a friendly cooking assistant.")

print(conv.say("What should I make for dinner tonight?"))
print(conv.say("I only have chicken and rice."))
print(conv.say("How long will it take?"))
```

You can inspect or reset the history at any time:

```python
# See the full message history
print(conv.history)

# Reset but keep the system prompt
conv.reset(keep_system=True)

# Reset everything
conv.reset(keep_system=False)
```

---

## Models

Pass any supported model name via the `model` parameter:

```python
response = client.chat(
    [{"role": "user", "content": "Hello!"}],
    model="gemma2:2b-instruct",
)
```

The default model is `gemma2:2b-instruct`.

---

## Response Objects

### Completion

Returned by `chat()` when not streaming.

```python
response = client.chat([{"role": "user", "content": "Hi"}])

response.text          # the generated reply as a string
response.content       # alias for response.text
response.id            # unique response ID
response.model         # model that generated the response
response.choices       # list of Choice objects
response.usage         # token usage info

response.to_message()  # {"role": "assistant", "content": "..."} ready to append to history
response.to_dict()     # raw API response as a dict
```

### Usage

```python
response.usage.prompt_tokens      # tokens in your input
response.usage.completion_tokens  # tokens in the reply
response.usage.total_tokens       # total tokens consumed
```

### StreamChunk

Yielded by streaming calls.

```python
chunk.delta          # the new text in this chunk (str or None)
chunk.finish_reason  # "stop" on the last chunk, None otherwise
chunk.is_final       # True when this is the last chunk
chunk.to_dict()      # raw chunk data
```

---

## Error Handling

The SDK raises typed exceptions so you can handle each failure case precisely.

```python
from renoai import (
    RenoError,
    RenoConnectionError,
    RenoTimeoutError,
    RenoValidationError,
)
import time

try:
    answer = client.ask("Hello")

except RenoValidationError as e:
    # Bad input before the request was even sent
    print("Fix your input:", e.message)

except RenoConnectionError as e:
    # Could not reach the server at all
    print("Server unreachable:", e.message)

except RenoTimeoutError as e:
    # Request started but took too long
    print("Timed out:", e.message)

except RenoError as e:
    # Any other API-level error
    print(e.user_friendly())

    if e.is_retryable:
        wait = e.retry_after or 5
        print(f"Retrying in {wait}s...")
        time.sleep(wait)
```

### Exception Hierarchy

```
RenoError                  # base class for all SDK exceptions
  RenoConnectionError      # network or DNS failure (code 6002)
  RenoTimeoutError         # request exceeded timeout (code 6003)
  RenoValidationError      # invalid input caught client-side (code 2001)
```

### RenoError Properties

| Property | Type | Description |
|---|---|---|
| `code` | `int` | Reno error code |
| `message` | `str` | Short description of the error |
| `details` | `str` | Extra context or suggestion from the server |
| `is_retryable` | `bool` | Whether it is safe to retry this request |
| `retry_after` | `float` | Seconds to wait before retrying (from `Retry-After` header) |
| `user_friendly()` | `str` | Formatted message with title, description, and tip |

### Production Loop Example

```python
import time
from renoai import Reno, RenoError, RenoConnectionError, RenoTimeoutError, RenoValidationError

client = Reno(api_key="reno_sk_xxx")

while True:
    try:
        answer = client.ask("Summarize today's AI news.")
        print(answer)

    except RenoValidationError as e:
        print("Validation error:", e.message)
        break  # code bug, do not retry

    except RenoConnectionError:
        print("Connection failed, retrying in 10s...")
        time.sleep(10)
        continue

    except RenoTimeoutError:
        print("Timed out, retrying in 5s...")
        time.sleep(5)
        continue

    except RenoError as e:
        if e.is_retryable:
            wait = e.retry_after or 5
            print(f"Retryable error [{e.code}], waiting {wait}s...")
            time.sleep(wait)
            continue
        else:
            print(e.user_friendly())
            break  # auth, billing, content policy, etc.

    except KeyboardInterrupt:
        print("Stopped.")
        break

    time.sleep(3)

client.close()
```

---

## Error Codes Reference

| Range | Category |
|---|---|
| 1001 to 1008 | Authentication and API key errors |
| 2001 to 2009 | Request validation errors |
| 3001 to 3007 | Model availability errors |
| 4001 to 4005 | Token and context length errors |
| 5001 to 5006 | Rate limiting and quota errors |
| 6001 to 6006 | Server and infrastructure errors |
| 7001 to 7004 | Content moderation errors |
| 8001 to 8004 | Billing and subscription errors |
| 9001 to 9004 | Internal and unexpected errors |

Call `error.user_friendly()` on any `RenoError` to get a plain-English title, description, and actionable tip for any of these codes.

---

## Configuration

### Client Options

```python
client = Reno(
    api_key="reno_sk_xxx",
    base_url="http://127.0.0.1:8000/api/v1",  # default
    timeout=30,                                  # seconds, default 30
    max_retries=3,                               # default 3
)
```

### Context Manager

The client can be used as a context manager to ensure the HTTP session is always closed:

```python
with Reno(api_key="reno_sk_xxx") as client:
    print(client.ask("Hello!"))
```

---

## Running Tests

```bash
pip install pytest
pytest tests/ -v
```

To run a specific test file or test:

```bash
pytest tests/test_renoai.py -v
pytest tests/test_renoai.py::TestClientRequests::test_ask_success -v
```

---

## License

MIT License. See `LICENSE` for details.