Skip to content

Separate the inference-router's decisions from its plugin glue #1504

Open

Description

@huang195

Part of #1437 (assessment: laptop data plane on Praxis) · prep work, useful to Cortex on its own

Status: there is no shipped Cortex–Praxis integration today (the paused cmd/cortex-praxis converter is not one), and the interim path is not decided (LLM traffic to praxis-ai as an inference server, or all traffic through Praxis via #1491). This is a refactor with no behaviour change: it is useful to Cortex on its own, and would make a possible move to Praxis easier whichever path is taken.

Problem

The inference-router is one of the larger plugins (1,942 non-blank lines; 2,407 with routerconfig/). Only cpex (5,135) and inference-parser (2,427) are bigger. Its orchestration is mixed with plugin glue: 116 pctx. references across its files.

File Lines Lines with pctx.
plugin.go 739 35
models.go 439 35
fit.go 288 14
strip.go 224 11
limits.go 155 4
pins.go 97 2

Much of the heavy logic is already pure. About 25 functions take no pctx, including the fitting and limit algorithms: fitBody, trim, capOutput, conversationKey, limitRefusal, plausible, learn, stripReasoning, fingerprint, refusesModel, capturePath, kindOf and turnOf. wentTo, serverOf and substitute don't take pctx either. Tests already call several of them directly (fitBody 17 times, learn 9).

What's tangled is the routing and model-choice orchestration: serverFor, unpinned, chooseModel, applyLimits and keepOwnReasoning take pctx, and tests never call them directly. Side effects are mixed into those decisions:

  • durable store reads and writes: pins (with a 24 h renewal throttle), substitutes, refusals, learned limits, reasoning records, catalogs; each with a pctx.Shared fallback;
  • the on-disk archive, read through wentTo (p.history.Earlier, up to 2,000 events);
  • request mutation: Redirect, SetRedirectPath, SetRequestModel, SetBody, credential headers (setKey, dropProviderCredentials), and MarkResend;
  • records and logs: 15 pctx.Record calls;
  • background state: the catalog-fetch goroutine.

So routing and model choice can only be tested through a full plugin request.

Proposal

Split into a decision core and a plugin shell:

  • Core: pure functions over plain inputs — ChooseServer(agent, pin, sessionFact, request) → (server, reason), ChooseModel(server, request, catalog, refusals) → model, Fit(server, model, request, limits) → edits — plus the steps the first sketch missed:
    • the pin's second phase. A pin is decided, then settled or abandoned after the redirect outcome and the observe check (plugin.go:334-402);
    • learning from a refusal (the Resend path, models.go:187-269);
    • reasoning stripping (strip.go:151).
  • Shell: reads pctx, calls the core, applies the redirect, path, model and header changes, and owns the store, credentials, resend and the archive read.

The router's session-history read (wentTo scans view.Events, then the archive) is replaced by a session fact in #1499, which owns that change; it must survive a restart. The core's input is InferenceExtension, so it stays on core/pipeline until #1502 lands.

This is the riskiest of the module splits (stateful, two-phase, with resend), so it goes last.

Done when

  • The decision core has table tests with no pipeline.Context.
  • The plugin's existing tests pass, with only those that call moved helpers changing their package qualifier.

Evidence read at main @ e455a04 (2026-10-25); not built.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestlaptopThe local (laptop) tool: install, abctl, the numbers

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions