SporeLabs Drop-in NOW

Status: Active launch contract (2026-08-11). Canonical product truth: SPORELABS_SPECIFICATION.md wins 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. Supersedes SPORELABS_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)

  1. Paste one line (base_url + key) into their coding agent — OpenAI or Anthropic SDK.
  2. Fund the prepaid wallet.
  3. Omit model (or send auto) → Kimi K3 via OpenRouter until a cheaper equal earns live for that work. Name a model → that model, every time.
  4. App behavior matches a direct frontier call (stream, tools, JSON, errors).
  5. Portal / MCP show completed work in past tense when we move spend (“We proved X equals Opus on your traffic. −$N/mo.”).
  6. 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_startcontent_block_*message_deltamessage_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)

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
  1. Authenticate the key (wallet, not routing prefs).
  2. Extract features from the request (shape only).
  3. If model is auto/omitted → resolve_route for that shape; fallback is the Kimi incumbent.
  4. If model is an explicit id → serve it; still extract features and enqueue the loop.
  5. Response model reports 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):

  1. Authenticate key (get_by_token).
  2. request_id = uuid.
  3. extract_features (shape only — never store content).
  4. Resolve serve target (§5 + resolve_route).
  5. Soft funds preflight (can_spend / ensure_funds_for_spend) using an estimate.
  6. Upstream complete or stream (OpenRouter or owned origin).
  7. Atomic settle (§7.2).
  8. loop_queue.touch.
  9. 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:

  1. OpenAI non-stream completion
  2. OpenAI stream (≥2 content chunks; first-byte meaningfully early)
  3. OpenAI tools + tool_choice
  4. OpenAI JSON mode
  5. Anthropic /v1/messages non-stream
  6. Anthropic SSE grammar order
  7. Anthropic tool_use round-trip
  8. Empty / bad key error parity
  9. Portal wallet display: after a completion, GET /api/account returns a wallet balance that fell by exactly the pass-through debit (no markup). Compare in millicents precision.
  10. Portal /v1/models list matches the minimal list served by the pipeline (auto, Kimi incumbent, owned adapters). No stale models, no full OpenRouter index leak.
  11. Agent auto-mode: omit model → serves the Kimi incumbent (response model field = incumbent).
  12. Agent named-mode: send a named model → response model field matches exactly (never swapped).
  13. Agent /v1/models includes auto + 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:

  1. Unit/pipeline tests green (including settle-on-disconnect, /v1/models dispatch, shared pipeline entrypoint).
  2. verify_dropin.py exit 0 against local stack and deployed surface.
  3. Canonical + this doc agree (no 402-vs-429 contradiction, Kimi default, pass-through).
  4. Portal/agent smoke: portal wallet reflects the exact pass-through debit after a completion (read account via GET /api/account, compare before/after wallet_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


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)

12.2 Hardening (fast-follow)

Last updated: 2026-08-12.