Metadata-Version: 2.5
Name: frutebox
Version: 0.1.0
Summary: Official Frutebox SDK for Python — call the Frutebox network. No third-party AI SDK required.
Project-URL: Homepage, https://frutebox.com
Project-URL: Documentation, https://frutebox.com/docs
Project-URL: Changelog, https://frutebox.com/docs
Author: Frutebox
License: MIT
License-File: LICENSE
Keywords: ai,assistant,frutebox,llm,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Requires-Dist: requests>=2.25
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: node
Requires-Dist: frutebox-node==0.1.0; extra == 'node'
Description-Content-Type: text/markdown

# frutebox

The official [Frutebox](https://frutebox.com) SDK for Python — call the Frutebox network with no
third-party AI library. Every answer comes from the real Frutebox network: no other provider is
called, paid or depended on.

```bash
pip install frutebox              # the client and the CLI
pip install "frutebox[node]"      # and the node runtime, to share your hardware
```

The client install is small on purpose — it needs `requests` and nothing else. Sharing hardware
needs a serving stack, so that ships as the separate `frutebox-node` distribution behind the
`[node]` extra. Most people want the first.

## Two surfaces, and which one to use

The API publishes two chat shapes and the website documents both, so this client covers both.

**Use the native one.** It is the long-term surface, and it returns something the other cannot:
the request's **real measured cost**, electricity and internet, per serving node.

```python
from frutebox import Frutebox

fb = Frutebox()                      # reads FRUTEBOX_API_KEY
res = fb.chat.create(message="Draft a friendly reminder email.", region="eu-west")

print(res["reply"])
print(res["model"], res["region"])
print(res["usage"]["cost"], res["usage"]["currency"])
for node in res["usage"]["nodes"]:
    print(node["name"], node["computeMs"], "ms", node["watts"], "W")
```

**The drop-in shape still works and is deprecated.** It exists so an app written against the
common AI SDKs moves to Frutebox by changing a base URL and a key. Nothing is refused, re-routed or
re-priced for using it — but it emits a `DeprecationWarning`, because the published spec marks the
operation `deprecated: true` and a `choices` array has nowhere to put a measured cost.

```python
res = fb.chat.completions.create(messages=[{"role": "user", "content": "Hello"}])
print(res["choices"][0]["message"]["content"])

for chunk in fb.chat.completions.create(messages=[...], stream=True):
    print(chunk["choices"][0]["delta"].get("content", ""), end="")
```

## Embeddings

Native: vectors come back directly as `embeddings`, in request order. You do **not** pick the
embedder — Frutebox chooses from its licence-cleared roster — and `model` reports what actually ran
on the node.

```python
res = fb.embeddings.create(input=["first text", "second text"])
print(res["dimensions"], len(res["embeddings"]), res["model"])
```

## Media is always asynchronous

Every image, video and audio call returns a **job**, never a file. Poll it, or let the client poll:

```python
# One call, polled for you. Raises JobFailed / JobTimeout rather than returning a half-answer.
done = fb.images.generate_and_wait(prompt="a pear, watercolour", size="1024x1024")
print(done["image_url"])

# Or drive it yourself.
job = fb.images.generate(prompt="a pear, watercolour", n=2)
while job["status"] in ("queued", "running"):
    job = fb.images.retrieve(job["id"])
```

`status` is the signal, not the presence of a url: the url field appears **only** once status is
`done`.

```python
fb.videos.generate_and_wait(prompt="a pear rolling downhill")      # POST /videos
fb.audio.generations.create_and_wait(prompt="a short jingle", kind="music", duration_s=30)
fb.audio.speech(input="Hello there")                               # returns raw audio bytes
fb.audio.transcriptions.create(file=open("clip.mp3", "rb"))
fb.moderations.create(input="some text")
models = fb.models.list()
fb.models.retrieve(models["data"][0]["id"])   # never a pinned literal
fb.skills.list()
```

**The model id is not yours to pin, and it is being renamed.** Frutebox serves one model and sizes
it to the task, so you never pick one — `chat` and `embeddings` take no `model` argument at all.
The published id is whatever `models.list()` reports; an older integration that still sends the
previous id keeps working, because the server accepts it as an undocumented alias. Reading the id
rather than pinning it makes the rename a non-event for your code — which is why the examples above
do exactly that.

## Errors

One class per published status, and both published error bodies are parsed — the native flat
`{error, code, retryAfterSeconds}` and the drop-in nested `{error: {message, type, code}}`.

```python
from frutebox import RateLimitError, BudgetError, NotImplementedByNetwork

try:
    fb.chat.create(message="…")
except RateLimitError as e:
    time.sleep(e.retry_after_seconds or 5)
except BudgetError:
    ...            # 402 — over the account's budget
except NotImplementedByNetwork:
    ...            # 501 — no model can serve this yet. Never a fabricated result.
```

`JobFailed` and `JobTimeout` carry the job on the exception. A timeout does **not** cancel the
job — it is still running on the network, so the id is there to keep polling with.

## Your own account — a different client, and a different credential

Some things are not on the public API at all: they authenticate your **account**, not a key. They get
their own client, because an `sk_live_…` key would return 401 for every one of them.

```python
from frutebox import FruteboxAccount

me = FruteboxAccount()                       # reads FRUTEBOX_SESSION_TOKEN
me.conversations.rename("conv_123", "Q3 planning")

me.feature_requests.create(title="Export to CSV", description="A button on the usage page.")
me.feature_requests.list(limit=20)           # ADMINS ONLY — 403 otherwise
```

The session token is the one `frutebox login` stores, so the CLI and this client share a credential.
Passing an API key raises immediately rather than letting the server answer an unexplained 401.

| | `Frutebox` | `FruteboxAccount` |
|---|---|---|
| credential | `sk_live_…` API key | session token |
| surface | `/api/v1/*`, 16 operations | first-party endpoints outside `/api/v1` |
| published in the OpenAPI document | yes | **no** |

> **These two endpoints are not deployed yet.** They were written on `lane2/backend` (`46a7a38d`,
> `a403cb55`) and reach production through MAIN's merge. Until then they return 404. The shapes here
> come from those routes' own validation schemas, and the test suite starts comparing against the
> real source the moment they land.

**Not here on purpose:** org, groups, rules and user import. They are session-authed today, but they
are being changed to accept an API key with an org-admin scope — at which point they belong on
`Frutebox`, with a scope entry and a spec entry. Putting them here now would mean moving them later.

## The CLI

Installing the package installs a `frutebox` command for sharing this machine's hardware with the
network. It is the same contribution model the desktop app uses, through the same endpoints and the
same node agent — not a second implementation.

```bash
pbpaste | frutebox login          # a SESSION token, read from stdin — never a flag
frutebox share --join --region eu-west
frutebox share --status           # node state and earnings
frutebox share --limits           # when this machine will and will not take work
frutebox share --leave            # stop taking work; keeps the node and its earnings
```

The node commands need a **session** token, not an `sk_live_…` API key: they authenticate the
account, the way the desktop app does. Secrets never go on argv — `login` reads stdin, and the node
credential is stored mode 600 and handed to the agent by file.

Sharing needs the runtime: `pip install "frutebox[node]"`. Without it the command still registers
the machine and prints the agent command, rather than pretending it started one.

`frutebox agents` is **not implemented**: Standby Agents has no endpoints yet, and the command says
so rather than guessing at shapes nobody has published.

## Keys and configuration

`FRUTEBOX_API_KEY` is read when no `api_key` is passed. The older `STEVAI_API_KEY` is still
accepted so existing environments keep working; `FRUTEBOX_API_KEY` wins when both are set. HTTPS is
enforced, so your key is never sent in plaintext (`localhost` is exempt for development).

The key is kept out of `repr()` and out of every exception this client raises, including transport
failures — a `requests` exception can carry the request, and the request carries the auth header.

## The contract

This client is generated against the published OpenAPI document and checked against it in CI:

- `GET https://frutebox.com/api/v1/openapi.json`
- `frutebox.OPERATIONS` maps every published operation to the method that calls it. CI fails if a
  published operation has no method, or a method has no published operation.

- Get an API key: <https://frutebox.com/app/keys>
- Documentation: <https://frutebox.com/docs>
