Neural Tech Daily
ai-tutorials

Build a Streamlit dashboard for LLM analytics in 60 minutes

Build a Streamlit LLM-pricing dashboard: load OpenAI + Anthropic spend logs, chart cost-per-day with Altair, add filters, deploy free on Community Cloud.

Updated ~12 min read
Share
Streamlit homepage hero from streamlit.io — the open-source Python app framework this tutorial builds against

Image: Streamlit homepage marketing imagery, used for editorial coverage of the framework taught in this tutorial.

What you’ll need

Python 3.10 or newer, a terminal, and a CSV file of your OpenAI or Anthropic API usage. 4 If you do not have a real one yet, the tutorial generates a sample CSV in Step 3 so you can run end to end without waiting for your bill.

You will also want a free GitHub account if you intend to deploy the dashboard at the end. Streamlit Community Cloud deploys directly from a public GitHub repository, no infrastructure setup required. 1

Total time: about 60 minutes if you copy code as you read, longer if you adapt it to your real cost-export schema as you go.

Why Streamlit, not Gradio or Dash

Streamlit is the fastest path from a Python script to an interactive web dashboard. 2 The whole framework is one decision: every line of your script becomes UI. You write st.title("LLM analytics") and the page gets a title. You write st.dataframe(df) and the page gets an interactive table. There is no template language, no callback graph to register, no separate frontend.

Gradio is the alternative most devs have used. It ships with the Hugging Face ecosystem and excels at chat and inference demos. Pick Gradio when the surface is a chat interface or a model playground. For analytics dashboards with charts, filters, and uploads, Streamlit’s component vocabulary fits the shape of the problem better.

Plotly Dash is the more enterprise alternative: full callback graph, role-based access, the ability to host inside a Flask app behind a corporate VPN. Pick Dash when the dashboard is a long-lived internal tool with many maintainers. For a 60-minute build that goes live the same day, Dash is overkill.

The Streamlit codebase is open-source under Apache 2.0 on GitHub at streamlit/streamlit, with active maintenance from Snowflake (which acquired the project) and a permissive contributor model. 3 Snowflake’s acquisition has not, so far, restricted the open-source Community Cloud free tier, but verify the current Community Cloud pricing on the Streamlit deployment docs before you commit a project to it for production use. 1

Time required

About 60 minutes, broken down as follows: 5 minutes for Step 1 (install), 5 for Step 2 (Hello World), 10 for Step 3 (loading the CSV), 10 for Step 4 (the chart), 10 for Step 5 (filters), 10 for Step 6 (uploads), and 10 for Step 7 (deploy). The “Common pitfalls” and “Where to go next” sections at the end are for after the dashboard is live.

Steps

1. Install Streamlit and the plotting library

Open a terminal in a clean folder and create a virtual environment. The virtualenv keeps Streamlit’s dependencies out of your system Python, which avoids the subtle conflicts that show up later with pandas or numpy versions.

python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install streamlit pandas altair

The three packages are: Streamlit itself, pandas for the CSV reading and dataframe filtering, and Altair for the cost-per-day chart in Step 4. Altair ships as a dependency of Streamlit anyway, but installing it explicitly makes the import line clearer and pins the version your project expects.

2. Hello World with streamlit run

Create a file called app.py in the same folder. Type the following four lines, save, and run.

import streamlit as st

st.title("LLM analytics dashboard")
st.write("If you can see this, Streamlit is installed correctly.")

In the terminal, run:

streamlit run app.py

A browser tab opens at http://localhost:8501 with your title and message rendered as a real web page. The first time you run streamlit run, the CLI asks for your email for the Streamlit newsletter; press Enter to skip.

Every save reloads the page automatically. Leave the terminal running for the rest of the tutorial; each step replaces the contents of app.py with a longer version, and the browser updates as you save.

Streamlit official documentation home page on docs.streamlit.io, the canonical reference for the framework's component vocabulary, deployment, and secrets management this tutorial covers

Image: Streamlit official documentation home (docs.streamlit.io), used for editorial coverage of the framework this tutorial builds against.

3. Load API logs from a CSV with pandas

Most LLM provider dashboards (OpenAI’s usage page, Anthropic’s console billing export) let you download usage as CSV. The exact column names vary, so the dashboard needs to be tolerant of small differences. For this tutorial, we work against a normalised schema with six columns: date, model, use_case, input_tokens, output_tokens, and cost_usd. In a real pipeline, cost_usd would be computed upstream from token counts and the provider’s per-model rates; the sample CSV below hardcodes typical values so you can run end to end without writing a price table.

Generate a sample CSV first so the tutorial runs without a real bill. Add this to the top of app.py, just after the imports.

import streamlit as st
import pandas as pd
from pathlib import Path

SAMPLE_CSV = Path("sample_usage.csv")

if not SAMPLE_CSV.exists():
    sample = pd.DataFrame({
        "date": pd.date_range("2026-04-01", periods=30, freq="D").repeat(3),
        "model": ["gpt-5.5", "claude-sonnet-4-6", "gemini-2-5-pro"] * 30,
        "use_case": ["chat", "rag", "agent"] * 30,
        "input_tokens": [12000, 18000, 9500] * 30,
        "output_tokens": [3200, 4100, 2800] * 30,
        "cost_usd": [0.18, 0.12, 0.07] * 30,
    })
    sample.to_csv(SAMPLE_CSV, index=False)

