# Python SDK

## Installation

```bash
pip install fleethq
```

Requires Python 3.10 or later. Install `huggingface_hub` separately if you want to use `client.models.from_huggingface()`:

```bash
pip install fleethq huggingface_hub
```

## Client

```python
from fleet import Fleet

client = Fleet()                          # reads ~/.fleet/credentials
client = Fleet(api_key="fleet_sk_...")    # explicit key
client = Fleet(base_url="http://localhost:8787")  # custom endpoint
```

---

## Models

### `client.models.from_huggingface(repo_id, ...)`

Download a model from HuggingFace and upload it to Fleet. Returns immediately if the model is already uploaded.

```python
model = client.models.from_huggingface("meta-llama/Llama-3.2-1B")
model = client.models.from_huggingface("mistralai/Mistral-7B-v0.1", revision="main")
```

| Argument | Type | Description |
|----------|------|-------------|
| `repo_id` | `str` | HuggingFace repo ID |
| `name` | `str` | Display name in Fleet (defaults to `repo_id`) |
| `revision` | `str` | Branch, tag, or commit hash (defaults to `main`) |

### `client.models.upload(directory, ...)`

Upload a local model directory to Fleet.

```python
model = client.models.upload("./my-model/")
model = client.models.upload("./my-model/", name="my-model-v2")
```

Fleet hashes the model files before uploading. Identical models are deduplicated automatically.

### `client.models.get(model_id)`

```python
model = client.models.get("model_abc123")
print(model.name, model.status)
```

### `client.models.list(source=None)`

```python
models = client.models.list()              # all models
models = client.models.list(source="upload")      # uploaded base models
models = client.models.list(source="job_output")  # fine-tuned models
models = client.models.list(source="checkpoint")  # training checkpoints
```

### Model object

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Model ID |
| `name` | `str` | Display name |
| `status` | `str` | `ready`, `pending`, `failed` |
| `source` | `str` | `upload`, `job_output`, `checkpoint` |
| `parent_id` | `str \| None` | Job ID that produced this model (fine-tunes/checkpoints) |
| `created_at` | `str` | ISO 8601 timestamp |

### `model.estimate(dataset, method, hyperparameters)`

Estimate cost and runtime for a training job without launching it. Returns a dict with per-tier ranges.

```python
estimate = model.estimate(dataset, method="lora")

print(estimate["recommended_tier"])   # e.g. "fleet:standard"
print(estimate["vram_estimated_gb"])  # estimated VRAM required

for tier in estimate["tiers"]:
    if tier["fits"]:
        print(
            tier["tier"],
            f"{tier['estimated_minutes_min']:.0f}–{tier['estimated_minutes_max']:.0f} min",
            f"${tier['estimated_cost_min']:.2f}–${tier['estimated_cost_max']:.2f}",
        )
```

| Key | Type | Description |
|-----|------|-------------|
| `vram_estimated_gb` | `float` | Estimated VRAM required |
| `vram_confidence` | `str` | `exact` (config.json found) or `estimated` |
| `recommended_tier` | `str \| None` | Most cost-effective tier for this job |
| `tiers` | `list` | Per-tier breakdown (see below) |

Each entry in `tiers`:

| Key | Type | Description |
|-----|------|-------------|
| `tier` | `str` | Tier ID |
| `fits` | `bool` | Whether the tier has enough VRAM |
| `estimated_minutes_min` | `float \| None` | Fastest GPU in tier |
| `estimated_minutes_max` | `float \| None` | Slowest GPU in tier |
| `estimated_cost_min` | `float \| None` | Lowest cost in tier (USD) |
| `estimated_cost_max` | `float \| None` | Highest cost in tier (USD) |
| `best_gpu_id` | `str \| None` | Most cost-effective GPU in tier |
| `recommended` | `bool` | Whether this is the recommended tier |

### `model.train(dataset, hardware, hyperparameters)`

See the [Training](/docs/training) page for full details.

```python
job = model.train(dataset, hardware="fleet:standard")
```

