Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions .claude/skills/cerebras/SKILL.md

This file was deleted.

80 changes: 80 additions & 0 deletions .claude/skills/openrouter/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: openrouter-inference
description: Use this to write code to call an LLM using LiteLLM and OpenRouter with a free model
---

# Calling an LLM via OpenRouter

These instructions allow you to write code to call an LLM through OpenRouter using LiteLLM.
The model is a free one, so no inference provider is pinned and no request costs money.

## Setup

The OPENROUTER_API_KEY must be set in the .env file and loaded in as an environment variable.

The uv project must include litellm and pydantic.
`uv add litellm pydantic`

## Code snippets

### Imports and constants

```python
from litellm import completion
MODEL = "openrouter/nvidia/nemotron-3.5-lightning:free"
```

The `openrouter/` prefix is what routes the call through OpenRouter — without it LiteLLM
cannot resolve the provider. Do not set `extra_body={"provider": {...}}`: pinning a provider
is what makes a request land on a paid one.

### Code to call for a text response

```python
response = completion(model=MODEL, messages=messages)
result = response.choices[0].message.content
```

### Code to call for a structured response

Free models do not support `response_format`, so Structured Outputs are unavailable. Use
forced tool calling instead: the schema is enforced server-side and the arguments come back
as JSON.

```python
TOOLS = [
{
"type": "function",
"function": {
"name": "submit_response",
"description": "Return the reply to the user.",
"parameters": MyBaseModelSubclass.model_json_schema(),
},
}
]

response = completion(
model=MODEL,
messages=messages,
tools=TOOLS,
tool_choice={"type": "function", "function": {"name": "submit_response"}},
)
raw = response.choices[0].message.tool_calls[0].function.arguments
result_as_object = MyBaseModelSubclass.model_validate_json(raw)
```

Always validate with Pydantic. A forced tool call is reliable but not guaranteed — check for
an empty `tool_calls` before indexing into it.

## Expect slow responses

Free endpoints are heavily shared and have no latency guarantee. Measured response times for
the model above ranged from 7 to 58 seconds across 18 calls, with a median around 20-30
seconds. Any UI calling this needs a progress indicator that sets an honest expectation, not
a bare spinner.

## Rate limits

Models with the `:free` suffix are limited to 20 requests per minute and 50 per day, rising
to 1000 per day once at least 10 USD of credit has been purchased on the account. Tests
should mock the LLM rather than spend this budget.
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Built entirely by coding agents as a capstone project for an agentic AI coding c
- **Live price streaming** via SSE with green/red flash animations
- **Simulated portfolio** — $10k virtual cash, market orders, instant fills
- **Portfolio visualizations** — heatmap (treemap), P&L chart, positions table
- **AI chat assistant** — analyzes holdings, suggests and auto-executes trades
- **AI chat assistant** — analyzes holdings, suggests and auto-executes trades; runs on a free model, so the whole app costs nothing to operate
- **Watchlist management** — track tickers manually or via AI
- **Dark terminal aesthetic** — Bloomberg-inspired, data-dense layout

Expand All @@ -20,7 +20,7 @@ Single Docker container serving everything on port 8000:
- **Frontend**: Next.js (static export) with TypeScript and Tailwind CSS
- **Backend**: FastAPI (Python/uv) with SSE streaming
- **Database**: SQLite with lazy initialization
- **AI**: LiteLLM → OpenRouter (Cerebras inference) with structured outputs
- **AI**: LiteLLM → OpenRouter with a free model, using forced tool calling
- **Market data**: Built-in GBM simulator (default) or Massive API (optional)

## Quick Start
Expand All @@ -32,7 +32,7 @@ cp .env.example .env

# Run with Docker
docker build -t finally .
docker run -v finally-data:/app/db -p 8000:8000 --env-file .env finally
docker run -v "$PWD/db:/app/db" -p 8000:8000 --env-file .env finally

# Open http://localhost:8000
```
Expand All @@ -42,9 +42,32 @@ docker run -v finally-data:/app/db -p 8000:8000 --env-file .env finally
| Variable | Required | Description |
|---|---|---|
| `OPENROUTER_API_KEY` | Yes | OpenRouter API key for AI chat |
| `OPENROUTER_MODEL` | No | Model to use; defaults to `openrouter/nvidia/nemotron-3.5-lightning:free` |
| `MASSIVE_API_KEY` | No | Massive (Polygon.io) key for real market data; omit to use simulator |
| `LLM_MOCK` | No | Set `true` for deterministic mock LLM responses (testing) |

Write variable names in uppercase. Docker passes `.env` through verbatim, and the Linux
container is case-sensitive even though Windows is not.

## Notes on the free model

The AI chat runs on a free OpenRouter model, so the app costs nothing to operate. Two things
follow from that, both by design rather than by accident:

- **Responses take 7-58 seconds**, typically 20-30. The chat panel shows an elapsed counter
and says so plainly instead of hiding the wait behind a spinner.
- **Free models allow 20 requests per minute and 50 per day** (1000 per day once 10 USD of
credit has been bought on the account). Run tests with `LLM_MOCK=true` rather than
spending that budget.

Setting `OPENROUTER_MODEL` to a paid model works and is much faster, but is not required.

## Troubleshooting

**Certificate errors during setup** (`CERTIFICATE_VERIFY_FAILED`, `invalid peer certificate`)
mean your network inspects TLS traffic, common on corporate networks. Use `uv --system-certs`
and add `truststore` so Python trusts the certificates in your OS store.

## Project Structure

```
Expand Down
Loading