add posts: gophergate update, and dumpsterchat last stand
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
---
|
||||
layout: post
|
||||
title: "gophergate grew up: the proxy got a brain and a dashboard"
|
||||
---
|
||||
|
||||
the gooseneck kettle grew a barista. last time i wrote about gophergate, it was a clean little proxy: requests go in, model alias maps to a provider, response streams back, spend gets logged. boring on purpose. that was the whole point. but a few months of running every ai call in the house through one box changes a thing, and gophergate stopped being a reverse proxy and turned into the front of house for a small café.
|
||||
|
||||
## the model pile
|
||||
|
||||
the provider list grew from five to seven, and the models behind them exploded. i'm routing across:
|
||||
|
||||
- **openai**, now up to gpt-5 and gpt-5.4, plus the o-series reasoning models and dalle image gen.
|
||||
- **google gemini**, including the gemini 3 flash and pro previews, with imagen 3 for images.
|
||||
- **deepseek**, chat, reasoner, and the v4 flash and pro models.
|
||||
- **moonshot**, kimi k2.5 and the corrected k2.6.
|
||||
- **xai grok**, grok-3, grok-4, grok-4.3.
|
||||
- **xiaomi mimo**, v2.5, the one that surprised me most.
|
||||
- **ollama**, for the local models on the homelab.
|
||||
|
||||
that's a lot of beans to keep straight, and the naming churn is real. kimi was `kimi-2.6` in one place and `kimi-k2.6` everywhere else, so i fixed that. `gemini-3.5-flash-lite` turned out to be a name that didn't quite hold up, so it came out of the targets. and `reasoning_effort=none` is an openai-only thing, so it's scoped there now instead of every provider rejecting it. the details are the whole game.
|
||||
|
||||
## routing stopped being a lookup
|
||||
|
||||
the first version of gophergate did a lookup: alias to provider, done. now it has actual routing strategies and it's a lot more interesting:
|
||||
|
||||
- **hierarchical routing** lets groups point at other groups and cascade until a concrete model is reached, with cycle detection and a depth cap.
|
||||
- **heuristic routing** is the free, instant one: keyword and condition checks against tags, token limits, multimodal inputs, reasoning, and tool calls.
|
||||
- **classifier routing** uses a cheap llm to rate the task on a 1-10 complexity scale, then picks the model bucket. it's the "how hard is this really?" strategy.
|
||||
- **two-level dispatch** runs a dispatcher group that reads the complexity score and hands off to tier groups, each with its own internal strategy.
|
||||
- and it's **provider-aware**, so classifier selector models route to the right provider automatically instead of bouncing off a wall.
|
||||
|
||||
the gooseneck kettle now has a barista who smells the beans before deciding which to grind.
|
||||
|
||||
## the dashboard
|
||||
|
||||
this is the part that stopped feeling like tinkering and started feeling like a product. there's a management dashboard now, with usage analytics, per-request cost tracking, and per-day activity. i added a **days active** card and the total lifetime span, and redid the stat-card grid so it reads clean on a laptop without collapsing into a mess on a phone. there's a mobile off-canvas sidebar now, so i can check the numbers from the couch.
|
||||
|
||||
roles matter here. there's an **admin** role with full access and a **viewer** role that's read-only on usage and cost. plus client api keys, so external integrations can hit the proxy without me handing out the master key. i can see exactly what every request cost, in real time, down to the token.
|
||||
|
||||
## boring reliability, dressed up
|
||||
|
||||
beneath all of it, the same philosophy: make it boring so i'm not paged at 3am. that turned into a specific engineering pass:
|
||||
|
||||
- **connection pooling** with a shared http transport, max idle connections up to 200, tcp keep-alives, and http/2 multiplexing across providers.
|
||||
- **in-memory token caching** with a sync.map, a 10 second ttl and a 2 second negative cache, so auth doesn't hammer sqlite on every request.
|
||||
- **thread safety** with rwmutex locks across the provider maps, model registry, and router reloads.
|
||||
- **circuit breaking** so a dead provider gets isolated and recovers on its own after a timeout.
|
||||
- **async logging** to sqlite from background workers, so the request path never blocks on a write.
|
||||
- **security**: hmac-sha256 signed session tokens for the dashboard, encrypted api keys in the database, and token redaction in logs so a secret can't leak out through a `?key=` in a url.
|
||||
|
||||
and since the last post, **deepseek got openai's responses api**. the `/v1/responses` endpoint now works for both openai and deepseek, which is a real chunk of new capability for a workday.
|
||||
|
||||
## where it's going
|
||||
|
||||
it's close to presentable. the codebase is tidy, the dashboard is genuinely useful, and the routing is the kind of thing i'd hand to a friend. the open source push is still on the list, and increasingly it's a "soon" instead of a "someday."
|
||||
|
||||
the ai model landscape is a moving target, and this month's hot model is next month's footnote. but the layer underneath, the proxy that routes, tracks, and fails over, that's stable. you can swap beans all day and the shop still runs.
|
||||
|
||||
i need to go re-check the classifier threshold. the coffee's fine, but i think the barista is over-tipping.
|
||||
|
||||
-dustin
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: post
|
||||
title: "dumpsterchat's last stand: hardening, a mobile app, and one horrible bug"
|
||||
---
|
||||
|
||||
we put the final coat of paint on a house we were about to move out of. that's the joke, and i'm the one who lived it. in the last couple of weeks before dumpsterChat got handed off to rocket chat, i did three of the most productive and honestly most frustrating things of the whole project: i hardended it like it was production software, i shipped a mobile app for it, and i chased down a single bug that made me seriously question whether owning a chat platform was a healthy hobby. that bug, more than anything, is what finally tipped me over.
|
||||
|
||||
so this is the last stand. it pairs with the sunset post i put up a couple days ago. if that one was about why i stopped, this one is about what happened on the way out.
|
||||
|
||||
## the security pass
|
||||
|
||||
the first few months of dumpsterChat, i was building features at a pace that would have made a security auditor weep. sessions, bots, auth, file uploads, all running on a box i was reconfiguring weekly. around mid-july i sat down and did the pass i kept promising to do:
|
||||
|
||||
- tokens are **hashed at rest** in the database now, instead of sitting there in plain text waiting to be leaked.
|
||||
- the tauri client got a proper **content security policy** so the desktop app isn't a free pass for any script it loads.
|
||||
- an **xss guard** on the message rendering side.
|
||||
- **coturn** got pointed at the live letsencrypt certs instead of a stale self-signed bundle.
|
||||
- secrets came **out of tracking**: api keys into the environment, an `.env.example`, no hardcoded credentials in the repo.
|
||||
- the container got a **non-root user**, network isolation, a `.dockerignore`, and a **deploy rollback**.
|
||||
- a **rate limiter** with a cleanup goroutine and **database pool constraints**, so a dozen friends can't accidentally melt postgres.
|
||||
|
||||
it was a genuinely satisfying week. dumpsterChat went from "homelab toy" to "service i could hand to someone and not be embarrassed about."
|
||||
|
||||
## we shipped a mobile app
|
||||
|
||||
yes. a mobile app. for a chat server with twelve registered humans. the tauri client went from v0.2.7 up through v0.2.10, with real version codes and everything. i fixed the android safe-area spacing so the login form didn't hide under the notch, made the toolbar icons actually tappable, sorted out the passkey text, and got the debug symbols structured properly so the ipa would actually submit to the play store.
|
||||
|
||||
and the icon is a dumpster fire. of course the icon is a dumpster fire. a cheerful little cartoon dumpster fire, right there in the launcher, daring you to question the naming.
|
||||
|
||||
here's a confession: finish a client app right before you kill the server is a weird feeling. i built it because i wanted the crew to have a real app on their phones, and it was good work. and it was also, in retrospect, a small monument to not wanting to let go.
|
||||
|
||||
## the horrible bug
|
||||
|
||||
this is the one that broke me. it started the day after the security pass, innocently enough. someone pinged the chat and it looked fine, but under the hood the websocket connection was dying constantly. every client would connect, and within about five seconds the socket would silently drop, and the client would immediately try to reconnect. connect, drop, retry. connect, drop, retry. all day.
|
||||
|
||||
the worst part was the lie. the ui would happily render "connected" while the socket was already dead in the water, and nobody could tell until a message never arrived.
|
||||
|
||||
i chased it for a while, reading the gateway code, checking the client. then it clicked. the security pass had started **hashing session tokens at rest** in the database. but the websocket handler, the one that authenticates every socket connection, was still querying the sessions table with the **raw token**. so every single websocket auth lookup was comparing a hashed value against an unhashed one, finding nothing, and rejecting the connection. the rest api went through the shared helper that hashed tokens, so it worked fine. the websocket path went around it, so it failed every time. one change, two code paths, one of them never got the memo.
|
||||
|
||||
the fix was one line: hash the token in the websocket auth path too. but that one line took me a whole evening to find, because the poison was in a path i'd stopped thinking about.
|
||||
|
||||
the rest of that day was a cleanup of every other seam the same class of bug could live in. i normalized channel and server ids to lowercase across the stores and the websocket layer so "ABC" and "abc" couldn't refer to the same room differently. i fixed a date-parsing bug that would plonk a brand-new message at the top of your history instead of the bottom. i made sure the spa index wasn't served stale, and that the caddy proxy preserved the `/api/v1` prefix. i added reconnect backoff so a flaky socket didn't hammer the server.
|
||||
|
||||
every fix was small. and there was always another one.
|
||||
|
||||
## the honest takeaway
|
||||
|
||||
that day is when the decision started to form. not because the bug was hard. it was because it was endless. each fix was correct and careful and satisfying, and the next morning there'd be another one waiting. that is what owning a chat platform is. not the fun build. the endless, correct, lonely maintenance.
|
||||
|
||||
you harden it, you ship the mobile app, you find the one-line bug that was eating your sockets, and then you look at the calendar and realize you've spent fourteen weekends and you still haven't touched the voice roadmap. that's not a failure of the project. it's a genuinely accurate accounting of the cost. and that's the number that made sun-setting it feel less like giving up and more like doing the math.
|
||||
|
||||
the work wasn't wasted. the security patterns, the tauri packaging, the lesson about auditing every auth path when you change how you hash credentials, all of it carries into the next thing. i built dumpsterChat to learn. i did. the final sprint just taught me the hardest lesson of the lot.
|
||||
|
||||
if you're running your own project and you're stuck in the same endless-correct-maintenance loop, take the note: finishing the polish isn't the same as the project being done. sometimes done is handing the keys to someone whose maintenance bill is lower than yours.
|
||||
|
||||
we got the dumpster fire onto a dozen phones and then we put it out. worth every weekend. and now the kettle's warm somewhere else.
|
||||
|
||||
what's the project you poured the most polish into right before you walked away?
|
||||
|
||||
-dustin
|
||||
Reference in New Issue
Block a user