返回 CodeWhale
request_manifest.rs
根目录 / crates / tui / src / request_manifest.rs
1 //! Redacted request manifest for `/preview-request` (#1004, #3928).
2 //!
3 //! A [`RequestManifest`] describes the request the **next primary agent turn**
4 //! would send: dialect, endpoint identity, wire model, reasoning resolution,
5 //! the exact active tool catalog, sizes, conservative offline token estimates,
6 //! and a whole-body hash.
7 //!
8 //! Four properties are load-bearing:
9 //!
10 //! 1. **Single-sourced.** Every wire fact is read off a
11 //! [`PreparedOutboundRequest`] — the same value the transport sends. There
12 //! is no second body builder, no Chat-shaped projection of a non-Chat
13 //! route, and no re-derivation of prompt or tool selection here.
14 //! 2. **Typed, allowlisted disclosure.** The manifest is a fixed set of
15 //! counts, hashes, enums, and short provenance labels, and every free-form
16 //! string crosses [`crate::safe_label`] first. Prompt text, project
17 //! instructions, memory, skill bodies, tool results, message content,
18 //! credentials, URL paths, and absolute workspace paths have no field to
19 //! occupy — and a hostile model or route id gets a fingerprint instead of
20 //! a verbatim copy.
21 //! 3. **Whole-body fidelity.** The body hash covers the complete canonicalized
22 //! wire body, so max-token fields, tool choice, nested reasoning controls,
23 //! transformed tool schemas, attachments, and stream options all move it.
24 //! 4. **Structural honesty.** Facts that are not yet knowable are *absent*,
25 //! not guessed. When auto model routing has not been resolved there is no
26 //! provider, route id, dialect, endpoint, wire model, billing, tool budget,
27 //! or body hash in this structure at all — a typed
28 //! [`Unavailable`] stands in its place on both the human and the JSON
29 //! surface. Recycling the current or previous route would be a lie about
30 //! what the next request will contain.
31 //!
32 //! Scope: this describes the **primary `LlmClient` agent turn** only —
33 //! `create_message` / `create_message_stream`. Auxiliary provider calls (chat
34 //! translation, FIM completion, speech, provider-native search, model
35 //! listing, and the auto-router classifier) are separate requests with their
36 //! own shapes and are deliberately not covered. See `docs/PREVIEW_REQUEST.md`.
37 //!
38 //! Token figures are *offline estimates* (~4 bytes/token plus a conservative
39 //! margin). They are never provider-authoritative token counts.
40 //!
41 //! The `dryrun` concept this serves — inspect the next request from the real
42 //! request-building seam instead of a hand-rolled summary — is harvested from
43 //! PR #1099 by TaoMu (GTC2080).
44
45 use serde::Serialize;
46
47 use crate::client::PreparedOutboundRequest;
48 use crate::safe_label::SafeLabel;
49
50 /// Bytes-per-token divisor for the offline estimator.
51 const BYTES_PER_TOKEN: usize = 4;
52 /// Conservative margin applied to the estimated total (percent).
53 const ESTIMATE_MARGIN_PERCENT: usize = 5;
54 /// Bumped whenever a field is renamed or removed, so scripted consumers can
55 /// detect an incompatible manifest instead of silently reading `null`.
56 ///
57 /// v3 introduced the sectioned `route` / `tools` / `body` availability shape.
58 /// v4 made the byte classes an exact accounting decomposition of the wire
59 /// body, moved the component digest onto the *wire* tool schemas, reported nested reasoning
60 /// efforts with their key path, and re-based headroom on the production input
61 /// budget rather than the raw context window.
62 /// v5 renamed the system/tools digest to state its local component scope and
63 /// removed the unsupported implication that it is a provider cache identity.
64 /// v6 replaced the raw endpoint host with a safe host class/digest and added
65 /// explicit route-limit provenance plus input/output budget facts.
66 /// v7 names canonical JSON sizes truthfully and adds primary-agent identity,
67 /// typed route provenance, and an explicit unavailable provider-usage receipt.
68 /// v8 makes authoritative Work state and the active goal-budget terminal gate
69 /// explicit fail-closed dependencies of an exact body.
70 pub(crate) const MANIFEST_SCHEMA_VERSION: u32 = 8;
71
72 /// Exact readable base-prompt-only disclosure for the explicit
73 /// `/preview-request base-prompt` mode. This deliberately returns no runtime
74 /// system layers: project instructions, skills, memory, and message content
75 /// remain represented only by the protected effective-system hash.
76 pub(crate) fn exact_base_prompt_only() -> String {
77 crate::prompts::effective_base_prompt_text().to_string()
78 }
79
80 /// A section of the manifest that is either exactly known or typed-absent.
81 ///
82 /// This is the whole point of the structure: there is no "unknown" *value*
83 /// anywhere in a manifest, because an unknown fact has no field.
84 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
85 #[serde(rename_all = "kebab-case")]
86 pub(crate) enum Availability<T> {
87 /// Exactly what the next turn would send.
88 Exact(T),
89 /// Not knowable without doing something a preview must not do.
90 Unavailable(Unavailable),
91 }
92
93 impl<T> Availability<T> {
94 pub(crate) fn unavailable(reason: UnavailableReason) -> Self {
95 Self::Unavailable(Unavailable {
96 reason,
97 detail: None,
98 })
99 }
100
101 pub(crate) fn unavailable_with(reason: UnavailableReason, detail: String) -> Self {
102 Self::Unavailable(Unavailable {
103 reason,
104 detail: Some(crate::safe_label::safe_error_text(&detail)),
105 })
106 }
107
108 pub(crate) fn map<U>(self, transform: impl FnOnce(T) -> U) -> Availability<U> {
109 match self {
110 Self::Exact(value) => Availability::Exact(transform(value)),
111 Self::Unavailable(unavailable) => Availability::Unavailable(unavailable),
112 }
113 }
114
115 #[cfg(test)]
116 pub(crate) fn exact(&self) -> Option<&T> {
117 match self {
118 Self::Exact(value) => Some(value),
119 Self::Unavailable(_) => None,
120 }
121 }
122
123 /// Carry this section's unavailability onto a section that depends on it.
124 ///
125 /// Returns `None` when this section is exact, so the caller falls through
126 /// to building the dependent section normally. This is what keeps a
127 /// dependency from silently becoming exact: when the MCP contribution to
128 /// the tool surface is unknown, the body built from that surface is a body
129 /// no turn would send, and it must inherit the same typed reason rather
130 /// than publish an exact hash of a fabricated request.
131 pub(crate) fn propagate<U>(&self) -> Option<Availability<U>> {
132 match self {
133 Self::Exact(_) => None,
134 Self::Unavailable(unavailable) => Some(Availability::Unavailable(unavailable.clone())),
135 }
136 }
137 }
138
139 /// Why a section could not be described exactly.
140 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
141 pub(crate) struct Unavailable {
142 pub(crate) reason: UnavailableReason,
143 /// Bounded, path- and URL-path-safe explanation. Never raw error text.
144 pub(crate) detail: Option<String>,
145 }
146
147 #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
148 #[serde(rename_all = "kebab-case")]
149 pub(crate) enum UnavailableReason {
150 /// Auto model routing is on and no hypothetical prompt was supplied, so
151 /// the concrete route is decided by text that does not exist yet.
152 AutoRouteUnresolvedUntilNextPrompt,
153 /// Resolving Auto would make a provider/model classifier call, which an
154 /// offline inspection command must never do.
155 AutoRouteClassificationNotExecuted,
156 /// No `--prompt` was supplied, so there is no exact next-turn body: the
157 /// next user message is part of the request.
158 NoHypotheticalPromptSupplied,
159 /// The host's shared route planner failed for this hypothetical turn.
160 RoutePlanFailed,
161 /// The exact MCP tool state is not knowable without connecting, which an
162 /// inspection must never do. Also carried by the body section: a body
163 /// built from a tool surface that is missing its MCP contribution is not
164 /// the body the next turn would send.
165 McpStateNotSnapshottable,
166 /// The shared prepared-request seam refused to build a body.
167 RequestPreparationFailed,
168 /// Mutable `message_submit` hooks are configured. They may rewrite or
169 /// block the next message before anything downstream sees it, and an
170 /// inspection must not execute them — so the route, tool surface, and body
171 /// they would shape are all unknowable from here.
172 MessageSubmitHooksNotExecuted,
173 /// Resolving the hypothetical prompt into model-facing content failed the
174 /// same way a real submit would have failed (skill authority, file
175 /// mentions).
176 PromptResolutionFailed,
177 /// The turn loop would transform this request between dispatch and the
178 /// wire — auto-compaction, context-overflow recovery, a background-shell
179 /// or queued sub-agent completion, or pending LSP diagnostics — and an
180 /// inspection may neither run nor consume any of them.
181 RuntimeTransformsBeforeSend,
182 /// The authoritative graph-backed Work projection could not be read. A
183 /// stale legacy To-do view must not be substituted into an exact preview.
184 WorkStateNotSnapshottable,
185 /// The active goal has consumed its token budget, so the continuation
186 /// gate stops before creating another provider request.
187 GoalTokenBudgetExhausted,
188 /// The live goal state could not be read without guessing whether its
189 /// terminal budget gate would permit another request.
190 GoalStateNotSnapshottable,
191 /// Preview never sends a provider request, so no provider-counted usage
192 /// receipt exists. This must not be represented by zero token counts.
193 ProviderRequestNotExecuted,
194 }
195
196 impl UnavailableReason {
197 pub(crate) fn label(self) -> &'static str {
198 match self {
199 Self::AutoRouteUnresolvedUntilNextPrompt => {
200 "auto model routing is unresolved until the next prompt"
201 }
202 Self::AutoRouteClassificationNotExecuted => {
203 "auto route classification was not executed because preview is offline"
204 }
205 Self::NoHypotheticalPromptSupplied => {
206 "no hypothetical prompt supplied — the next user message is part of the request"
207 }
208 Self::RoutePlanFailed => "the shared route planner could not resolve this turn",
209 Self::McpStateNotSnapshottable => {
210 "MCP tool state cannot be snapshotted without connecting"
211 }
212 Self::RequestPreparationFailed => "request preparation failed",
213 Self::MessageSubmitHooksNotExecuted => {
214 "message-submit hooks are configured and an inspection must not run them"
215 }
216 Self::PromptResolutionFailed => {
217 "the hypothetical prompt could not be resolved into model-facing content"
218 }
219 Self::RuntimeTransformsBeforeSend => {
220 "the turn loop would transform this request before sending it"
221 }
222 Self::WorkStateNotSnapshottable => {
223 "authoritative Work state cannot be snapshotted exactly"
224 }
225 Self::GoalTokenBudgetExhausted => {
226 "active goal token budget is exhausted; no outbound request is eligible"
227 }
228 Self::GoalStateNotSnapshottable => "active goal state cannot be snapshotted exactly",
229 Self::ProviderRequestNotExecuted => {
230 "provider request not executed; provider-reported usage is unavailable"
231 }
232 }
233 }
234 }
235
236 /// How the reasoning tier for the next request was determined.
237 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
238 #[serde(rename_all = "kebab-case")]
239 pub(crate) enum ReasoningResolution {
240 /// The user pinned a concrete tier; the next request will use it.
241 Explicit,
242 /// Auto routing, resolved against the supplied hypothetical prompt by the
243 /// same planner a real turn runs.
244 ResolvedFromHypotheticalPrompt,
245 /// The body carries a reasoning control the user never asked for: the
246 /// dialect or the route shapes one in by default. Reporting this as
247 /// `Explicit` would credit the user with a selection they did not make.
248 RouteDefault,
249 /// The route asks for no reasoning at all. A Responses body that carries
250 /// only `include` — which *discloses* reasoning output rather than
251 /// requesting a tier — lands here, not on `Explicit`.
252 NotApplicable,
253 }
254
255 impl ReasoningResolution {
256 fn label(self) -> &'static str {
257 match self {
258 Self::Explicit => "explicit user selection",
259 Self::ResolvedFromHypotheticalPrompt => {
260 "auto, resolved against the supplied hypothetical prompt"
261 }
262 Self::RouteDefault => "route default (no user selection)",
263 Self::NotApplicable => "route sends no reasoning control",
264 }
265 }
266 }
267
268 /// How the effective system prompt was assembled, without quoting any of it.
269 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
270 #[serde(rename_all = "kebab-case")]
271 pub(crate) enum SystemPromptAssembly {
272 /// The effective system prompt is exactly the base-prompt bytes.
273 BaseOnly,
274 /// Base prompt plus the configured static layers, and nothing else.
275 BaseWithConfiguredLayers,
276 /// Runtime or session layers (environment, project instructions, skills,
277 /// memory, mode) were appended on top.
278 BaseWithRuntimeAdditions,
279 /// No system prompt would be sent.
280 None,
281 }
282
283 impl SystemPromptAssembly {
284 fn label(self) -> &'static str {
285 match self {
286 Self::BaseOnly => "base prompt only",
287 Self::BaseWithConfiguredLayers => "base prompt + configured static layers",
288 Self::BaseWithRuntimeAdditions => {
289 "base prompt + configured layers + runtime/session additions"
290 }
291 Self::None => "no system prompt",
292 }
293 }
294 }
295
296 /// How this route is billed, as typed facts with static labels.
297 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
298 #[serde(tag = "kind", rename_all = "kebab-case")]
299 pub(crate) enum BillingFacts {
300 /// Per-token API usage.
301 Metered,
302 /// Account/subscription quota; per-token dollar estimates are not spend.
303 Subscription { plan: &'static str },
304 /// Local route with no provider bill.
305 Local,
306 /// Billing basis unknown; never invent dollars or a fake zero.
307 Unknown,
308 /// Endpoint-derived pricing surface classification for routes that have
309 /// one (`Stepfun` today). Never a URL.
310 Surface { surface: &'static str },
311 }
312
313 impl BillingFacts {
314 fn label(&self) -> String {
315 match self {
316 Self::Metered => "metered API (per-token)".to_string(),
317 Self::Subscription { plan } => format!("subscription quota ({plan})"),
318 Self::Local => "local route (no provider bill)".to_string(),
319 Self::Unknown => "unknown billing basis".to_string(),
320 Self::Surface { surface } => format!("metered API, pricing surface `{surface}`"),
321 }
322 }
323 }
324
325 /// Base-prompt provenance: labels, byte counts, and hashes only (#3928).
326 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
327 pub(crate) struct BasePromptProvenance {
328 /// Where the base-prompt bytes came from: bundled or configured override.
329 /// A static runtime label, never a source-tree path.
330 pub(crate) origin: String,
331 pub(crate) bytes: usize,
332 pub(crate) sha256: String,
333 }
334
335 /// System-prompt provenance of the *prepared request*.
336 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
337 pub(crate) struct PromptProvenance {
338 /// How the effective prompt was assembled from the base prompt.
339 pub(crate) assembly: SystemPromptAssembly,
340 /// Canonical JSON bytes and hash of the system region of the prepared
341 /// request — the same semantic prompt value production sends.
342 pub(crate) effective_system_canonical_json_bytes: usize,
343 pub(crate) effective_system_sha256: String,
344 }
345
346 /// Session posture that does not depend on the route or the next message.
347 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
348 pub(crate) struct SessionFacts {
349 /// Exact execution identity for this manifest's deliberately narrow scope.
350 pub(crate) agent_role: String,
351 pub(crate) lane_kind: String,
352 /// Primary interactive turns are not Fleet workers. Say that explicitly
353 /// rather than inventing a Fleet role or leaving an ambiguous null.
354 pub(crate) fleet_assignment: String,
355 /// The model the user selected, before route remapping. `auto` when auto
356 /// model routing is on — never a concrete model the user did not pick.
357 pub(crate) requested_model: SafeLabel,
358 /// Whether auto model routing is selected.
359 pub(crate) auto_model_routing: bool,
360 /// Reasoning tier the user asked for (`auto`, `high`, `off`, …).
361 pub(crate) requested_reasoning: SafeLabel,
362 /// Whether the caller supplied a hypothetical next prompt.
363 pub(crate) hypothetical_prompt_supplied: bool,
364 /// Operating mode the catalog would be built under.
365 pub(crate) mode: String,
366 /// Approval posture the catalog would be filtered under.
367 pub(crate) approval_mode: String,
368 /// Number of entries in the allow-list gate, if one is configured.
369 pub(crate) allowed_tool_gate_count: Option<usize>,
370 /// Number of entries in the deny-list gate, if one is configured.
371 pub(crate) disallowed_tool_gate_count: Option<usize>,
372 pub(crate) base_prompt: BasePromptProvenance,
373 }
374
375 /// Exactly which route the next turn would use.
376 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
377 pub(crate) struct RouteFacts {
378 pub(crate) provider_id: SafeLabel,
379 pub(crate) provider_display: SafeLabel,
380 /// Named custom-provider / route identity from the resolved turn plan.
381 pub(crate) route_id: Option<SafeLabel>,
382 pub(crate) dialect: String,
383 pub(crate) route_shape: String,
384 /// Safe endpoint class. Remote authorities are represented only by a
385 /// bounded digest; even a credential-shaped tenant subdomain is never
386 /// printed. Loopback is reported as a class, never a raw host.
387 pub(crate) endpoint_host_class: String,
388 /// SHA-256 of the full endpoint URL, for "same endpoint?" comparisons.
389 pub(crate) endpoint_fingerprint: String,
390 /// The model id literally placed on the wire, after route remapping.
391 pub(crate) wire_model: SafeLabel,
392 /// Which transport entry point this manifest described.
393 pub(crate) caller_entrypoint: String,
394 /// The `stream` field **as it appears on the body**, or `null` when the
395 /// body carries no such field. Derived from the body, never from the
396 /// caller entry point: the Responses blocking path sends `stream: true`.
397 ///
398 /// This lives in the *route* section, and stays exact even when the body
399 /// section does not, because every dialect builder writes it from the
400 /// caller entry point and the provider alone — never from the message list
401 /// or the tool set. It is a property of the route, not of the payload.
402 pub(crate) body_stream_field: Option<bool>,
403 /// Active context-window ceiling and the resolver receipt for its source.
404 pub(crate) context_limit_tokens: u32,
405 pub(crate) context_limit_source: crate::route_runtime::ContextWindowSource,
406 /// Concrete route/offering limits, when advertised. These remain
407 /// separate from the effective wire output cap.
408 pub(crate) route_input_limit_tokens: Option<u64>,
409 pub(crate) route_output_limit_tokens: Option<u64>,
410 pub(crate) billing: BillingFacts,
411 /// Upstream planner receipt for why this concrete route was selected.
412 pub(crate) routing_source: String,
413 /// How the auto router chose this route, when auto routing ran.
414 pub(crate) auto_route_source: Option<SafeLabel>,
415 }
416
417 /// Everything the manifest reports about the next request's tool surface.
418 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
419 pub(crate) struct ToolSurfaceFacts {
420 /// Tools in the full built catalog, including deferred ones.
421 pub(crate) catalog_tool_count: usize,
422 /// Tools deferred (discoverable via tool search, not sent eagerly).
423 pub(crate) deferred_tool_count: usize,
424 /// Tools that would actually be serialized into this request.
425 pub(crate) active_tool_count: usize,
426 /// Stable hash over the *current active* catalog, before dialect shaping:
427 /// name, description, and canonical logical schema, in catalog order. Not
428 /// the last turn's.
429 ///
430 /// This is a *catalog identity*, not a wire fact. Two routes can agree
431 /// here and still send different bytes, because each dialect transforms
432 /// schemas its own way and strict mode sanitizes them further. The hash
433 /// the provider actually receives is `body.tool_schema_wire_sha256`, and
434 /// that is the one the local system/tools component digest is built from.
435 pub(crate) active_tool_catalog_sha256: String,
436 /// The capability profile's surface budget label for this route.
437 pub(crate) tool_surface_budget: String,
438 /// Truthful disclosure (#1004): whether Standard and Full currently
439 /// produce the same catalog. Derived by running the surface shaper under
440 /// both budgets over this exact catalog — not asserted.
441 pub(crate) standard_and_full_surfaces_collapsed: bool,
442 /// MCP servers connected and contributing tools.
443 pub(crate) mcp_server_count: usize,
444 /// Tools in the active catalog that came from MCP.
445 pub(crate) mcp_tool_count: usize,
446 }
447
448 /// Per-class conservative offline estimates, derived from the wire body.
449 ///
450 /// `system`, `tool_schemas`, `messages`, and `framing` are estimates over the
451 /// four classes of the exact byte accounting (see [`crate::client::WireBodyView`]),
452 /// so they sum to the whole body rather than a selected part of it.
453 /// `tool_results` and `attachments` are *subsets* of `messages`, reported for
454 /// attribution and never added again.
455 #[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
456 pub(crate) struct TokenEstimates {
457 pub(crate) system: usize,
458 pub(crate) tool_schemas: usize,
459 pub(crate) messages: usize,
460 pub(crate) tool_results: usize,
461 pub(crate) attachments: usize,
462 pub(crate) framing: usize,
463 /// Estimate over the **whole** canonical JSON body plus a conservative margin.
464 ///
465 /// Derived from the complete canonical body rather than by summing the
466 /// per-class estimates, so per-class rounding cannot make the total drift
467 /// away from the semantic JSON value production sends.
468 pub(crate) total_conservative: usize,
469 }
470
471 /// Facts read off the exact next-turn body.
472 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
473 pub(crate) struct BodyFacts {
474 pub(crate) reasoning_resolution: ReasoningResolution,
475 /// Reasoning-control keys present on the wire. Keys only, never values
476 /// that could carry free text.
477 pub(crate) reasoning_wire_control_keys: Vec<String>,
478 /// The effort string actually on the wire, whether the dialect writes it
479 /// flat (`reasoning_effort`) or nested (`thinking.effort`,
480 /// `reasoning.effort`, `output_config.effort`).
481 pub(crate) reasoning_wire_effort: Option<SafeLabel>,
482 /// Which key path [`Self::reasoning_wire_effort`] was read from. A
483 /// compile-time constant from the dialect allowlist, never a key taken
484 /// out of the body.
485 pub(crate) reasoning_wire_effort_source: Option<String>,
486 /// `tool_choice` as it appears on the wire, as a short shape label.
487 pub(crate) tool_choice: Option<SafeLabel>,
488 pub(crate) prompt: PromptProvenance,
489
490 /// Canonical JSON byte length of the complete body. The four accounting
491 /// class counts below sum to this number; they are not HTTP byte ranges.
492 pub(crate) body_canonical_json_bytes: usize,
493 pub(crate) system_canonical_json_bytes: usize,
494 pub(crate) tool_schema_canonical_json_bytes: usize,
495 pub(crate) message_count: usize,
496 pub(crate) message_canonical_json_bytes: usize,
497 /// Subset of [`Self::message_canonical_json_bytes`].
498 pub(crate) tool_result_canonical_json_bytes: usize,
499 pub(crate) attachment_count: usize,
500 /// Subset of [`Self::message_canonical_json_bytes`].
501 pub(crate) attachment_canonical_json_bytes: usize,
502 /// Algebraic remainder after the selected canonical value-region sizes.
503 pub(crate) framing_canonical_json_bytes: usize,
504
505 pub(crate) estimates: TokenEstimates,
506 /// Estimated input tokens still available before this route's **input
507 /// budget ceiling** — the production seam
508 /// (`context_input_budget_for_route`) the turn loop itself checks, which
509 /// is the context window minus the output reservation and safety
510 /// headroom. Deliberately *not* the raw context limit: subtracting input
511 /// from a window the route also has to fit its output into reports
512 /// headroom the turn does not have. Negative when the turn would exceed
513 /// the budget; absent when the route publishes no budget.
514 pub(crate) estimated_input_headroom_tokens: Option<i64>,
515 /// The exact production input-budget ceiling from which headroom was
516 /// computed, after output reservation and safety headroom.
517 pub(crate) input_budget_ceiling_tokens: Option<usize>,
518 /// Output cap literally present on the prepared wire body. Absent means
519 /// this dialect did not publish one; no inferred value is substituted.
520 pub(crate) wire_output_cap_tokens: Option<u64>,
521
522 /// SHA-256 over the canonicalized **complete** wire body.
523 pub(crate) body_sha256: String,
524 /// SHA-256 over the canonicalized wire `tools` region — the schemas the
525 /// provider receives, after dialect transforms and strict-mode
526 /// sanitizing. Absent when the body carries no tools.
527 pub(crate) tool_schema_wire_sha256: Option<String>,
528 /// Local fingerprint over the final wire system-region hash and final wire
529 /// tool-region hash. This is not a provider cache key and makes no claim
530 /// that those regions are adjacent in a provider-specific prefix.
531 pub(crate) local_system_tools_component_sha256: Option<String>,
532 /// Provider-authoritative counts are available only after a real response.
533 pub(crate) provider_reported_usage: Availability<ProviderReportedUsage>,
534 }
535
536 /// Provider-authoritative usage, populated only from a completed response.
537 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
538 pub(crate) struct ProviderReportedUsage {
539 pub(crate) input_tokens: u64,
540 pub(crate) output_tokens: u64,
541 }
542
543 /// What the engine hands to [`RequestManifest::build`] for the body section.
544 pub(crate) struct PreparedBodyInputs<'a> {
545 pub(crate) prepared: &'a PreparedOutboundRequest,
546 pub(crate) reasoning_resolution: ReasoningResolution,
547 pub(crate) prompt: PromptProvenance,
548 /// This route's production input-budget ceiling in tokens, from
549 /// `context_input_budget_for_route` — the same seam the turn loop checks
550 /// before it sends. `None` when the route publishes no budget.
551 pub(crate) input_budget_ceiling_tokens: Option<usize>,
552 /// Input estimate from production's overflow contract,
553 /// the base `estimate_input_tokens_conservative(messages, system)` plus a
554 /// separately framed transient Work-tail estimate, evaluated over the
555 /// exact hypothetical turn. This is intentionally independent of the
556 /// manifest's whole-wire-body estimate.
557 pub(crate) production_input_estimate_tokens: usize,
558 /// Whether the tool surface is exactly known. The local component digest
559 /// is published only when it is: a fingerprint computed over a tool region
560 /// missing its MCP contribution would compare equal to nothing real.
561 pub(crate) tool_surface_is_exact: bool,
562 }
563
564 /// The complete draft the engine assembles before rendering.
565 pub(crate) struct ManifestDraft<'a> {
566 pub(crate) session: SessionFacts,
567 pub(crate) route: Availability<RouteFacts>,
568 pub(crate) tools: Availability<ToolSurfaceFacts>,
569 pub(crate) body: Availability<PreparedBodyInputs<'a>>,
570 }
571
572 /// A redacted description of the request that would be sent.
573 #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
574 pub(crate) struct RequestManifest {
575 pub(crate) schema_version: u32,
576 pub(crate) session: SessionFacts,
577 pub(crate) route: Availability<RouteFacts>,
578 pub(crate) tools: Availability<ToolSurfaceFacts>,
579 pub(crate) body: Availability<BodyFacts>,
580 }
581
582 /// Signed production input headroom for one route ceiling and estimate.
583 /// Negative means the turn loop's preflight gate must recover or stop before
584 /// sending. Both the manifest and preview overflow decision use this helper so
585 /// the displayed headroom cannot disagree with request eligibility.
586 pub(crate) fn production_input_headroom(
587 ceiling_tokens: Option<usize>,
588 estimate_tokens: usize,
589 ) -> Option<i64> {
590 ceiling_tokens
591 .and_then(|ceiling| {
592 Some((
593 i64::try_from(ceiling).ok()?,
594 i64::try_from(estimate_tokens).ok()?,
595 ))
596 })
597 .map(|(ceiling, estimate)| ceiling - estimate)
598 }
599
600 #[must_use]
601 pub(crate) fn production_input_budget_exceeded(
602 ceiling_tokens: Option<usize>,
603 estimate_tokens: usize,
604 ) -> bool {
605 production_input_headroom(ceiling_tokens, estimate_tokens).is_some_and(|headroom| headroom < 0)
606 }
607
608 impl RequestManifest {
609 /// Build a manifest from a draft whose route/tool/body sections have
610 /// already been resolved — or typed as unavailable — by the engine.
611 pub(crate) fn build(draft: ManifestDraft<'_>) -> Self {
612 let body = draft.body.map(|inputs| {
613 let view = inputs.prepared.wire_view();
614 // The accounting classes below must sum to the body. They describe
615 // canonical value-region sizes plus a remainder, not disjoint
616 // borrowed ranges in the serialized JSON buffer.
617 debug_assert!(
618 view.partition_is_exact(),
619 "wire byte accounting must sum to the wire body exactly"
620 );
621 let estimates = TokenEstimates::from_view(&view);
622 // Headroom comes from the production input-budget seam, never from
623 // the raw context window: the window has to hold the response too.
624 let estimated_input_headroom_tokens = production_input_headroom(
625 inputs.input_budget_ceiling_tokens,
626 inputs.production_input_estimate_tokens,
627 );
628 let tool_schema_wire_sha256 =
629 (!view.tool_schema_sha256.is_empty()).then(|| view.tool_schema_sha256.clone());
630 // A local identity for the two final wire components. This hashes
631 // their digests rather than claiming they form one contiguous
632 // provider-cache prefix or a route-scoped cache key.
633 let local_system_tools_component_sha256 = inputs.tool_surface_is_exact.then(|| {
634 crate::hashing::sha256_hex(
635 format!(
636 "system={}\ntools={}\n",
637 view.system_sha256, view.tool_schema_sha256
638 )
639 .as_bytes(),
640 )
641 });
642 let wire_effort = inputs.prepared.reasoning.wire_effort();
643
644 BodyFacts {
645 reasoning_resolution: inputs.reasoning_resolution,
646 reasoning_wire_control_keys: inputs
647 .prepared
648 .reasoning
649 .wire_controls
650 .iter()
651 .map(|(key, _)| key.clone())
652 .collect(),
653 reasoning_wire_effort: wire_effort.map(|(_, effort)| SafeLabel::identifier(effort)),
654 reasoning_wire_effort_source: wire_effort.map(|(source, _)| source.to_string()),
655 // Read the final provider-shaped body. The logical request can
656 // be remapped (Anthropic/Responses) or omitted altogether
657 // (DeepSeek thinking), so carrying the pre-transform value
658 // would report a choice the provider never receives.
659 tool_choice: tool_choice_label(inputs.prepared.body.get("tool_choice")),
660 prompt: inputs.prompt,
661 body_canonical_json_bytes: view.body_bytes,
662 system_canonical_json_bytes: view.system_bytes,
663 tool_schema_canonical_json_bytes: view.tool_schema_bytes,
664 message_count: view.items.len(),
665 message_canonical_json_bytes: view.item_bytes,
666 tool_result_canonical_json_bytes: view.tool_result_bytes,
667 attachment_count: view.attachment_count,
668 attachment_canonical_json_bytes: view.attachment_bytes,
669 framing_canonical_json_bytes: view.framing_bytes,
670 estimates,
671 estimated_input_headroom_tokens,
672 input_budget_ceiling_tokens: inputs.input_budget_ceiling_tokens,
673 wire_output_cap_tokens: inputs.prepared.wire_output_cap_tokens(),
674 body_sha256: inputs.prepared.body_sha256(),
675 tool_schema_wire_sha256,
676 local_system_tools_component_sha256,
677 provider_reported_usage: Availability::unavailable(
678 UnavailableReason::ProviderRequestNotExecuted,
679 ),
680 }
681 });
682
683 Self {
684 schema_version: MANIFEST_SCHEMA_VERSION,
685 session: draft.session,
686 route: draft.route,
687 tools: draft.tools,
688 body,
689 }
690 }
691
692 /// Pretty JSON rendering. Redacted by construction.
693 pub(crate) fn to_json(&self) -> String {
694 serde_json::to_string_pretty(self)
695 .unwrap_or_else(|error| format!("{{\"error\":\"{error}\"}}"))
696 }
697
698 /// Human-readable manifest for the transcript.
699 pub(crate) fn render(&self) -> String {
700 let mut out = String::new();
701 out.push_str("Request manifest (preview only — nothing was sent)\n");
702 out.push_str(
703 "Typed counts, hashes, and provenance for the next primary agent turn. \
704 No prompt or message text.\n\n",
705 );
706
707 self.render_session(&mut out);
708 self.render_route(&mut out);
709 self.render_tools(&mut out);
710 self.render_body(&mut out);
711
712 if !self.session.hypothetical_prompt_supplied {
713 if self.session.auto_model_routing {
714 out.push_str(
715 "\nAuto route and body remain unavailable: preview never runs the \
716 provider-backed classifier. Select a fixed route for an exact preview.\n",
717 );
718 } else {
719 out.push_str(
720 "\nPass `--prompt <text>` to resolve the fixed route and describe the exact \
721 next-turn body.\n",
722 );
723 }
724 }
725 out.push_str(
726 "Token figures are offline estimates (~4 bytes/token + 5% margin), \
727 never exact provider tokens.\n",
728 );
729 out.push_str(
730 "Scope: the primary agent turn only. Translation, FIM, speech, \
731 provider-native search, and the auto-router classifier are separate \
732 auxiliary calls; preview never executes them.\n",
733 );
734 out
735 }
736
737 fn render_session(&self, out: &mut String) {
738 out.push_str("Session\n");
739 push_row(out, "agent role", &self.session.agent_role);
740 push_row(out, "lane", &self.session.lane_kind);
741 push_row(out, "Fleet assignment", &self.session.fleet_assignment);
742 push_row(
743 out,
744 "model (requested)",
745 self.session.requested_model.as_str(),
746 );
747 push_row(
748 out,
749 "model routing",
750 if self.session.auto_model_routing {
751 "auto"
752 } else {
753 "fixed"
754 },
755 );
756 push_row(
757 out,
758 "reasoning (requested)",
759 self.session.requested_reasoning.as_str(),
760 );
761 push_row(
762 out,
763 "mode / approval",
764 &format!("{} / {}", self.session.mode, self.session.approval_mode),
765 );
766 push_row(
767 out,
768 "gates (allow / deny)",
769 &format!(
770 "{} / {}",
771 count_label(self.session.allowed_tool_gate_count),
772 count_label(self.session.disallowed_tool_gate_count),
773 ),
774 );
775 push_row(out, "base prompt origin", &self.session.base_prompt.origin);
776 push_row(
777 out,
778 "base prompt",
779 &format!(
780 "{} bytes (sha256 {})",
781 self.session.base_prompt.bytes, self.session.base_prompt.sha256
782 ),
783 );
784 }
785
786 fn render_route(&self, out: &mut String) {
787 out.push_str("\nRoute\n");
788 let route = match &self.route {
789 Availability::Unavailable(unavailable) => {
790 push_unavailable(out, unavailable);
791 return;
792 }
793 Availability::Exact(route) => route,
794 };
795 push_row(
796 out,
797 "provider",
798 &format!("{} ({})", route.provider_display, route.provider_id),
799 );
800 if let Some(route_id) = &route.route_id {
801 push_row(out, "route id", route_id.as_str());
802 }
803 if let Some(source) = &route.auto_route_source {
804 push_row(out, "auto route source", source.as_str());
805 }
806 push_row(out, "routing source", &route.routing_source);
807 push_row(out, "dialect", &route.dialect);
808 push_row(out, "route shape", &route.route_shape);
809 push_row(out, "endpoint host class", &route.endpoint_host_class);
810 push_row(out, "endpoint fingerprint", &route.endpoint_fingerprint);
811 push_row(out, "model (wire)", route.wire_model.as_str());
812 push_row(out, "prepared for", &route.caller_entrypoint);
813 push_row(
814 out,
815 "body `stream` field",
816 match route.body_stream_field {
817 Some(true) => "true",
818 Some(false) => "false",
819 None => "not present on the body",
820 },
821 );
822 push_row(
823 out,
824 "context limit",
825 &format!(
826 "{} ({})",
827 route.context_limit_tokens,
828 route.context_limit_source.label()
829 ),
830 );
831 push_row(
832 out,
833 "route input limit",
834 &route
835 .route_input_limit_tokens
836 .map_or_else(|| "unknown".to_string(), |limit| limit.to_string()),
837 );
838 push_row(
839 out,
840 "route output limit",
841 &route
842 .route_output_limit_tokens
843 .map_or_else(|| "unknown".to_string(), |limit| limit.to_string()),
844 );
845 push_row(out, "billing", &route.billing.label());
846 }
847
848 fn render_tools(&self, out: &mut String) {
849 out.push_str("\nTools (the exact catalog this request would send)\n");
850 let tools = match &self.tools {
851 Availability::Unavailable(unavailable) => {
852 push_unavailable(out, unavailable);
853 return;
854 }
855 Availability::Exact(tools) => tools,
856 };
857 push_row(out, "active", &tools.active_tool_count.to_string());
858 push_row(
859 out,
860 "catalog / deferred",
861 &format!(
862 "{} / {}",
863 tools.catalog_tool_count, tools.deferred_tool_count
864 ),
865 );
866 push_row(
867 out,
868 "active catalog sha256",
869 &tools.active_tool_catalog_sha256,
870 );
871 push_row(out, "surface budget", &tools.tool_surface_budget);
872 push_row(
873 out,
874 "Standard vs Full",
875 if tools.standard_and_full_surfaces_collapsed {
876 "collapsed — both budgets currently produce the same catalog"
877 } else {
878 "distinct — the budgets produce different catalogs"
879 },
880 );
881 push_row(
882 out,
883 "MCP (servers / tools)",
884 &format!("{} / {}", tools.mcp_server_count, tools.mcp_tool_count),
885 );
886 }
887
888 fn render_body(&self, out: &mut String) {
889 out.push_str("\nWire body\n");
890 let body = match &self.body {
891 Availability::Unavailable(unavailable) => {
892 push_unavailable(out, unavailable);
893 return;
894 }
895 Availability::Exact(body) => body,
896 };
897
898 push_row(
899 out,
900 "canonical JSON bytes (total)",
901 &body.body_canonical_json_bytes.to_string(),
902 );
903 push_row(
904 out,
905 "canonical JSON bytes (system)",
906 &body.system_canonical_json_bytes.to_string(),
907 );
908 push_row(
909 out,
910 "canonical JSON bytes (tool schemas)",
911 &body.tool_schema_canonical_json_bytes.to_string(),
912 );
913 push_row(
914 out,
915 "messages / canonical JSON bytes",
916 &format!(
917 "{} / {}",
918 body.message_count, body.message_canonical_json_bytes
919 ),
920 );
921 push_row(
922 out,
923 "tool-result canonical JSON bytes",
924 &body.tool_result_canonical_json_bytes.to_string(),
925 );
926 push_row(
927 out,
928 "attachments / canonical JSON bytes",
929 &format!(
930 "{} / {}",
931 body.attachment_count, body.attachment_canonical_json_bytes
932 ),
933 );
934 push_row(
935 out,
936 "framing canonical JSON bytes",
937 &format!(
938 "{} (key names, punctuation, all other fields)",
939 body.framing_canonical_json_bytes
940 ),
941 );
942 push_row(
943 out,
944 "byte classes",
945 "system + tool schemas + messages + framing = total, exactly",
946 );
947
948 out.push_str("\nReasoning (as prepared)\n");
949 push_row(out, "resolution", body.reasoning_resolution.label());
950 push_row(
951 out,
952 "wire controls",
953 if body.reasoning_wire_control_keys.is_empty() {
954 "none".to_string()
955 } else {
956 body.reasoning_wire_control_keys.join(", ")
957 }
958 .as_str(),
959 );
960 push_row(
961 out,
962 "wire effort",
963 &match (
964 &body.reasoning_wire_effort,
965 &body.reasoning_wire_effort_source,
966 ) {
967 (Some(effort), Some(source)) => format!("{effort} (from `{source}`)"),
968 (Some(effort), None) => effort.as_str().to_string(),
969 _ => "not sent".to_string(),
970 },
971 );
972 push_row(
973 out,
974 "tool_choice",
975 body.tool_choice
976 .as_ref()
977 .map_or("not sent", SafeLabel::as_str),
978 );
979
980 out.push_str("\nSystem prompt (as prepared)\n");
981 push_row(out, "assembly", body.prompt.assembly.label());
982 push_row(
983 out,
984 "effective system",
985 &format!(
986 "{} canonical JSON bytes (sha256 {})",
987 body.prompt.effective_system_canonical_json_bytes,
988 body.prompt.effective_system_sha256
989 ),
990 );
991
992 out.push_str("\nEstimated tokens (offline estimate, not provider-counted)\n");
993 push_row(out, "system", &body.estimates.system.to_string());
994 push_row(
995 out,
996 "tool schemas",
997 &body.estimates.tool_schemas.to_string(),
998 );
999 push_row(out, "messages", &body.estimates.messages.to_string());
1000 push_row(
1001 out,
1002 " of which tool results",
1003 &body.estimates.tool_results.to_string(),
1004 );
1005 push_row(
1006 out,
1007 " of which attachments",
1008 &body.estimates.attachments.to_string(),
1009 );
1010 push_row(out, "framing", &body.estimates.framing.to_string());
1011 push_row(
1012 out,
1013 "total (conservative)",
1014 &format!("~{}", body.estimates.total_conservative),
1015 );
1016 push_row(
1017 out,
1018 "input budget ceiling",
1019 &body
1020 .input_budget_ceiling_tokens
1021 .map_or_else(|| "unknown".to_string(), |ceiling| ceiling.to_string()),
1022 );
1023 push_row(
1024 out,
1025 "headroom (input budget)",
1026 &body.estimated_input_headroom_tokens.map_or_else(
1027 || "unknown".to_string(),
1028 |headroom| {
1029 format!("~{headroom} (production estimate; window minus output reservation)")
1030 },
1031 ),
1032 );
1033 push_row(
1034 out,
1035 "output cap (wire)",
1036 &body
1037 .wire_output_cap_tokens
1038 .map_or_else(|| "unknown".to_string(), |cap| cap.to_string()),
1039 );
1040 push_row(
1041 out,
1042 "provider-reported usage",
1043 UnavailableReason::ProviderRequestNotExecuted.label(),
1044 );
1045
1046 out.push_str("\nHashes\n");
1047 push_row(out, "whole body", &body.body_sha256);
1048 push_row(
1049 out,
1050 "wire tool schemas",
1051 body.tool_schema_wire_sha256
1052 .as_deref()
1053 .unwrap_or("no tools on this request"),
1054 );
1055 push_row(
1056 out,
1057 "local system + tools component",
1058 body.local_system_tools_component_sha256
1059 .as_deref()
1060 .unwrap_or("unavailable — tool surface is not exactly known"),
1061 );
1062 }
1063 }
1064
1065 impl TokenEstimates {
1066 fn from_view(view: &crate::client::WireBodyView<'_>) -> Self {
1067 // The classes partition the body exactly, so the total is taken over
1068 // the body itself rather than summed from four independently rounded
1069 // per-class estimates.
1070 Self {
1071 system: estimate_bytes(view.system_bytes),
1072 tool_schemas: estimate_bytes(view.tool_schema_bytes),
1073 messages: estimate_bytes(view.item_bytes),
1074 // Tool results and attachments are *subsets* of the message bytes,
1075 // reported for attribution and deliberately not added again.
1076 tool_results: estimate_bytes(view.tool_result_bytes),
1077 attachments: estimate_bytes(view.attachment_bytes),
1078 framing: estimate_bytes(view.framing_bytes),
1079 total_conservative: conservative_token_estimate(view.body_bytes),
1080 }
1081 }
1082 }
1083
1084 fn push_row(out: &mut String, label: &str, value: &str) {
1085 out.push_str(&format!(" {label:<30} {value}\n"));
1086 }
1087
1088 fn push_unavailable(out: &mut String, unavailable: &Unavailable) {
1089 push_row(out, "unavailable", unavailable.reason.label());
1090 if let Some(detail) = &unavailable.detail {
1091 push_row(out, " detail", detail);
1092 }
1093 }
1094
1095 fn count_label(count: Option<usize>) -> String {
1096 count.map_or_else(|| "none".to_string(), |count| count.to_string())
1097 }
1098
1099 fn estimate_bytes(bytes: usize) -> usize {
1100 bytes.div_ceil(BYTES_PER_TOKEN)
1101 }
1102
1103 /// The offline input-token estimate this manifest publishes, for `bytes` of
1104 /// wire body.
1105 ///
1106 /// This is an independent provider-body observability estimate. Production's
1107 /// overflow decision and the manifest's headroom deliberately use
1108 /// `compaction::estimate_input_tokens_conservative(messages, system)` instead.
1109 pub(crate) fn conservative_token_estimate(bytes: usize) -> usize {
1110 let whole = estimate_bytes(bytes);
1111 whole.saturating_add(whole * ESTIMATE_MARGIN_PERCENT / 100)
1112 }
1113
1114 /// Short shape label for a wire `tool_choice` value.
1115 ///
1116 /// Structural only, and bounded: the forced-function *name* is a tool name,
1117 /// which is safe, but it still crosses the safe-label boundary because a
1118 /// provider-shaped body can carry anything under that key.
1119 pub(crate) fn tool_choice_label(value: Option<&serde_json::Value>) -> Option<SafeLabel> {
1120 let value = value?;
1121 if let Some(text) = value.as_str() {
1122 return Some(SafeLabel::identifier(text));
1123 }
1124 let kind = value
1125 .get("type")
1126 .and_then(serde_json::Value::as_str)
1127 .unwrap_or("object");
1128 match value
1129 .pointer("/function/name")
1130 .and_then(serde_json::Value::as_str)
1131 {
1132 Some(name) => Some(SafeLabel::identifier(&format!("{kind}:{name}"))),
1133 None => Some(SafeLabel::identifier(kind)),
1134 }
1135 }
1136
1137 #[cfg(test)]
1138 pub(crate) mod test_support {
1139 use super::*;
1140
1141 pub(crate) fn session() -> SessionFacts {
1142 SessionFacts {
1143 agent_role: "primary".to_string(),
1144 lane_kind: "interactive-primary".to_string(),
1145 fleet_assignment: "not-applicable-primary-agent".to_string(),
1146 requested_model: SafeLabel::identifier("glm-5.2"),
1147 auto_model_routing: false,
1148 requested_reasoning: SafeLabel::identifier("high"),
1149 hypothetical_prompt_supplied: true,
1150 mode: "Agent".to_string(),
1151 approval_mode: "Prompt".to_string(),
1152 allowed_tool_gate_count: None,
1153 disallowed_tool_gate_count: None,
1154 base_prompt: BasePromptProvenance {
1155 origin: "bundled in this codewhale-tui build".to_string(),
1156 bytes: 11,
1157 sha256: crate::hashing::sha256_hex(b"BASE PROMPT"),
1158 },
1159 }
1160 }
1161
1162 pub(crate) fn route() -> RouteFacts {
1163 RouteFacts {
1164 provider_id: SafeLabel::identifier("zhipu"),
1165 provider_display: SafeLabel::identifier("Z.ai"),
1166 route_id: Some(SafeLabel::identifier("my-gateway")),
1167 dialect: "chat-completions".to_string(),
1168 route_shape: "standard".to_string(),
1169 endpoint_host_class: "https remote sha256:0123456789ab".to_string(),
1170 endpoint_fingerprint: crate::hashing::sha256_hex(b"endpoint"),
1171 wire_model: SafeLabel::identifier("glm-5.2"),
1172 caller_entrypoint: "streaming".to_string(),
1173 body_stream_field: Some(true),
1174 context_limit_tokens: 200_000,
1175 context_limit_source: crate::route_runtime::ContextWindowSource::Catalog,
1176 route_input_limit_tokens: Some(180_000),
1177 route_output_limit_tokens: Some(20_000),
1178 billing: BillingFacts::Metered,
1179 routing_source: "active-fixed-route".to_string(),
1180 auto_route_source: None,
1181 }
1182 }
1183
1184 pub(crate) fn tools() -> ToolSurfaceFacts {
1185 ToolSurfaceFacts {
1186 catalog_tool_count: 4,
1187 deferred_tool_count: 2,
1188 active_tool_count: 2,
1189 active_tool_catalog_sha256: crate::hashing::sha256_hex(b"tools"),
1190 tool_surface_budget: "Standard".to_string(),
1191 standard_and_full_surfaces_collapsed: true,
1192 mcp_server_count: 0,
1193 mcp_tool_count: 0,
1194 }
1195 }
1196
1197 pub(crate) fn prompt() -> PromptProvenance {
1198 PromptProvenance {
1199 assembly: SystemPromptAssembly::BaseOnly,
1200 effective_system_canonical_json_bytes: 11,
1201 effective_system_sha256: crate::hashing::sha256_hex(b"BASE PROMPT"),
1202 }
1203 }
1204 }
1205
1206 #[cfg(test)]
1207 mod tests {
1208 use super::test_support::{prompt, route, session, tools};
1209 use super::*;
1210 use crate::client::{CallerStreamMode, EndpointIdentity, RouteShape, WireDialect};
1211 use serde_json::json;
1212
1213 fn endpoint(url: &str) -> EndpointIdentity {
1214 EndpointIdentity {
1215 provider_id: "zhipu".to_string(),
1216 provider_display: "Z.ai".to_string(),
1217 route_id: Some("my-gateway".to_string()),
1218 url: url.to_string(),
1219 shape: RouteShape::Standard,
1220 }
1221 }
1222
1223 fn chat_body() -> serde_json::Value {
1224 json!({
1225 "model": "glm-5.2",
1226 "messages": [
1227 {"role": "system", "content": "SECRET SYSTEM PROMPT"},
1228 {"role": "user", "content": "SECRET USER MESSAGE"},
1229 {"role": "tool", "tool_call_id": "c1", "content": "SECRET TOOL OUTPUT"},
1230 ],
1231 "tools": [{"type": "function", "function": {"name": "read_file"}}],
1232 "tool_choice": {"type": "auto"},
1233 "max_tokens": 4096,
1234 "reasoning_effort": "high",
1235 "stream": true,
1236 })
1237 }
1238
1239 fn prepared_chat(body: serde_json::Value) -> PreparedOutboundRequest {
1240 let fixture_url = format!(
1241 "https://user:{}@api.z.ai:8443/api/paas/v4/chat/completions?api_key={}{}",
1242 "hunter2", "sk", "-fixture-not-a-real-key-00000000"
1243 );
1244 PreparedOutboundRequest::new(
1245 WireDialect::ChatCompletions,
1246 endpoint(&fixture_url),
1247 "glm-5.2".to_string(),
1248 body,
1249 Some("high".to_string()),
1250 None,
1251 CallerStreamMode::Streaming,
1252 )
1253 }
1254
1255 /// A stand-in for this route's production input-budget ceiling. Real
1256 /// values come from `context_input_budget_for_route`.
1257 const INPUT_BUDGET_CEILING: usize = 150_000;
1258 const PRODUCTION_INPUT_ESTIMATE: usize = 42_000;
1259
1260 fn manifest_from(prepared: &PreparedOutboundRequest) -> RequestManifest {
1261 RequestManifest::build(ManifestDraft {
1262 session: session(),
1263 route: Availability::Exact(route()),
1264 tools: Availability::Exact(tools()),
1265 body: Availability::Exact(PreparedBodyInputs {
1266 prepared,
1267 reasoning_resolution: ReasoningResolution::Explicit,
1268 prompt: prompt(),
1269 input_budget_ceiling_tokens: Some(INPUT_BUDGET_CEILING),
1270 production_input_estimate_tokens: PRODUCTION_INPUT_ESTIMATE,
1271 tool_surface_is_exact: true,
1272 }),
1273 })
1274 }
1275
1276 fn manifest(body: serde_json::Value) -> RequestManifest {
1277 let prepared = prepared_chat(body);
1278 manifest_from(&prepared)
1279 }
1280
1281 fn body_facts(manifest: &RequestManifest) -> &BodyFacts {
1282 manifest
1283 .body
1284 .exact()
1285 .expect("body is exact in this fixture")
1286 }
1287
1288 #[test]
1289 fn manifest_reports_typed_counts_and_hashes() {
1290 let manifest = manifest(chat_body());
1291 assert_eq!(manifest.schema_version, MANIFEST_SCHEMA_VERSION);
1292 let route = manifest.route.exact().expect("route is exact");
1293 assert_eq!(route.dialect, "chat-completions");
1294 assert_eq!(route.routing_source, "active-fixed-route");
1295 assert_eq!(manifest.session.agent_role, "primary");
1296 assert_eq!(manifest.session.lane_kind, "interactive-primary");
1297 assert_eq!(
1298 manifest.session.fleet_assignment,
1299 "not-applicable-primary-agent"
1300 );
1301 assert_eq!(route.context_limit_tokens, 200_000);
1302 assert_eq!(
1303 route.context_limit_source,
1304 crate::route_runtime::ContextWindowSource::Catalog
1305 );
1306 assert_eq!(
1307 route.route_id.as_ref().map(SafeLabel::as_str),
1308 Some("my-gateway")
1309 );
1310 let body = body_facts(&manifest);
1311 assert_eq!(body.message_count, 2, "system message is not a message");
1312 assert!(body.tool_result_canonical_json_bytes > 0);
1313 assert_eq!(body.body_sha256.len(), 64);
1314 assert_eq!(body.input_budget_ceiling_tokens, Some(INPUT_BUDGET_CEILING));
1315 assert_eq!(body.wire_output_cap_tokens, Some(4_096));
1316 assert!(matches!(
1317 &body.provider_reported_usage,
1318 Availability::Unavailable(Unavailable {
1319 reason: UnavailableReason::ProviderRequestNotExecuted,
1320 ..
1321 })
1322 ));
1323 assert_eq!(
1324 body.local_system_tools_component_sha256
1325 .as_deref()
1326 .map(str::len),
1327 Some(64)
1328 );
1329 }
1330
1331 #[test]
1332 fn explicit_base_prompt_preview_is_exact_and_base_only() {
1333 assert_eq!(
1334 exact_base_prompt_only().as_bytes(),
1335 crate::prompts::effective_base_prompt_text().as_bytes()
1336 );
1337 }
1338
1339 #[test]
1340 fn no_prompt_message_secret_or_path_reaches_any_surface() {
1341 let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
1342 let mut body = chat_body();
1343 body["messages"][1]["content"] = json!(format!("look at {home}/.codewhale/config.toml"));
1344 let manifest = manifest(body);
1345
1346 for surface in [
1347 manifest.render(),
1348 manifest.to_json(),
1349 format!("{manifest:?}"),
1350 ] {
1351 for forbidden in [
1352 "SECRET SYSTEM PROMPT",
1353 "SECRET USER MESSAGE",
1354 "SECRET TOOL OUTPUT",
1355 "hunter2",
1356 "sk-abcdef0123456789",
1357 "api_key=",
1358 "/api/paas/v4/chat/completions",
1359 ] {
1360 assert!(
1361 !surface.contains(forbidden),
1362 "`{forbidden}` leaked into a manifest surface:\n{surface}"
1363 );
1364 }
1365 assert!(!surface.contains(&home), "home path leaked:\n{surface}");
1366 }
1367 }
1368
1369 #[test]
1370 fn hostile_route_and_model_identifiers_are_bounded_on_both_surfaces() {
1371 // A custom route id and a wire model are user-authored text: they can
1372 // be absolute paths, URLs, or deployment secrets.
1373 let mut route = route();
1374 route.route_id = Some(SafeLabel::identifier(
1375 "https://internal.example.com/v1/deployments/prod-key-8f2a",
1376 ));
1377 route.provider_id = SafeLabel::identifier("openai/secrets/config");
1378 route.wire_model = SafeLabel::catalog_model("qwen/src/lib.rs");
1379 route.provider_display = SafeLabel::identifier("sk-live-abcdef0123456789abcdef");
1380
1381 let manifest = RequestManifest::build(ManifestDraft {
1382 session: session(),
1383 route: Availability::Exact(route),
1384 tools: Availability::Exact(tools()),
1385 body: Availability::unavailable(UnavailableReason::NoHypotheticalPromptSupplied),
1386 });
1387
1388 for surface in [manifest.render(), manifest.to_json()] {
1389 for forbidden in [
1390 "internal.example.com",
1391 "deployments/prod-key-8f2a",
1392 "openai/secrets/config",
1393 "qwen/src/lib.rs",
1394 "sk-live-",
1395 ] {
1396 assert!(
1397 !surface.contains(forbidden),
1398 "`{forbidden}` leaked into a manifest surface:\n{surface}"
1399 );
1400 }
1401 assert!(surface.contains("sha256:"), "{surface}");
1402 }
1403 }
1404
1405 #[test]
1406 fn unresolved_auto_publishes_no_route_tool_or_body_facts() {
1407 let mut session = session();
1408 session.requested_model = SafeLabel::identifier("auto");
1409 session.auto_model_routing = true;
1410 session.hypothetical_prompt_supplied = false;
1411 session.requested_reasoning = SafeLabel::identifier("auto");
1412
1413 let manifest = RequestManifest::build(ManifestDraft {
1414 session,
1415 route: Availability::unavailable(UnavailableReason::AutoRouteUnresolvedUntilNextPrompt),
1416 tools: Availability::unavailable(UnavailableReason::AutoRouteUnresolvedUntilNextPrompt),
1417 body: Availability::unavailable(UnavailableReason::AutoRouteUnresolvedUntilNextPrompt),
1418 });
1419
1420 let json = manifest.to_json();
1421 for forbidden in [
1422 "provider_id",
1423 "route_id",
1424 "dialect",
1425 "endpoint_host",
1426 "endpoint_fingerprint",
1427 "wire_model",
1428 "billing",
1429 "tool_surface_budget",
1430 "body_sha256",
1431 ] {
1432 assert!(
1433 !json.contains(forbidden),
1434 "`{forbidden}` must not appear when auto routing is unresolved:\n{json}"
1435 );
1436 }
1437 assert!(
1438 json.contains("auto-route-unresolved-until-next-prompt"),
1439 "{json}"
1440 );
1441 assert_eq!(manifest.session.requested_model.as_str(), "auto");
1442
1443 let rendered = manifest.render();
1444 assert!(
1445 rendered.contains("auto model routing is unresolved until the next prompt"),
1446 "{rendered}"
1447 );
1448 assert!(rendered.contains("preview never runs"), "{rendered}");
1449 assert!(rendered.contains("Select a fixed route"), "{rendered}");
1450 }
1451
1452 #[test]
1453 fn prompted_auto_reports_the_offline_classifier_boundary() {
1454 let mut session = session();
1455 session.requested_model = SafeLabel::identifier("auto");
1456 session.auto_model_routing = true;
1457 session.hypothetical_prompt_supplied = true;
1458 let reason = UnavailableReason::AutoRouteClassificationNotExecuted;
1459 let manifest = RequestManifest::build(ManifestDraft {
1460 session,
1461 route: Availability::unavailable(reason),
1462 tools: Availability::unavailable(reason),
1463 body: Availability::unavailable(reason),
1464 });
1465
1466 let json = manifest.to_json();
1467 assert!(
1468 json.contains("auto-route-classification-not-executed"),
1469 "{json}"
1470 );
1471 for forbidden in [
1472 "provider_id",
1473 "endpoint_host_class",
1474 "wire_model",
1475 "body_sha256",
1476 ] {
1477 assert!(!json.contains(forbidden), "{forbidden} leaked:\n{json}");
1478 }
1479 assert!(manifest.render().contains("preview is offline"));
1480 }
1481
1482 #[test]
1483 fn repeated_previews_are_byte_stable() {
1484 let first = manifest(chat_body());
1485 let second = manifest(chat_body());
1486 assert_eq!(first, second);
1487 assert_eq!(first.to_json(), second.to_json());
1488 }
1489
1490 #[test]
1491 fn body_hash_moves_for_every_wire_mutation() {
1492 type BodyMutation = (&'static str, Box<dyn Fn(&mut serde_json::Value)>);
1493
1494 let baseline = body_facts(&manifest(chat_body())).body_sha256.clone();
1495 let mutations: Vec<BodyMutation> = vec![
1496 (
1497 "max_tokens",
1498 Box::new(|b: &mut serde_json::Value| b["max_tokens"] = json!(1024)),
1499 ),
1500 (
1501 "tool_choice",
1502 Box::new(|b: &mut serde_json::Value| b["tool_choice"] = json!("required")),
1503 ),
1504 (
1505 "nested reasoning control",
1506 Box::new(|b: &mut serde_json::Value| {
1507 b["thinking"] = json!({"type": "enabled", "effort": "max"});
1508 }),
1509 ),
1510 (
1511 "transformed tool schema",
1512 Box::new(|b: &mut serde_json::Value| {
1513 b["tools"][0]["function"]["parameters"] = json!({"type": "object"});
1514 }),
1515 ),
1516 (
1517 "attachment",
1518 Box::new(|b: &mut serde_json::Value| {
1519 b["messages"].as_array_mut().unwrap().push(json!({
1520 "role": "user",
1521 "content": [{"type": "image_url", "image_url": {"url": "data:x"}}],
1522 }));
1523 }),
1524 ),
1525 (
1526 "stream options",
1527 Box::new(|b: &mut serde_json::Value| {
1528 b["stream_options"] = json!({"include_usage": true});
1529 }),
1530 ),
1531 (
1532 "appended user message",
1533 Box::new(|b: &mut serde_json::Value| {
1534 b["messages"].as_array_mut().unwrap().push(json!({
1535 "role": "user",
1536 "content": "the hypothetical next prompt",
1537 }));
1538 }),
1539 ),
1540 ];
1541 for (what, mutate) in mutations {
1542 let mut body = chat_body();
1543 mutate(&mut body);
1544 assert_ne!(
1545 baseline,
1546 body_facts(&manifest(body)).body_sha256,
1547 "changing {what} must change the whole-body hash"
1548 );
1549 }
1550 }
1551
1552 #[test]
1553 fn manifest_is_not_a_request_body_export() {
1554 // The inspectability slice must never become a way to dump the wire
1555 // body: no field may reproduce request-body content keys. Human-safe
1556 // explanatory strings may still use words such as `messages`.
1557 fn contains_request_content(value: &serde_json::Value) -> bool {
1558 match value {
1559 serde_json::Value::Object(object) => object.iter().any(|(key, value)| {
1560 (key == "messages" && value.is_array())
1561 || matches!(key.as_str(), "content" | "input_schema")
1562 || contains_request_content(value)
1563 }),
1564 serde_json::Value::Array(values) => values.iter().any(contains_request_content),
1565 _ => false,
1566 }
1567 }
1568
1569 let json = manifest(chat_body()).to_json();
1570 let value: serde_json::Value = serde_json::from_str(&json).expect("valid manifest JSON");
1571 assert!(!contains_request_content(&value), "{json}");
1572 }
1573
1574 #[test]
1575 fn collapsed_tool_surfaces_are_disclosed_not_asserted_away() {
1576 let collapsed = manifest(chat_body());
1577 assert!(
1578 collapsed
1579 .tools
1580 .exact()
1581 .expect("tools exact")
1582 .standard_and_full_surfaces_collapsed
1583 );
1584 assert!(collapsed.render().contains("collapsed — both budgets"));
1585
1586 let mut distinct_tools = tools();
1587 distinct_tools.standard_and_full_surfaces_collapsed = false;
1588 let prepared = prepared_chat(chat_body());
1589 let distinct = RequestManifest::build(ManifestDraft {
1590 session: session(),
1591 route: Availability::Exact(route()),
1592 tools: Availability::Exact(distinct_tools),
1593 body: Availability::Exact(PreparedBodyInputs {
1594 prepared: &prepared,
1595 reasoning_resolution: ReasoningResolution::Explicit,
1596 prompt: prompt(),
1597 input_budget_ceiling_tokens: Some(INPUT_BUDGET_CEILING),
1598 production_input_estimate_tokens: PRODUCTION_INPUT_ESTIMATE,
1599 tool_surface_is_exact: false,
1600 }),
1601 });
1602 assert!(distinct.render().contains("distinct — the budgets"));
1603 assert!(
1604 body_facts(&distinct)
1605 .local_system_tools_component_sha256
1606 .is_none(),
1607 "no local component hash without an exact tool catalog"
1608 );
1609 }
1610
1611 #[test]
1612 fn estimates_do_not_double_count_subsets() {
1613 let manifest = manifest(chat_body());
1614 let body = body_facts(&manifest);
1615 assert!(
1616 body.estimates.tool_results <= body.estimates.messages,
1617 "tool results are a subset of message bytes"
1618 );
1619 assert!(
1620 body.estimates.attachments <= body.estimates.messages,
1621 "attachments are a subset of message bytes"
1622 );
1623 assert!(manifest.render().contains("not provider-counted"));
1624 }
1625
1626 /// The reviewed defect: the published byte classes counted selected values
1627 /// and omitted JSON key names, brackets, and separators, so they summed to
1628 /// less than the request. They are exact facts, so they must partition the
1629 /// body they describe.
1630 #[test]
1631 fn published_byte_classes_partition_the_whole_body() {
1632 for body in [
1633 chat_body(),
1634 json!({"model": "m", "messages": []}),
1635 json!({
1636 "model": "m",
1637 "messages": [{"role": "user", "content": "hi"}],
1638 "max_tokens": 32,
1639 }),
1640 ] {
1641 let manifest = manifest(body);
1642 let facts = body_facts(&manifest);
1643 assert_eq!(
1644 facts.system_canonical_json_bytes
1645 + facts.tool_schema_canonical_json_bytes
1646 + facts.message_canonical_json_bytes
1647 + facts.framing_canonical_json_bytes,
1648 facts.body_canonical_json_bytes,
1649 "classes must sum to the wire body:\n{}",
1650 manifest.render()
1651 );
1652 }
1653 }
1654
1655 /// Headroom is measured against the production input budget, not the raw
1656 /// context window: the window has to hold the response too.
1657 #[test]
1658 fn headroom_comes_from_the_input_budget_not_the_context_window() {
1659 let manifest = manifest(chat_body());
1660 let body = body_facts(&manifest);
1661 assert_eq!(
1662 body.estimated_input_headroom_tokens,
1663 Some(INPUT_BUDGET_CEILING as i64 - PRODUCTION_INPUT_ESTIMATE as i64)
1664 );
1665 assert_ne!(
1666 body.estimated_input_headroom_tokens,
1667 Some(INPUT_BUDGET_CEILING as i64 - body.estimates.total_conservative as i64),
1668 "the independent wire estimate must not drive production headroom"
1669 );
1670 assert_ne!(
1671 body.estimated_input_headroom_tokens,
1672 Some(200_000 - PRODUCTION_INPUT_ESTIMATE as i64),
1673 "the route's context limit is not the input budget"
1674 );
1675 assert!(
1676 manifest
1677 .render()
1678 .contains("window minus output reservation"),
1679 "{}",
1680 manifest.render()
1681 );
1682 }
1683
1684 /// A request over budget reports negative headroom rather than clamping to
1685 /// zero and reading as "it fits".
1686 #[test]
1687 fn headroom_goes_negative_when_the_request_would_not_fit() {
1688 let prepared = prepared_chat(chat_body());
1689 let manifest = RequestManifest::build(ManifestDraft {
1690 session: session(),
1691 route: Availability::Exact(route()),
1692 tools: Availability::Exact(tools()),
1693 body: Availability::Exact(PreparedBodyInputs {
1694 prepared: &prepared,
1695 reasoning_resolution: ReasoningResolution::Explicit,
1696 prompt: prompt(),
1697 input_budget_ceiling_tokens: Some(1),
1698 production_input_estimate_tokens: PRODUCTION_INPUT_ESTIMATE,
1699 tool_surface_is_exact: true,
1700 }),
1701 });
1702 assert!(
1703 body_facts(&manifest)
1704 .estimated_input_headroom_tokens
1705 .is_some_and(|headroom| headroom < 0)
1706 );
1707 }
1708
1709 /// The local component identity follows final wire-shaped schemas even
1710 /// when the logical catalog is untouched.
1711 #[test]
1712 fn local_system_tools_component_follows_wire_regions() {
1713 let baseline = manifest(chat_body());
1714 let baseline_component = body_facts(&baseline)
1715 .local_system_tools_component_sha256
1716 .clone();
1717 let baseline_tools = body_facts(&baseline).tool_schema_wire_sha256.clone();
1718 assert!(baseline_component.is_some());
1719
1720 let mut shaped = chat_body();
1721 shaped["tools"][0]["function"]["parameters"] =
1722 json!({"type": "object", "additionalProperties": false});
1723 shaped["tools"][0]["function"]["strict"] = json!(true);
1724 let shaped = manifest(shaped);
1725 assert_ne!(
1726 baseline_tools,
1727 body_facts(&shaped).tool_schema_wire_sha256,
1728 "a shaped schema must move the wire tool hash"
1729 );
1730 assert_ne!(
1731 baseline_component,
1732 body_facts(&shaped).local_system_tools_component_sha256,
1733 "a shaped schema must move the local system/tools component"
1734 );
1735
1736 // …and the system region moves it too.
1737 let mut other_system = chat_body();
1738 other_system["messages"][0]["content"] = json!("A DIFFERENT SYSTEM PROMPT");
1739 assert_ne!(
1740 baseline_component,
1741 body_facts(&manifest(other_system)).local_system_tools_component_sha256
1742 );
1743 }
1744
1745 #[test]
1746 fn nested_reasoning_effort_is_reported_with_its_key_path() {
1747 let mut body = chat_body();
1748 body.as_object_mut()
1749 .expect("object")
1750 .remove("reasoning_effort");
1751 body["thinking"] = json!({"type": "enabled", "effort": "max"});
1752 let manifest = manifest(body);
1753 let facts = body_facts(&manifest);
1754 assert_eq!(
1755 facts.reasoning_wire_effort.as_ref().map(SafeLabel::as_str),
1756 Some("max")
1757 );
1758 assert_eq!(
1759 facts.reasoning_wire_effort_source.as_deref(),
1760 Some("thinking.effort")
1761 );
1762 assert!(manifest.render().contains("max (from `thinking.effort`)"));
1763 }
1764
1765 /// A body with no reasoning request must not read as an explicit user
1766 /// selection just because the caller happened to pass `Explicit`.
1767 #[test]
1768 fn route_default_and_not_applicable_are_distinguishable_labels() {
1769 assert_eq!(
1770 ReasoningResolution::RouteDefault.label(),
1771 "route default (no user selection)"
1772 );
1773 assert_ne!(
1774 ReasoningResolution::RouteDefault.label(),
1775 ReasoningResolution::Explicit.label()
1776 );
1777 }
1778
1779 /// A section that depends on an unavailable section inherits its typed
1780 /// reason instead of quietly becoming exact.
1781 #[test]
1782 fn unavailability_propagates_to_dependent_sections() {
1783 let unavailable_tools: Availability<ToolSurfaceFacts> =
1784 Availability::unavailable(UnavailableReason::McpStateNotSnapshottable);
1785 let body: Availability<BodyFacts> = unavailable_tools.propagate().expect("propagates");
1786 assert!(body.exact().is_none());
1787 assert!(
1788 Availability::Exact(tools())
1789 .propagate::<BodyFacts>()
1790 .is_none(),
1791 "an exact section propagates nothing"
1792 );
1793 }
1794
1795 #[test]
1796 fn scope_exclusions_are_stated_on_the_human_surface() {
1797 let rendered = manifest(chat_body()).render();
1798 assert!(rendered.contains("primary agent turn"), "{rendered}");
1799 assert!(rendered.contains("auxiliary"), "{rendered}");
1800 }
1801
1802 #[test]
1803 fn tool_choice_labels_are_short_shapes() {
1804 assert_eq!(tool_choice_label(None), None);
1805 assert_eq!(
1806 tool_choice_label(Some(&json!("required")))
1807 .as_ref()
1808 .map(SafeLabel::as_str),
1809 Some("required")
1810 );
1811 assert_eq!(
1812 tool_choice_label(Some(&json!({"type": "auto"})))
1813 .as_ref()
1814 .map(SafeLabel::as_str),
1815 Some("auto")
1816 );
1817 assert_eq!(
1818 tool_choice_label(Some(&json!({
1819 "type": "function",
1820 "function": {"name": "read_file"}
1821 })))
1822 .as_ref()
1823 .map(SafeLabel::as_str),
1824 Some("function:read_file")
1825 );
1826 }
1827
1828 /// The manifest consumes provider wire truth, never the logical
1829 /// `MessageRequest.tool_choice`. These are the three reviewed divergences:
1830 /// Anthropic keeps an object, Responses maps it to a string, and DeepSeek
1831 /// thinking omits the field.
1832 #[test]
1833 fn manifest_tool_choice_is_read_from_the_final_provider_body() {
1834 for (dialect, body, expected) in [
1835 (
1836 WireDialect::AnthropicMessages,
1837 json!({
1838 "model": "claude-sonnet-4-6",
1839 "messages": [],
1840 "tool_choice": {"type": "auto"}
1841 }),
1842 Some("auto"),
1843 ),
1844 (
1845 WireDialect::OpenAiResponses,
1846 json!({
1847 "model": "gpt-5-codex",
1848 "input": [],
1849 "tool_choice": "auto"
1850 }),
1851 Some("auto"),
1852 ),
1853 (
1854 WireDialect::ChatCompletions,
1855 json!({
1856 "model": "deepseek-reasoner",
1857 "messages": [],
1858 "reasoning_effort": "high"
1859 }),
1860 None,
1861 ),
1862 ] {
1863 let prepared = PreparedOutboundRequest::new(
1864 dialect,
1865 endpoint("https://api.example.com/v1/messages"),
1866 "wire-model".to_string(),
1867 body,
1868 Some("high".to_string()),
1869 None,
1870 CallerStreamMode::Streaming,
1871 );
1872 assert_eq!(
1873 body_facts(&manifest_from(&prepared))
1874 .tool_choice
1875 .as_ref()
1876 .map(SafeLabel::as_str),
1877 expected,
1878 "{dialect:?}"
1879 );
1880 }
1881 }
1882 }
1883
1883 lines RUST