Skip to content

Commit dcd421c

Browse files
committed
Initial commit: KORA routing agent skeleton for AMD hackathon Track 1
0 parents  commit dcd421c

10 files changed

Lines changed: 478 additions & 0 deletions

File tree

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__/
2+
*.pyc
3+
.env
4+
results.json
5+
6+
# internal handoff - never commit to public repo
7+
HANDOFF.md

Dockerfile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# KORA token-efficient routing agent, container image.
2+
#
3+
# The routing core is pure Python with a single runtime dependency (the
4+
# OpenAI-compatible client used for remote Fireworks calls), so the base image
5+
# stays slim. The local small-model layer (e.g. Gemma weights + runtime) is
6+
# added on launch day once the allowed models and scoring environment are known.
7+
8+
FROM python:3.11-slim
9+
10+
ENV PYTHONUNBUFFERED=1 \
11+
PYTHONDONTWRITEBYTECODE=1
12+
13+
WORKDIR /app
14+
15+
COPY requirements.txt ./
16+
RUN pip install --no-cache-dir -r requirements.txt
17+
18+
COPY kora_router ./kora_router
19+
20+
# FIREWORKS_API_KEY and REMOTE_MODEL are supplied at run time via env vars.
21+
# Example:
22+
# docker run --rm -e FIREWORKS_API_KEY=... -e REMOTE_MODEL=accounts/fireworks/models/... \
23+
# -v "$PWD":/data kora-router \
24+
# python -m kora_router.main --tasks /data/tasks.json --out /data/results.json
25+
26+
ENTRYPOINT ["python", "-m", "kora_router.main"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Krako Labs, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# KORA Router (AMD Developer Hackathon, Track 1)
2+
3+
A token-efficient routing agent. KORA decides, before any remote inference, how
4+
each unit of work should be handled: resolved by cheap deterministic or local
5+
computation, or escalated to a remote model only when nothing cheaper is
6+
confident. Remote token usage is a direct, measurable consequence of that
7+
routing.
8+
9+
## Status
10+
11+
This is the launch-day skeleton. The pipeline runs end to end with a placeholder
12+
routing policy that escalates every task to the remote model, so the container
13+
is runnable and testable before the task set is published. The real routing
14+
logic (deterministic rules first, a local small model for cheap-but-non-trivial
15+
work, remote only when needed) is added once the task I/O format and the allowed
16+
models are known.
17+
18+
## Layout
19+
20+
```
21+
kora_router/
22+
main.py entry point: load tasks, route each, write results
23+
router.py Route / RouteDecision / Router
24+
local_model.py local backend
25+
fireworks_client.py remote backend (OpenAI-compatible Fireworks endpoint)
26+
Dockerfile python:3.11-slim image, single runtime dependency
27+
requirements.txt openai client (local backend deps added on launch day)
28+
```
29+
30+
## Setup
31+
32+
```
33+
pip install -r requirements.txt
34+
```
35+
36+
Remote calls use the OpenAI-compatible Fireworks endpoint. Supply credentials
37+
and the model id at run time via environment variables:
38+
39+
```
40+
export FIREWORKS_API_KEY=...
41+
export REMOTE_MODEL=accounts/fireworks/models/<model>
42+
```
43+
44+
## Usage
45+
46+
```
47+
python -m kora_router.main --tasks tasks.json --out results.json
48+
```
49+
50+
Arguments:
51+
52+
- `--tasks` path to the task JSON (required). Accepts either a list of tasks or
53+
an object with a `tasks` key.
54+
- `--out` output path (default `results.json`).
55+
- `--remote-model` Fireworks model id. Falls back to the `REMOTE_MODEL`
56+
environment variable.
57+
58+
The output records, per task, the chosen route and the remote token count, plus
59+
a summary with total remote tokens, remote call count, and route counts.
60+
61+
## Docker
62+
63+
```
64+
docker build -t kora-router .
65+
docker run --rm \
66+
-e FIREWORKS_API_KEY=... \
67+
-e REMOTE_MODEL=accounts/fireworks/models/<model> \
68+
-v "$PWD":/data \
69+
kora-router --tasks /data/tasks.json --out /data/results.json
70+
```
71+
72+
## License
73+
74+
MIT. See LICENSE.

kora_router/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""KORA token-efficient routing agent (AMD Hackathon ACT II, Track 1)."""
2+
3+
from .router import Route, RouteDecision, Router, TaskResult
4+
5+
__all__ = ["Route", "RouteDecision", "Router", "TaskResult"]

kora_router/fireworks_client.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Remote model client for the Fireworks AI inference API.
2+
3+
Fireworks exposes an OpenAI-compatible endpoint, so the standard `openai`
4+
client works by pointing `base_url` at the Fireworks inference host. Every
5+
call records prompt/completion token usage, which the routing layer uses to
6+
account for remote spend (local tokens are free under the challenge scoring).
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
from dataclasses import dataclass, field
13+
14+
from openai import OpenAI
15+
16+
FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1"
17+
18+
19+
@dataclass
20+
class RemoteUsage:
21+
"""Running tally of remote token spend."""
22+
23+
prompt_tokens: int = 0
24+
completion_tokens: int = 0
25+
calls: int = 0
26+
27+
@property
28+
def total_tokens(self) -> int:
29+
return self.prompt_tokens + self.completion_tokens
30+
31+
def add(self, prompt: int, completion: int) -> None:
32+
self.prompt_tokens += prompt
33+
self.completion_tokens += completion
34+
self.calls += 1
35+
36+
37+
@dataclass
38+
class RemoteResult:
39+
text: str
40+
prompt_tokens: int
41+
completion_tokens: int
42+
model: str
43+
44+
45+
class FireworksClient:
46+
"""Thin wrapper over the OpenAI-compatible Fireworks chat endpoint."""
47+
48+
def __init__(self, model: str, api_key: str | None = None,
49+
base_url: str = FIREWORKS_BASE_URL) -> None:
50+
key = api_key or os.getenv("FIREWORKS_API_KEY")
51+
if not key:
52+
raise RuntimeError("FIREWORKS_API_KEY is not set")
53+
self._client = OpenAI(base_url=base_url, api_key=key)
54+
self.model = model
55+
self.usage = RemoteUsage()
56+
57+
def chat(self, messages: list[dict], *, temperature: float = 0.0,
58+
max_tokens: int = 512) -> RemoteResult:
59+
resp = self._client.chat.completions.create(
60+
model=self.model,
61+
messages=messages,
62+
temperature=temperature,
63+
max_tokens=max_tokens,
64+
)
65+
usage = resp.usage
66+
prompt = getattr(usage, "prompt_tokens", 0) or 0
67+
completion = getattr(usage, "completion_tokens", 0) or 0
68+
self.usage.add(prompt, completion)
69+
return RemoteResult(
70+
text=resp.choices[0].message.content or "",
71+
prompt_tokens=prompt,
72+
completion_tokens=completion,
73+
model=self.model,
74+
)

kora_router/local_model.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Local small-model wrapper.
2+
3+
The routing layer sends "easy" work here instead of the remote model. Under
4+
the challenge scoring, tokens spent locally count as zero, so the local model
5+
should be sized to run inside the standardized scoring environment (a small
6+
open model such as Gemma is the intended fit).
7+
8+
The concrete backend is deliberately pluggable: the exact model and runtime
9+
(transformers, llama.cpp, a served endpoint, etc.) are fixed on launch day once
10+
the allowed models and environment constraints are published. Everything above
11+
this module only depends on the `LocalResult` shape and the `generate` method,
12+
so swapping the backend does not touch the router.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
from dataclasses import dataclass
18+
from typing import Protocol
19+
20+
21+
@dataclass
22+
class LocalResult:
23+
text: str
24+
# Local tokens are free under scoring, but we still track them for analysis.
25+
prompt_tokens: int = 0
26+
completion_tokens: int = 0
27+
28+
29+
class LocalBackend(Protocol):
30+
def generate(self, messages: list[dict], *, temperature: float,
31+
max_tokens: int) -> LocalResult:
32+
...
33+
34+
35+
class EchoBackend:
36+
"""Placeholder backend used until the real local model is wired in.
37+
38+
Returns an empty completion so the pipeline is runnable end-to-end before
39+
launch day. Replaced by the actual small-model backend once the allowed
40+
models are known.
41+
"""
42+
43+
def generate(self, messages: list[dict], *, temperature: float = 0.0,
44+
max_tokens: int = 512) -> LocalResult:
45+
return LocalResult(text="", prompt_tokens=0, completion_tokens=0)
46+
47+
48+
class LocalModel:
49+
def __init__(self, backend: LocalBackend | None = None) -> None:
50+
self._backend = backend or EchoBackend()
51+
52+
def chat(self, messages: list[dict], *, temperature: float = 0.0,
53+
max_tokens: int = 512) -> LocalResult:
54+
return self._backend.generate(
55+
messages, temperature=temperature, max_tokens=max_tokens
56+
)

kora_router/main.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Entry point for running the KORA routing agent over a task set.
2+
3+
Usage (finalized on launch day once the task I/O format is published):
4+
5+
python -m kora_router.main --tasks tasks.json --out results.json
6+
7+
For now this wires the pipeline end-to-end with a placeholder decision function
8+
so the container is runnable and testable before the tasks are released.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import json
15+
import os
16+
from pathlib import Path
17+
from typing import Any
18+
19+
from .fireworks_client import FireworksClient
20+
from .local_model import LocalModel
21+
from .router import Route, RouteDecision, Router
22+
23+
24+
def default_decision(task: dict[str, Any]) -> RouteDecision:
25+
"""Placeholder routing policy.
26+
27+
Replaced on launch day with the real KORA decision logic (deterministic
28+
rules first, local model for cheap-but-non-trivial work, remote only when
29+
nothing cheaper is confident). Until then, everything escalates to remote so
30+
the pipeline produces answers end-to-end.
31+
"""
32+
return RouteDecision(route=Route.REMOTE, reason="placeholder: escalate all")
33+
34+
35+
def load_tasks(path: Path) -> list[dict[str, Any]]:
36+
data = json.loads(path.read_text(encoding="utf-8"))
37+
if isinstance(data, dict) and "tasks" in data:
38+
return list(data["tasks"])
39+
if isinstance(data, list):
40+
return data
41+
raise ValueError("unrecognized task file shape")
42+
43+
44+
def main() -> None:
45+
ap = argparse.ArgumentParser()
46+
ap.add_argument("--tasks", required=True, help="path to task JSON")
47+
ap.add_argument("--out", default="results.json")
48+
ap.add_argument("--remote-model",
49+
default=os.getenv("REMOTE_MODEL", ""),
50+
help="Fireworks model id (accounts/fireworks/models/...)")
51+
args = ap.parse_args()
52+
53+
tasks = load_tasks(Path(args.tasks))
54+
local = LocalModel()
55+
remote = FireworksClient(model=args.remote_model)
56+
router = Router(decide=default_decision, local=local, remote=remote)
57+
58+
results = []
59+
for task in tasks:
60+
r = router.run_task(task)
61+
results.append({
62+
"id": r.task_id,
63+
"answer": r.answer,
64+
"route": r.route.value,
65+
"reason": r.reason,
66+
"remote_tokens": r.remote_tokens,
67+
})
68+
69+
total_remote = sum(r["remote_tokens"] for r in results)
70+
payload = {
71+
"results": results,
72+
"summary": {
73+
"n_tasks": len(results),
74+
"total_remote_tokens": total_remote,
75+
"remote_calls": remote.usage.calls,
76+
"route_counts": _route_counts(results),
77+
},
78+
}
79+
Path(args.out).write_text(json.dumps(payload, indent=2), encoding="utf-8")
80+
print(f"wrote {args.out}: {len(results)} tasks, "
81+
f"{total_remote} remote tokens, {remote.usage.calls} remote calls")
82+
83+
84+
def _route_counts(results: list[dict]) -> dict[str, int]:
85+
counts: dict[str, int] = {}
86+
for r in results:
87+
counts[r["route"]] = counts.get(r["route"], 0) + 1
88+
return counts
89+
90+
91+
if __name__ == "__main__":
92+
main()

0 commit comments

Comments
 (0)