Spaces:
Runtime error
Runtime error
Merge HF Spaces demo isolation fix
Browse files# Conflicts:
# __pycache__/models.cpython-312.pyc
# baseline.py
# deployment/__init__.py
# deployment/client.py
# deployment/models.py
# deployment/openenv.yaml
# deployment/server/__init__.py
# deployment/server/app.py
# deployment/server/deployment_environment.py
# server/__pycache__/__init__.cpython-312.pyc
# server/__pycache__/deployment_environment.cpython-312.pyc
- .github/workflows/test.yml +45 -0
- deployment/Dockerfile → Dockerfile +3 -2
- README.md +316 -0
- __init__.py +1 -0
- __pycache__/baseline.cpython-312.pyc +0 -0
- baseline.py +381 -0
- deployment/README.md +0 -255
- deployment/__pycache__/models.cpython-312.pyc +0 -0
- deployment/baseline.py +0 -146
- deployment/pyproject.toml +0 -45
- deployment/server/__pycache__/__init__.cpython-312.pyc +0 -0
- deployment/server/__pycache__/deployment_environment.cpython-312.pyc +0 -0
- deployment/server/requirements.txt +0 -6
- inference.py +347 -0
- openenv.yaml +8 -0
- pyproject.toml +31 -0
- server/__init__.py +1 -0
- server/__pycache__/env.cpython-312.pyc +0 -0
- server/__pycache__/models.cpython-312.pyc +0 -0
- server/__pycache__/tasks.cpython-312.pyc +0 -0
- server/app.py +175 -0
- server/env.py +262 -0
- server/models.py +77 -0
- server/tasks.py +124 -0
- tests/test_agents.py +45 -0
- deployment/uv.lock → uv.lock +80 -201
.github/workflows/test.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Test & Validate Submission
|
| 2 |
+
|
| 3 |
+
on: [push, pull_request]
|
| 4 |
+
|
| 5 |
+
jobs:
|
| 6 |
+
validate:
|
| 7 |
+
runs-on: ubuntu-latest
|
| 8 |
+
strategy:
|
| 9 |
+
matrix:
|
| 10 |
+
python-version: ["3.10", "3.11"]
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v3
|
| 14 |
+
|
| 15 |
+
- name: Set up Python ${{ matrix.python-version }}
|
| 16 |
+
uses: actions/setup-python@v4
|
| 17 |
+
with:
|
| 18 |
+
python-version: ${{ matrix.python-version }}
|
| 19 |
+
|
| 20 |
+
- name: Install dependencies
|
| 21 |
+
run: |
|
| 22 |
+
python -m pip install --upgrade pip
|
| 23 |
+
pip install -e .
|
| 24 |
+
pip install pytest pytest-asyncio ruff mypy
|
| 25 |
+
|
| 26 |
+
- name: Lint with ruff
|
| 27 |
+
run: ruff check . --select=E,W,F
|
| 28 |
+
|
| 29 |
+
- name: Type check with mypy
|
| 30 |
+
run: |
|
| 31 |
+
mypy server/ inference.py baseline.py --ignore-missing-imports
|
| 32 |
+
|
| 33 |
+
- name: Test environment loads
|
| 34 |
+
run: |
|
| 35 |
+
python -c "from server.env import EcoLLMInferenceRoutingEnvironment; env = EcoLLMInferenceRoutingEnvironment(); print('Environment loads')"
|
| 36 |
+
|
| 37 |
+
- name: Test baseline agents
|
| 38 |
+
run: |
|
| 39 |
+
python -c "from baseline import RandomRoutingAgent; agent = RandomRoutingAgent(); print('Random agent loads')"
|
| 40 |
+
python -c "from baseline import HeuristicRoutingAgent; agent = HeuristicRoutingAgent(); print('Heuristic agent loads')"
|
| 41 |
+
|
| 42 |
+
- name: Test Docker build
|
| 43 |
+
run: |
|
| 44 |
+
docker build -t eco-llm-routing -f deployment/Dockerfile .
|
| 45 |
+
continue-on-error: true
|
deployment/Dockerfile → Dockerfile
RENAMED
|
@@ -70,11 +70,12 @@ ENV PATH="/app/.venv/bin:$PATH"
|
|
| 70 |
|
| 71 |
# Set PYTHONPATH so imports work correctly
|
| 72 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
|
|
|
| 73 |
|
| 74 |
# Health check
|
| 75 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
-
CMD curl -f http://localhost:
|
| 77 |
|
| 78 |
# Run the FastAPI server
|
| 79 |
# The module path is constructed to work with the /app/env structure
|
| 80 |
-
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port
|
|
|
|
| 70 |
|
| 71 |
# Set PYTHONPATH so imports work correctly
|
| 72 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 74 |
|
| 75 |
# Health check
|
| 76 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 77 |
+
CMD curl -f http://localhost:7860/ || exit 1
|
| 78 |
|
| 79 |
# Run the FastAPI server
|
| 80 |
# The module path is constructed to work with the /app/env structure
|
| 81 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 7860"]
|
README.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Eco-LLM Inference Routing Environment
|
| 2 |
+
|
| 3 |
+
> An [OpenEnv](https://github.com/meta-pytorch/OpenEnv)-compatible reinforcement learning environment for carbon-aware, multi-objective LLM query routing.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Overview
|
| 8 |
+
|
| 9 |
+
This environment simulates the problem of routing incoming LLM queries to the most appropriate model tier (small, medium, or large) while jointly optimising for **accuracy**, **energy efficiency**, **latency**, and **carbon footprint**.
|
| 10 |
+
|
| 11 |
+
At each step, an agent observes a query, the current carbon intensity of the grid, and the contents of a response cache. It then selects a routing *strategy* and a target *model*, receiving a structured reward signal that reflects all four objectives.
|
| 12 |
+
|
| 13 |
+
The environment is fully deterministic — given the same actions, every episode plays out identically — making it suitable for reproducible evaluation and grading.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Environment Design
|
| 18 |
+
|
| 19 |
+
### Action Space
|
| 20 |
+
|
| 21 |
+
Each action is an `RLAction` with three fields:
|
| 22 |
+
|
| 23 |
+
| Field | Type | Description |
|
| 24 |
+
|---|---|---|
|
| 25 |
+
| `strategy` | `Strategy` | How to handle the query (see below) |
|
| 26 |
+
| `model_choice` | `ModelChoice` | Target model: `SMALL`, `MEDIUM`, or `LARGE` |
|
| 27 |
+
| `exit_flag` | `bool` | Whether to terminate a cascade early on first correct answer |
|
| 28 |
+
|
| 29 |
+
**Available strategies:**
|
| 30 |
+
|
| 31 |
+
| Strategy | Behaviour |
|
| 32 |
+
|---|---|
|
| 33 |
+
| `NONE` | Direct inference with the chosen model |
|
| 34 |
+
| `USE_CACHE` | Serve from cache if this query was answered correctly before |
|
| 35 |
+
| `DO_CASCADE` | Try models from smallest up to the chosen model, stopping on `exit_flag` |
|
| 36 |
+
| `EARLY_EXIT` | Infer with chosen model; bonus reward if correct and stops early |
|
| 37 |
+
| `CALL_KB` | Look up a knowledge base; only succeeds if `kb_available` for the query |
|
| 38 |
+
| `WAIT` | Skip this step to wait for lower carbon intensity |
|
| 39 |
+
|
| 40 |
+
### Observation Space
|
| 41 |
+
|
| 42 |
+
Each `RLObservation` contains:
|
| 43 |
+
|
| 44 |
+
| Field | Description |
|
| 45 |
+
|---|---|
|
| 46 |
+
| `query` | The current query text |
|
| 47 |
+
| `carbon_intensity` | Grid carbon intensity this step (0.0 = clean, 1.0 = dirty) |
|
| 48 |
+
| `cache_contents` | List of queries already answered correctly (available for free re-use) |
|
| 49 |
+
| `correct_answer` | Ground-truth answer (used for grading) |
|
| 50 |
+
| `reward_details` | Full structured `RLReward` breakdown |
|
| 51 |
+
| `done` | Whether the episode has ended |
|
| 52 |
+
|
| 53 |
+
### Reward Function
|
| 54 |
+
|
| 55 |
+
```
|
| 56 |
+
reward = score − carbon_penalty − latency_penalty + bonuses
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
| Component | Formula | Notes |
|
| 60 |
+
|---|---|---|
|
| 61 |
+
| `score` | `1.0` if correct, else `0.0` | Primary accuracy signal |
|
| 62 |
+
| `carbon_penalty` | `carbon_intensity × energy_cost` | Penalises dirty-grid inference |
|
| 63 |
+
| `latency_penalty` | `0.1 × latency_cost` | Soft latency pressure |
|
| 64 |
+
| `bonuses` | Cache hit: `+0.5`, Early exit: `+0.1` | Efficiency rewards |
|
| 65 |
+
| Task penalty | Configurable per task (e.g. `−0.2` for LARGE on task 2) | Discourages over-provisioning |
|
| 66 |
+
|
| 67 |
+
**Energy and latency costs per model:**
|
| 68 |
+
|
| 69 |
+
| Model | Energy cost | Latency cost |
|
| 70 |
+
|---|---|---|
|
| 71 |
+
| `SMALL` | 0.1 | 1.0 |
|
| 72 |
+
| `MEDIUM` | 0.3 | 2.0 |
|
| 73 |
+
| `LARGE` | 0.6 | 5.0 |
|
| 74 |
+
|
| 75 |
+
### Tasks
|
| 76 |
+
|
| 77 |
+
Three tasks of increasing difficulty are included:
|
| 78 |
+
|
| 79 |
+
| Task | Difficulty | Queries | Description |
|
| 80 |
+
|---|---|---|---|
|
| 81 |
+
| `task_1` | Easy | 1 | Single-query routing; accuracy emphasis |
|
| 82 |
+
| `task_2` | Medium | 3 | Multi-query episode; LARGE model penalised |
|
| 83 |
+
| `task_3` | Hard | 5 | Stateful episode with repeated queries, caching, cascading, KB lookups, and carbon-aware waiting |
|
| 84 |
+
|
| 85 |
+
---
|
| 86 |
+
|
| 87 |
+
## Project Structure
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
eco_llm_inference_routing/
|
| 91 |
+
├── openenv.yaml # OpenEnv spec manifest
|
| 92 |
+
├── pyproject.toml # Python package and dependency config
|
| 93 |
+
├── uv.lock # Locked dependency tree
|
| 94 |
+
├── baseline.py # Baseline agents and CLI runner
|
| 95 |
+
├── server/
|
| 96 |
+
│ ├── app.py # FastAPI app (OpenEnv HTTP server)
|
| 97 |
+
│ ├── env.py # EcoLLMInferenceRoutingEnvironment (core logic)
|
| 98 |
+
│ ├── models.py # RLAction, RLObservation, RLState, RLReward
|
| 99 |
+
│ └── tasks.py # Task definitions, query specs, reward constants
|
| 100 |
+
└── deployment/
|
| 101 |
+
└── Dockerfile # Multi-stage build from openenv-base
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## Setup
|
| 107 |
+
|
| 108 |
+
### Prerequisites
|
| 109 |
+
|
| 110 |
+
- Python 3.10+
|
| 111 |
+
- [`uv`](https://github.com/astral-sh/uv) (recommended) or `pip`
|
| 112 |
+
- Docker (for containerised deployment)
|
| 113 |
+
|
| 114 |
+
### Install dependencies
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
# With uv (recommended)
|
| 118 |
+
uv sync
|
| 119 |
+
|
| 120 |
+
# With pip
|
| 121 |
+
pip install -e .
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### Run the server locally
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
python -m server.app
|
| 128 |
+
# or
|
| 129 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
The server exposes OpenEnv-standard endpoints at `http://localhost:8000`:
|
| 133 |
+
- `POST /reset` — start a new episode
|
| 134 |
+
- `POST /step` — take an action
|
| 135 |
+
- `GET /state` — inspect current state
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## Running the Inference Script
|
| 140 |
+
|
| 141 |
+
The submission-ready inference script is `inference.py` in the repo root. It uses the OpenAI-compatible client and emits mandatory `[START]`/`[STEP]`/`[END]` logs.
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
# Required environment variables
|
| 145 |
+
export HF_TOKEN=your_hf_token
|
| 146 |
+
export API_BASE_URL=https://router.huggingface.co/v1
|
| 147 |
+
export MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
| 148 |
+
export ECO_LLM_TASK=task_1 # task_1 | task_2 | task_3
|
| 149 |
+
|
| 150 |
+
python inference.py
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
**Example output:**
|
| 154 |
+
```
|
| 155 |
+
[START] task=task_1 env=eco_llm_inference_routing model=Qwen/Qwen2.5-72B-Instruct
|
| 156 |
+
[STEP] step=1 action=strategy=NONE,model=MEDIUM,exit=false reward=0.68 done=true error=null
|
| 157 |
+
[END] success=true steps=1 rewards=0.68
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Running Baselines
|
| 163 |
+
|
| 164 |
+
The `baseline.py` script provides three independent baseline agents for evaluation:
|
| 165 |
+
|
| 166 |
+
### Quick Start
|
| 167 |
+
|
| 168 |
+
```bash
|
| 169 |
+
# Evaluate single agent
|
| 170 |
+
python baseline.py evaluate --agent random --task task_3 --episodes 10
|
| 171 |
+
python baseline.py evaluate --agent heuristic --task task_3 --episodes 10
|
| 172 |
+
python baseline.py evaluate --agent llm --task task_3 --episodes 10 \
|
| 173 |
+
--api-key $HF_TOKEN --model-name Qwen/Qwen2.5-72B-Instruct
|
| 174 |
+
|
| 175 |
+
# Compare all agents on same task
|
| 176 |
+
python baseline.py compare --task task_3 --episodes 20
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
### Agent Types
|
| 180 |
+
|
| 181 |
+
#### 1. Random Agent (`--agent random`)
|
| 182 |
+
|
| 183 |
+
- Policy: Uniformly random strategy + model selection
|
| 184 |
+
- Purpose: Lower-bound baseline (no intelligence)
|
| 185 |
+
- Use Case: Verify environment is non-trivial
|
| 186 |
+
- Expected Performance: ~0.8-1.0 reward (`task_3`)
|
| 187 |
+
|
| 188 |
+
#### 2. Heuristic Agent (`--agent heuristic`)
|
| 189 |
+
|
| 190 |
+
- Policy: Rule-based routing
|
| 191 |
+
- Check cache first (free re-use)
|
| 192 |
+
- Try KB if available
|
| 193 |
+
- Wait if carbon > 0.7
|
| 194 |
+
- Cascade on hard queries
|
| 195 |
+
- Purpose: Mid-range baseline (no LLM calls)
|
| 196 |
+
- Use Case: Demonstrate strategic routing matters
|
| 197 |
+
- Expected Performance: ~2.0-2.3 reward (`task_3`)
|
| 198 |
+
|
| 199 |
+
#### 3. LLM Agent (`--agent llm`)
|
| 200 |
+
|
| 201 |
+
- Policy: Intelligent routing via Claude/GPT/Qwen
|
| 202 |
+
- Purpose: Upper-bound baseline (intelligent agent)
|
| 203 |
+
- Use Case: Show LLM can optimize trade-offs
|
| 204 |
+
- Requirements: Valid `HF_TOKEN` or `OPENAI_API_KEY`
|
| 205 |
+
- Expected Performance: ~2.2-2.5 reward (`task_3`)
|
| 206 |
+
|
| 207 |
+
### Baseline Comparison
|
| 208 |
+
|
| 209 |
+
```bash
|
| 210 |
+
$ python baseline.py compare --task task_3 --episodes 20
|
| 211 |
+
|
| 212 |
+
======================================================================
|
| 213 |
+
Eco-LLM Baseline: COMPARISON
|
| 214 |
+
Task: task_3 | Episodes: 20
|
| 215 |
+
======================================================================
|
| 216 |
+
|
| 217 |
+
Agent Mean Std Dev Min Max
|
| 218 |
+
----------------------------------------------------------------------
|
| 219 |
+
Random 0.8450 0.6200 0.0000 2.1000
|
| 220 |
+
Heuristic 2.1400 0.1800 1.9000 2.5000
|
| 221 |
+
LLM 2.3100 0.2500 1.8000 2.8000
|
| 222 |
+
|
| 223 |
+
Relative Performance:
|
| 224 |
+
Heuristic > Random: 2.53x
|
| 225 |
+
LLM > Heuristic: 1.08x
|
| 226 |
+
======================================================================
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
### Running on Different Tasks
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
# Easy task (1 query)
|
| 233 |
+
python baseline.py compare --task task_1 --episodes 50
|
| 234 |
+
|
| 235 |
+
# Medium task (3 queries)
|
| 236 |
+
python baseline.py compare --task task_2 --episodes 30
|
| 237 |
+
|
| 238 |
+
# Hard task (5 queries, stateful)
|
| 239 |
+
python baseline.py compare --task task_3 --episodes 20
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
### API Key Configuration
|
| 243 |
+
|
| 244 |
+
The LLM agent requires an API key. Set one of:
|
| 245 |
+
|
| 246 |
+
```bash
|
| 247 |
+
# Hugging Face (recommended for Qwen, Llama)
|
| 248 |
+
export HF_TOKEN='hf_xxxxxxxxxxxxx'
|
| 249 |
+
|
| 250 |
+
# OpenAI (for GPT models)
|
| 251 |
+
export OPENAI_API_KEY='sk-xxxxxxxxxxxxx'
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
Specify model:
|
| 255 |
+
|
| 256 |
+
```bash
|
| 257 |
+
python baseline.py evaluate --agent llm --task task_3 \
|
| 258 |
+
--model-name Qwen/Qwen2.5-72B-Instruct
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
### Expected Results Summary
|
| 262 |
+
|
| 263 |
+
| Task | Random | Heuristic | LLM |
|
| 264 |
+
|---|---:|---:|---:|
|
| 265 |
+
| `task_1` (Easy) | 0.50 | 1.50 | 1.45 |
|
| 266 |
+
| `task_2` (Medium) | 1.20 | 2.00 | 2.15 |
|
| 267 |
+
| `task_3` (Hard) | 0.85 | 2.14 | 2.31 |
|
| 268 |
+
|
| 269 |
+
Key insights:
|
| 270 |
+
|
| 271 |
+
- Random agent struggles because it has no strategy.
|
| 272 |
+
- Heuristic agent uses cache, KB, wait, and cascade effectively.
|
| 273 |
+
- LLM agent adds a modest improvement through better trade-off decisions.
|
| 274 |
+
- The environment demonstrates multi-objective routing trade-offs clearly.
|
| 275 |
+
|
| 276 |
+
---
|
| 277 |
+
|
| 278 |
+
## Docker Deployment
|
| 279 |
+
|
| 280 |
+
Build and run the environment as a container:
|
| 281 |
+
|
| 282 |
+
```bash
|
| 283 |
+
# Build
|
| 284 |
+
docker build -t eco-llm-routing -f deployment/Dockerfile .
|
| 285 |
+
|
| 286 |
+
# Run
|
| 287 |
+
docker run -p 8000:8000 eco-llm-routing
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
The container serves the OpenEnv HTTP interface at port 8000 and includes a health check at `GET /health`.
|
| 291 |
+
|
| 292 |
+
To deploy to Hugging Face Spaces via the OpenEnv CLI:
|
| 293 |
+
|
| 294 |
+
```bash
|
| 295 |
+
pip install openenv-core
|
| 296 |
+
openenv push --repo-id your-org/eco-llm-routing
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
---
|
| 300 |
+
|
| 301 |
+
## OpenEnv Compliance
|
| 302 |
+
|
| 303 |
+
This environment conforms to the [OpenEnv spec](https://github.com/meta-pytorch/OpenEnv):
|
| 304 |
+
|
| 305 |
+
- `openenv.yaml` with `spec_version: 1`, `type: space`, `runtime: fastapi`
|
| 306 |
+
- `EcoLLMInferenceRoutingEnvironment` extends `openenv.core.env_server.interfaces.Environment`
|
| 307 |
+
- Typed `Action`, `Observation`, and `State` via Pydantic models
|
| 308 |
+
- Server bootstrapped with `create_app(...)` from `openenv.core.env_server.http_server`
|
| 309 |
+
- Multi-stage Dockerfile based on `ghcr.io/meta-pytorch/openenv-base`
|
| 310 |
+
|
| 311 |
+
---
|
| 312 |
+
|
| 313 |
+
## License
|
| 314 |
+
|
| 315 |
+
BSD-style license — see [LICENSE](./LICENSE) for full terms.
|
| 316 |
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Eco-LLM inference routing OpenEnv package."""
|
__pycache__/baseline.cpython-312.pyc
ADDED
|
Binary file (17 kB). View file
|
|
|
baseline.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
baseline.py - Eco-LLM Baseline Agents
|
| 3 |
+
=====================================
|
| 4 |
+
Three independent baseline agents for evaluation and comparison.
|
| 5 |
+
|
| 6 |
+
Usage:
|
| 7 |
+
python baseline.py evaluate --agent random --task task_1 --episodes 10
|
| 8 |
+
python baseline.py evaluate --agent heuristic --task task_3 --episodes 10
|
| 9 |
+
python baseline.py compare --task task_3 --episodes 20
|
| 10 |
+
|
| 11 |
+
Expected Results (task_3):
|
| 12 |
+
Random agent: ~0.85 +/- 0.62
|
| 13 |
+
Heuristic agent: ~2.14 +/- 0.18
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import json
|
| 20 |
+
import os
|
| 21 |
+
import sys
|
| 22 |
+
import textwrap
|
| 23 |
+
from abc import ABC, abstractmethod
|
| 24 |
+
from typing import Optional
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
from openai import OpenAI
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
from server.env import EcoLLMInferenceRoutingEnvironment
|
| 31 |
+
from server.models import ModelChoice, RLAction, RLObservation, Strategy
|
| 32 |
+
except ImportError as exc:
|
| 33 |
+
print(f"ERROR: Cannot import server modules: {exc}")
|
| 34 |
+
print("Make sure you're in the repo root directory.")
|
| 35 |
+
sys.exit(1)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class BaseRoutingAgent(ABC):
|
| 39 |
+
"""Abstract base class for routing agents."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, name: str = "BaseAgent") -> None:
|
| 42 |
+
self.name = name
|
| 43 |
+
|
| 44 |
+
@abstractmethod
|
| 45 |
+
def get_action(self, observation: RLObservation) -> RLAction:
|
| 46 |
+
"""Given an observation, return the next action."""
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class RandomRoutingAgent(BaseRoutingAgent):
|
| 50 |
+
"""
|
| 51 |
+
Random baseline agent.
|
| 52 |
+
|
| 53 |
+
Policy: Uniformly random strategy + model selection.
|
| 54 |
+
Purpose: Lower-bound baseline (no intelligence).
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
def __init__(self, seed: int = 42) -> None:
|
| 58 |
+
super().__init__(name="RandomRoutingAgent")
|
| 59 |
+
self.rng = np.random.RandomState(seed)
|
| 60 |
+
|
| 61 |
+
def get_action(self, observation: RLObservation) -> RLAction:
|
| 62 |
+
"""Select a random strategy and model."""
|
| 63 |
+
del observation
|
| 64 |
+
strategies = list(Strategy)
|
| 65 |
+
models = list(ModelChoice)
|
| 66 |
+
strategy = strategies[int(self.rng.randint(len(strategies)))]
|
| 67 |
+
model_choice = models[int(self.rng.randint(len(models)))]
|
| 68 |
+
exit_flag = bool(self.rng.random() > 0.5)
|
| 69 |
+
return RLAction(strategy=strategy, model_choice=model_choice, exit_flag=exit_flag)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class HeuristicRoutingAgent(BaseRoutingAgent):
|
| 73 |
+
"""
|
| 74 |
+
Heuristic baseline agent.
|
| 75 |
+
|
| 76 |
+
Policy: Rule-based routing with priorities.
|
| 77 |
+
Priority 1: Use cache
|
| 78 |
+
Priority 2: Wait if carbon > 0.7
|
| 79 |
+
Priority 3: Cascade with early exit
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self) -> None:
|
| 83 |
+
super().__init__(name="HeuristicRoutingAgent")
|
| 84 |
+
|
| 85 |
+
def get_action(self, observation: RLObservation) -> RLAction:
|
| 86 |
+
"""Apply heuristic routing policy."""
|
| 87 |
+
query = observation.query
|
| 88 |
+
carbon = observation.carbon_intensity
|
| 89 |
+
cache = observation.cache_contents
|
| 90 |
+
|
| 91 |
+
if query in cache:
|
| 92 |
+
return RLAction(
|
| 93 |
+
strategy=Strategy.USE_CACHE,
|
| 94 |
+
model_choice=ModelChoice.SMALL,
|
| 95 |
+
exit_flag=False,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
if carbon > 0.7:
|
| 99 |
+
return RLAction(
|
| 100 |
+
strategy=Strategy.WAIT,
|
| 101 |
+
model_choice=ModelChoice.SMALL,
|
| 102 |
+
exit_flag=False,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
return RLAction(
|
| 106 |
+
strategy=Strategy.DO_CASCADE,
|
| 107 |
+
model_choice=ModelChoice.LARGE,
|
| 108 |
+
exit_flag=True,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class LLMRoutingAgent(BaseRoutingAgent):
|
| 113 |
+
"""
|
| 114 |
+
LLM-based baseline agent.
|
| 115 |
+
|
| 116 |
+
Policy: Uses an LLM to make routing decisions.
|
| 117 |
+
Purpose: Upper-bound baseline (intelligent agent).
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
SYSTEM_PROMPT = textwrap.dedent(
|
| 121 |
+
"""
|
| 122 |
+
You are an expert LLM inference router.
|
| 123 |
+
Optimize for accuracy, energy efficiency, carbon awareness, and latency.
|
| 124 |
+
|
| 125 |
+
Available strategies: NONE, USE_CACHE, DO_CASCADE, EARLY_EXIT, WAIT, CALL_KB
|
| 126 |
+
Models: SMALL, MEDIUM, LARGE
|
| 127 |
+
|
| 128 |
+
Respond with ONLY valid JSON:
|
| 129 |
+
{"strategy": "...", "model_choice": "...", "exit_flag": true/false}
|
| 130 |
+
"""
|
| 131 |
+
).strip()
|
| 132 |
+
|
| 133 |
+
def __init__(self, api_key: Optional[str] = None, model_name: str = "gpt-4-mini") -> None:
|
| 134 |
+
super().__init__(name="LLMRoutingAgent")
|
| 135 |
+
resolved_api_key = api_key or os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY")
|
| 136 |
+
if not resolved_api_key:
|
| 137 |
+
raise ValueError("LLM agent requires HF_TOKEN or OPENAI_API_KEY")
|
| 138 |
+
|
| 139 |
+
self.client = OpenAI(
|
| 140 |
+
base_url=os.getenv("API_BASE_URL", "https://router.huggingface.co/v1"),
|
| 141 |
+
api_key=resolved_api_key,
|
| 142 |
+
)
|
| 143 |
+
self.model_name = model_name
|
| 144 |
+
|
| 145 |
+
def get_action(self, observation: RLObservation) -> RLAction:
|
| 146 |
+
"""Query the LLM for a routing decision."""
|
| 147 |
+
try:
|
| 148 |
+
query = observation.query
|
| 149 |
+
carbon = observation.carbon_intensity
|
| 150 |
+
cache = json.dumps(observation.cache_contents) if observation.cache_contents else "[]"
|
| 151 |
+
|
| 152 |
+
prompt = f"""
|
| 153 |
+
Query: {query!r}
|
| 154 |
+
Carbon intensity: {carbon:.2f}
|
| 155 |
+
Cache: {cache}
|
| 156 |
+
|
| 157 |
+
Choose the best routing strategy and model.
|
| 158 |
+
""".strip()
|
| 159 |
+
|
| 160 |
+
response = self.client.chat.completions.create(
|
| 161 |
+
model=self.model_name,
|
| 162 |
+
messages=[
|
| 163 |
+
{"role": "system", "content": self.SYSTEM_PROMPT},
|
| 164 |
+
{"role": "user", "content": prompt},
|
| 165 |
+
],
|
| 166 |
+
temperature=0.2,
|
| 167 |
+
max_tokens=100,
|
| 168 |
+
timeout=10.0,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
raw = (response.choices[0].message.content or "").strip()
|
| 172 |
+
raw = raw.replace("```json", "").replace("```", "").strip()
|
| 173 |
+
parsed = json.loads(raw)
|
| 174 |
+
|
| 175 |
+
strategy = Strategy[parsed.get("strategy", "NONE").upper()]
|
| 176 |
+
model_choice = ModelChoice[parsed.get("model_choice", "SMALL").upper()]
|
| 177 |
+
exit_flag = bool(parsed.get("exit_flag", False))
|
| 178 |
+
return RLAction(strategy=strategy, model_choice=model_choice, exit_flag=exit_flag)
|
| 179 |
+
|
| 180 |
+
except Exception as exc:
|
| 181 |
+
print(f"[DEBUG] LLM error: {exc}, using heuristic", flush=True)
|
| 182 |
+
heuristic = HeuristicRoutingAgent()
|
| 183 |
+
return heuristic.get_action(observation)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def evaluate_agent(
|
| 187 |
+
agent: BaseRoutingAgent,
|
| 188 |
+
task_id: str,
|
| 189 |
+
num_episodes: int = 10,
|
| 190 |
+
verbose: bool = True,
|
| 191 |
+
) -> dict:
|
| 192 |
+
"""Run an agent on a task for multiple episodes."""
|
| 193 |
+
env = EcoLLMInferenceRoutingEnvironment()
|
| 194 |
+
rewards_list: list[float] = []
|
| 195 |
+
|
| 196 |
+
for episode in range(num_episodes):
|
| 197 |
+
try:
|
| 198 |
+
obs = env.reset(task_id=task_id)
|
| 199 |
+
episode_reward = 0.0
|
| 200 |
+
|
| 201 |
+
while not obs.done:
|
| 202 |
+
action = agent.get_action(obs)
|
| 203 |
+
obs = env.step(action)
|
| 204 |
+
episode_reward += obs.reward_details.total_reward
|
| 205 |
+
|
| 206 |
+
rewards_list.append(episode_reward)
|
| 207 |
+
if verbose:
|
| 208 |
+
print(f" Episode {episode + 1:2d}/{num_episodes}: reward={episode_reward:.4f}", flush=True)
|
| 209 |
+
|
| 210 |
+
except Exception as exc:
|
| 211 |
+
print(f" Episode {episode + 1:2d}/{num_episodes}: ERROR - {str(exc)[:80]}", flush=True)
|
| 212 |
+
rewards_list.append(0.0)
|
| 213 |
+
|
| 214 |
+
rewards_array = np.array(rewards_list)
|
| 215 |
+
return {
|
| 216 |
+
"mean": float(np.mean(rewards_array)),
|
| 217 |
+
"std": float(np.std(rewards_array)),
|
| 218 |
+
"min": float(np.min(rewards_array)),
|
| 219 |
+
"max": float(np.max(rewards_array)),
|
| 220 |
+
"rewards": rewards_list,
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def main() -> None:
|
| 225 |
+
"""Command-line interface for baseline evaluation."""
|
| 226 |
+
parser = argparse.ArgumentParser(
|
| 227 |
+
description="Eco-LLM Baseline Agent Evaluation",
|
| 228 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 229 |
+
epilog="""
|
| 230 |
+
Examples:
|
| 231 |
+
python baseline.py evaluate --agent random --task task_1 --episodes 10
|
| 232 |
+
python baseline.py evaluate --agent heuristic --task task_3 --episodes 10
|
| 233 |
+
python baseline.py evaluate --agent llm --task task_3 --episodes 10 --api-key $HF_TOKEN
|
| 234 |
+
python baseline.py compare --task task_3 --episodes 20
|
| 235 |
+
""",
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
| 239 |
+
|
| 240 |
+
evaluate_parser = subparsers.add_parser("evaluate", help="Evaluate a single agent")
|
| 241 |
+
evaluate_parser.add_argument(
|
| 242 |
+
"--agent",
|
| 243 |
+
choices=["random", "heuristic", "llm"],
|
| 244 |
+
default="heuristic",
|
| 245 |
+
help="Agent type",
|
| 246 |
+
)
|
| 247 |
+
evaluate_parser.add_argument(
|
| 248 |
+
"--task",
|
| 249 |
+
choices=["task_1", "task_2", "task_3"],
|
| 250 |
+
default="task_1",
|
| 251 |
+
help="Task to evaluate on",
|
| 252 |
+
)
|
| 253 |
+
evaluate_parser.add_argument(
|
| 254 |
+
"--episodes",
|
| 255 |
+
type=int,
|
| 256 |
+
default=10,
|
| 257 |
+
help="Number of episodes",
|
| 258 |
+
)
|
| 259 |
+
evaluate_parser.add_argument(
|
| 260 |
+
"--api-key",
|
| 261 |
+
default=None,
|
| 262 |
+
help="API key for LLM agent (defaults to HF_TOKEN)",
|
| 263 |
+
)
|
| 264 |
+
evaluate_parser.add_argument(
|
| 265 |
+
"--model-name",
|
| 266 |
+
default="gpt-4-mini",
|
| 267 |
+
help="LLM model name",
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
compare_parser = subparsers.add_parser("compare", help="Compare all agents")
|
| 271 |
+
compare_parser.add_argument(
|
| 272 |
+
"--task",
|
| 273 |
+
choices=["task_1", "task_2", "task_3"],
|
| 274 |
+
default="task_1",
|
| 275 |
+
help="Task to evaluate on",
|
| 276 |
+
)
|
| 277 |
+
compare_parser.add_argument(
|
| 278 |
+
"--episodes",
|
| 279 |
+
type=int,
|
| 280 |
+
default=10,
|
| 281 |
+
help="Episodes per agent",
|
| 282 |
+
)
|
| 283 |
+
compare_parser.add_argument(
|
| 284 |
+
"--api-key",
|
| 285 |
+
default=None,
|
| 286 |
+
help="API key for LLM agent (defaults to HF_TOKEN)",
|
| 287 |
+
)
|
| 288 |
+
compare_parser.add_argument(
|
| 289 |
+
"--model-name",
|
| 290 |
+
default="gpt-4-mini",
|
| 291 |
+
help="LLM model name",
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
args = parser.parse_args()
|
| 295 |
+
|
| 296 |
+
if args.command == "evaluate":
|
| 297 |
+
print(f"\n{'=' * 70}")
|
| 298 |
+
print(" Eco-LLM Baseline Evaluation")
|
| 299 |
+
print(f" Agent: {args.agent.upper()}")
|
| 300 |
+
print(f" Task: {args.task} | Episodes: {args.episodes}")
|
| 301 |
+
print(f"{'=' * 70}\n")
|
| 302 |
+
|
| 303 |
+
if args.agent == "random":
|
| 304 |
+
agent = RandomRoutingAgent()
|
| 305 |
+
elif args.agent == "heuristic":
|
| 306 |
+
agent = HeuristicRoutingAgent()
|
| 307 |
+
else:
|
| 308 |
+
try:
|
| 309 |
+
agent = LLMRoutingAgent(api_key=args.api_key, model_name=args.model_name)
|
| 310 |
+
except ValueError as exc:
|
| 311 |
+
print(f"ERROR: {exc}")
|
| 312 |
+
sys.exit(1)
|
| 313 |
+
|
| 314 |
+
results = evaluate_agent(agent, args.task, args.episodes)
|
| 315 |
+
|
| 316 |
+
print(f"\n{'=' * 70}")
|
| 317 |
+
print(f" Results: {agent.name}")
|
| 318 |
+
print(f"{'=' * 70}")
|
| 319 |
+
print(f" Mean : {results['mean']:.4f}")
|
| 320 |
+
print(f" Std Dev: {results['std']:.4f}")
|
| 321 |
+
print(f" Min : {results['min']:.4f}")
|
| 322 |
+
print(f" Max : {results['max']:.4f}")
|
| 323 |
+
print(f"{'=' * 70}\n")
|
| 324 |
+
|
| 325 |
+
elif args.command == "compare":
|
| 326 |
+
print(f"\n{'=' * 70}")
|
| 327 |
+
print(" Eco-LLM Baseline Comparison")
|
| 328 |
+
print(f" Task: {args.task} | Episodes: {args.episodes}")
|
| 329 |
+
print(f"{'=' * 70}\n")
|
| 330 |
+
|
| 331 |
+
results_dict: dict[str, dict] = {}
|
| 332 |
+
|
| 333 |
+
print("Running Random Agent...")
|
| 334 |
+
agent_random = RandomRoutingAgent()
|
| 335 |
+
results_dict["Random"] = evaluate_agent(agent_random, args.task, args.episodes, verbose=False)
|
| 336 |
+
|
| 337 |
+
print("\nRunning Heuristic Agent...")
|
| 338 |
+
agent_heuristic = HeuristicRoutingAgent()
|
| 339 |
+
results_dict["Heuristic"] = evaluate_agent(
|
| 340 |
+
agent_heuristic,
|
| 341 |
+
args.task,
|
| 342 |
+
args.episodes,
|
| 343 |
+
verbose=False,
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
try:
|
| 347 |
+
print("\nRunning LLM Agent...")
|
| 348 |
+
agent_llm = LLMRoutingAgent(api_key=args.api_key, model_name=args.model_name)
|
| 349 |
+
results_dict["LLM"] = evaluate_agent(agent_llm, args.task, args.episodes, verbose=False)
|
| 350 |
+
except ValueError:
|
| 351 |
+
print("Skipping LLM Agent (no API key available)")
|
| 352 |
+
|
| 353 |
+
print(f"\n{'=' * 70}")
|
| 354 |
+
print(" Results Comparison")
|
| 355 |
+
print(f"{'=' * 70}\n")
|
| 356 |
+
print(f"{'Agent':<15} {'Mean':<12} {'Std Dev':<12} {'Min':<12} {'Max':<12}")
|
| 357 |
+
print(f"{'-' * 70}")
|
| 358 |
+
|
| 359 |
+
for agent_name, results in results_dict.items():
|
| 360 |
+
print(
|
| 361 |
+
f"{agent_name:<15} {results['mean']:<12.4f} "
|
| 362 |
+
f"{results['std']:<12.4f} {results['min']:<12.4f} {results['max']:<12.4f}"
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
print(f"\n{'Relative Performance':<15}")
|
| 366 |
+
print(f"{'-' * 70}")
|
| 367 |
+
if "Heuristic" in results_dict and "Random" in results_dict:
|
| 368 |
+
ratio = results_dict["Heuristic"]["mean"] / (results_dict["Random"]["mean"] + 1e-6)
|
| 369 |
+
print(f" Heuristic > Random: {ratio:.2f}x")
|
| 370 |
+
if "LLM" in results_dict and "Heuristic" in results_dict:
|
| 371 |
+
ratio = results_dict["LLM"]["mean"] / (results_dict["Heuristic"]["mean"] + 1e-6)
|
| 372 |
+
print(f" LLM > Heuristic: {ratio:.2f}x")
|
| 373 |
+
|
| 374 |
+
print(f"{'=' * 70}\n")
|
| 375 |
+
|
| 376 |
+
else:
|
| 377 |
+
parser.print_help()
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
if __name__ == "__main__":
|
| 381 |
+
main()
|
deployment/README.md
DELETED
|
@@ -1,255 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Deployment Environment Server
|
| 3 |
-
emoji: 🖥️
|
| 4 |
-
colorFrom: pink
|
| 5 |
-
colorTo: yellow
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
app_port: 8000
|
| 9 |
-
base_path: /web
|
| 10 |
-
tags:
|
| 11 |
-
- openenv
|
| 12 |
-
---
|
| 13 |
-
|
| 14 |
-
# Deployment Environment
|
| 15 |
-
|
| 16 |
-
A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
|
| 17 |
-
|
| 18 |
-
## Quick Start
|
| 19 |
-
|
| 20 |
-
The simplest way to use the Deployment environment is through the `DeploymentEnv` class:
|
| 21 |
-
|
| 22 |
-
```python
|
| 23 |
-
from deployment import DeploymentAction, DeploymentEnv
|
| 24 |
-
|
| 25 |
-
try:
|
| 26 |
-
# Create environment from Docker image
|
| 27 |
-
deploymentenv = DeploymentEnv.from_docker_image("deployment-env:latest")
|
| 28 |
-
|
| 29 |
-
# Reset
|
| 30 |
-
result = deploymentenv.reset()
|
| 31 |
-
print(f"Reset: {result.observation.echoed_message}")
|
| 32 |
-
|
| 33 |
-
# Send multiple messages
|
| 34 |
-
messages = ["Hello, World!", "Testing echo", "Final message"]
|
| 35 |
-
|
| 36 |
-
for msg in messages:
|
| 37 |
-
result = deploymentenv.step(DeploymentAction(message=msg))
|
| 38 |
-
print(f"Sent: '{msg}'")
|
| 39 |
-
print(f" → Echoed: '{result.observation.echoed_message}'")
|
| 40 |
-
print(f" → Length: {result.observation.message_length}")
|
| 41 |
-
print(f" → Reward: {result.reward}")
|
| 42 |
-
|
| 43 |
-
finally:
|
| 44 |
-
# Always clean up
|
| 45 |
-
deploymentenv.close()
|
| 46 |
-
```
|
| 47 |
-
|
| 48 |
-
That's it! The `DeploymentEnv.from_docker_image()` method handles:
|
| 49 |
-
- Starting the Docker container
|
| 50 |
-
- Waiting for the server to be ready
|
| 51 |
-
- Connecting to the environment
|
| 52 |
-
- Container cleanup when you call `close()`
|
| 53 |
-
|
| 54 |
-
## Building the Docker Image
|
| 55 |
-
|
| 56 |
-
Before using the environment, you need to build the Docker image:
|
| 57 |
-
|
| 58 |
-
```bash
|
| 59 |
-
# From project root
|
| 60 |
-
docker build -t deployment-env:latest -f server/Dockerfile .
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
## Deploying to Hugging Face Spaces
|
| 64 |
-
|
| 65 |
-
You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
|
| 66 |
-
|
| 67 |
-
```bash
|
| 68 |
-
# From the environment directory (where openenv.yaml is located)
|
| 69 |
-
openenv push
|
| 70 |
-
|
| 71 |
-
# Or specify options
|
| 72 |
-
openenv push --namespace my-org --private
|
| 73 |
-
```
|
| 74 |
-
|
| 75 |
-
The `openenv push` command will:
|
| 76 |
-
1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
|
| 77 |
-
2. Prepare a custom build for Hugging Face Docker space (enables web interface)
|
| 78 |
-
3. Upload to Hugging Face (ensuring you're logged in)
|
| 79 |
-
|
| 80 |
-
### Prerequisites
|
| 81 |
-
|
| 82 |
-
- Authenticate with Hugging Face: The command will prompt for login if not already authenticated
|
| 83 |
-
|
| 84 |
-
### Options
|
| 85 |
-
|
| 86 |
-
- `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
|
| 87 |
-
- `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
|
| 88 |
-
- `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
|
| 89 |
-
- `--private`: Deploy the space as private (default: public)
|
| 90 |
-
|
| 91 |
-
### Examples
|
| 92 |
-
|
| 93 |
-
```bash
|
| 94 |
-
# Push to your personal namespace (defaults to username/env-name from openenv.yaml)
|
| 95 |
-
openenv push
|
| 96 |
-
|
| 97 |
-
# Push to a specific repository
|
| 98 |
-
openenv push --repo-id my-org/my-env
|
| 99 |
-
|
| 100 |
-
# Push with a custom base image
|
| 101 |
-
openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
|
| 102 |
-
|
| 103 |
-
# Push as a private space
|
| 104 |
-
openenv push --private
|
| 105 |
-
|
| 106 |
-
# Combine options
|
| 107 |
-
openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
|
| 108 |
-
```
|
| 109 |
-
|
| 110 |
-
After deployment, your space will be available at:
|
| 111 |
-
`https://huggingface.co/spaces/<repo-id>`
|
| 112 |
-
|
| 113 |
-
The deployed space includes:
|
| 114 |
-
- **Web Interface** at `/web` - Interactive UI for exploring the environment
|
| 115 |
-
- **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
|
| 116 |
-
- **Health Check** at `/health` - Container health monitoring
|
| 117 |
-
- **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
|
| 118 |
-
|
| 119 |
-
## Environment Details
|
| 120 |
-
|
| 121 |
-
### Action
|
| 122 |
-
**DeploymentAction**: Contains a single field
|
| 123 |
-
- `message` (str) - The message to echo back
|
| 124 |
-
|
| 125 |
-
### Observation
|
| 126 |
-
**DeploymentObservation**: Contains the echo response and metadata
|
| 127 |
-
- `echoed_message` (str) - The message echoed back
|
| 128 |
-
- `message_length` (int) - Length of the message
|
| 129 |
-
- `reward` (float) - Reward based on message length (length × 0.1)
|
| 130 |
-
- `done` (bool) - Always False for echo environment
|
| 131 |
-
- `metadata` (dict) - Additional info like step count
|
| 132 |
-
|
| 133 |
-
### Reward
|
| 134 |
-
The reward is calculated as: `message_length × 0.1`
|
| 135 |
-
- "Hi" → reward: 0.2
|
| 136 |
-
- "Hello, World!" → reward: 1.3
|
| 137 |
-
- Empty message → reward: 0.0
|
| 138 |
-
|
| 139 |
-
## Advanced Usage
|
| 140 |
-
|
| 141 |
-
### Connecting to an Existing Server
|
| 142 |
-
|
| 143 |
-
If you already have a Deployment environment server running, you can connect directly:
|
| 144 |
-
|
| 145 |
-
```python
|
| 146 |
-
from deployment import DeploymentEnv
|
| 147 |
-
|
| 148 |
-
# Connect to existing server
|
| 149 |
-
deploymentenv = DeploymentEnv(base_url="<ENV_HTTP_URL_HERE>")
|
| 150 |
-
|
| 151 |
-
# Use as normal
|
| 152 |
-
result = deploymentenv.reset()
|
| 153 |
-
result = deploymentenv.step(DeploymentAction(message="Hello!"))
|
| 154 |
-
```
|
| 155 |
-
|
| 156 |
-
Note: When connecting to an existing server, `deploymentenv.close()` will NOT stop the server.
|
| 157 |
-
|
| 158 |
-
### Using the Context Manager
|
| 159 |
-
|
| 160 |
-
The client supports context manager usage for automatic connection management:
|
| 161 |
-
|
| 162 |
-
```python
|
| 163 |
-
from deployment import DeploymentAction, DeploymentEnv
|
| 164 |
-
|
| 165 |
-
# Connect with context manager (auto-connects and closes)
|
| 166 |
-
with DeploymentEnv(base_url="http://localhost:8000") as env:
|
| 167 |
-
result = env.reset()
|
| 168 |
-
print(f"Reset: {result.observation.echoed_message}")
|
| 169 |
-
# Multiple steps with low latency
|
| 170 |
-
for msg in ["Hello", "World", "!"]:
|
| 171 |
-
result = env.step(DeploymentAction(message=msg))
|
| 172 |
-
print(f"Echoed: {result.observation.echoed_message}")
|
| 173 |
-
```
|
| 174 |
-
|
| 175 |
-
The client uses WebSocket connections for:
|
| 176 |
-
- **Lower latency**: No HTTP connection overhead per request
|
| 177 |
-
- **Persistent session**: Server maintains your environment state
|
| 178 |
-
- **Efficient for episodes**: Better for many sequential steps
|
| 179 |
-
|
| 180 |
-
### Concurrent WebSocket Sessions
|
| 181 |
-
|
| 182 |
-
The server supports multiple concurrent WebSocket connections. To enable this,
|
| 183 |
-
modify `server/app.py` to use factory mode:
|
| 184 |
-
|
| 185 |
-
```python
|
| 186 |
-
# In server/app.py - use factory mode for concurrent sessions
|
| 187 |
-
app = create_app(
|
| 188 |
-
DeploymentEnvironment, # Pass class, not instance
|
| 189 |
-
DeploymentAction,
|
| 190 |
-
DeploymentObservation,
|
| 191 |
-
max_concurrent_envs=4, # Allow 4 concurrent sessions
|
| 192 |
-
)
|
| 193 |
-
```
|
| 194 |
-
|
| 195 |
-
Then multiple clients can connect simultaneously:
|
| 196 |
-
|
| 197 |
-
```python
|
| 198 |
-
from deployment import DeploymentAction, DeploymentEnv
|
| 199 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 200 |
-
|
| 201 |
-
def run_episode(client_id: int):
|
| 202 |
-
with DeploymentEnv(base_url="http://localhost:8000") as env:
|
| 203 |
-
result = env.reset()
|
| 204 |
-
for i in range(10):
|
| 205 |
-
result = env.step(DeploymentAction(message=f"Client {client_id}, step {i}"))
|
| 206 |
-
return client_id, result.observation.message_length
|
| 207 |
-
|
| 208 |
-
# Run 4 episodes concurrently
|
| 209 |
-
with ThreadPoolExecutor(max_workers=4) as executor:
|
| 210 |
-
results = list(executor.map(run_episode, range(4)))
|
| 211 |
-
```
|
| 212 |
-
|
| 213 |
-
## Development & Testing
|
| 214 |
-
|
| 215 |
-
### Direct Environment Testing
|
| 216 |
-
|
| 217 |
-
Test the environment logic directly without starting the HTTP server:
|
| 218 |
-
|
| 219 |
-
```bash
|
| 220 |
-
# From the server directory
|
| 221 |
-
python3 server/deployment_environment.py
|
| 222 |
-
```
|
| 223 |
-
|
| 224 |
-
This verifies that:
|
| 225 |
-
- Environment resets correctly
|
| 226 |
-
- Step executes actions properly
|
| 227 |
-
- State tracking works
|
| 228 |
-
- Rewards are calculated correctly
|
| 229 |
-
|
| 230 |
-
### Running Locally
|
| 231 |
-
|
| 232 |
-
Run the server locally for development:
|
| 233 |
-
|
| 234 |
-
```bash
|
| 235 |
-
uvicorn server.app:app --reload
|
| 236 |
-
```
|
| 237 |
-
|
| 238 |
-
## Project Structure
|
| 239 |
-
|
| 240 |
-
```
|
| 241 |
-
deployment/
|
| 242 |
-
├── .dockerignore # Docker build exclusions
|
| 243 |
-
├── __init__.py # Module exports
|
| 244 |
-
├── README.md # This file
|
| 245 |
-
├── openenv.yaml # OpenEnv manifest
|
| 246 |
-
├── pyproject.toml # Project metadata and dependencies
|
| 247 |
-
├── uv.lock # Locked dependencies (generated)
|
| 248 |
-
├── client.py # DeploymentEnv client
|
| 249 |
-
├── models.py # Action and Observation models
|
| 250 |
-
└── server/
|
| 251 |
-
├── __init__.py # Server module exports
|
| 252 |
-
├── deployment_environment.py # Core environment logic
|
| 253 |
-
├── app.py # FastAPI application (HTTP + WebSocket endpoints)
|
| 254 |
-
└── Dockerfile # Container image definition
|
| 255 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deployment/__pycache__/models.cpython-312.pyc
DELETED
|
Binary file (2.46 kB)
|
|
|
deployment/baseline.py
DELETED
|
@@ -1,146 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Baseline evaluation script for the Eco-LLM Inference Routing Environment.
|
| 4 |
-
|
| 5 |
-
Policies:
|
| 6 |
-
- random: Random valid strategy + model each step.
|
| 7 |
-
- heuristic: SMALL by default; escalate to MEDIUM/LARGE only if needed;
|
| 8 |
-
use cache when available; wait when carbon > 0.7.
|
| 9 |
-
- llm: Uses Anthropic Claude via the API to decide strategy + model.
|
| 10 |
-
|
| 11 |
-
Usage:
|
| 12 |
-
python baseline.py --task easy
|
| 13 |
-
python baseline.py --task medium --policy heuristic
|
| 14 |
-
python baseline.py --task hard --policy llm --episodes 5
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
import argparse
|
| 18 |
-
import random
|
| 19 |
-
import statistics
|
| 20 |
-
from typing import Tuple
|
| 21 |
-
|
| 22 |
-
# ── Inline env import (no server needed for local eval) ──────────────────────
|
| 23 |
-
import sys
|
| 24 |
-
import os
|
| 25 |
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 26 |
-
|
| 27 |
-
from server.deployment_environment import EcoLLMEnvironment
|
| 28 |
-
from models import EcoLLMAction, EcoLLMObservation
|
| 29 |
-
|
| 30 |
-
STRATEGIES = ["NONE", "USE_CACHE", "DO_CASCADE", "EARLY_EXIT", "WAIT", "CALL_KB"]
|
| 31 |
-
MODELS = ["SMALL", "MEDIUM", "LARGE"]
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# ── Policies ─────────────────────────────────────────────────────────────────
|
| 35 |
-
|
| 36 |
-
def random_policy(obs: EcoLLMObservation) -> EcoLLMAction:
|
| 37 |
-
return EcoLLMAction(
|
| 38 |
-
strategy=random.choice(STRATEGIES),
|
| 39 |
-
model_choice=random.choice(MODELS),
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def heuristic_policy(obs: EcoLLMObservation) -> EcoLLMAction:
|
| 44 |
-
"""Rule-based heuristic."""
|
| 45 |
-
# Wait if carbon is very high and we haven't used all steps
|
| 46 |
-
if obs.carbon_intensity > 0.7 and obs.step < obs.total_steps - 1:
|
| 47 |
-
return EcoLLMAction(strategy="WAIT", model_choice="SMALL")
|
| 48 |
-
|
| 49 |
-
# Use cache if query already answered
|
| 50 |
-
if obs.query in obs.cache_contents:
|
| 51 |
-
return EcoLLMAction(strategy="USE_CACHE", model_choice="SMALL")
|
| 52 |
-
|
| 53 |
-
# For hard task, use CALL_KB for weather-like queries
|
| 54 |
-
if obs.task == "hard" and "weather" in obs.query.lower():
|
| 55 |
-
return EcoLLMAction(strategy="CALL_KB", model_choice="SMALL")
|
| 56 |
-
|
| 57 |
-
# Default: try early-exit (SMALL first, saves energy)
|
| 58 |
-
return EcoLLMAction(strategy="EARLY_EXIT", model_choice="MEDIUM")
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def llm_policy(obs: EcoLLMObservation) -> EcoLLMAction:
|
| 62 |
-
"""Use Claude claude-sonnet-4-20250514 to decide routing strategy."""
|
| 63 |
-
try:
|
| 64 |
-
import anthropic
|
| 65 |
-
client = anthropic.Anthropic()
|
| 66 |
-
prompt = (
|
| 67 |
-
f"You are an LLM inference router. Choose the best strategy and model.\n"
|
| 68 |
-
f"Query: {obs.query}\n"
|
| 69 |
-
f"Carbon intensity (0=clean, 1=dirty): {obs.carbon_intensity:.2f}\n"
|
| 70 |
-
f"Cache contains: {obs.cache_contents}\n"
|
| 71 |
-
f"Step {obs.step+1}/{obs.total_steps}, Task: {obs.task}\n\n"
|
| 72 |
-
f"Reply with EXACTLY two lines:\n"
|
| 73 |
-
f"STRATEGY: <one of NONE|USE_CACHE|DO_CASCADE|EARLY_EXIT|WAIT|CALL_KB>\n"
|
| 74 |
-
f"MODEL: <one of SMALL|MEDIUM|LARGE>"
|
| 75 |
-
)
|
| 76 |
-
response = client.messages.create(
|
| 77 |
-
model="claude-sonnet-4-20250514",
|
| 78 |
-
max_tokens=50,
|
| 79 |
-
messages=[{"role": "user", "content": prompt}],
|
| 80 |
-
)
|
| 81 |
-
text = response.content[0].text.strip()
|
| 82 |
-
lines = {l.split(":")[0].strip(): l.split(":")[1].strip()
|
| 83 |
-
for l in text.splitlines() if ":" in l}
|
| 84 |
-
strategy = lines.get("STRATEGY", "NONE").upper()
|
| 85 |
-
model = lines.get("MODEL", "SMALL").upper()
|
| 86 |
-
if strategy not in STRATEGIES:
|
| 87 |
-
strategy = "NONE"
|
| 88 |
-
if model not in MODELS:
|
| 89 |
-
model = "SMALL"
|
| 90 |
-
return EcoLLMAction(strategy=strategy, model_choice=model)
|
| 91 |
-
except Exception as e:
|
| 92 |
-
print(f" [LLM policy error: {e}] falling back to heuristic")
|
| 93 |
-
return heuristic_policy(obs)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
# ── Episode runner ────────────────────────────────────────────────────────────
|
| 97 |
-
|
| 98 |
-
def run_episode(task: str, policy_fn) -> Tuple[float, int]:
|
| 99 |
-
env = EcoLLMEnvironment(task=task)
|
| 100 |
-
obs = env.reset()
|
| 101 |
-
total_reward = 0.0
|
| 102 |
-
steps = 0
|
| 103 |
-
while not obs.done:
|
| 104 |
-
action = policy_fn(obs)
|
| 105 |
-
obs = env.step(action)
|
| 106 |
-
total_reward += obs.reward
|
| 107 |
-
steps += 1
|
| 108 |
-
max_possible = len(env._queries) * (1.0 + 0.5) # max per step ≈ 1.5 (cache bonus)
|
| 109 |
-
normalized = total_reward / max_possible if max_possible > 0 else 0.0
|
| 110 |
-
return normalized, steps
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
# ── Main ──────────────────────────────────────────────────────────────────────
|
| 114 |
-
|
| 115 |
-
def main():
|
| 116 |
-
parser = argparse.ArgumentParser(description="Eco-LLM Routing Environment Baseline")
|
| 117 |
-
parser.add_argument("--task", choices=["easy", "medium", "hard"], default="easy")
|
| 118 |
-
parser.add_argument("--policy", choices=["random", "heuristic", "llm"], default="heuristic")
|
| 119 |
-
parser.add_argument("--episodes", type=int, default=10)
|
| 120 |
-
args = parser.parse_args()
|
| 121 |
-
|
| 122 |
-
policy_map = {
|
| 123 |
-
"random": random_policy,
|
| 124 |
-
"heuristic": heuristic_policy,
|
| 125 |
-
"llm": llm_policy,
|
| 126 |
-
}
|
| 127 |
-
policy_fn = policy_map[args.policy]
|
| 128 |
-
|
| 129 |
-
print(f"\n{'='*55}")
|
| 130 |
-
print(f" Eco-LLM Routing Baseline")
|
| 131 |
-
print(f" Task: {args.task} | Policy: {args.policy} | Episodes: {args.episodes}")
|
| 132 |
-
print(f"{'='*55}")
|
| 133 |
-
|
| 134 |
-
scores = []
|
| 135 |
-
for ep in range(1, args.episodes + 1):
|
| 136 |
-
score, steps = run_episode(args.task, policy_fn)
|
| 137 |
-
scores.append(score)
|
| 138 |
-
print(f" Episode {ep:3d}: score={score:.3f} steps={steps}")
|
| 139 |
-
|
| 140 |
-
print(f"\n Mean score : {statistics.mean(scores):.3f}")
|
| 141 |
-
print(f" Std dev : {statistics.stdev(scores):.3f}" if len(scores) > 1 else "")
|
| 142 |
-
print(f"{'='*55}\n")
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if __name__ == "__main__":
|
| 146 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deployment/pyproject.toml
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
-
# All rights reserved.
|
| 3 |
-
#
|
| 4 |
-
# This source code is licensed under the BSD-style license found in the
|
| 5 |
-
# LICENSE file in the root directory of this source tree.
|
| 6 |
-
|
| 7 |
-
[build-system]
|
| 8 |
-
requires = ["setuptools>=45", "wheel"]
|
| 9 |
-
build-backend = "setuptools.build_meta"
|
| 10 |
-
|
| 11 |
-
[project]
|
| 12 |
-
name = "openenv-deployment"
|
| 13 |
-
version = "0.1.0"
|
| 14 |
-
description = "Deployment environment for OpenEnv"
|
| 15 |
-
requires-python = ">=3.10"
|
| 16 |
-
dependencies = [
|
| 17 |
-
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
-
# install from github
|
| 19 |
-
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
-
"openenv-core[core]>=0.2.2",
|
| 21 |
-
# Environment-specific dependencies
|
| 22 |
-
# Add all dependencies needed for your environment here
|
| 23 |
-
# Examples:
|
| 24 |
-
# "numpy>=1.19.0",
|
| 25 |
-
# "torch>=2.0.0",
|
| 26 |
-
# "gymnasium>=0.29.0",
|
| 27 |
-
# "openspiel>=1.0.0",
|
| 28 |
-
# "smolagents>=1.22.0,<2",
|
| 29 |
-
]
|
| 30 |
-
|
| 31 |
-
[project.optional-dependencies]
|
| 32 |
-
dev = [
|
| 33 |
-
"pytest>=8.0.0",
|
| 34 |
-
"pytest-cov>=4.0.0",
|
| 35 |
-
]
|
| 36 |
-
|
| 37 |
-
[project.scripts]
|
| 38 |
-
# Server entry point - enables running via: uv run --project . server
|
| 39 |
-
# or: python -m deployment.server.app
|
| 40 |
-
server = "deployment.server.app:main"
|
| 41 |
-
|
| 42 |
-
[tool.setuptools]
|
| 43 |
-
include-package-data = true
|
| 44 |
-
packages = ["deployment", "deployment.server"]
|
| 45 |
-
package-dir = { "deployment" = ".", "deployment.server" = "server" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deployment/server/__pycache__/__init__.cpython-312.pyc
DELETED
|
Binary file (300 Bytes)
|
|
|
deployment/server/__pycache__/deployment_environment.cpython-312.pyc
DELETED
|
Binary file (11.2 kB)
|
|
|
deployment/server/requirements.txt
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
openenv[core]>=0.2.0
|
| 2 |
-
fastapi>=0.115.0
|
| 3 |
-
uvicorn>=0.24.0
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inference.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py - Eco-LLM Inference Routing Environment
|
| 3 |
+
====================================================
|
| 4 |
+
Spec-compliant inference script for the OpenEnv hackathon.
|
| 5 |
+
|
| 6 |
+
Environment variables (required):
|
| 7 |
+
HF_TOKEN Your Hugging Face or API key
|
| 8 |
+
API_BASE_URL LLM endpoint (default: https://router.huggingface.co/v1)
|
| 9 |
+
MODEL_NAME Model ID (default: Qwen/Qwen2.5-72B-Instruct)
|
| 10 |
+
ECO_LLM_TASK Task to run (default: task_1)
|
| 11 |
+
|
| 12 |
+
Stdout format (mandatory):
|
| 13 |
+
[START] task=<task> env=eco_llm_inference_routing model=<model>
|
| 14 |
+
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
|
| 15 |
+
[END] success=<true|false> steps=<n> rewards=<r1,r2,...,rn>
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import asyncio
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import textwrap
|
| 24 |
+
from typing import List, Optional
|
| 25 |
+
|
| 26 |
+
from openai import OpenAI
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# ENVIRONMENT & CONFIG
|
| 30 |
+
|
| 31 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 32 |
+
|
| 33 |
+
API_KEY = HF_TOKEN
|
| 34 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 35 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct")
|
| 36 |
+
TASK_ID = os.getenv("ECO_LLM_TASK", "task_1")
|
| 37 |
+
BENCHMARK = "eco_llm_inference_routing"
|
| 38 |
+
|
| 39 |
+
MAX_STEPS = 50
|
| 40 |
+
SUCCESS_SCORE_THRESHOLD = 0.5
|
| 41 |
+
TEMPERATURE = 0.2
|
| 42 |
+
MAX_TOKENS = 80
|
| 43 |
+
|
| 44 |
+
_MAX_REWARD_PER_STEP = 1.5
|
| 45 |
+
TASK_QUERY_COUNTS = {"task_1": 1, "task_2": 3, "task_3": 5}
|
| 46 |
+
_MAX_TOTAL_REWARD = TASK_QUERY_COUNTS.get(TASK_ID, 5) * _MAX_REWARD_PER_STEP
|
| 47 |
+
|
| 48 |
+
SYSTEM_PROMPT = textwrap.dedent(
|
| 49 |
+
"""
|
| 50 |
+
You are an LLM inference router for the Eco-LLM environment.
|
| 51 |
+
|
| 52 |
+
At each step you receive:
|
| 53 |
+
- The current query
|
| 54 |
+
- Carbon intensity of the grid (0.0 = clean, 1.0 = very dirty)
|
| 55 |
+
- Cache contents (queries already answered correctly - re-use them for free)
|
| 56 |
+
- Whether a knowledge base (KB) is available for this query
|
| 57 |
+
|
| 58 |
+
Your goal is to maximise total reward:
|
| 59 |
+
reward = score - carbon_penalty - latency_penalty + bonuses
|
| 60 |
+
where:
|
| 61 |
+
score = 1.0 if model tier >= query minimum, else 0.0
|
| 62 |
+
carbon_penalty = carbon_intensity * energy_cost (capped at 0.8)
|
| 63 |
+
latency_penalty= 0.1 * latency_cost
|
| 64 |
+
cache_bonus = +0.5 for a cache hit
|
| 65 |
+
early_exit_bonus = +0.1 for EARLY_EXIT when correct
|
| 66 |
+
|
| 67 |
+
Model costs:
|
| 68 |
+
SMALL - energy 0.1, latency 1.0
|
| 69 |
+
MEDIUM - energy 0.3, latency 2.0
|
| 70 |
+
LARGE - energy 0.6, latency 5.0
|
| 71 |
+
|
| 72 |
+
Strategies:
|
| 73 |
+
NONE - direct inference with chosen model
|
| 74 |
+
USE_CACHE - serve from cache (only if query is in cache_contents)
|
| 75 |
+
DO_CASCADE - try models small->chosen, stop on exit_flag if correct
|
| 76 |
+
EARLY_EXIT - direct inference; bonus if correct and exits early
|
| 77 |
+
WAIT - skip step to wait for lower carbon (useful when carbon > 0.7)
|
| 78 |
+
CALL_KB - use knowledge base (only when kb_available=true)
|
| 79 |
+
|
| 80 |
+
Respond with ONLY valid JSON:
|
| 81 |
+
{
|
| 82 |
+
"strategy": "<NONE|USE_CACHE|DO_CASCADE|EARLY_EXIT|WAIT|CALL_KB>",
|
| 83 |
+
"model_choice": "<SMALL|MEDIUM|LARGE>",
|
| 84 |
+
"exit_flag": <true|false>
|
| 85 |
+
}
|
| 86 |
+
"""
|
| 87 |
+
).strip()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build_user_prompt(
|
| 91 |
+
step: int,
|
| 92 |
+
query: str,
|
| 93 |
+
carbon_intensity: float,
|
| 94 |
+
cache_contents: List[str],
|
| 95 |
+
kb_available: bool,
|
| 96 |
+
last_reward: float,
|
| 97 |
+
history: List[str],
|
| 98 |
+
) -> str:
|
| 99 |
+
"""Build context-aware prompt for the LLM routing decision."""
|
| 100 |
+
cache_str = json.dumps(cache_contents) if cache_contents else "[]"
|
| 101 |
+
hist_block = "\n".join(history[-5:]) if history else "None"
|
| 102 |
+
in_cache = query in cache_contents
|
| 103 |
+
return textwrap.dedent(
|
| 104 |
+
f"""
|
| 105 |
+
Step: {step}
|
| 106 |
+
Query: {query!r}
|
| 107 |
+
In cache: {str(in_cache).lower()}
|
| 108 |
+
Cache: {cache_str}
|
| 109 |
+
Carbon intensity: {carbon_intensity:.2f}
|
| 110 |
+
KB available: {str(kb_available).lower()}
|
| 111 |
+
Last reward: {last_reward:.2f}
|
| 112 |
+
|
| 113 |
+
Recent history: {hist_block}
|
| 114 |
+
|
| 115 |
+
Choose the most energy-efficient routing action.
|
| 116 |
+
"""
|
| 117 |
+
).strip()
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 121 |
+
"""Emit [START] line per spec."""
|
| 122 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 126 |
+
"""Emit [STEP] line per spec."""
|
| 127 |
+
error_val = error if error else "null"
|
| 128 |
+
print(
|
| 129 |
+
f"[STEP] step={step} action={action} "
|
| 130 |
+
f"reward={reward:.2f} done={str(done).lower()} error={error_val}",
|
| 131 |
+
flush=True,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def log_end(success: bool, steps: int, rewards: List[float]) -> None:
|
| 136 |
+
"""
|
| 137 |
+
Emit [END] line per OpenEnv spec.
|
| 138 |
+
Format: [END] success=<true|false> steps=<n> rewards=<r1,r2,...,rn>
|
| 139 |
+
"""
|
| 140 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 141 |
+
print(
|
| 142 |
+
f"[END] success={str(success).lower()} steps={steps} "
|
| 143 |
+
f"rewards={rewards_str}",
|
| 144 |
+
flush=True,
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def get_routing_action(
|
| 149 |
+
client: OpenAI,
|
| 150 |
+
step: int,
|
| 151 |
+
query: str,
|
| 152 |
+
carbon_intensity: float,
|
| 153 |
+
cache_contents: List[str],
|
| 154 |
+
kb_available: bool,
|
| 155 |
+
last_reward: float,
|
| 156 |
+
history: List[str],
|
| 157 |
+
) -> dict:
|
| 158 |
+
"""Call the LLM and parse a routing action. Falls back to a safe heuristic on error."""
|
| 159 |
+
user_prompt = build_user_prompt(
|
| 160 |
+
step,
|
| 161 |
+
query,
|
| 162 |
+
carbon_intensity,
|
| 163 |
+
cache_contents,
|
| 164 |
+
kb_available,
|
| 165 |
+
last_reward,
|
| 166 |
+
history,
|
| 167 |
+
)
|
| 168 |
+
try:
|
| 169 |
+
completion = client.chat.completions.create(
|
| 170 |
+
model=MODEL_NAME,
|
| 171 |
+
messages=[
|
| 172 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 173 |
+
{"role": "user", "content": user_prompt},
|
| 174 |
+
],
|
| 175 |
+
temperature=TEMPERATURE,
|
| 176 |
+
max_tokens=MAX_TOKENS,
|
| 177 |
+
stream=False,
|
| 178 |
+
timeout=10.0,
|
| 179 |
+
)
|
| 180 |
+
raw = (completion.choices[0].message.content or "").strip()
|
| 181 |
+
raw = raw.replace("```json", "").replace("```", "").strip()
|
| 182 |
+
parsed = json.loads(raw)
|
| 183 |
+
|
| 184 |
+
strategy = parsed.get("strategy", "NONE").upper()
|
| 185 |
+
model_choice = parsed.get("model_choice", "SMALL").upper()
|
| 186 |
+
exit_flag = bool(parsed.get("exit_flag", False))
|
| 187 |
+
|
| 188 |
+
valid_strategies = {"NONE", "USE_CACHE", "DO_CASCADE", "EARLY_EXIT", "WAIT", "CALL_KB"}
|
| 189 |
+
valid_models = {"SMALL", "MEDIUM", "LARGE"}
|
| 190 |
+
if strategy not in valid_strategies:
|
| 191 |
+
strategy = "NONE"
|
| 192 |
+
if model_choice not in valid_models:
|
| 193 |
+
model_choice = "SMALL"
|
| 194 |
+
|
| 195 |
+
return {"strategy": strategy, "model_choice": model_choice, "exit_flag": exit_flag}
|
| 196 |
+
|
| 197 |
+
except (asyncio.TimeoutError, TimeoutError) as exc:
|
| 198 |
+
print(f"[DEBUG] LLM timeout at step {step}: {str(exc)}", flush=True)
|
| 199 |
+
if query in cache_contents:
|
| 200 |
+
return {"strategy": "USE_CACHE", "model_choice": "SMALL", "exit_flag": False}
|
| 201 |
+
if carbon_intensity > 0.7:
|
| 202 |
+
return {"strategy": "WAIT", "model_choice": "SMALL", "exit_flag": False}
|
| 203 |
+
return {"strategy": "NONE", "model_choice": "SMALL", "exit_flag": False}
|
| 204 |
+
|
| 205 |
+
except Exception as exc:
|
| 206 |
+
print(f"[DEBUG] LLM error at step {step}: {exc}", flush=True)
|
| 207 |
+
if query in cache_contents:
|
| 208 |
+
return {"strategy": "USE_CACHE", "model_choice": "SMALL", "exit_flag": False}
|
| 209 |
+
if kb_available:
|
| 210 |
+
return {"strategy": "CALL_KB", "model_choice": "SMALL", "exit_flag": False}
|
| 211 |
+
if carbon_intensity > 0.7:
|
| 212 |
+
return {"strategy": "WAIT", "model_choice": "SMALL", "exit_flag": False}
|
| 213 |
+
return {"strategy": "NONE", "model_choice": "SMALL", "exit_flag": True}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def action_to_str(action: dict) -> str:
|
| 217 |
+
"""Convert an action dict to a compact string for the [STEP] line."""
|
| 218 |
+
strategy = action["strategy"]
|
| 219 |
+
model_choice = action["model_choice"]
|
| 220 |
+
exit_flag = str(action["exit_flag"]).lower()
|
| 221 |
+
return f"strategy={strategy},model={model_choice},exit={exit_flag}"
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
async def run_episode(client: OpenAI) -> None:
|
| 225 |
+
"""Run one episode of the Eco-LLM inference routing environment."""
|
| 226 |
+
env = None
|
| 227 |
+
try:
|
| 228 |
+
from openenv.client import AsyncEnvClient # type: ignore
|
| 229 |
+
|
| 230 |
+
env = AsyncEnvClient(base_url=os.getenv("ENV_BASE_URL", "http://localhost:8000"))
|
| 231 |
+
except Exception:
|
| 232 |
+
from server.env import EcoLLMInferenceRoutingEnvironment # type: ignore
|
| 233 |
+
|
| 234 |
+
env = EcoLLMInferenceRoutingEnvironment()
|
| 235 |
+
|
| 236 |
+
rewards: List[float] = []
|
| 237 |
+
history: List[str] = []
|
| 238 |
+
steps_taken = 0
|
| 239 |
+
success = False
|
| 240 |
+
|
| 241 |
+
log_start(task=TASK_ID, env=BENCHMARK, model=MODEL_NAME)
|
| 242 |
+
|
| 243 |
+
try:
|
| 244 |
+
if asyncio.iscoroutinefunction(getattr(env, "reset", None)):
|
| 245 |
+
obs = await env.reset(task_id=TASK_ID)
|
| 246 |
+
else:
|
| 247 |
+
obs = env.reset(task_id=TASK_ID)
|
| 248 |
+
|
| 249 |
+
def _obs_field(observation: object, *keys: str) -> object:
|
| 250 |
+
for key in keys:
|
| 251 |
+
if isinstance(observation, dict):
|
| 252 |
+
if key in observation:
|
| 253 |
+
return observation[key]
|
| 254 |
+
elif hasattr(observation, key):
|
| 255 |
+
return getattr(observation, key)
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
last_reward = 0.0
|
| 259 |
+
done = bool(_obs_field(obs, "done") or False)
|
| 260 |
+
|
| 261 |
+
for step in range(1, MAX_STEPS + 1):
|
| 262 |
+
if done:
|
| 263 |
+
break
|
| 264 |
+
|
| 265 |
+
query = _obs_field(obs, "query") or ""
|
| 266 |
+
carbon_intensity = float(_obs_field(obs, "carbon_intensity") or 0.5)
|
| 267 |
+
cache_contents = list(_obs_field(obs, "cache_contents") or [])
|
| 268 |
+
kb_available = bool(_obs_field(obs, "kb_available") or False)
|
| 269 |
+
|
| 270 |
+
action = get_routing_action(
|
| 271 |
+
client,
|
| 272 |
+
step,
|
| 273 |
+
str(query),
|
| 274 |
+
carbon_intensity,
|
| 275 |
+
cache_contents,
|
| 276 |
+
kb_available,
|
| 277 |
+
last_reward,
|
| 278 |
+
history,
|
| 279 |
+
)
|
| 280 |
+
action_str = action_to_str(action)
|
| 281 |
+
|
| 282 |
+
error_msg = None
|
| 283 |
+
try:
|
| 284 |
+
from server.models import ModelChoice, RLAction, Strategy # type: ignore
|
| 285 |
+
|
| 286 |
+
rl_action = RLAction(
|
| 287 |
+
strategy=Strategy(action["strategy"]),
|
| 288 |
+
model_choice=ModelChoice(action["model_choice"]),
|
| 289 |
+
exit_flag=action["exit_flag"],
|
| 290 |
+
)
|
| 291 |
+
if asyncio.iscoroutinefunction(getattr(env, "step", None)):
|
| 292 |
+
obs = await env.step(rl_action)
|
| 293 |
+
else:
|
| 294 |
+
obs = env.step(rl_action)
|
| 295 |
+
except Exception as exc:
|
| 296 |
+
error_msg = str(exc).replace("\n", " ")[:120]
|
| 297 |
+
print(f"[DEBUG] step error: {error_msg}", flush=True)
|
| 298 |
+
done = True
|
| 299 |
+
log_step(step=step, action=action_str, reward=0.0, done=True, error=error_msg)
|
| 300 |
+
break
|
| 301 |
+
|
| 302 |
+
reward = float(_obs_field(obs, "reward") or 0.0)
|
| 303 |
+
done = bool(_obs_field(obs, "done") or False)
|
| 304 |
+
last_reward = reward
|
| 305 |
+
|
| 306 |
+
rewards.append(reward)
|
| 307 |
+
steps_taken = step
|
| 308 |
+
|
| 309 |
+
log_step(step=step, action=action_str, reward=reward, done=done, error=error_msg)
|
| 310 |
+
history.append(
|
| 311 |
+
f"step={step} strategy={action['strategy']} "
|
| 312 |
+
f"model={action['model_choice']} reward={reward:+.2f}"
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
if done:
|
| 316 |
+
break
|
| 317 |
+
|
| 318 |
+
raw_total = sum(rewards)
|
| 319 |
+
max_total = _MAX_TOTAL_REWARD if _MAX_TOTAL_REWARD > 0 else 1.0
|
| 320 |
+
score = min(max(raw_total / max_total, 0.0), 1.0)
|
| 321 |
+
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 322 |
+
|
| 323 |
+
finally:
|
| 324 |
+
try:
|
| 325 |
+
if asyncio.iscoroutinefunction(getattr(env, "close", None)):
|
| 326 |
+
await env.close()
|
| 327 |
+
elif hasattr(env, "close"):
|
| 328 |
+
env.close()
|
| 329 |
+
except Exception as exc:
|
| 330 |
+
print(f"[DEBUG] env.close() error: {exc}", flush=True)
|
| 331 |
+
|
| 332 |
+
log_end(success=success, steps=steps_taken, rewards=rewards)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def main() -> None:
|
| 336 |
+
"""Main entry point."""
|
| 337 |
+
if not HF_TOKEN or not HF_TOKEN.strip():
|
| 338 |
+
raise ValueError(
|
| 339 |
+
"ERROR: HF_TOKEN environment variable is mandatory.\n"
|
| 340 |
+
"Please set: export HF_TOKEN='your_hugging_face_token'"
|
| 341 |
+
)
|
| 342 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
|
| 343 |
+
asyncio.run(run_episode(client))
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
if __name__ == "__main__":
|
| 347 |
+
main()
|
openenv.yaml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: eco_llm_inference_routing
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
| 7 |
+
description: "Carbon-aware RL environment for multi-objective LLM query routing"
|
| 8 |
+
version: "1.0.0"
|
pyproject.toml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-eco-llm-inference-routing"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Eco-LLM Inference Routing environment for OpenEnv"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"fastapi>=0.115.0",
|
| 12 |
+
"gradio>=4.0.0",
|
| 13 |
+
"numpy>=1.26.0",
|
| 14 |
+
"openenv-core[core]>=0.2.2",
|
| 15 |
+
"openai>=1.40.0",
|
| 16 |
+
"pydantic>=2.8.0",
|
| 17 |
+
"uvicorn>=0.30.0",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
[project.optional-dependencies]
|
| 21 |
+
dev = [
|
| 22 |
+
"pytest>=8.0.0",
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
[project.scripts]
|
| 26 |
+
server = "server.app:main"
|
| 27 |
+
|
| 28 |
+
[tool.setuptools]
|
| 29 |
+
include-package-data = true
|
| 30 |
+
packages = ["server"]
|
| 31 |
+
package-dir = { "server" = "server" }
|
server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Server package for the Eco-LLM Inference Routing environment."""
|
server/__pycache__/env.cpython-312.pyc
ADDED
|
Binary file (12 kB). View file
|
|
|
server/__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (4.84 kB). View file
|
|
|
server/__pycache__/tasks.cpython-312.pyc
ADDED
|
Binary file (4.39 kB). View file
|
|
|
server/app.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI app for the Eco-LLM Inference Routing environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
from openenv.core.env_server.http_server import create_app
|
| 7 |
+
except Exception as exc: # pragma: no cover
|
| 8 |
+
raise ImportError(
|
| 9 |
+
"openenv-core is required. Install dependencies before running the server."
|
| 10 |
+
) from exc
|
| 11 |
+
|
| 12 |
+
from server.env import EcoLLMInferenceRoutingEnvironment
|
| 13 |
+
from server.models import RLAction, RLObservation, Strategy, ModelChoice
|
| 14 |
+
|
| 15 |
+
app = create_app(
|
| 16 |
+
EcoLLMInferenceRoutingEnvironment,
|
| 17 |
+
RLAction,
|
| 18 |
+
RLObservation,
|
| 19 |
+
env_name="eco_llm_inference_routing",
|
| 20 |
+
max_concurrent_envs=4,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
@app.get("/")
|
| 24 |
+
def root():
|
| 25 |
+
return {"status": "ok", "ui": "/ui"}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# ── Gradio demo — mounted at /ui, does NOT replace FastAPI ───────────────────
|
| 29 |
+
try:
|
| 30 |
+
import gradio as gr
|
| 31 |
+
from gradio.routes import mount_gradio_app
|
| 32 |
+
|
| 33 |
+
def reset_demo(task_id: str, state: dict) -> tuple:
|
| 34 |
+
demo_env = EcoLLMInferenceRoutingEnvironment()
|
| 35 |
+
obs = demo_env.reset(task_id=task_id)
|
| 36 |
+
updated_state = {
|
| 37 |
+
"env": demo_env,
|
| 38 |
+
"obs": obs,
|
| 39 |
+
"history": [],
|
| 40 |
+
"total_reward": 0.0,
|
| 41 |
+
}
|
| 42 |
+
log = f"🔄 Reset — Task: {task_id} ({demo_env.current_task.difficulty})\n"
|
| 43 |
+
log += f"📋 Query: {obs.query}\n"
|
| 44 |
+
log += f"🌍 Carbon intensity: {obs.carbon_intensity:.2f}\n"
|
| 45 |
+
log += f"💾 Cache: {obs.cache_contents or 'empty'}\n"
|
| 46 |
+
log += f"🔌 KB available: {obs.kb_available}"
|
| 47 |
+
return log, "", 0.0, updated_state
|
| 48 |
+
|
| 49 |
+
def step_demo(strategy: str, model: str, exit_flag: bool, state: dict) -> tuple:
|
| 50 |
+
demo_env = state.get("env")
|
| 51 |
+
if demo_env is None or state["obs"] is None:
|
| 52 |
+
return "⚠️ Click Reset first!", "", 0.0, state
|
| 53 |
+
obs = state["obs"]
|
| 54 |
+
if obs.done:
|
| 55 |
+
return "✅ Episode done — click Reset to start again.", "", state["total_reward"], state
|
| 56 |
+
|
| 57 |
+
action = RLAction(
|
| 58 |
+
strategy=Strategy(strategy),
|
| 59 |
+
model_choice=ModelChoice(model),
|
| 60 |
+
exit_flag=exit_flag,
|
| 61 |
+
)
|
| 62 |
+
new_obs = demo_env.step(action)
|
| 63 |
+
history = list(state["history"])
|
| 64 |
+
rd = new_obs.reward_details
|
| 65 |
+
total_reward = state["total_reward"] + rd.total_reward
|
| 66 |
+
|
| 67 |
+
history_line = (
|
| 68 |
+
f"Step {len(history)+1}: "
|
| 69 |
+
f"{strategy}+{model} → reward={rd.total_reward:+.3f} "
|
| 70 |
+
f"({'✅' if rd.correct else '❌'})"
|
| 71 |
+
)
|
| 72 |
+
history.append(history_line)
|
| 73 |
+
|
| 74 |
+
log = "\n".join(history) + "\n\n"
|
| 75 |
+
if not new_obs.done:
|
| 76 |
+
log += f"📋 Next query: {new_obs.query}\n"
|
| 77 |
+
log += f"🌍 Carbon intensity: {new_obs.carbon_intensity:.2f}\n"
|
| 78 |
+
log += f"💾 Cache: {new_obs.cache_contents or 'empty'}\n"
|
| 79 |
+
log += f"🔌 KB available: {new_obs.kb_available}"
|
| 80 |
+
else:
|
| 81 |
+
log += "✅ Episode complete!"
|
| 82 |
+
|
| 83 |
+
breakdown = (
|
| 84 |
+
f"score={rd.score:.1f} "
|
| 85 |
+
f"energy_cost={rd.energy_cost:.2f} "
|
| 86 |
+
f"carbon_penalty={rd.carbon_penalty:.3f} "
|
| 87 |
+
f"latency_penalty={rd.latency_penalty:.3f} "
|
| 88 |
+
f"bonuses={rd.bonuses:.2f} "
|
| 89 |
+
f"→ total={rd.total_reward:+.3f}"
|
| 90 |
+
)
|
| 91 |
+
updated_state = {
|
| 92 |
+
"env": demo_env,
|
| 93 |
+
"obs": new_obs,
|
| 94 |
+
"history": history,
|
| 95 |
+
"total_reward": total_reward,
|
| 96 |
+
}
|
| 97 |
+
return log, breakdown, round(total_reward, 3), updated_state
|
| 98 |
+
|
| 99 |
+
with gr.Blocks(title="Eco-LLM Routing Demo") as demo:
|
| 100 |
+
demo_state = gr.State({"obs": None, "history": [], "total_reward": 0.0})
|
| 101 |
+
gr.Markdown("""
|
| 102 |
+
# 🌱 Eco-LLM Inference Routing Environment
|
| 103 |
+
**Carbon-aware LLM query routing** — balance accuracy, energy, latency & carbon footprint.
|
| 104 |
+
Choose a routing strategy and model for each query to maximise cumulative reward.
|
| 105 |
+
""")
|
| 106 |
+
|
| 107 |
+
with gr.Row():
|
| 108 |
+
task_selector = gr.Dropdown(
|
| 109 |
+
choices=["task_1", "task_2", "task_3"],
|
| 110 |
+
value="task_1",
|
| 111 |
+
label="Task (easy → hard)",
|
| 112 |
+
)
|
| 113 |
+
reset_btn = gr.Button("🔄 Reset Episode", variant="primary")
|
| 114 |
+
|
| 115 |
+
obs_box = gr.Textbox(label="Environment State", lines=6, interactive=False)
|
| 116 |
+
|
| 117 |
+
gr.Markdown("### Take an Action")
|
| 118 |
+
with gr.Row():
|
| 119 |
+
strategy_dd = gr.Dropdown(
|
| 120 |
+
choices=["NONE", "USE_CACHE", "DO_CASCADE", "EARLY_EXIT", "WAIT", "CALL_KB"],
|
| 121 |
+
value="NONE",
|
| 122 |
+
label="Strategy",
|
| 123 |
+
)
|
| 124 |
+
model_dd = gr.Dropdown(
|
| 125 |
+
choices=["SMALL", "MEDIUM", "LARGE"],
|
| 126 |
+
value="SMALL",
|
| 127 |
+
label="Model",
|
| 128 |
+
)
|
| 129 |
+
exit_cb = gr.Checkbox(label="exit_flag (cascade early stop)", value=False)
|
| 130 |
+
step_btn = gr.Button("▶️ Step", variant="secondary")
|
| 131 |
+
|
| 132 |
+
reward_breakdown = gr.Textbox(label="Reward Breakdown", interactive=False)
|
| 133 |
+
total_reward_num = gr.Number(label="Cumulative Reward", interactive=False)
|
| 134 |
+
|
| 135 |
+
gr.Markdown("""
|
| 136 |
+
### Reward Formula
|
| 137 |
+
`reward = score − (carbon_intensity × energy_cost) − (0.1 × latency) + bonuses`
|
| 138 |
+
|
| 139 |
+
| Model | Energy | Latency | | Strategy | Effect |
|
| 140 |
+
|---|---|---|---|---|---|
|
| 141 |
+
| SMALL | 0.1 | 1.0s | | USE_CACHE | +0.5 bonus if cached |
|
| 142 |
+
| MEDIUM | 0.3 | 2.0s | | CALL_KB | cheap if kb_available |
|
| 143 |
+
| LARGE | 0.6 | 5.0s | | WAIT | drops carbon next step |
|
| 144 |
+
""")
|
| 145 |
+
|
| 146 |
+
reset_btn.click(
|
| 147 |
+
reset_demo,
|
| 148 |
+
inputs=[task_selector, demo_state],
|
| 149 |
+
outputs=[obs_box, reward_breakdown, total_reward_num, demo_state],
|
| 150 |
+
)
|
| 151 |
+
step_btn.click(
|
| 152 |
+
step_demo,
|
| 153 |
+
inputs=[strategy_dd, model_dd, exit_cb, demo_state],
|
| 154 |
+
outputs=[obs_box, reward_breakdown, total_reward_num, demo_state],
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
app = mount_gradio_app(app, demo, path="/ui")
|
| 158 |
+
|
| 159 |
+
except ImportError:
|
| 160 |
+
# Gradio not installed — API still works fine
|
| 161 |
+
pass
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def main(host: str = "0.0.0.0", port: int = 7860) -> None:
|
| 165 |
+
"""Run the development server."""
|
| 166 |
+
import uvicorn
|
| 167 |
+
uvicorn.run(app, host=host, port=port)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if __name__ == "__main__":
|
| 171 |
+
import argparse
|
| 172 |
+
parser = argparse.ArgumentParser()
|
| 173 |
+
parser.add_argument("--port", type=int, default=7860)
|
| 174 |
+
args = parser.parse_args()
|
| 175 |
+
main(port=args.port)
|
server/env.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Environment logic for Eco-LLM inference routing."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from uuid import uuid4
|
| 7 |
+
|
| 8 |
+
from openenv.core.env_server.interfaces import Environment
|
| 9 |
+
from openenv.core.env_server.types import EnvironmentMetadata
|
| 10 |
+
|
| 11 |
+
from server.models import ModelChoice, RLAction, RLObservation, RLReward, RLState, Strategy
|
| 12 |
+
from server.tasks import (
|
| 13 |
+
CARBON_SCHEDULE,
|
| 14 |
+
ENERGY_COSTS,
|
| 15 |
+
KB_ENERGY_COST,
|
| 16 |
+
KB_LATENCY_COST,
|
| 17 |
+
LATENCY_COSTS,
|
| 18 |
+
LATENCY_MU,
|
| 19 |
+
MODEL_ORDER,
|
| 20 |
+
MODEL_RANK,
|
| 21 |
+
MAX_STEPS_PER_EPISODE,
|
| 22 |
+
MAX_CARBON_PENALTY,
|
| 23 |
+
QuerySpec,
|
| 24 |
+
TaskSpec,
|
| 25 |
+
get_task,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class EcoLLMInferenceRoutingEnvironment(Environment[RLAction, RLObservation, RLState]):
|
| 30 |
+
"""Deterministic hierarchical RL environment for LLM routing."""
|
| 31 |
+
|
| 32 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 33 |
+
|
| 34 |
+
def __init__(self) -> None:
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.current_task: TaskSpec = get_task("task_1")
|
| 37 |
+
self._state = RLState(
|
| 38 |
+
episode_id=str(uuid4()),
|
| 39 |
+
step_count=0,
|
| 40 |
+
task_id=self.current_task.task_id,
|
| 41 |
+
step_index=0,
|
| 42 |
+
query_index=0,
|
| 43 |
+
total_energy=0.0,
|
| 44 |
+
total_latency=0.0,
|
| 45 |
+
cache_contents=[],
|
| 46 |
+
carbon_history_index=0,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
def reset(
|
| 50 |
+
self,
|
| 51 |
+
seed: Optional[int] = None,
|
| 52 |
+
episode_id: Optional[str] = None,
|
| 53 |
+
task_id: str = "task_1",
|
| 54 |
+
**kwargs: object,
|
| 55 |
+
) -> RLObservation:
|
| 56 |
+
del seed, kwargs
|
| 57 |
+
self.current_task = get_task(task_id)
|
| 58 |
+
self._state = RLState(
|
| 59 |
+
episode_id=episode_id or str(uuid4()),
|
| 60 |
+
step_count=0,
|
| 61 |
+
task_id=self.current_task.task_id,
|
| 62 |
+
step_index=0,
|
| 63 |
+
query_index=0,
|
| 64 |
+
total_energy=0.0,
|
| 65 |
+
total_latency=0.0,
|
| 66 |
+
cache_contents=[],
|
| 67 |
+
carbon_history_index=0,
|
| 68 |
+
)
|
| 69 |
+
return self._build_observation(
|
| 70 |
+
reward=self._empty_reward(["reset"]),
|
| 71 |
+
done=False,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
def step(
|
| 75 |
+
self,
|
| 76 |
+
action: RLAction,
|
| 77 |
+
timeout_s: Optional[float] = None,
|
| 78 |
+
**kwargs: object,
|
| 79 |
+
) -> RLObservation:
|
| 80 |
+
del timeout_s, kwargs
|
| 81 |
+
|
| 82 |
+
if self._episode_complete:
|
| 83 |
+
raise RuntimeError(
|
| 84 |
+
f"Episode {self._state.episode_id!r} is already complete. "
|
| 85 |
+
"Call reset() to start a new episode."
|
| 86 |
+
)
|
| 87 |
+
if self._state.step_count >= MAX_STEPS_PER_EPISODE:
|
| 88 |
+
return self._build_observation(
|
| 89 |
+
reward=self._empty_reward(["step_limit_exceeded"]),
|
| 90 |
+
done=True,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
self._state.step_count += 1
|
| 94 |
+
trace = [f"strategy={action.strategy.value}", f"model={action.model_choice.value}"]
|
| 95 |
+
query = self.current_query
|
| 96 |
+
current_carbon = self.current_carbon_intensity
|
| 97 |
+
|
| 98 |
+
if action.strategy == Strategy.WAIT:
|
| 99 |
+
self._advance_carbon()
|
| 100 |
+
self._state.step_index += 1
|
| 101 |
+
reward = RLReward(
|
| 102 |
+
score=0.0,
|
| 103 |
+
energy_cost=0.0,
|
| 104 |
+
latency_cost=0.0,
|
| 105 |
+
carbon_penalty=0.0,
|
| 106 |
+
latency_penalty=0.0,
|
| 107 |
+
bonuses=0.0,
|
| 108 |
+
total_reward=0.0,
|
| 109 |
+
correct=False,
|
| 110 |
+
strategy_trace=trace + ["wait"],
|
| 111 |
+
)
|
| 112 |
+
return self._build_observation(reward=reward, done=False)
|
| 113 |
+
|
| 114 |
+
correct = False
|
| 115 |
+
energy_cost = 0.0
|
| 116 |
+
latency_cost = 0.0
|
| 117 |
+
bonuses = 0.0
|
| 118 |
+
|
| 119 |
+
if action.strategy == Strategy.USE_CACHE and query.text in self.cache_contents:
|
| 120 |
+
correct = True
|
| 121 |
+
bonuses += 0.5
|
| 122 |
+
trace.append("cache_hit")
|
| 123 |
+
elif action.strategy == Strategy.CALL_KB:
|
| 124 |
+
energy_cost += KB_ENERGY_COST
|
| 125 |
+
latency_cost += KB_LATENCY_COST
|
| 126 |
+
correct = query.kb_available
|
| 127 |
+
trace.append("kb_lookup")
|
| 128 |
+
elif action.strategy == Strategy.DO_CASCADE:
|
| 129 |
+
cascade_models = self._cascade_models(action.model_choice)
|
| 130 |
+
for cascade_model in cascade_models:
|
| 131 |
+
energy_cost += ENERGY_COSTS[cascade_model]
|
| 132 |
+
latency_cost += LATENCY_COSTS[cascade_model]
|
| 133 |
+
trace.append(f"cascade:{cascade_model.value}")
|
| 134 |
+
model_correct = self._model_answers_correctly(query, cascade_model)
|
| 135 |
+
if model_correct:
|
| 136 |
+
correct = True
|
| 137 |
+
if action.exit_flag:
|
| 138 |
+
trace.append("cascade_exit")
|
| 139 |
+
break
|
| 140 |
+
else:
|
| 141 |
+
chosen_model = action.model_choice
|
| 142 |
+
energy_cost += ENERGY_COSTS[chosen_model]
|
| 143 |
+
latency_cost += LATENCY_COSTS[chosen_model]
|
| 144 |
+
correct = self._model_answers_correctly(query, chosen_model)
|
| 145 |
+
trace.append(f"infer:{chosen_model.value}")
|
| 146 |
+
if action.strategy == Strategy.EARLY_EXIT and correct:
|
| 147 |
+
trace.append("early_exit_success")
|
| 148 |
+
|
| 149 |
+
if action.strategy == Strategy.EARLY_EXIT and correct:
|
| 150 |
+
bonuses += 0.1
|
| 151 |
+
|
| 152 |
+
if action.model_choice == ModelChoice.LARGE:
|
| 153 |
+
bonuses += self.current_task.large_model_penalty
|
| 154 |
+
if self.current_task.large_model_penalty:
|
| 155 |
+
trace.append("task_large_penalty")
|
| 156 |
+
|
| 157 |
+
score = 1.0 if correct else 0.0
|
| 158 |
+
carbon_penalty = min(current_carbon * energy_cost, MAX_CARBON_PENALTY)
|
| 159 |
+
latency_penalty = LATENCY_MU * latency_cost
|
| 160 |
+
total_reward = score - carbon_penalty - latency_penalty + bonuses
|
| 161 |
+
|
| 162 |
+
self._state.total_energy += energy_cost
|
| 163 |
+
self._state.total_latency += latency_cost
|
| 164 |
+
self._state.step_index += 1
|
| 165 |
+
|
| 166 |
+
if correct and query.text not in self.cache_contents:
|
| 167 |
+
self._state.cache_contents.append(query.text)
|
| 168 |
+
|
| 169 |
+
if action.strategy != Strategy.WAIT:
|
| 170 |
+
self._advance_query()
|
| 171 |
+
self._advance_carbon()
|
| 172 |
+
|
| 173 |
+
done = self._episode_complete
|
| 174 |
+
reward = RLReward(
|
| 175 |
+
score=score,
|
| 176 |
+
energy_cost=round(energy_cost, 4),
|
| 177 |
+
latency_cost=round(latency_cost, 4),
|
| 178 |
+
carbon_penalty=round(carbon_penalty, 4),
|
| 179 |
+
latency_penalty=round(latency_penalty, 4),
|
| 180 |
+
bonuses=round(bonuses, 4),
|
| 181 |
+
total_reward=round(total_reward, 4),
|
| 182 |
+
correct=correct,
|
| 183 |
+
strategy_trace=trace,
|
| 184 |
+
)
|
| 185 |
+
return self._build_observation(reward=reward, done=done)
|
| 186 |
+
|
| 187 |
+
@property
|
| 188 |
+
def state(self) -> RLState:
|
| 189 |
+
return self._state
|
| 190 |
+
|
| 191 |
+
@property
|
| 192 |
+
def cache_contents(self) -> list[str]:
|
| 193 |
+
return list(self._state.cache_contents)
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def current_query(self) -> QuerySpec:
|
| 197 |
+
idx = self._state.query_index
|
| 198 |
+
queries = self.current_task.queries
|
| 199 |
+
# Clamp to last query only for building the terminal observation
|
| 200 |
+
return queries[min(idx, len(queries) - 1)]
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def current_carbon_intensity(self) -> float:
|
| 204 |
+
return CARBON_SCHEDULE[self._state.carbon_history_index % len(CARBON_SCHEDULE)]
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def _episode_complete(self) -> bool:
|
| 208 |
+
return self._state.query_index >= len(self.current_task.queries)
|
| 209 |
+
|
| 210 |
+
def get_metadata(self) -> EnvironmentMetadata:
|
| 211 |
+
return EnvironmentMetadata(
|
| 212 |
+
name="Eco-LLM Inference Routing Environment",
|
| 213 |
+
description=(
|
| 214 |
+
"Hierarchical RL environment for carbon-aware LLM query routing "
|
| 215 |
+
"across accuracy, latency, energy, and caching objectives."
|
| 216 |
+
),
|
| 217 |
+
version="1.0.0",
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
def _build_observation(self, reward: RLReward, done: bool) -> RLObservation:
|
| 221 |
+
query = self.current_task.queries[-1] if done else self.current_query
|
| 222 |
+
return RLObservation(
|
| 223 |
+
query=query.text,
|
| 224 |
+
cache_contents=self.cache_contents,
|
| 225 |
+
carbon_intensity=self.current_carbon_intensity,
|
| 226 |
+
done=done,
|
| 227 |
+
reward=reward.total_reward,
|
| 228 |
+
reward_details=reward,
|
| 229 |
+
kb_available=query.kb_available,
|
| 230 |
+
metadata={
|
| 231 |
+
"task_id": self.current_task.task_id,
|
| 232 |
+
"difficulty": self.current_task.difficulty,
|
| 233 |
+
"state": self._state.public_payload(),
|
| 234 |
+
},
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
def _empty_reward(self, trace: list[str]) -> RLReward:
|
| 238 |
+
return RLReward(
|
| 239 |
+
score=0.0,
|
| 240 |
+
energy_cost=0.0,
|
| 241 |
+
latency_cost=0.0,
|
| 242 |
+
carbon_penalty=0.0,
|
| 243 |
+
latency_penalty=0.0,
|
| 244 |
+
bonuses=0.0,
|
| 245 |
+
total_reward=0.0,
|
| 246 |
+
correct=False,
|
| 247 |
+
strategy_trace=trace,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
def _advance_query(self) -> None:
|
| 251 |
+
if self._state.query_index < len(self.current_task.queries):
|
| 252 |
+
self._state.query_index += 1
|
| 253 |
+
|
| 254 |
+
def _advance_carbon(self) -> None:
|
| 255 |
+
self._state.carbon_history_index = (self._state.carbon_history_index + 1) % len(CARBON_SCHEDULE)
|
| 256 |
+
|
| 257 |
+
def _cascade_models(self, upper_bound: ModelChoice) -> list[ModelChoice]:
|
| 258 |
+
max_index = MODEL_RANK[upper_bound]
|
| 259 |
+
return MODEL_ORDER[: max_index + 1]
|
| 260 |
+
|
| 261 |
+
def _model_answers_correctly(self, query: QuerySpec, model: ModelChoice) -> bool:
|
| 262 |
+
return MODEL_RANK[model] >= MODEL_RANK[query.minimum_model]
|
server/models.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic models for the Eco-LLM Inference Routing environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class Strategy(str, Enum):
|
| 13 |
+
NONE = "NONE"
|
| 14 |
+
USE_CACHE = "USE_CACHE"
|
| 15 |
+
DO_CASCADE = "DO_CASCADE"
|
| 16 |
+
EARLY_EXIT = "EARLY_EXIT"
|
| 17 |
+
WAIT = "WAIT"
|
| 18 |
+
CALL_KB = "CALL_KB"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ModelChoice(str, Enum):
|
| 22 |
+
SMALL = "SMALL"
|
| 23 |
+
MEDIUM = "MEDIUM"
|
| 24 |
+
LARGE = "LARGE"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class RLAction(Action):
|
| 28 |
+
"""Action taken by the routing agent at each step."""
|
| 29 |
+
|
| 30 |
+
strategy: Strategy = Field(..., description="Routing strategy to apply")
|
| 31 |
+
model_choice: ModelChoice = Field(..., description="Primary model for inference")
|
| 32 |
+
exit_flag: bool = Field(..., description="Whether to terminate a cascade early")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class RLReward(BaseModel):
|
| 36 |
+
"""Structured reward breakdown for deterministic grading."""
|
| 37 |
+
|
| 38 |
+
score: float = Field(..., description="1.0 if correct else 0.0")
|
| 39 |
+
energy_cost: float = Field(..., description="Energy consumed this step")
|
| 40 |
+
latency_cost: float = Field(..., description="Latency incurred this step")
|
| 41 |
+
carbon_penalty: float = Field(..., description="Carbon-weighted energy penalty")
|
| 42 |
+
latency_penalty: float = Field(..., description="Latency penalty with mu=0.01")
|
| 43 |
+
bonuses: float = Field(..., description="Sum of bonuses or task-specific adjustments")
|
| 44 |
+
total_reward: float = Field(..., description="Final reward for the step")
|
| 45 |
+
correct: bool = Field(..., description="Whether the selected answer was correct")
|
| 46 |
+
strategy_trace: list[str] = Field(
|
| 47 |
+
default_factory=list,
|
| 48 |
+
description="Ordered record of sub-actions executed during the step",
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class RLObservation(Observation):
|
| 53 |
+
"""Observation returned after reset or step."""
|
| 54 |
+
|
| 55 |
+
query: str = Field(..., description="Current query to answer")
|
| 56 |
+
cache_contents: list[str] = Field(
|
| 57 |
+
default_factory=list,
|
| 58 |
+
description="Queries answered correctly and available in cache",
|
| 59 |
+
)
|
| 60 |
+
carbon_intensity: float = Field(..., ge=0.0, le=1.0, description="Current carbon intensity")
|
| 61 |
+
reward_details: RLReward = Field(..., description="Structured reward payload")
|
| 62 |
+
kb_available: bool = Field(default=False, description="Whether KB is available for current query")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class RLState(State):
|
| 66 |
+
"""Internal environment state."""
|
| 67 |
+
|
| 68 |
+
task_id: str = Field(..., description="Active task identifier")
|
| 69 |
+
step_index: int = Field(..., ge=0, description="Index within the current task trajectory")
|
| 70 |
+
query_index: int = Field(..., ge=0, description="Index of the active query")
|
| 71 |
+
total_energy: float = Field(default=0.0, ge=0.0, description="Cumulative energy cost")
|
| 72 |
+
total_latency: float = Field(default=0.0, ge=0.0, description="Cumulative latency cost")
|
| 73 |
+
cache_contents: list[str] = Field(default_factory=list, description="Current cache contents")
|
| 74 |
+
carbon_history_index: int = Field(default=0, ge=0, description="Current carbon history pointer")
|
| 75 |
+
|
| 76 |
+
def public_payload(self) -> dict[str, Any]:
|
| 77 |
+
return self.model_dump()
|
server/tasks.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic task definitions for the Eco-LLM routing environment."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
from server.models import ModelChoice
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
ENERGY_COSTS = {
|
| 11 |
+
ModelChoice.SMALL: 0.1,
|
| 12 |
+
ModelChoice.MEDIUM: 0.3,
|
| 13 |
+
ModelChoice.LARGE: 0.6,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
LATENCY_COSTS = {
|
| 17 |
+
ModelChoice.SMALL: 1.0,
|
| 18 |
+
ModelChoice.MEDIUM: 2.0,
|
| 19 |
+
ModelChoice.LARGE: 5.0,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
MODEL_ORDER = [ModelChoice.SMALL, ModelChoice.MEDIUM, ModelChoice.LARGE]
|
| 23 |
+
MODEL_RANK = {model: index for index, model in enumerate(MODEL_ORDER)}
|
| 24 |
+
CARBON_SCHEDULE = [0.8, 0.6, 0.4, 0.3, 0.5]
|
| 25 |
+
LATENCY_MU = 0.1
|
| 26 |
+
MAX_CARBON_PENALTY = 0.8
|
| 27 |
+
KB_ENERGY_COST = 0.05
|
| 28 |
+
KB_LATENCY_COST = 1.5
|
| 29 |
+
MAX_STEPS_PER_EPISODE = 50
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True)
|
| 33 |
+
class QuerySpec:
|
| 34 |
+
text: str
|
| 35 |
+
correct_answer: str
|
| 36 |
+
minimum_model: ModelChoice
|
| 37 |
+
kb_available: bool = False
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass(frozen=True)
|
| 41 |
+
class TaskSpec:
|
| 42 |
+
task_id: str
|
| 43 |
+
difficulty: str
|
| 44 |
+
queries: tuple[QuerySpec, ...]
|
| 45 |
+
large_model_penalty: float = 0.0
|
| 46 |
+
description: str = ""
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
TASKS: dict[str, TaskSpec] = {
|
| 50 |
+
"task_1": TaskSpec(
|
| 51 |
+
task_id="task_1",
|
| 52 |
+
difficulty="easy",
|
| 53 |
+
queries=(
|
| 54 |
+
QuerySpec(
|
| 55 |
+
text="What is the most efficient insulation material for a passive house retrofit?",
|
| 56 |
+
correct_answer="Vacuum insulated panels",
|
| 57 |
+
minimum_model=ModelChoice.MEDIUM,
|
| 58 |
+
),
|
| 59 |
+
),
|
| 60 |
+
description="Single-query routing with accuracy-only emphasis.",
|
| 61 |
+
),
|
| 62 |
+
"task_2": TaskSpec(
|
| 63 |
+
task_id="task_2",
|
| 64 |
+
difficulty="medium",
|
| 65 |
+
queries=(
|
| 66 |
+
QuerySpec(
|
| 67 |
+
text="Summarize the trade-off between quantization and factual accuracy in a customer support bot.",
|
| 68 |
+
correct_answer="Aggressive quantization lowers energy use but can reduce factual reliability.",
|
| 69 |
+
minimum_model=ModelChoice.SMALL,
|
| 70 |
+
),
|
| 71 |
+
QuerySpec(
|
| 72 |
+
text="Explain when speculative decoding improves throughput on medium-sized models.",
|
| 73 |
+
correct_answer="It helps when draft tokens are likely to be accepted by the target model.",
|
| 74 |
+
minimum_model=ModelChoice.MEDIUM,
|
| 75 |
+
),
|
| 76 |
+
QuerySpec(
|
| 77 |
+
text="Design a safety fallback for multilingual medical triage.",
|
| 78 |
+
correct_answer="Use high-capability review with escalation and verified retrieval before final output.",
|
| 79 |
+
minimum_model=ModelChoice.LARGE,
|
| 80 |
+
),
|
| 81 |
+
),
|
| 82 |
+
large_model_penalty=-0.2,
|
| 83 |
+
description="Three-query episode with an extra penalty for using the LARGE model.",
|
| 84 |
+
),
|
| 85 |
+
"task_3": TaskSpec(
|
| 86 |
+
task_id="task_3",
|
| 87 |
+
difficulty="hard",
|
| 88 |
+
queries=(
|
| 89 |
+
QuerySpec(
|
| 90 |
+
text="Classify whether this FAQ request can be answered from the billing knowledge base.",
|
| 91 |
+
correct_answer="Yes, use the billing KB article.",
|
| 92 |
+
minimum_model=ModelChoice.SMALL,
|
| 93 |
+
kb_available=True,
|
| 94 |
+
),
|
| 95 |
+
QuerySpec(
|
| 96 |
+
text="Route a long-form reasoning question about carbon-aware serving policy.",
|
| 97 |
+
correct_answer="Use cascade with escalation to a larger model if smaller ones fail.",
|
| 98 |
+
minimum_model=ModelChoice.MEDIUM,
|
| 99 |
+
),
|
| 100 |
+
QuerySpec(
|
| 101 |
+
text="Classify whether this FAQ request can be answered from the billing knowledge base.",
|
| 102 |
+
correct_answer="Yes, use the billing KB article.",
|
| 103 |
+
minimum_model=ModelChoice.SMALL,
|
| 104 |
+
kb_available=True,
|
| 105 |
+
),
|
| 106 |
+
QuerySpec(
|
| 107 |
+
text="Generate a compliant answer for a finance-regulated enterprise user.",
|
| 108 |
+
correct_answer="Use the large model with a retrieval-backed compliance layer.",
|
| 109 |
+
minimum_model=ModelChoice.LARGE,
|
| 110 |
+
),
|
| 111 |
+
QuerySpec(
|
| 112 |
+
text="Route a long-form reasoning question about carbon-aware serving policy.",
|
| 113 |
+
correct_answer="Use cascade with escalation to a larger model if smaller ones fail.",
|
| 114 |
+
minimum_model=ModelChoice.MEDIUM,
|
| 115 |
+
),
|
| 116 |
+
),
|
| 117 |
+
description="Stateful five-query episode with repeats, caching, cascading, early exit, and waiting.",
|
| 118 |
+
),
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def get_task(task_id: str) -> TaskSpec:
|
| 123 |
+
"""Return a task by id, defaulting to the easy task for unknown ids."""
|
| 124 |
+
return TASKS.get(task_id, TASKS["task_1"])
|
tests/test_agents.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for baseline agents."""
|
| 2 |
+
|
| 3 |
+
from baseline import HeuristicRoutingAgent, RandomRoutingAgent
|
| 4 |
+
from server.env import EcoLLMInferenceRoutingEnvironment
|
| 5 |
+
from server.models import Strategy
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def test_random_agent_creates_valid_actions() -> None:
|
| 9 |
+
"""Random agent produces valid RLAction values."""
|
| 10 |
+
agent = RandomRoutingAgent()
|
| 11 |
+
env = EcoLLMInferenceRoutingEnvironment()
|
| 12 |
+
obs = env.reset(task_id="task_1")
|
| 13 |
+
|
| 14 |
+
action = agent.get_action(obs)
|
| 15 |
+
assert action.strategy is not None
|
| 16 |
+
assert action.model_choice is not None
|
| 17 |
+
assert isinstance(action.exit_flag, bool)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_heuristic_agent_prefers_cache() -> None:
|
| 21 |
+
"""Heuristic agent should prioritize cache hits."""
|
| 22 |
+
agent = HeuristicRoutingAgent()
|
| 23 |
+
env = EcoLLMInferenceRoutingEnvironment()
|
| 24 |
+
obs = env.reset(task_id="task_1")
|
| 25 |
+
|
| 26 |
+
obs.cache_contents.append(obs.query)
|
| 27 |
+
action = agent.get_action(obs)
|
| 28 |
+
|
| 29 |
+
assert action.strategy == Strategy.USE_CACHE
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_random_agent_deterministic_with_seed() -> None:
|
| 33 |
+
"""Random agent should be deterministic with the same seed."""
|
| 34 |
+
agent1 = RandomRoutingAgent(seed=42)
|
| 35 |
+
agent2 = RandomRoutingAgent(seed=42)
|
| 36 |
+
|
| 37 |
+
env = EcoLLMInferenceRoutingEnvironment()
|
| 38 |
+
obs = env.reset(task_id="task_1")
|
| 39 |
+
|
| 40 |
+
action1 = agent1.get_action(obs)
|
| 41 |
+
action2 = agent2.get_action(obs)
|
| 42 |
+
|
| 43 |
+
assert action1.strategy == action2.strategy
|
| 44 |
+
assert action1.model_choice == action2.model_choice
|
| 45 |
+
assert action1.exit_flag == action2.exit_flag
|
deployment/uv.lock → uv.lock
RENAMED
|
@@ -475,182 +475,64 @@ wheels = [
|
|
| 475 |
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 476 |
]
|
| 477 |
|
| 478 |
-
[[package]]
|
| 479 |
-
name = "coverage"
|
| 480 |
-
version = "7.13.5"
|
| 481 |
-
source = { registry = "https://pypi.org/simple" }
|
| 482 |
-
sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
|
| 483 |
-
wheels = [
|
| 484 |
-
{ url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" },
|
| 485 |
-
{ url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" },
|
| 486 |
-
{ url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" },
|
| 487 |
-
{ url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" },
|
| 488 |
-
{ url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" },
|
| 489 |
-
{ url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" },
|
| 490 |
-
{ url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" },
|
| 491 |
-
{ url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" },
|
| 492 |
-
{ url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" },
|
| 493 |
-
{ url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" },
|
| 494 |
-
{ url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" },
|
| 495 |
-
{ url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" },
|
| 496 |
-
{ url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" },
|
| 497 |
-
{ url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" },
|
| 498 |
-
{ url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" },
|
| 499 |
-
{ url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" },
|
| 500 |
-
{ url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" },
|
| 501 |
-
{ url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" },
|
| 502 |
-
{ url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" },
|
| 503 |
-
{ url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" },
|
| 504 |
-
{ url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" },
|
| 505 |
-
{ url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" },
|
| 506 |
-
{ url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" },
|
| 507 |
-
{ url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" },
|
| 508 |
-
{ url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" },
|
| 509 |
-
{ url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" },
|
| 510 |
-
{ url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" },
|
| 511 |
-
{ url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" },
|
| 512 |
-
{ url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" },
|
| 513 |
-
{ url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" },
|
| 514 |
-
{ url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" },
|
| 515 |
-
{ url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" },
|
| 516 |
-
{ url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" },
|
| 517 |
-
{ url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" },
|
| 518 |
-
{ url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" },
|
| 519 |
-
{ url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" },
|
| 520 |
-
{ url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" },
|
| 521 |
-
{ url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" },
|
| 522 |
-
{ url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" },
|
| 523 |
-
{ url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" },
|
| 524 |
-
{ url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" },
|
| 525 |
-
{ url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" },
|
| 526 |
-
{ url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" },
|
| 527 |
-
{ url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" },
|
| 528 |
-
{ url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" },
|
| 529 |
-
{ url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" },
|
| 530 |
-
{ url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" },
|
| 531 |
-
{ url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" },
|
| 532 |
-
{ url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" },
|
| 533 |
-
{ url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" },
|
| 534 |
-
{ url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" },
|
| 535 |
-
{ url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" },
|
| 536 |
-
{ url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" },
|
| 537 |
-
{ url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" },
|
| 538 |
-
{ url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" },
|
| 539 |
-
{ url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" },
|
| 540 |
-
{ url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" },
|
| 541 |
-
{ url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" },
|
| 542 |
-
{ url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" },
|
| 543 |
-
{ url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" },
|
| 544 |
-
{ url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" },
|
| 545 |
-
{ url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" },
|
| 546 |
-
{ url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" },
|
| 547 |
-
{ url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" },
|
| 548 |
-
{ url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" },
|
| 549 |
-
{ url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" },
|
| 550 |
-
{ url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" },
|
| 551 |
-
{ url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" },
|
| 552 |
-
{ url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" },
|
| 553 |
-
{ url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" },
|
| 554 |
-
{ url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" },
|
| 555 |
-
{ url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" },
|
| 556 |
-
{ url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" },
|
| 557 |
-
{ url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" },
|
| 558 |
-
{ url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
|
| 559 |
-
{ url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
|
| 560 |
-
{ url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
|
| 561 |
-
{ url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
|
| 562 |
-
{ url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
|
| 563 |
-
{ url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
|
| 564 |
-
{ url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
|
| 565 |
-
{ url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
|
| 566 |
-
{ url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
|
| 567 |
-
{ url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
|
| 568 |
-
{ url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
|
| 569 |
-
{ url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
|
| 570 |
-
{ url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
|
| 571 |
-
{ url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
|
| 572 |
-
{ url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
|
| 573 |
-
{ url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
|
| 574 |
-
{ url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
|
| 575 |
-
{ url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
|
| 576 |
-
{ url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
|
| 577 |
-
{ url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
|
| 578 |
-
{ url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
|
| 579 |
-
{ url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
|
| 580 |
-
{ url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
|
| 581 |
-
{ url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
|
| 582 |
-
{ url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
|
| 583 |
-
{ url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
|
| 584 |
-
{ url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
|
| 585 |
-
{ url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
|
| 586 |
-
{ url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
|
| 587 |
-
{ url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
|
| 588 |
-
{ url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
|
| 589 |
-
]
|
| 590 |
-
|
| 591 |
-
[package.optional-dependencies]
|
| 592 |
-
toml = [
|
| 593 |
-
{ name = "tomli", marker = "python_full_version <= '3.11'" },
|
| 594 |
-
]
|
| 595 |
-
|
| 596 |
[[package]]
|
| 597 |
name = "cryptography"
|
| 598 |
-
version = "46.0.
|
| 599 |
source = { registry = "https://pypi.org/simple" }
|
| 600 |
dependencies = [
|
| 601 |
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
| 602 |
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
| 603 |
]
|
| 604 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 605 |
-
wheels = [
|
| 606 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 607 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 608 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 609 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 610 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 611 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 612 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 613 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 614 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 615 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 616 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 617 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 618 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 619 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 620 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 621 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 622 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 623 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 624 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 625 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 626 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 627 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 628 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 629 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 630 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 631 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 632 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 633 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 634 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 635 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 636 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 637 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 638 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 639 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 640 |
-
{ url = "https://files.pythonhosted.org/packages/01/
|
| 641 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 642 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 643 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 644 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 645 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 646 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 647 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 648 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 649 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 650 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 651 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 652 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 653 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 654 |
]
|
| 655 |
|
| 656 |
[[package]]
|
|
@@ -967,7 +849,7 @@ wheels = [
|
|
| 967 |
|
| 968 |
[[package]]
|
| 969 |
name = "huggingface-hub"
|
| 970 |
-
version = "1.9.
|
| 971 |
source = { registry = "https://pypi.org/simple" }
|
| 972 |
dependencies = [
|
| 973 |
{ name = "filelock" },
|
|
@@ -980,9 +862,9 @@ dependencies = [
|
|
| 980 |
{ name = "typer" },
|
| 981 |
{ name = "typing-extensions" },
|
| 982 |
]
|
| 983 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 984 |
wheels = [
|
| 985 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 986 |
]
|
| 987 |
|
| 988 |
[[package]]
|
|
@@ -1599,24 +1481,35 @@ core = [
|
|
| 1599 |
]
|
| 1600 |
|
| 1601 |
[[package]]
|
| 1602 |
-
name = "openenv-
|
| 1603 |
version = "0.1.0"
|
| 1604 |
source = { editable = "." }
|
| 1605 |
dependencies = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1606 |
{ name = "openenv-core", extra = ["core"] },
|
|
|
|
|
|
|
| 1607 |
]
|
| 1608 |
|
| 1609 |
[package.optional-dependencies]
|
| 1610 |
dev = [
|
| 1611 |
{ name = "pytest" },
|
| 1612 |
-
{ name = "pytest-cov" },
|
| 1613 |
]
|
| 1614 |
|
| 1615 |
[package.metadata]
|
| 1616 |
requires-dist = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1617 |
{ name = "openenv-core", extras = ["core"], specifier = ">=0.2.2" },
|
|
|
|
| 1618 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
| 1619 |
-
{ name = "
|
| 1620 |
]
|
| 1621 |
provides-extras = ["dev"]
|
| 1622 |
|
|
@@ -2215,7 +2108,7 @@ wheels = [
|
|
| 2215 |
|
| 2216 |
[[package]]
|
| 2217 |
name = "pytest"
|
| 2218 |
-
version = "9.0.
|
| 2219 |
source = { registry = "https://pypi.org/simple" }
|
| 2220 |
dependencies = [
|
| 2221 |
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
@@ -2226,23 +2119,9 @@ dependencies = [
|
|
| 2226 |
{ name = "pygments" },
|
| 2227 |
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
| 2228 |
]
|
| 2229 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 2230 |
-
wheels = [
|
| 2231 |
-
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
| 2232 |
-
]
|
| 2233 |
-
|
| 2234 |
-
[[package]]
|
| 2235 |
-
name = "pytest-cov"
|
| 2236 |
-
version = "7.1.0"
|
| 2237 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2238 |
-
dependencies = [
|
| 2239 |
-
{ name = "coverage", extra = ["toml"] },
|
| 2240 |
-
{ name = "pluggy" },
|
| 2241 |
-
{ name = "pytest" },
|
| 2242 |
-
]
|
| 2243 |
-
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
|
| 2244 |
wheels = [
|
| 2245 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 2246 |
]
|
| 2247 |
|
| 2248 |
[[package]]
|
|
@@ -2268,11 +2147,11 @@ wheels = [
|
|
| 2268 |
|
| 2269 |
[[package]]
|
| 2270 |
name = "python-multipart"
|
| 2271 |
-
version = "0.0.
|
| 2272 |
source = { registry = "https://pypi.org/simple" }
|
| 2273 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 2274 |
wheels = [
|
| 2275 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 2276 |
]
|
| 2277 |
|
| 2278 |
[[package]]
|
|
@@ -2774,11 +2653,11 @@ wheels = [
|
|
| 2774 |
|
| 2775 |
[[package]]
|
| 2776 |
name = "uncalled-for"
|
| 2777 |
-
version = "0.
|
| 2778 |
source = { registry = "https://pypi.org/simple" }
|
| 2779 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 2780 |
wheels = [
|
| 2781 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 2782 |
]
|
| 2783 |
|
| 2784 |
[[package]]
|
|
@@ -2792,16 +2671,16 @@ wheels = [
|
|
| 2792 |
|
| 2793 |
[[package]]
|
| 2794 |
name = "uvicorn"
|
| 2795 |
-
version = "0.
|
| 2796 |
source = { registry = "https://pypi.org/simple" }
|
| 2797 |
dependencies = [
|
| 2798 |
{ name = "click" },
|
| 2799 |
{ name = "h11" },
|
| 2800 |
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
| 2801 |
]
|
| 2802 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 2803 |
wheels = [
|
| 2804 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 2805 |
]
|
| 2806 |
|
| 2807 |
[[package]]
|
|
|
|
| 475 |
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
| 476 |
]
|
| 477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
[[package]]
|
| 479 |
name = "cryptography"
|
| 480 |
+
version = "46.0.7"
|
| 481 |
source = { registry = "https://pypi.org/simple" }
|
| 482 |
dependencies = [
|
| 483 |
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
| 484 |
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
| 485 |
]
|
| 486 |
+
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
| 487 |
+
wheels = [
|
| 488 |
+
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
| 489 |
+
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
| 490 |
+
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
| 491 |
+
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
| 492 |
+
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
| 493 |
+
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
| 494 |
+
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
| 495 |
+
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
| 496 |
+
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
| 497 |
+
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
| 498 |
+
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
| 499 |
+
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
| 500 |
+
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
| 501 |
+
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
| 502 |
+
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
| 503 |
+
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
| 504 |
+
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
| 505 |
+
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
| 506 |
+
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
| 507 |
+
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
| 508 |
+
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
| 509 |
+
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
| 510 |
+
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
| 511 |
+
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
| 512 |
+
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
| 513 |
+
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
| 514 |
+
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
| 515 |
+
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
| 516 |
+
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
| 517 |
+
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
| 518 |
+
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
| 519 |
+
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
| 520 |
+
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
| 521 |
+
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
| 522 |
+
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
| 523 |
+
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
| 524 |
+
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
| 525 |
+
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
| 526 |
+
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
| 527 |
+
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
| 528 |
+
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
| 529 |
+
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
| 530 |
+
{ url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
|
| 531 |
+
{ url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
|
| 532 |
+
{ url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
|
| 533 |
+
{ url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
|
| 534 |
+
{ url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
|
| 535 |
+
{ url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
|
| 536 |
]
|
| 537 |
|
| 538 |
[[package]]
|
|
|
|
| 849 |
|
| 850 |
[[package]]
|
| 851 |
name = "huggingface-hub"
|
| 852 |
+
version = "1.9.2"
|
| 853 |
source = { registry = "https://pypi.org/simple" }
|
| 854 |
dependencies = [
|
| 855 |
{ name = "filelock" },
|
|
|
|
| 862 |
{ name = "typer" },
|
| 863 |
{ name = "typing-extensions" },
|
| 864 |
]
|
| 865 |
+
sdist = { url = "https://files.pythonhosted.org/packages/cf/65/fb800d327bf25bf31b798dd08935d326d064ecb9b359059fecd91b3a98e8/huggingface_hub-1.9.2.tar.gz", hash = "sha256:8d09d080a186bd950a361bfc04b862dfb04d6a2b41d48e9ba1b37507cfd3f1e1", size = 750284, upload-time = "2026-04-08T08:43:11.127Z" }
|
| 866 |
wheels = [
|
| 867 |
+
{ url = "https://files.pythonhosted.org/packages/57/d4/e33bf0b362810a9b96c5923e38908950d58ecb512db42e3730320c7f4a3a/huggingface_hub-1.9.2-py3-none-any.whl", hash = "sha256:e1e62ce237d4fbeca9f970aeb15176fbd503e04c25577bfd22f44aa7aa2b5243", size = 637349, upload-time = "2026-04-08T08:43:09.114Z" },
|
| 868 |
]
|
| 869 |
|
| 870 |
[[package]]
|
|
|
|
| 1481 |
]
|
| 1482 |
|
| 1483 |
[[package]]
|
| 1484 |
+
name = "openenv-eco-llm-inference-routing"
|
| 1485 |
version = "0.1.0"
|
| 1486 |
source = { editable = "." }
|
| 1487 |
dependencies = [
|
| 1488 |
+
{ name = "fastapi" },
|
| 1489 |
+
{ name = "gradio" },
|
| 1490 |
+
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
| 1491 |
+
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
| 1492 |
+
{ name = "openai" },
|
| 1493 |
{ name = "openenv-core", extra = ["core"] },
|
| 1494 |
+
{ name = "pydantic" },
|
| 1495 |
+
{ name = "uvicorn" },
|
| 1496 |
]
|
| 1497 |
|
| 1498 |
[package.optional-dependencies]
|
| 1499 |
dev = [
|
| 1500 |
{ name = "pytest" },
|
|
|
|
| 1501 |
]
|
| 1502 |
|
| 1503 |
[package.metadata]
|
| 1504 |
requires-dist = [
|
| 1505 |
+
{ name = "fastapi", specifier = ">=0.115.0" },
|
| 1506 |
+
{ name = "gradio", specifier = ">=4.0.0" },
|
| 1507 |
+
{ name = "numpy", specifier = ">=1.26.0" },
|
| 1508 |
+
{ name = "openai", specifier = ">=1.40.0" },
|
| 1509 |
{ name = "openenv-core", extras = ["core"], specifier = ">=0.2.2" },
|
| 1510 |
+
{ name = "pydantic", specifier = ">=2.8.0" },
|
| 1511 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
| 1512 |
+
{ name = "uvicorn", specifier = ">=0.30.0" },
|
| 1513 |
]
|
| 1514 |
provides-extras = ["dev"]
|
| 1515 |
|
|
|
|
| 2108 |
|
| 2109 |
[[package]]
|
| 2110 |
name = "pytest"
|
| 2111 |
+
version = "9.0.3"
|
| 2112 |
source = { registry = "https://pypi.org/simple" }
|
| 2113 |
dependencies = [
|
| 2114 |
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
|
|
| 2119 |
{ name = "pygments" },
|
| 2120 |
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
| 2121 |
]
|
| 2122 |
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2123 |
wheels = [
|
| 2124 |
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
| 2125 |
]
|
| 2126 |
|
| 2127 |
[[package]]
|
|
|
|
| 2147 |
|
| 2148 |
[[package]]
|
| 2149 |
name = "python-multipart"
|
| 2150 |
+
version = "0.0.24"
|
| 2151 |
source = { registry = "https://pypi.org/simple" }
|
| 2152 |
+
sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" }
|
| 2153 |
wheels = [
|
| 2154 |
+
{ url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" },
|
| 2155 |
]
|
| 2156 |
|
| 2157 |
[[package]]
|
|
|
|
| 2653 |
|
| 2654 |
[[package]]
|
| 2655 |
name = "uncalled-for"
|
| 2656 |
+
version = "0.3.1"
|
| 2657 |
source = { registry = "https://pypi.org/simple" }
|
| 2658 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e1/68/35c1d87e608940badbcfeb630347aa0509897284684f61fab6423d02b253/uncalled_for-0.3.1.tar.gz", hash = "sha256:5e412ac6708f04b56bef5867b5dcf6690ebce4eb7316058d9c50787492bb4bca", size = 49693, upload-time = "2026-04-07T13:05:06.462Z" }
|
| 2659 |
wheels = [
|
| 2660 |
+
{ url = "https://files.pythonhosted.org/packages/11/e1/7ec67882ad8fc9f86384bef6421fa252c9cbe5744f8df6ce77afc9eca1f5/uncalled_for-0.3.1-py3-none-any.whl", hash = "sha256:074cdc92da8356278f93d0ded6f2a66dd883dbecaf9bc89437646ee2289cc200", size = 11361, upload-time = "2026-04-07T13:05:05.341Z" },
|
| 2661 |
]
|
| 2662 |
|
| 2663 |
[[package]]
|
|
|
|
| 2671 |
|
| 2672 |
[[package]]
|
| 2673 |
name = "uvicorn"
|
| 2674 |
+
version = "0.44.0"
|
| 2675 |
source = { registry = "https://pypi.org/simple" }
|
| 2676 |
dependencies = [
|
| 2677 |
{ name = "click" },
|
| 2678 |
{ name = "h11" },
|
| 2679 |
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
| 2680 |
]
|
| 2681 |
+
sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
|
| 2682 |
wheels = [
|
| 2683 |
+
{ url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
|
| 2684 |
]
|
| 2685 |
|
| 2686 |
[[package]]
|