Route with a decision model. Authorize the tool in code.

A Spring Boot pipeline uses a decision model to route, a tool gateway to authorize, and OpenTelemetry to score cost, latency, and policy violations. Confidence is not permission.

Where is order 1842?" does not need the large model. "Write the apology" does. "Refund it" needs a role check, and a confidence score must not grant that role. This is a fictional support desk. No company, no real order. The waste is the one I keep seeing in agent demos: one chat model classifies the request, writes the answer, and is also trusted to skip the dangerous tool. I want those as three different steps, on Spring Boot 4.1, with a trace that can tell me whether the specialized decision step actually reduced total cost. The large model was answering a lookup A lookup is a closed tool call. An apology is language. A refund is a write that a policy already describes. Sending all three through the same generative call makes the bill, the latency, and the failure mode look the same, which is how a demo hides the design. I already sort calls into code, a decision model, and a chat model. This pipeline is that sort, plus the part the sort does not cover: once a tool is chosen, who is allowed to run it, and how I know the large model was not invoked for the lookup. The same split is what I want before an expensive resume rewrite. A fit label is a classification. The rewrite is the draft. Neither one is the permission check. Rules, then a decision model, then the draft The request enters as an envelope: principal, text, and an explicit operation when the client already knows it. Java rules run first, but only over facts code actually has. A blank request, a missing principal, or a forbidden structured operation can stop there. Java does not pretend it can discover every disguised refund in arbitrary prose. For the unresolved text, a decision model answers one closed question: lookup, draft, or review. Only the draft path calls the large model. Every proposed tool call, even one produced after a wrong route, crosses a separate execution plane. The gateway receives the principal, tool name, arguments, idempotency key, and deadline. It never receives the routing probability. A wrong route may waste work. It still cannot create authority. The article tests routing quality and cost. The gateway enforces authorization independently. Those are different acceptance criteria. The route, in LangChain4j 1.21 LangChain4j 1.21.0 shipped DecisionModel on 2 October 2026 and marked it experimental. One call takes named questions and returns a typed answer, not a paragraph. A ChoiceQuestion picks one option you listed. margin() is the gap between the two most likely options, and it throws IllegalStateException when the client did not supply probabilities. I send missing probabilities and a narrow margin to review. I do not silently turn uncertainty into a lookup. A decision model is an API shape, not proof that the implementation is small or cheap. The experiment must pin the decision-model provider and version, the generative model and version, both price snapshots, and the exact rubric. Until those are fixed and measured, I call this a specialized decision model, not a cheaper model. rules first, then one choice, and no draft in this class public enum Route { REFUSE, LOOKUP, DRAFT, REVIEW } public Route route(RequestEnvelope request) { if (!request.authenticated() || request.text().isBlank()) { return Route.REFUSE; } if (request.explicitOperation().isPresent() && !authz.permits(request.principal(), request.explicitOperation().get(), request.structuredArguments())) { return Route.REFUSE; } DecisionResponse response = decisions.decide(DecisionRequest.builder() .input(Map.of("request", request.text())) .question("route", ChoiceQuestion.builder() .text("Which path should handle this request?") .option("lookup", "Read status. Do not write.") .option("draft", "Write a reply a person will read.") .option("review", "Intent is ambiguous or requests a sensitive operation.") .build()) .build()); ChoiceAnswer answer = response.choice("route"); if (answer.probabilities().isEmpty() || answer.margin() < 0.2) { return Route.REVIEW; } return switch (answer.value()) { case "lookup" -> Route.LOOKUP; case "draft" -> Route.DRAFT; default -> Route.REVIEW; }; } 0.2 is an unlabeled starting point. Measure it on requests you have already labeled, and measure it again when the model name changes. DRAFT permits a later writing call. It does not permit a business operation. The early authorization check applies only when the client supplied a structured operation. The gateway still checks every eventual tool call. LangChain4j 1.21 also names tool selection as a decision-model integration. The filtering provider I read on the later aggregator javadoc keeps tools whose probability of being useful clears a minimum, so the chat request is smaller. That is relevance. It is not authority. A tool can be relevant and still forbidden for this caller. The gateway does not read a probability Authorization stays deterministic. The principal, the tool name, the arguments, and the approval state are the inputs. The decision model's score is not an input. A risk policy can stop a call that authorization would otherwise allow, for example a refund above a written amount, and put it into PENDING_APPROVAL . Only an explicit APPROVED transition releases that call. permission is a method, not a threshold public enum ToolDecision { ALLOW, DENY, ASK } public ToolDecision decide( Principal principal, ToolCall call, Approval approval) { if (!authz.permits(principal, call.name(), call.arguments())) { return ToolDecision.DENY; } if (risk.requiresHuman(call.name(), call.arguments()) && !approval.approves(call.id())) { return ToolDecision.ASK; // persist as PENDING_APPROVAL } return ToolDecision.ALLOW; } REST, MCP, and events are adapters behind this method. A second host that skips it is a second policy, which is the failure I do not want. A tool in ASK has not been authorized yet. It is not a violation if a person approves it and the gateway records that transition before dispatch. Cancellation before dispatch is simple. Cancellation after dispatch cannot promise rollback. Once an adapter sends the request, cancelling the HTTP response does not erase the remote side effect. The gateway therefore sends an idempotency key, propagates a deadline where the protocol supports it, and records an in-flight outcome as unknown until it reconciles. An event-producing tool uses a transactional outbox. A non-reversible remote call needs a compensating operation or an operator-visible failure state. The invariant is not “cancel always undoes.” It is “no duplicate side effect, and no unknown outcome hidden as success.” A model's confidence score must never become permission to perform a sensitive business operation. What I would measure before calling it a win I have not scored this pipeline. The experiment is the point of building it: can a pinned decision model route a labeled set without sending every request to the large model, while the execution policy remains independent and complete. I want at least 100 requests for the prototype, labeled before the run as lookup, draft, review, or explicit rule refusal. That floor is not enough to claim production safety. The set must be stratified across ordinary phrasing, paraphrases, ambiguous requests, sensitive operations, prompt injection, and out-of-distribution text. I keep a holdout set and repeat requests when the provider can return different routes. The dataset version, model ids, rubric hash, thresholds, price snapshot, and application commit travel with the result. I compare three systems. The first runs Java rules and sends every unresolved request to the large model. The second adds the decision model. The third uses...