st.title("LLM analytics dashboard")

df = pd.read_csv(SAMPLE_CSV, parse_dates=["date"])
st.dataframe(df, width="stretch")

Save and look at the browser. The dashboard now shows a sortable, scrollable, 90-row dataframe. st.dataframe is interactive by default: click any column header to sort, drag the corner to resize.

When you replace sample_usage.csv with your real export, normalise the column names first. A two-line df.rename(columns={...}) call before st.dataframe(df) is enough to map an OpenAI export’s snapshot_id and n_context_tokens_total to this tutorial’s model and input_tokens. The dashboard does not care which provider the rows came from once the schema matches.

4. Add a cost-per-day chart with Altair

The dataframe is useful but a chart is the load-bearing visualisation for cost. Streamlit ships with a built-in st.line_chart that takes a dataframe directly, but Altair gives you control over axis formatting and tooltip content for the same line of code count.

Add this below the st.dataframe call.

import altair as alt

daily_cost = (
    df.groupby("date", as_index=False)["cost_usd"].sum()
)

chart = (
    alt.Chart(daily_cost)
    .mark_line(point=True)
    .encode(
        x=alt.X("date:T", title="Date"),
        y=alt.Y("cost_usd:Q", title="Cost (USD)"),
        tooltip=["date:T", "cost_usd:Q"],
    )
    .properties(height=320)
)

st.subheader("Daily spend")
st.altair_chart(chart, width="stretch")

Save. The chart renders below the dataframe with one point per day, hover-tooltips showing the date and the daily total in USD. Because the sample data has the same numbers each day, the line is flat. That is correct, and it tells you the chart is wired up. When you load a real CSV, the spikes will appear.

For dev teams running this against an OpenAI export billed in USD and needing to report in a local currency, multiply cost_usd by the current FX rate (USD-INR was roughly ₹95 per dollar as of 2026-05-05; verify the live rate before relying on the number, currencies fluctuate). 5 A more honest version reads the rate from a config file rather than hardcoding it, and that pattern shows up in Step 7 once we add secrets.

Streamlit open-source repository on GitHub at github.com/streamlit/streamlit, the Apache 2.0 codebase under Snowflake stewardship that this tutorial builds against

Image: streamlit/streamlit GitHub repository (github.com/streamlit/streamlit), used for editorial coverage of the open-source framework this tutorial uses.

5. Add filters with st.selectbox and st.date_input

The dashboard is a single global view right now. The first useful filter is by model, since dev teams typically run multiple models in parallel and want to see spend on each separately. The second is by date range, for monthly close-out reporting.

Add a sidebar above the chart by inserting these lines after df = pd.read_csv(...) and before st.dataframe(...).

st.sidebar.header("Filters")

models = ["All"] + sorted(df["model"].unique().tolist())
model_filter = st.sidebar.selectbox("Model", models)

date_range = st.sidebar.date_input(
    "Date range",
    value=(df["date"].min(), df["date"].max()),
    min_value=df["date"].min(),
    max_value=df["date"].max(),
)

filtered = df.copy()
if model_filter != "All":
    filtered = filtered[filtered["model"] == model_filter]
if isinstance(date_range, tuple) and len(date_range) == 2:
    start, end = pd.to_datetime(date_range[0]), pd.to_datetime(date_range[1])
    filtered = filtered[(filtered["date"] >= start) & (filtered["date"] <= end)]

Then change df to filtered in the st.dataframe and daily_cost = filtered.groupby(...) calls. Save, and the sidebar appears with two filter widgets. Selecting a model name re-renders the dataframe and chart with only that model’s rows. Picking a narrower date range trims the chart to that window.

A useful pattern: surface the filtered total as a metric tile above the chart, so the reader sees the headline number before scrolling. Add this above st.subheader("Daily spend").

total_cost = filtered["cost_usd"].sum()
st.metric("Filtered spend (USD)", f"${total_cost:,.2f}")

st.metric is the standard Streamlit component for headline numbers. A single call gives you a labelled, large-font tile with an optional delta indicator.

6. Let the user upload a new logs CSV

Hardcoding the sample CSV path was fine for the build. The version of the dashboard you actually share with your team needs to take a CSV upload from the browser. st.file_uploader is the one-line component for that.

Replace the df = pd.read_csv(SAMPLE_CSV, ...) line with this block.

uploaded = st.sidebar.file_uploader(
    "Upload your usage CSV",
    type=["csv"],
    help="Columns: date, model, use_case, input_tokens, output_tokens, cost_usd",
)

if uploaded is not None:
    df = pd.read_csv(uploaded, parse_dates=["date"])
else:
    df = pd.read_csv(SAMPLE_CSV, parse_dates=["date"])
    st.info("Using sample data. Upload your own CSV in the sidebar to replace it.")