### `model.evaluate(dataset, hardware)`

Run a forward-pass evaluation on a held-out dataset. No weights are updated. Returns loss and perplexity.

```python
job = model.evaluate("./held-out.jsonl")
job.monitor()
results = job.eval_results()
print(results["loss"])        # e.g. 1.23
print(results["perplexity"])  # e.g. 3.42
```

The dataset must be a `.jsonl` file in the same format as your training data. Fleet runs a forward pass over every row and computes cross-entropy loss — no gradient updates occur.

### `model.deploy(hardware, workers_min, workers_max)`

```python
endpoint = model.deploy(hardware="fleet:economy")
```

### `model.download()`

Returns presigned download URLs for all files in the model. URLs expire after 1 hour.

```python
files = model.download()
for f in files:
    print(f["filename"], f["url"])
```

| Key | Type | Description |
|-----|------|-------------|
| `filename` | `str` | Relative filename |
| `url` | `str` | Presigned download URL (1 hour TTL) |
| `size` | `int` | File size in bytes |

### `model.infer(prompt, max_tokens, temperature)`

Proxy inference through the model's active deployment.

```python
response = model.infer("Summarise this text:")
```

---

## Datasets

### `client.datasets.upload(path, name=None)`

Upload a `.jsonl` file to Fleet. Returns immediately if the same file was already uploaded (content-hash deduplication).

```python
dataset = client.datasets.upload("./train.jsonl")
dataset = client.datasets.upload("./train.jsonl", name="my-dataset")
```

### `client.datasets.get(dataset_id)`

```python
dataset = client.datasets.get("dataset_abc123")
```

### `client.datasets.list(page, limit)`

```python
result = client.datasets.list()
for d in result["datasets"]:
    print(d["id"], d["name"])
```

### Dataset object

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Dataset ID |
| `name` | `str` | Display name |
| `size` | `int \| None` | File size in bytes |
| `created_at` | `str \| None` | ISO 8601 timestamp |

### `client.datasets.delete(dataset_id)`

```python
client.datasets.delete("dataset_abc123")
```

### `dataset.download()`

Returns a presigned download URL for this dataset. URL expires after 1 hour.

```python
result = dataset.download()
print(result["url"])
print(result["size"])  # bytes
```

---

## Jobs

### `client.jobs.get(job_id)`

Retrieve a job by ID. Use this to reattach to a running job.

```python
job = client.jobs.get("job_abc123")
job.monitor()
```

### `client.jobs.list()`

```python
jobs = client.jobs.list()
for job in jobs:
    print(job.id, job.status, job.hardware)
```

### Job object

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Job ID |
| `status` | `str` | `queued`, `running`, `completed`, `failed`, `cancelled` |
| `type` | `str` | `train`, `evaluate` |
| `hardware_requested` | `str` | Hardware requested (tier or auto) |
| `hardware_assigned` | `str` | Hardware actually assigned |
| `hardware` | `str` | Alias for `hardware_assigned` |
| `vram_estimated_gb` | `float \| None` | Estimated VRAM requirement |
| `vram_confidence` | `str \| None` | `exact` or `estimated` |
| `estimated_minutes` | `float \| None` | Point estimate of runtime (best GPU) |
| `estimated_cost_usd` | `float \| None` | Point estimate of cost (best GPU, USD) |
| `estimated_cost_min` | `float \| None` | Minimum cost across tier GPUs (USD) |
| `estimated_cost_max` | `float \| None` | Maximum cost across tier GPUs (USD) |
| `hardware_reason` | `str \| None` | Explanation of the hardware selection |
| `created_at` | `str` | ISO 8601 timestamp |

### `job.monitor(poll_interval, verbose)`

Block until the job finishes. Streams a live progress bar with loss and learning rate.

```python
job.monitor()                   # default: poll every 10s
job.monitor(verbose=True)       # also print all log lines
job.monitor(poll_interval=30)   # poll every 30s
```

Raises `RuntimeError` if the job fails.

