srouter-server
Skill24 starsChanged 16 days agoMITView on GitHub
Use when editing `server/` — the SRouter Rust crate (axum 0.8, tokio, sqlx/SQLite, specta). Covers HTTP routes and router layering, middleware, `APIError` and `constants.rs` wire copy, provider drivers and registries, SQLite stores and migrations, SSE streaming, and the integration test harness. Trigger on changes under `server/src`, `server/tests`, or `server/migrations`, or when a task mentions a provider, executor, gateway route, or upstream.
- Reads credentials
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 5 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 20
Process is not optional: Every change runs through the superpowers workflow (AGENTS.md → ## Workflow: Superpowers Skills (Mandatory)): …
- Very specific: line 27
Not for dashboard code: client/ has its own skill, skills/srouter-frontend/.
- Very specific: line 32
Wire fields are
snake_case— always, on both directions: All 238 fields rendered into client/src/generated/typed.ts are snake_case, and … - Very specific: line 36
Guards are per-mount, and layer order is load-bearing: Read the router section of references/http-layer.md before touching app.rs.
- Very specific: line 56
No
#[serde(rename_all = "camelCase")]: Do not add a per-struct or crate-wide rename to "modernise" the API, and do not add … - Distinctive: line 62
Before committing a wire change:
- Distinctive: line 69
If the diff shows a camelCase field, the fix belongs in the Rust type — never in the generated file.
- Very specific: line 73
Read references/architecture.md for boot order, module layout, and where a change belongs.
- Very specific: line 80
Verify: cargo fmt --check, cargo clippy --all-targets --all-features --locked -- -D warnings, then the specific suite — and exercise the …
- Distinctive: line 102
Five traits exist in the whole crate: Do not add another abstraction, generic, or registry until a second concrete caller exists.
- Distinctive: line 103
Match the file: Read the surrounding lines before adding to a module.
- Distinctive: line 105
A rename lands every callsite in the same diff: No shims or re-exports.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SRouter Server
Work inside server/. One Rust crate (srouter-server) that is the whole product: an OpenAI/Anthropic-compatible LLM gateway plus its operator API.
The contract is the Rust types and the route tests. Comments citing docs/api-v1-contract.md or docs/schemas-database.md point at files that no longer exist — those contracts now live in the types, constants.rs, and server/tests/.
Process is not optional. Every change runs through the superpowers workflow (AGENTS.md → ## Workflow: Superpowers Skills (Mandatory)): brainstorming before design, writing-plans before multi-step work, test-driven-development before product code, systematic-debugging before any fix, verification-before-completion before claiming done, requesting-code-review before merge.
When to Use
- Anything under
server/src,server/tests, orserver/migrations. - Adding or fixing a route, a middleware, a provider driver, a store, or a migration.
- Anything that changes a JSON wire shape — that also changes
client/, via the generated bindings. - Not for dashboard code:
client/has its own skill,skills/srouter-frontend/.
Non-negotiables
- Every client-visible string, code, and header name comes from
constants.rs. Never inline a message in a handler; messages that interpolate become functions. Codes are theErrorCodeenum (not string constants), sowith_codetakes a variant and the client receives a closed union plus a same-named const object (ErrorCode.InvalidJson's mirror) to compare against at runtime. - Wire fields are
snake_case— always, on both directions. All 238 fields rendered intoclient/src/generated/typed.tsare snake_case, and the client depends on it (key_prefix,setup_required,allowed_models). See Wire field casing before naming a field. - One error type.
APIErrorwith builders, propagated by?. Noanyhow,thiserror, or per-module error enums. - No
unwrap()on a production path. Lock poisoning is recovered, not unwrapped:.unwrap_or_else(|e| e.into_inner()). ProviderStreamnever yieldsErr. A stalled or failed upstream ends the stream with an in-stream SSE error event.- Guards are per-mount, and layer order is load-bearing. Read the router section of
references/http-layer.mdbefore touchingapp.rs. - Generated files are output.
server/bindings.tsis written byexport_ts; a wire-type change means regenerating and committing it alongside the client copy.
Wire field casing
Rust field names and JSON field names must be the same thing: snake_case, never camelCase.
// correct — the Rust name is the wire name
pub struct CreatedAPIKeyResponse { pub key: String, pub key_prefix: String }
// never — a camelCase wire name breaks the client contract
#[serde(rename = "keyPrefix")] // do not add this
pub key_prefix: String,The whole served surface holds this: CreatedAPIKeyResponse, APIKeyResponse, AdminStatus, AnalyticsReport, RequestLog, ProviderEntry, ModelPricingItem, and the rest — 57 exported types, 238 fields, zero camelCase. Request bodies read by hand follow the same spelling (object.get("allowed_models"), custom_headers, credit_limit).
Rules that follow from it:
- No
#[serde(rename_all = "camelCase")]. Do not add a per-struct or crate-wide rename to "modernise" the API, and do not add#[serde(rename)]on a field merely to change its case. #[serde(rename)]is for a name that is not a valid Rust identifier, not for style. The two legitimate cases in this crate:error.type(a reserved word) and the kebab/dotted tag values inprotocol/sse.rs(rename_all = "kebab-case",usage.updated).- Enum values are a separate decision and are not snake_case by default — they mirror what the protocol already ships:
"UPPERCASE"forHTTPMethod,"lowercase"forObjectKindand the provider protocols,"kebab-case"for SSE event tags. Match the existing enum in your area rather than inventing a case. - A new type is a wire contract. Fields nobody asked about are not free: adding one changes
bindings.tsand the client, so add only what the endpoint needs. - Request bodies are wire contracts too. A handler that reads a JSON body by hand (
parse_setup,parse_create_input) still declares the shape it accepts as aspecta::Typestruct and returns it from the parser, so the client'srequest<T>()argument is generated rather than re-declared. A body type carrying a secret derives noDebug/PartialEq.
Before committing a wire change:
cargo run --manifest-path server/Cargo.toml --bin export_ts # rewrites both committed copies
git diff -- client/src/generated/typed.ts # every added field must read snake_caseIf the diff shows a camelCase field, the fix belongs in the Rust type — never in the generated file.
Workflow
- Read
references/architecture.mdfor boot order, module layout, and where a change belongs. - Pick the reference that matches the work:
- routes, middleware, error envelopes, streaming →
references/http-layer.md - provider drivers, model resolution, rotation, catalogs →
references/providers.md - stores, migrations, upstream/SSRF, logging →
references/database.md - harness, isolation, contract suites →
references/testing.md
- routes, middleware, error envelopes, streaming →
- Implement, matching the module you are in.
- Verify:
cargo fmt --check,cargo clippy --all-targets --all-features --locked -- -D warnings, then the specific suite — and exercise the real path.
Verification
cargo test --manifest-path server/Cargo.toml --test chat_completions # one suite
cargo test --manifest-path server/Cargo.toml --locked # everything
cargo fmt --manifest-path server/Cargo.toml -- --check
cargo clippy --manifest-path server/Cargo.toml --all-targets --all-features --locked -- -D warnings
cargo run --manifest-path server/Cargo.toml # run it, cwd server/ for .env
cargo run --manifest-path server/Cargo.toml --bin srouter-server # the daemon target is NOT named `srouter`
cargo run --manifest-path server/Cargo.toml --bin export_ts # after any wire-type changeA green cargo check proves nothing about behaviour. For a route, call it against a running server; for a provider, run its suite plus a real gateway request; for a migration, boot against a database at the previous version.
There is no CI — the full suite is a manual pre-push obligation.
Rules that keep diffs small
- Reuse the vocabulary.
APIError+constants.rs,create_*_router, theTestDatabaseharness,ProviderAdapter/ProviderExecutor,upstream::UpstreamClient. A second convention for a job that already has one is the defect. - Acronyms and identifiers stay capitalised:
API(neverApi),ID(neverId). Examples:APIError,APIKey,HTTPMethod,OAuthSession,SQLxAPIKeyStore. This is the house style across the crate, notApiError/HttpMethod/ProviderIdas upstream Rust guidance would suggest —clippydoes not flag it here, and the generatedtyped.tscarries the name through verbatim, so renaming one type changes a wire-visible identifier. Match the neighbours rather than the style guide. - Five traits exist in the whole crate. Do not add another abstraction, generic, or registry until a second concrete caller exists.
- Match the file. Read the surrounding lines before adding to a module; the naming and comment style in this crate are deliberate.
//!comments explain why — not what. They historically cited the deleted Node oracle (apps/api/src/...:line); those citations are now historical pointers (the code lives onbackup/pre-apps-api-removal). Do not add new oracle citations, and do not narrate what the next line does.- A rename lands every callsite in the same diff. No shims or re-exports.
References
| File | Read it for |
references/architecture.md | Boot order, module map, request path, layer boundaries |
references/http-layer.md | Router mounts and guards, middleware patterns, APIError, SSE, request helpers |
references/providers.md | Driver contract, per-vendor file set, registration, rotation, new-provider recipe |
references/database.md | AppDatabase, store patterns, migrations, upstream client, SSRF, telemetry |
references/testing.md | Harness helpers, isolation rules, which suite pins which contract |
In this repository3 other files it gives its agents.
- AGENTS.mdAGENTS.md
- llms.txtllms.txt
- srouter-frontendSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