The uploader is in the sidebar to keep the main canvas clean. When the user picks a file, uploaded becomes a file-like object that pd.read_csv reads directly, with no temporary file path needed. When no file is uploaded, the dashboard falls back to the sample CSV with an st.info banner reminding the reader.

For multi-month analysis, accept multiple files at once by adding accept_multiple_files=True. With that flag set, uploaded becomes a list of files, so the if uploaded is not None: df = pd.read_csv(uploaded, ...) block also has to change to if uploaded: df = pd.concat([pd.read_csv(f, parse_dates=["date"]) for f in uploaded]). The parse_dates argument has to repeat on every file in the list, or the date column comes back as strings and the chart breaks. That single change lets a finance team drop in twelve monthly exports and see the year in one chart.

7. Deploy free to Streamlit Community Cloud

The dashboard runs on localhost:8501 so far, which is fine for development but not for sharing a link with a teammate. Streamlit Community Cloud deploys public Streamlit apps directly from a GitHub repository, free of charge for the open-source tier. 1

The deployment flow is three steps. First, push the repository to a public GitHub repo with app.py, a requirements.txt listing streamlit, pandas, and altair, and a sample CSV (or a .gitignore excluding the real one if it has private data). Second, sign in to share.streamlit.io with your GitHub account. Third, click “New app”, point at your repository and app.py, click “Deploy”.

Within about two minutes the app is live at https://<your-username>-<repo-name>-<hash>.streamlit.app. Every git push to the connected branch redeploys automatically.

For private data, do not commit the CSV. Use Streamlit Secrets to store credentials and any URL-fetch keys, and have the app pull the data from an authenticated endpoint at startup. Secrets are configured in the Community Cloud app settings, not in the repository, which keeps them out of git history. 6

Streamlit Community Cloud deployment guide on docs.streamlit.io/deploy/streamlit-community-cloud, walking through the GitHub-connected deployment flow this step uses

Image: Streamlit Community Cloud deployment guide (docs.streamlit.io/deploy/streamlit-community-cloud), used for editorial coverage of the deployment flow in this step.

Common pitfalls

The first pitfall is a slow rerun on every keystroke. Streamlit re-executes the entire script every time a widget changes. For small datasets this is fine; for a 50,000-row CSV it is not. Wrap the data-loading function in @st.cache_data and Streamlit caches the result keyed on the input, so the function only re-runs when the input changes. 7

@st.cache_data
def load_csv(path_or_file):
    return pd.read_csv(path_or_file, parse_dates=["date"])

The second pitfall is widget state outliving the user’s session. By default Streamlit re-initialises every widget on every rerun, which is what you want most of the time. When you do need state to persist across reruns (a counter, an accumulating list, a “this user has dismissed the welcome banner” flag), use st.session_state, a dictionary scoped to the user’s tab.

The third pitfall is mistaking layout for logic. Streamlit’s column layout (st.columns([2, 1])) is for visual placement of widgets, not for parallel computation. Two side-by-side st.dataframe calls in two columns still execute sequentially in the script’s order. If a chart takes 800ms to render, putting it in a column does not speed it up.

The fourth pitfall is committing API keys to a public repository. Streamlit Community Cloud apps are deployed from GitHub, and a public repo with a hardcoded OPENAI_API_KEY = "sk-..." is a near-instant key leak — automated scanners catalogue exposed keys on public repositories within minutes of the push. Use Streamlit Secrets, never inline credentials. The deployment docs cover the secrets workflow with a worked example. 6

The Streamlit homepage on streamlit.io framing the open-source Python app framework, the canonical surface for tutorial readers continuing past this 60-minute build

Image: Streamlit homepage (streamlit.io), used for editorial coverage of the framework this tutorial covers.

Where to go next

A few directions readers commonly take this from. Multipage apps split the dashboard into separate pages, for example a “Spend” page, a “Latency” page, an “Errors” page, using a pages/ directory in the repository. Streamlit auto-routes them with a sidebar nav, no router code required. 8

Custom components extend Streamlit with React-rendered widgets when the built-in vocabulary is not enough; the typical case is a domain-specific visualisation like a network graph or a code diff viewer. The Streamlit component gallery lists ready-made integrations including Pydeck for maps, Plotly for richer charts, and AgGrid for spreadsheet-style tables.

For team-internal dashboards behind authentication, the Streamlit-in-Snowflake path makes sense if the org already runs Snowflake. The same app.py runs inside a Snowflake account with native role-based access control, no separate deployment needed. Verify pricing on the Snowflake docs before committing. The Community Cloud free tier is for public apps; Snowflake’s hosted Streamlit is a separate paid product.

For teams that need to bill in a local currency (INR with GST in India, EUR with VAT in EU, GBP in UK) the dashboard is a small extension. Read the rate from st.secrets["fx_rate"], compute the local-currency column alongside USD in the dataframe, and add a second st.metric tile next to the USD total. The framework’s strength is exactly this kind of single-line extension.

Sources

How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.

Sources consulted

Anonymous · no cookies set

Report a problem with this article

Articles are produced by an autonomous AI pipeline; mistakes do happen. Tell us what's wrong and the editorial review will revisit the claim.

Category

Found this useful? Share it.