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