### `job.logs()`

```python
for line in job.logs():
    print(line)
```

### `job.model()`

Register and return the fine-tuned model produced by this job.

```python
fine_tuned = job.model()
```

### `job.cancel()`

```python
job.cancel()
```

### `job.delete()`

```python
job.delete()
```

### `job.metrics()`

Return the full training metrics series for this job.

```python
data = job.metrics()
print(data["current_step"])
print(data["latest"])   # {'step': 100, 'loss': 2.34, 'lr': 1.38e-05, 'val_loss': None}

for point in data["metrics"]:
    print(point["step"], point["loss"])
```

The `metrics` array is ordered by step and safe to feed directly into a chart. `val_loss` is non-null when validation has run at that step.

### `job.eval_results()`

Return evaluation results for a completed eval job. Raises if called on a training job.

```python
results = job.eval_results()
```

| Key | Type | Description |
|-----|------|-------------|
| `loss` | `float` | Cross-entropy loss on the eval dataset |
| `perplexity` | `float` | `exp(loss)` — lower is better |
| `num_samples` | `int` | Number of rows evaluated |
| `duration_seconds` | `float` | Wall-clock time for the eval pass |

### `job.refresh()`

Re-fetch the job's status from the API.

```python
job.refresh()
print(job.status)
```

---

## Deployments

### `client.deployments.get(deployment_id)`

```python
endpoint = client.deployments.get("dep_abc123")
```

### `client.deployments.list()`

```python
deployments = client.deployments.list()
for d in deployments:
    print(d.id, d.status, d.hardware)
```

### Deployment object

| Attribute | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Deployment ID |
| `status` | `str` | `active`, `deleted` |
| `model_id` | `str` | Model being served |
| `hardware` | `str` | Hardware tier |
| `url` | `str \| None` | Inference endpoint URL |
| `created_at` | `str` | ISO 8601 timestamp |

### `deployment.infer(prompt, max_tokens, temperature)`

```python
result = endpoint.infer("What is LoRA?")
result = endpoint.infer("Summarise:", max_tokens=512, temperature=0.5)
```

Blocks until the response is complete. Cold starts may take up to a minute.

### `deployment.delete()`

```python
endpoint.delete()
```

---

## Account

### `client.billing()`

Returns current balance and up to 3 recent transactions.

```python
info = client.billing()
print(info["balance"])
```

### `client.transactions(page, limit)`

```python
result = client.transactions(page=1, limit=50)
for tx in result["transactions"]:
    print(tx["type"], tx["amount"])
```

### `client.overview()`

Returns a dashboard summary: balance, active jobs, recent activity.

```python
summary = client.overview()
```

### `client.settings()`

Return current account settings.

```python
settings = client.settings()
print(settings["max_checkpoints"])               # 1
print(settings["prune_checkpoints_on_completion"])  # True
print(settings["auto_recharge_enabled"])         # False
print(settings["auto_recharge_status"])          # "no_card"
```

### `client.update_settings(...)`

Update account settings. All parameters are optional; only those provided are changed. Returns the updated settings object.

```python
client.update_settings(max_checkpoints=5)
client.update_settings(prune_checkpoints_on_completion=False)
client.update_settings(
    auto_recharge_enabled=True,
    auto_recharge_threshold=5.0,
    auto_recharge_amount=25.0,
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `max_checkpoints` | `int` | `1` | Maximum checkpoints kept per job |
| `prune_checkpoints_on_completion` | `bool` | `True` | Delete checkpoints when job completes |
| `auto_recharge_enabled` | `bool` | `False` | Enable automatic balance top-up |
| `auto_recharge_threshold` | `float` | `10.0` | Balance (USD) that triggers recharge |
| `auto_recharge_amount` | `float` | `25.0` | Amount (USD) to charge per recharge |

See the [Billing](/docs/billing) page for details on auto-recharge and payment methods.

### `client.hardware()`

Returns available hardware tiers and their pricing.

```python
tiers = client.hardware()
```
