SporeLabs Drop-in NOW
Status: Active launch contract (2026-08-11). Canonical product truth:
SPORELABS_SPECIFICATION.mdwins on any product conflict. This file is the implementation contract for the drop-in path: what customers experience, what the backend must support, and the merge gates before users send real traffic. SupersedesSPORELABS_DROPIN_COMPAT_SPEC.md(surface fidelity alone).
0. Promise (non-negotiable)
Point the app at SporeLabs. It keeps working like a frontier API. The bill falls only when we have proven a cheaper equal on their traffic.
Customers will forgive higher inference cost early. They will not forgive a silent quality drop. When unsure, do not cut over.
| Non-negotiable | Meaning |
|---|---|
| Fidelity | OpenAI and Anthropic SDKs work with a base URL + key change. Streaming, tools, JSON mode, errors, and request ids behave like a real provider. |
| Kimi floor | auto starts on Kimi K3 via OpenRouter. That incumbent defines “good” until a challenger earns live. |
| No silent downgrade | Live cheaper routes require the high prove-gate. A request that names a model always gets that model; we still evaluate and suggest, we never swap it. |
| Pass-through money | Wallet debit = our real OpenRouter (or owned-adapter) spend. No markup until an explicit human decision. |
| One pipeline | OpenAI and Anthropic are envelopes over one shared drop-in pipeline. No second decider, no forked settle logic. |
| Settle once | Every metered call debits and audits exactly once — including client disconnect mid-stream. Retries cannot double-debit. |
Phasing: §0 is the end state; §9 splits what is required to start production (OpenAI + Kimi + eval) from hardening (Anthropic surface, atomic settle lock, full verifier).
1. Golden path (customer does not think)
- Paste one line (
base_url+ key) into their coding agent — OpenAI or Anthropic SDK. - Fund the prepaid wallet.
- Omit
model(or sendauto) → Kimi K3 via OpenRouter until a cheaper equal earns live for that work. Name a model → that model, every time. - App behavior matches a direct frontier call (stream, tools, JSON, errors).
- Portal / MCP show completed work in past tense when we move spend (“We proved X equals Opus on your traffic. −$N/mo.”).
- They never operate a router, name projects, or pick a key mode. We infer jobs from traffic. Higher early cost is expected and honest.
Copy (use verbatim)
Omit / auto (recommended):
Starts on Kimi K3. We only move traffic when a cheaper model matches on your traffic.
Named model:
We’ll use this model for this call. We’ll still measure cheaper options on the same work and show you what we’d change — we won’t switch this request.
2. What “works like OpenRouter NOW” means
OpenRouter-like usage hook, not OpenRouter clone as the product:
| Capability | NOW bar |
|---|---|
| Base URL + key drop-in | OpenAI /v1/chat/completions + Anthropic /v1/messages |
| Named cloud models | Honored via OpenRouter when the id is an OpenRouter model |
auto |
SporeLabs brain: Kimi incumbent → proven cheaper equals |
| True SSE | Token/delta streaming from upstream; not a three-frame fake stream |
| Tools | Parallel tool calls, tool_choice, tool_result / tool round-trips |
| JSON mode | Enforced (constrained decode or validate + one retry) |
| Billing | Prepaid wallet; debit = real upstream cost |
| Catalog homepage | Demoted. /v1/models lists useful ids (auto, Kimi, a short named-cloud set, owned), not the entire OpenRouter index. Each row includes architecture.input_modalities so harnesses do not drop images on vision models. |
Differentiation remains: we do the optimization work on their traffic and prove it. Upstream OpenRouter is plumbing for fidelity and the Kimi quality floor.
3. Request support matrix
3.1 OpenAI surface — POST /v1/chat/completions, GET /v1/models
| Feature | Required behavior |
|---|---|
| Messages | Standard chat messages; multimodal when upstream supports it |
stream: true |
Incremental chat.completion.chunk SSE; data: [DONE] terminator |
stream_options.include_usage |
Final usage chunk when set; none when absent |
Tools / tool_choice |
Native upstream tools when capable; never invent calls; unsupported constraint → 400 naming tool_choice |
response_format |
json_object / json_schema enforced; failure → 502 json_mode_failed with tokens/cost debited |
model |
See §5 |
| Errors | Envelope via error_body(); empty wallet → 429 insufficient_quota / insufficient_funds; genuine client faults (400/401/403/404) stay 4xx invalid_request_error; OpenRouter provider 429 ("Provider returned error"), timeouts, and 5xx are retried then 503 upstream_unavailable / api_error. Never forward a provider blip as invalid_request_error — coding-agent SDKs stop and do not retry. |
| Headers | Every response: x-request-id (same id on audit row) |
3.2 Anthropic surface — POST /v1/messages, GET /v1/models
| Feature | Required behavior |
|---|---|
| Auth | x-api-key or Bearer (same SporeLabs key) |
| Content blocks | text, tool_use, tool_result |
stream: true |
Full Anthropic event grammar: message_start → content_block_* → message_delta → message_stop |
Tools / tool_choice |
auto / any / tool / none mapped to upstream |
| Errors | Anthropic envelope; empty wallet → 400 invalid_request_error; upstream down → 503 api_error |
| Headers | x-request-id, anthropic-version |
GET /v1/models is shared by path. Mount Anthropic router before OpenAI so its header-dispatching handler owns the route (x-api-key → Anthropic shape, Bearer → OpenAI shape). Regression test required.
3.3 Acceptance (fail-closed)
Every row above maps to automated tests and to scripts/verify_dropin.py. A change ships only with a recorded green verifier run against the deployed surface (§8).
4. Upstream and billing
4.1 Upstream router (thin)
| Target | Where it runs |
|---|---|
| Frontier / named cloud model ids | OpenRouter POST /api/v1/chat/completions (SSE when streaming). Retry transient 429/5xx a few times; remaining provider blips surface as 503, never as invalid_request_error. |
| Owned SporeLabs adapters (specializations, operator GGUF) | Existing Modal / local origin (EDGEFLOW_INFERENCE_ORIGIN) |
| Auto incumbent | OpenRouter model id from config (below) |
Customer traffic never sees Modal URLs. Accounts app (Lambda / same-origin /api/*) is the only customer door. Modal stays GPUs only.
4.2 Auto incumbent (frozen)
| Config | Value |
|---|---|
| Env | EDGEFLOW_AUTO_INCUMBENT_MODEL |
| Default | moonshotai/kimi-k3 (Kimi K3 on OpenRouter; bump via env without a product change) |
| Aliases treated as auto | "",auto,default,sporelabs,sporelabs-auto` |
Platform default for omitted / auto requests is this incumbent — not starter.
4.3 Pass-through metering (no markup)
- Debit the wallet in millicents for our real spend.
- Prefer OpenRouter-reported cost on the response / final stream usage when present.
- If cost is missing: fail-closed estimate from tokens × a maintained OpenRouter price table for that model id — never skip the debit.
- Owned-adapter traffic continues to use catalog class rates (
products.yamlS/M/L) until those adapters also have pass-through cost signals. *_centsfields remain display mirrors; never debit them directly.- No SporeLabs markup on OpenRouter pass-through until an explicit human decision (canonical §15.1).
4.4 Empty wallet (SDK mapping)
| Surface | HTTP | Envelope |
|---|---|---|
| OpenAI drop-in | 429 | insufficient_quota / insufficient_funds |
| Anthropic drop-in | 400 | invalid_request_error (insufficient funds message) |
| Intelligence / portal “upgrade” paywalls | Forbidden | Any such response is a bug |
402 is not used on drop-in SDK paths (stock OpenAI SDKs mishandle it). The only legitimate refusal for metered inference is an empty (or hard-capped) wallet.
4.5 OpenRouter spend float and caps
Pass-through means we pay OpenRouter and debit the wallet after. A funded account can fire Opus traffic faster than wallet debits + 429s can stop it, so launch-day hardening is not optional:
| Control | Requirement |
|---|---|
| OpenRouter-side concurrent spend | Per-account (or global) OpenRouter spend ceiling / app limit so a runaway customer cannot drain the platform OpenRouter balance |
| Reserve-then-settle (preflight) | Soft funds preflight reserves an estimated spend before upstream is called; the call does not start without a reserve |
| Hard wallet cap | Existing hard-capped wallet stays the customer-side guard; the OpenRouter ceiling is the platform-side guard |
| Reconciliation | OpenRouter-reported spend vs wallet debit reconciled periodically; shortfalls are platform loss, not customer chargebacks |
This is the one place “ship now” needs more machinery, not less: a customer who empties a funded wallet mid-day on Opus traffic must be stopped at the OpenRouter boundary, not only at the wallet.
5. Routing: request model only (no key mode, no projects)
5.1 Keys are auth, not policy
API keys authenticate and debit the wallet. They do not carry routing_mode or pinned_model. There is no Project entity. Wallet, patterns, eval cases, and intelligence.py stay account-scoped (one bill, one traffic brain).
Jobs and sub-work are inferred from traffic (extract_features / shape_hash / patterns). The customer does not name a project, task, or mode when they call.
5.2 Resolving model on a request
Request model |
Serve | Loop |
|---|---|---|
Omitted, "",auto,default,sporelabs,sporelabs-auto` |
Incumbent (Kimi) until a cheaper equal is live for that inferred shape | Eval on every call; may cut over after the high gate |
| Explicit OpenRouter id or owned adapter id | That model, this call | Eval on every call; cheaper equals are suggestions only for this traffic — never swap the named model |
- Authenticate the key (wallet, not routing prefs).
- Extract features from the request (shape only).
- If
modelis auto/omitted →resolve_routefor that shape; fallback is the Kimi incumbent. - If
modelis an explicit id → serve it; still extract features and enqueue the loop. - Response
modelreports what actually ran.
Every call — auto or named — goes through features, audit, patterns, and eval. Named-model traffic teaches us the jobs; it does not opt out of the loop.
5.3 Auto vs named and the loop
| Request | Live traffic | Shadow eval | Change records |
|---|---|---|---|
| auto / omitted | Kimi, or earned live cheaper for that shape | Yes | May go MODE_LIVE after high gate |
| named model | Always the named model | Yes | Suggestions only until they stop naming it (or accept a change) |
Do not invent a key-level pin. A customer who wants Opus forever sends anthropic/claude-opus-4.6 (or the Anthropic-surface equivalent) on the request. A customer who wants us to pick omits model.
6. Prove-gate (quality floor)
Incumbent answers define “good.” For auto/omitted traffic that is Kimi K3 (or the current live model after a proven cutover). For named-model traffic, compare challengers against that model’s answers on the same inferred jobs — suggestions only.
6.1 Live reroute bar (NOW — stricter than early soft thresholds)
| Guard | Value | Why |
|---|---|---|
| Shared cases before live | ≥ 20 from the account’s own traffic | Six cases is too thin to move real spend |
| Pass rate | Challenger ≥ incumbent − 1.0 pct | Noise must not beat the incumbent |
| Agreement | ≥ 85% on shared cases | “Same answer” is the product claim |
| Ties | Keep incumbent | When unsure, do not cut over |
| Continuous re-check | Fail → automatic revert to incumbent | Cutover is probation, not permanent |
| Specialize | After cheaper-catalog reroute attempts exhausted | Rudimentary NOW; polish later |
Platform-funded eval calls (billed=False for loop inference) stay as today: the customer is not charged for us proving a change.
6.2 Shadow before live
Request path never serves a shadow challenger. MODE_SHADOW records intent; only MODE_LIVE after eval_engine.run_comparison clears §6.1. One move in flight per account (intelligence.py).
6.3 Rudimentary NOW vs later
NOW: Kimi floor, OpenRouter fidelity, high-gate reroute to a small challenger catalog (cheaper OpenRouter models + owned adapters), suggestions when the request named a model, existing specialize machinery without blocking launch on train UX polish.
Later: richer challenger search, better specialization UX, optional markup, broader /v1/models listing if product asks.
7. Pipeline architecture (simple + correct)
7.1 One function
New module: platform/server/dropin_pipeline.py.
Order of operations (both surfaces call this — two envelopes, one guts):
- Authenticate key (
get_by_token). request_id = uuid.extract_features(shape only — never store content).- Resolve serve target (§5 +
resolve_route). - Soft funds preflight (
can_spend/ensure_funds_for_spend) using an estimate. - Upstream complete or stream (OpenRouter or owned origin).
- Atomic settle (§7.2).
loop_queue.touch.- Return envelope-specific response (OpenAI or Anthropic).
openai_api.py / anthropic_api.py translate shapes only. Tests assert both call the same pipeline entrypoint (not merely shared _debit).
intelligence.py remains the only offline decider. The pipeline does not invent routing policy.
7.2 Atomic settle
Single settle path for stream and non-stream:
try:
run upstream (stream or not)
finally:
settle_once(request_id) # debit + audit; idempotent
| Rule | Detail |
|---|---|
| Exactly once | Guard with request_id + Dynamo conditional write / settle lock |
| Client disconnect | GeneratorExit still settles (debit reported or estimated use; audit may flag truncation) |
| Mid-stream upstream death | Error frame + settle what was consumed |
| Double debit | Impossible under the lock; retries with same request_id no-op |
| Audit | Same request_id; features + metering; never raw prompts/completions |
This closes fail-open revenue leaks from streaming disconnects.
The audit row remains shape-only. Separately, successful calls may enter a bounded owner-scoped eval reservoir (40 prompt/reply pairs per pattern, 30-day TTL, deletable with account audit data). That sample is the minimum production trace needed to replay held-out cases, prove a cheaper equal, and train only that account's specialist. Materialized eval cases and run results carry the same TTL and are deleted with the reservoir; this is not an unbounded request log.
7.3 Diagram
Client (OpenAI or Anthropic SDK)
│
▼
accounts_app (same-origin /api)
│
▼
dropin_pipeline ──► OpenRouter (frontier) ──┐
│ ├──► atomic settle (millicents)
└──────────► Modal origin (owned) ───┘
│
├──► call_event_store + loop_queue
▼
envelope (openai_compat | anthropic_compat)
8. Verifier and merge gate
8.1 scripts/verify_dropin.py
Fail-closed SDK smoke against a base URL + key:
- OpenAI non-stream completion
- OpenAI stream (≥2 content chunks; first-byte meaningfully early)
- OpenAI tools +
tool_choice - OpenAI JSON mode
- Anthropic
/v1/messagesnon-stream - Anthropic SSE grammar order
- Anthropic tool_use round-trip
- Empty / bad key error parity
- Portal wallet display: after a completion,
GET /api/accountreturns a wallet balance that fell by exactly the pass-through debit (no markup). Compare in millicents precision. - Portal
/v1/modelslist matches the minimal list served by the pipeline (auto, Kimi incumbent, owned adapters). No stale models, no full OpenRouter index leak. - Agent auto-mode: omit
model→ serves the Kimi incumbent (responsemodelfield = incumbent). - Agent named-mode: send a named model → response
modelfield matches exactly (never swapped). - Agent
/v1/modelsincludesauto+ Kimi + owned (mirrors the portal list — verifies a single pipeline serves both surfaces).
Exit 1 on any failure. Per-check PASS/FAIL table.
8.2 Merge / ship gate
A drop-in change is not done until:
- Unit/pipeline tests green (including settle-on-disconnect,
/v1/modelsdispatch, shared pipeline entrypoint). verify_dropin.pyexit 0 against local stack and deployed surface.- Canonical + this doc agree (no 402-vs-429 contradiction, Kimi default, pass-through).
- Portal/agent smoke: portal wallet reflects the exact pass-through debit after a completion (read account via
GET /api/account, compare before/afterwallet_balance_millicents); agent auto → Kimi, named → honored, models list = pipeline list.
No “verified by inspection.”
8.3 Launch-phase gate
Real traffic is gated on the Launch surface (§9.1) only; the Anthropic surface and the full verifier are hardening, landed as fast-follow.
9. Launch phasing
Each phase is independently shippable. Do not sequence Anthropic before real OpenAI traffic — collecting the first 20 eval cases is time-bound and only OpenAI traffic starts the clock.
9.1 Launch (production with real users)
Ship this and only this first. Everything required to route real requests and begin evaluations:
| Required | Bar |
|---|---|
OpenAI /v1/chat/completions |
Stream + non-stream; Kimi via OpenRouter |
auto / omitted model |
Kimi incumbent (EDGEFLOW_AUTO_INCUMBENT_MODEL) |
Named model |
Honored for that call |
| Pass-through debit | OpenRouter-reported cost (or fail-closed estimate) → millicents wallet debit |
| Audit | call_event_store row per request (shape only; request_id) |
| Eval start | loop_queue.touch on every call so cases begin accumulating toward the §6.1 gate |
/v1/models |
Minimal list (auto, Kimi, owned) — not the full OpenRouter index |
| OpenRouter spend cap | §4.5 — platform-side spend ceiling before any real traffic |
| Empty wallet | OpenAI 429 (§4.4) |
Launch settle may be best-effort debit + audit, not the idempotent Dynamo lock. The lock (§7.2) lands in Hardening. A missed/disconnected debit at launch is a platform loss, not a customer double-charge; that is an acceptable launch risk and strictly better than blocking traffic.
9.2 Hardening (fast-follow, parallel-safe)
| Item | Why |
|---|---|
Anthropic /v1/messages surface + SSE grammar |
Second envelope over the same pipeline; a feature for Anthropic-SDK users, not a launch gate |
Atomic settle lock (idempotent request_id conditional write) |
Closes disconnect double-debit / skip-debit leaks (§7.2) |
verify_dropin.py green on deployed surface as a merge gate (§8.2) |
Full fail-closed SDK smoke incl. Anthropic rows |
| Settle-on-disconnect unit tests | Lock behavior under GeneratorExit |
10. Out of scope / later
- SporeLabs markup on pass-through
- Mega public catalog as homepage
- Project objects above keys
- Per-key
routing_mode/pinned_model - DIY train-tier shopping
- Weakening the prove-gate for faster savings
- Parallel intelligence deciders
- Exposing Modal or OpenRouter credentials to the browser
11. Implementation map
One shared platform/server/dropin_pipeline.py; OpenAI/Anthropic envelopes translate shapes only; Anthropic router mounts before OpenAI; settle = millicents debit + call_event_store + settle lock; verifier = scripts/verify_dropin.py. Full file map lives in the repo's AGENTS.md (Quick orientation).
12. Success criteria (“users can start NOW”)
12.1 Launch (production)
- [ ] OpenAI SDK → omit
model→ Kimi-quality completions with real streaming - [ ] Named
modelon the request is honored; that call is never silently swapped - [ ] Wallet falls by exact upstream spend (no markup)
- [ ] OpenRouter-side spend cap in place (§4.5) before real traffic
- [ ] Every call feeds
loop_queue; cases begin accumulating toward the §6.1 gate - [ ] Empty wallet → OpenAI 429 (no 402 on SDK paths)
12.2 Hardening (fast-follow)
- [ ] Anthropic
/v1/messagesworks (stream, tools, grammar order) over the shared pipeline - [ ] No live cheaper route on auto traffic without clearing §6.1 on their cases
- [ ] Disconnect / stream errors never skip debit; retries never double-debit
- [ ]
verify_dropin.pygreen on deployed surface
Last updated: 2026-08-12.