harn_vm/llm/capabilities/model.rs
1//! Capability DTOs and the wire-dialect model.
2//!
3//! Pure data types: the on-disk [`CapabilitiesFile`] schema, per-provider
4//! [`ProviderDefaults`], the resolved [`Capabilities`] struct callers consume,
5//! and the [`WireDialect`] enum that types a route's message wire format. The
6//! `ProviderRule` matrix row and the resolution engine that turns these DTOs
7//! into a `Capabilities` live in `super::rule`.
8
9use std::collections::BTreeMap;
10
11use serde::Deserialize;
12
13use super::rule::ProviderRule;
14
15/// Parsed on-disk capabilities schema. Public so harn-cli can
16/// construct one directly when wiring harn.toml overrides.
17#[derive(Debug, Clone, Deserialize, Default)]
18pub struct CapabilitiesFile {
19 /// Per-provider ordered rule lists. The first matching rule wins; a
20 /// matching rule with `extends = true` contributes only the fields it
21 /// sets and lets resolution continue to later matching rules (see
22 /// [`ProviderRule::extends`]).
23 #[serde(default)]
24 pub provider: BTreeMap<String, Vec<ProviderRule>>,
25 /// Per-provider defaults applied to every matching row and to
26 /// provider/model pairs that have no model-specific row. This keeps
27 /// transport-shape facts in data without repeating them on every
28 /// generation-specific capability row.
29 #[serde(default)]
30 pub provider_defaults: BTreeMap<String, ProviderDefaults>,
31 /// Sibling → canonical family mapping. Providers with no rule of
32 /// their own fall through to the named family (recursively).
33 #[serde(default)]
34 pub provider_family: BTreeMap<String, String>,
35 /// Per-provider adaptive rate/concurrency governor limits, keyed by
36 /// provider id. Consumed by `crate::llm::rate_governor` when the
37 /// `llm.rate_governor` flag is enabled, so provider limits stay catalog
38 /// data instead of call-site branches.
39 #[serde(default)]
40 pub provider_limits: BTreeMap<String, ProviderLimits>,
41}
42
43/// Adaptive-governor limits for one provider. Every field is optional so a
44/// catalog fragment can pin just the axes it knows; unset axes fall back to the
45/// governor's conservative built-in defaults.
46#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
47pub struct ProviderLimits {
48 /// Ceiling the AIMD concurrency limiter additively climbs toward on
49 /// sustained success.
50 #[serde(default)]
51 pub max_concurrency: Option<u32>,
52 /// Floor the AIMD limiter multiplicatively decreases toward on a throttle
53 /// signal.
54 #[serde(default)]
55 pub min_concurrency: Option<u32>,
56 /// Requests-per-minute token bucket. `None` disables the RPM bucket.
57 #[serde(default)]
58 pub rpm: Option<u32>,
59 /// Tokens-per-minute token bucket, charged by estimated input + output
60 /// tokens. `None` disables the TPM bucket.
61 #[serde(default)]
62 pub tpm: Option<u64>,
63 /// Whether the AIMD adaptive concurrency loop is active. When `false`, the
64 /// concurrency limit is pinned at `max_concurrency`.
65 #[serde(default)]
66 pub adaptive: Option<bool>,
67 /// Circuit-breaker / backoff parameters. Absent means built-in defaults.
68 #[serde(default)]
69 pub backoff: Option<GovernorBackoff>,
70}
71
72/// Exponential-backoff-with-jitter parameters for the governor circuit breaker.
73/// Provider `Retry-After` values always take precedence over the computed
74/// window.
75#[derive(Debug, Clone, Deserialize, PartialEq)]
76pub struct GovernorBackoff {
77 /// First OPEN window, in milliseconds.
78 #[serde(default)]
79 pub base_ms: Option<u64>,
80 /// Ceiling for the OPEN window, in milliseconds.
81 #[serde(default)]
82 pub max_ms: Option<u64>,
83 /// Growth factor applied per consecutive OPEN cycle.
84 #[serde(default)]
85 pub multiplier: Option<f64>,
86 /// Full-jitter toggle.
87 #[serde(default)]
88 pub jitter: Option<bool>,
89}
90
91/// Provider-wide default fields merged into matching rules.
92#[derive(Debug, Clone, Deserialize, Default)]
93pub struct ProviderDefaults {
94 /// Message/request/response wire format used by shared helpers.
95 /// Known values are `openai`, `anthropic`, `gemini`, and `ollama`.
96 #[serde(default)]
97 pub message_wire_format: Option<String>,
98 /// Which synchronous endpoint family the dialect's routes dispatch to.
99 /// See [`LiveEndpointFamily`].
100 #[serde(default)]
101 pub live_endpoint_family: Option<LiveEndpointFamily>,
102 /// Native tool definition wire shape. Known values are `openai`
103 /// and `anthropic`.
104 #[serde(default)]
105 pub native_tool_wire_format: Option<String>,
106 /// Whether image content blocks may reference remote URLs.
107 #[serde(default)]
108 pub image_url_input_supported: Option<bool>,
109 /// File-upload transport used by `std/files.upload`. Known values
110 /// are `anthropic` and `gemini`.
111 #[serde(default)]
112 pub file_upload_wire_format: Option<String>,
113 /// Provider-specific reasoning request shape for OpenAI-compatible
114 /// transports. Known values are `openrouter` and `enabled`.
115 #[serde(default)]
116 pub reasoning_wire_format: Option<String>,
117 #[serde(default)]
118 pub files_api_supported: Option<bool>,
119 #[serde(default)]
120 pub batch_api: Option<bool>,
121 #[serde(default)]
122 pub batch_wire_format: Option<String>,
123 #[serde(default)]
124 pub batch_input_mode: Option<String>,
125 #[serde(default)]
126 pub batch_discount_percent: Option<u32>,
127 #[serde(default)]
128 pub batch_turnaround_hours: Option<u32>,
129 #[serde(default)]
130 pub batch_max_requests: Option<u64>,
131 #[serde(default)]
132 pub batch_max_input_bytes: Option<u64>,
133 #[serde(default)]
134 pub batch_result_retention_days: Option<u32>,
135 #[serde(default)]
136 pub batch_result_ordering: Option<String>,
137 #[serde(default)]
138 pub batch_partial_failure: Option<String>,
139 #[serde(default)]
140 pub batch_cancellation: Option<String>,
141 #[serde(default)]
142 pub batch_security_notes: Option<Vec<String>>,
143 #[serde(default)]
144 pub batch_operational_notes: Option<Vec<String>>,
145 /// Regions where the provider/model batch route is explicitly supported.
146 /// Empty means the capability is not region-scoped.
147 #[serde(default)]
148 pub batch_regions: Option<Vec<String>>,
149 /// Explicit prompt-cache TTL values this provider can honor on request.
150 /// Empty means the route may cache, but Harn has no explicit TTL knob for
151 /// it. Known values today: `5m`, `1h`.
152 #[serde(default)]
153 pub prompt_cache_ttls: Option<Vec<String>>,
154 /// Shortest prefix this provider will actually cache, in tokens. See
155 /// [`crate::llm::capabilities::rule::ProviderRule::prompt_cache_min_prefix_tokens`].
156 #[serde(default)]
157 pub prompt_cache_min_prefix_tokens: Option<u32>,
158 #[serde(default)]
159 pub seed_supported: Option<bool>,
160 #[serde(default)]
161 pub top_k_supported: Option<bool>,
162 #[serde(default)]
163 pub temperature_supported: Option<bool>,
164 #[serde(default)]
165 pub top_p_supported: Option<bool>,
166 #[serde(default)]
167 pub frequency_penalty_supported: Option<bool>,
168 #[serde(default)]
169 pub presence_penalty_supported: Option<bool>,
170 #[serde(default)]
171 pub stop_supported: Option<bool>,
172}
173
174/// Copies `src` into `dst` when `src` is set (last-writer-wins overlay).
175pub(super) fn overlay_opt<T: Clone>(dst: &mut Option<T>, src: &Option<T>) {
176 if src.is_some() {
177 dst.clone_from(src);
178 }
179}
180
181/// Copies `src` into `dst` only when `dst` is still unset (fill-the-gaps).
182pub(super) fn fill_opt<T: Clone>(dst: &mut Option<T>, src: &Option<T>) {
183 if dst.is_none() {
184 dst.clone_from(src);
185 }
186}
187
188/// Visits every `ProviderDefaults` field once, applying `$op` (`overlay_opt`
189/// or `fill_opt`) to each `(dst, src)` pair. The field roster lives here only;
190/// `overlay`/`fill_missing_from` differ solely in the merge rule they pass.
191macro_rules! merge_provider_defaults {
192 ($dst:expr, $src:expr, $op:path) => {{
193 $op(&mut $dst.message_wire_format, &$src.message_wire_format);
194 $op(&mut $dst.live_endpoint_family, &$src.live_endpoint_family);
195 $op(
196 &mut $dst.native_tool_wire_format,
197 &$src.native_tool_wire_format,
198 );
199 $op(
200 &mut $dst.image_url_input_supported,
201 &$src.image_url_input_supported,
202 );
203 $op(
204 &mut $dst.file_upload_wire_format,
205 &$src.file_upload_wire_format,
206 );
207 $op(&mut $dst.reasoning_wire_format, &$src.reasoning_wire_format);
208 $op(&mut $dst.files_api_supported, &$src.files_api_supported);
209 $op(&mut $dst.batch_api, &$src.batch_api);
210 $op(&mut $dst.batch_wire_format, &$src.batch_wire_format);
211 $op(&mut $dst.batch_input_mode, &$src.batch_input_mode);
212 $op(
213 &mut $dst.batch_discount_percent,
214 &$src.batch_discount_percent,
215 );
216 $op(
217 &mut $dst.batch_turnaround_hours,
218 &$src.batch_turnaround_hours,
219 );
220 $op(&mut $dst.batch_max_requests, &$src.batch_max_requests);
221 $op(&mut $dst.batch_max_input_bytes, &$src.batch_max_input_bytes);
222 $op(
223 &mut $dst.batch_result_retention_days,
224 &$src.batch_result_retention_days,
225 );
226 $op(&mut $dst.batch_result_ordering, &$src.batch_result_ordering);
227 $op(&mut $dst.batch_partial_failure, &$src.batch_partial_failure);
228 $op(&mut $dst.batch_cancellation, &$src.batch_cancellation);
229 $op(&mut $dst.batch_security_notes, &$src.batch_security_notes);
230 $op(
231 &mut $dst.batch_operational_notes,
232 &$src.batch_operational_notes,
233 );
234 $op(&mut $dst.batch_regions, &$src.batch_regions);
235 $op(&mut $dst.prompt_cache_ttls, &$src.prompt_cache_ttls);
236 $op(
237 &mut $dst.prompt_cache_min_prefix_tokens,
238 &$src.prompt_cache_min_prefix_tokens,
239 );
240 $op(&mut $dst.seed_supported, &$src.seed_supported);
241 $op(&mut $dst.top_k_supported, &$src.top_k_supported);
242 $op(&mut $dst.temperature_supported, &$src.temperature_supported);
243 $op(&mut $dst.top_p_supported, &$src.top_p_supported);
244 $op(
245 &mut $dst.frequency_penalty_supported,
246 &$src.frequency_penalty_supported,
247 );
248 $op(
249 &mut $dst.presence_penalty_supported,
250 &$src.presence_penalty_supported,
251 );
252 $op(&mut $dst.stop_supported, &$src.stop_supported);
253 }};
254}
255
256impl ProviderDefaults {
257 pub(super) fn overlay(&mut self, other: &ProviderDefaults) {
258 merge_provider_defaults!(self, other, overlay_opt);
259 }
260
261 pub(super) fn fill_missing_from(&mut self, other: &ProviderDefaults) {
262 merge_provider_defaults!(self, other, fill_opt);
263 }
264
265 pub(super) fn has_any_field(&self) -> bool {
266 self.message_wire_format.is_some()
267 || self.live_endpoint_family.is_some()
268 || self.native_tool_wire_format.is_some()
269 || self.image_url_input_supported.is_some()
270 || self.file_upload_wire_format.is_some()
271 || self.reasoning_wire_format.is_some()
272 || self.files_api_supported.is_some()
273 || self.batch_api.is_some()
274 || self.batch_wire_format.is_some()
275 || self.batch_input_mode.is_some()
276 || self.batch_discount_percent.is_some()
277 || self.batch_turnaround_hours.is_some()
278 || self.batch_max_requests.is_some()
279 || self.batch_max_input_bytes.is_some()
280 || self.batch_result_retention_days.is_some()
281 || self.batch_result_ordering.is_some()
282 || self.batch_partial_failure.is_some()
283 || self.batch_cancellation.is_some()
284 || self.batch_security_notes.is_some()
285 || self.batch_operational_notes.is_some()
286 || self.batch_regions.is_some()
287 || self.prompt_cache_ttls.is_some()
288 || self.prompt_cache_min_prefix_tokens.is_some()
289 || self.seed_supported.is_some()
290 || self.top_k_supported.is_some()
291 || self.temperature_supported.is_some()
292 || self.top_p_supported.is_some()
293 || self.frequency_penalty_supported.is_some()
294 || self.presence_penalty_supported.is_some()
295 || self.stop_supported.is_some()
296 }
297}
298
299/// The message/request/response wire dialect a route speaks.
300///
301/// This is the single typed representation of what used to be encoded two
302/// different, drift-prone ways: the stringly `Capabilities.message_wire_format`
303/// field (compared against `"anthropic"`/`"gemini"`/`"ollama"` literals at a
304/// dozen call sites) and the `(is_anthropic_style, is_ollama)` boolean pair
305/// threaded independently through the transport/response layers. A closed enum
306/// makes an unhandled or mistyped dialect a compile error and removes the
307/// boolean-blindness where two `bool`s could silently disagree.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub enum WireDialect {
310 /// Anthropic native Messages API (`/v1/messages`). The only dialect that
311 /// surfaces Claude's extended-thinking stream. `message_wire_format =
312 /// "anthropic"`.
313 Anthropic,
314 /// OpenAI-compatible Chat Completions (`/v1/chat/completions`). The default
315 /// for hosted/openai-shape routes. `message_wire_format = "openai"`.
316 OpenAiCompat,
317 /// Ollama native `/api/chat`. `message_wire_format = "ollama"`.
318 Ollama,
319 /// Google Gemini `generateContent`. `message_wire_format = "gemini"`.
320 Gemini,
321}
322
323impl WireDialect {
324 /// Parse the catalog's `message_wire_format` string. Unrecognized values
325 /// (including the explicit `"openai"`) resolve to [`WireDialect::OpenAiCompat`],
326 /// exactly matching the pre-cutover behavior where every
327 /// `== "anthropic"/"gemini"/"ollama"` check fell through to the
328 /// OpenAI-compatible path.
329 pub fn from_message_wire_format(value: &str) -> WireDialect {
330 match value {
331 "anthropic" => WireDialect::Anthropic,
332 "ollama" => WireDialect::Ollama,
333 "gemini" => WireDialect::Gemini,
334 _ => WireDialect::OpenAiCompat,
335 }
336 }
337
338 /// The canonical `message_wire_format` string for display and round-trip.
339 pub fn as_str(self) -> &'static str {
340 match self {
341 WireDialect::Anthropic => "anthropic",
342 WireDialect::OpenAiCompat => "openai",
343 WireDialect::Ollama => "ollama",
344 WireDialect::Gemini => "gemini",
345 }
346 }
347
348 /// Whether this route speaks Anthropic's native Messages shape.
349 pub fn is_anthropic(self) -> bool {
350 matches!(self, WireDialect::Anthropic)
351 }
352
353 /// Whether this route speaks Ollama's native `/api/chat` shape.
354 pub fn is_ollama(self) -> bool {
355 matches!(self, WireDialect::Ollama)
356 }
357
358 /// Whether this route speaks Google Gemini's `generateContent` shape.
359 pub fn is_gemini(self) -> bool {
360 matches!(self, WireDialect::Gemini)
361 }
362}
363
364/// Which synchronous ("live") endpoint family a route dispatches to.
365///
366/// [`WireDialect`] answers "what does the JSON look like"; most dialects have
367/// exactly one live endpoint, so the two questions collapse. Gemini is the
368/// exception: Google serves the same models over two incompatible synchronous
369/// shapes, so a second axis is needed to say *which* one a route uses.
370///
371/// This axis is deliberately independent of `batch_wire_format`. Gemini Batch
372/// only accepts `generateContent`-shaped bodies, so a route can select
373/// [`LiveEndpointFamily::GeminiInteractions`] for its live traffic while its
374/// batch submissions stay `batch_wire_format = "gemini"`.
375///
376/// It is also deliberately independent of the advisory `recommended_endpoint`
377/// string: this is the value the runtime actually switches on, so it is typed
378/// and an unknown value in a capability source fails the load instead of
379/// silently falling back to the legacy transport.
380#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
381#[serde(rename_all = "snake_case")]
382pub enum LiveEndpointFamily {
383 /// Google Gemini `POST /v1beta/models/{model}:generateContent`. The legacy
384 /// path, still the only shape Gemini Batch accepts and the only one Vertex
385 /// delegates to.
386 GeminiGenerateContent,
387 /// Google Gemini `POST /v1beta/interactions` (GA June 2026). Adds
388 /// server-side conversation state (`previous_interaction_id`), observable
389 /// execution steps, streaming, and background execution.
390 GeminiInteractions,
391}
392
393impl LiveEndpointFamily {
394 /// The canonical capability-source string for display and round-trip.
395 pub const fn as_str(self) -> &'static str {
396 match self {
397 Self::GeminiGenerateContent => "gemini_generate_content",
398 Self::GeminiInteractions => "gemini_interactions",
399 }
400 }
401
402 /// Whether this endpoint family keeps conversation state on the provider,
403 /// making the `previous_response_id` / `store` / `background` options
404 /// meaningful on the route.
405 pub const fn is_stateful(self) -> bool {
406 matches!(self, Self::GeminiInteractions)
407 }
408
409 /// Whether this route dispatches over Gemini's Interactions transport.
410 pub const fn is_gemini_interactions(self) -> bool {
411 matches!(self, Self::GeminiInteractions)
412 }
413}
414
415/// How the neutral `computer` tool projects onto a route's native computer-use
416/// surface (the `computer_use_style` capability). A typed enum rather than a raw
417/// string so an unknown value in a capability source is a load-time
418/// deserialize error instead of a silently-disabled computer tool.
419#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
420#[serde(rename_all = "snake_case")]
421pub enum ComputerUseStyle {
422 /// Anthropic `computer_20251124` native tool.
423 NativeAnthropic,
424 /// OpenAI Responses `computer` native tool.
425 NativeOpenai,
426 /// Accessibility / set-of-marks grounding over the universal function tool.
427 Grounded,
428 /// The plain function-schema `computer` tool (the universal default).
429 Function,
430}
431
432/// Screenshot downscaling policy applied before an image reaches the model (the
433/// `screenshot_scaling` capability). Typed for the same reason as
434/// [`ComputerUseStyle`] — an unknown value fails the capability load loudly.
435#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
436#[serde(rename_all = "snake_case")]
437pub enum ScreenshotScaling {
438 /// Fit within Anthropic's XGA (1024x768), preserving aspect ratio.
439 Xga,
440 /// Send the capture at its native resolution (OpenAI et al.).
441 Original,
442}
443
444/// How a route carries a `system`/`developer`-role message that appears at a
445/// non-leading position in the conversation — the operator-instruction channel
446/// (OpenAI developer messages, Anthropic mid-conversation system messages).
447/// Leading system content is always the system prompt; this governs only the
448/// *interleaved* case. Typed so an unknown value in a capability
449/// source fails the load loudly. `None` on [`Capabilities`] derives a safe
450/// default from the wire dialect — see
451/// [`resolve_system_message_placement`](super::resolve_system_message_placement).
452#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
453#[serde(rename_all = "snake_case")]
454pub enum SystemMessagePlacement {
455 /// The wire API accepts a `system`/`developer` message verbatim at any
456 /// position (OpenAI Chat Completions & Responses, Ollama). Pass through.
457 Inline,
458 /// The API accepts an interleaved `system`-role directive but enforces
459 /// placement rules (Anthropic Opus 4.8: one message must follow a `user`
460 /// turn — or an assistant turn ending in `server_tool_use` — and be the
461 /// last message or be followed by an `assistant` turn; never
462 /// `messages[0]`). A valid consecutive section is merged and kept native;
463 /// anything else folds. `developer` collapses to `system` (Anthropic has no
464 /// developer role).
465 NativeDirective,
466 /// The API has no positional system channel (Anthropic pre-4.8, Gemini
467 /// `systemInstruction`, Bedrock Converse `system[]`). A leading run folds
468 /// into the top-level system prompt; an interleaved directive folds into
469 /// the adjacent user turn as a `<system-reminder>` block so its position
470 /// and operator intent survive. Never 400s, never silently repositioned to
471 /// the global system prompt.
472 Fold,
473}
474
475/// Provider field that must carry Harn's private reasoning from an assistant
476/// turn into that provider's next request.
477///
478/// This is deliberately an enum, rather than an arbitrary catalog string:
479/// replaying private reasoning changes the provider-visible transcript, so an
480/// unknown field must fail capability loading rather than silently leak or
481/// discard it. The durable Harn transcript keeps this as `reasoning`; the
482/// OpenAI-compatible request boundary projects it only for the selected mode.
483#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
484#[serde(rename_all = "snake_case")]
485pub enum ReasoningHistoryWireField {
486 /// Moonshot's OpenAI-compatible API requires this on the prior assistant
487 /// message before matching tool results.
488 ReasoningContent,
489}
490
491impl ReasoningHistoryWireField {
492 pub const fn as_str(self) -> &'static str {
493 match self {
494 Self::ReasoningContent => "reasoning_content",
495 }
496 }
497}
498
499/// Resolved capabilities for a `(provider, model)` pair. Unset rule
500/// fields resolve to `false` / empty / `None` so callers never have to
501/// unwrap an `Option<bool>` for what are really boolean gates.
502#[derive(Debug, Clone, PartialEq, Eq)]
503pub struct Capabilities {
504 pub native_tools: bool,
505 pub message_wire_format: WireDialect,
506 pub native_tool_wire_format: String,
507 pub defer_loading: bool,
508 pub tool_search: Vec<String>,
509 pub responses_api: bool,
510 pub hosted_tools: Vec<String>,
511 pub remote_mcp: bool,
512 pub conversation_state: bool,
513 pub compaction: bool,
514 pub background_mode: bool,
515 pub batch_api: bool,
516 pub batch_wire_format: Option<String>,
517 pub batch_input_mode: Option<String>,
518 pub batch_discount_percent: Option<u32>,
519 pub batch_turnaround_hours: Option<u32>,
520 pub batch_max_requests: Option<u64>,
521 pub batch_max_input_bytes: Option<u64>,
522 pub batch_result_retention_days: Option<u32>,
523 pub batch_result_ordering: Option<String>,
524 pub batch_partial_failure: Option<String>,
525 pub batch_cancellation: Option<String>,
526 pub batch_security_notes: Vec<String>,
527 pub batch_operational_notes: Vec<String>,
528 pub batch_regions: Vec<String>,
529 pub tool_approval_policy: Option<String>,
530 pub max_tools: Option<u32>,
531 pub prompt_caching: bool,
532 pub prompt_cache_ttls: Vec<String>,
533 /// Shortest prefix this route will actually cache, in tokens. `None` means
534 /// the route has no model-specific floor and the wire-dialect default in
535 /// [`crate::llm::cache_conformance::CacheControlProfile`] applies.
536 pub prompt_cache_min_prefix_tokens: Option<u32>,
537 pub cache_breakpoint_style: String,
538 pub vision: bool,
539 pub audio: bool,
540 pub pdf: bool,
541 pub video: bool,
542 pub files_api_supported: bool,
543 pub file_upload_wire_format: Option<String>,
544 pub structured_output: Option<String>,
545 /// Legacy mirror for CLI display and older callers.
546 pub json_schema: Option<String>,
547 pub prefers_xml_scaffolding: bool,
548 /// See [`ProviderRule::reserved_tool_call_token`].
549 pub reserved_tool_call_token: bool,
550 pub prefers_markdown_scaffolding: bool,
551 pub structured_output_mode: String,
552 pub supports_assistant_prefill: bool,
553 pub prefers_role_developer: bool,
554 pub prefers_xml_tools: bool,
555 pub thinking_block_style: String,
556 /// Which synchronous ("live") endpoint family this route dispatches to,
557 /// when its wire dialect serves more than one. `None` means the dialect has
558 /// exactly one live endpoint, so there is nothing to select. See
559 /// [`LiveEndpointFamily`].
560 pub live_endpoint_family: Option<LiveEndpointFamily>,
561 /// Whether this route emits its reasoning INLINE in the text channel as
562 /// `<think>...</think>` blocks (local Ollama/llama.cpp reasoning models,
563 /// Qwen3 via vLLM, Kimi) rather than in a separate provider reasoning
564 /// field. When true, the `llm_call` envelope builder splits those blocks
565 /// out of `text`/`prose`/`visible_text` and folds them into the reasoning
566 /// channel, mirroring how hosted providers surface a dedicated thinking
567 /// field. Derived from `thinking_block_style == "inline"` — the same
568 /// population that represents reasoning as inline `<think>` in prompt
569 /// context is the one that emits it that way in responses.
570 pub emits_inline_reasoning: bool,
571 pub thinking_modes: Vec<String>,
572 pub interleaved_thinking_supported: bool,
573 pub anthropic_beta_features: Vec<String>,
574 pub vision_supported: bool,
575 pub image_url_input_supported: bool,
576 pub preserve_thinking: bool,
577 /// Provider-specific wire field used to replay Harn's private reasoning
578 /// on the preceding assistant turn. `None` preserves the privacy default:
579 /// reasoning is never sent back to an OpenAI-compatible provider.
580 pub reasoning_history_wire_field: Option<ReasoningHistoryWireField>,
581 pub server_parser: String,
582 pub honors_chat_template_kwargs: bool,
583 pub chat_template_options_field: Option<String>,
584 pub requires_completion_tokens: bool,
585 /// True when the route is served ONLY by the provider Responses API and
586 /// rejects `/v1/chat/completions` (OpenAI `*-codex` models). Harn routes
587 /// such calls through the Responses provider automatically.
588 pub chat_completions_unsupported: bool,
589 /// See [`ProviderRule::reasoning_tools_require_responses`].
590 pub reasoning_tools_require_responses: bool,
591 pub requires_streaming: bool,
592 pub reasoning_effort_supported: bool,
593 pub reasoning_effort_levels: Vec<String>,
594 pub reasoning_none_supported: bool,
595 /// See [`ProviderRule::max_thinking_budget`]. `None` means the model uses
596 /// the provider's own default ceiling.
597 pub max_thinking_budget: Option<i64>,
598 pub reasoning_disable_supported: bool,
599 /// See [`ProviderRule::reasoning_required_for_tools`].
600 pub reasoning_required_for_tools: bool,
601 pub reasoning_text_promotable: bool,
602 pub reasoning_wire_format: Option<String>,
603 pub seed_supported: bool,
604 pub top_k_supported: bool,
605 pub temperature_supported: bool,
606 pub top_p_supported: bool,
607 pub frequency_penalty_supported: bool,
608 pub presence_penalty_supported: bool,
609 pub stop_supported: bool,
610 pub allowed_tool_choice_modes: Vec<String>,
611 pub requires_tool_result_adjacency: bool,
612 pub supports_parallel_tool_calls: bool,
613 pub tools_exclude_response_format: bool,
614 pub recommended_endpoint: Option<String>,
615 pub text_tool_wire_format_supported: bool,
616 pub preferred_tool_format: Option<String>,
617 pub tool_mode_parity: Option<String>,
618 pub tool_mode_parity_notes: Option<String>,
619 pub thinking_disable_directive: Option<String>,
620 /// Per-task auto-policy reasoning-level overrides for this route.
621 /// See [`ProviderRule::auto_reasoning_overrides`].
622 pub auto_reasoning_overrides: BTreeMap<String, String>,
623 /// OpenRouter upstream provider names to exclude from routing for this
624 /// row. See [`ProviderRule::provider_route_denylist`]. Empty means "no
625 /// route restriction".
626 pub provider_route_denylist: Vec<String>,
627 /// OpenRouter upstream provider names this row is PINNED to (allowlist), in
628 /// preference order. See [`ProviderRule::openrouter_provider_order`]. Empty
629 /// means "no pin" (free OpenRouter routing).
630 pub openrouter_provider_order: Vec<String>,
631 /// Serving-quality / precision trust verdict for this route. See
632 /// [`ProviderRule::serving_precision`]. `"unverified"` when unset.
633 pub serving_precision: String,
634 /// How the neutral `computer` tool projects onto this route's native
635 /// computer-use surface. `None` means the route exposes no computer-use
636 /// surface. See [`ComputerUseStyle`].
637 pub computer_use_style: Option<ComputerUseStyle>,
638 /// Screenshot downscaling policy applied before the image reaches the
639 /// model. `None` means unset. See [`ScreenshotScaling`].
640 pub screenshot_scaling: Option<ScreenshotScaling>,
641 /// Whether this route requires echoing acknowledged safety checks on the
642 /// computer-use follow-up turn (OpenAI Responses `pending_safety_checks`
643 /// → `acknowledged_safety_checks`). See [`ProviderRule::safety_ack_flow`].
644 pub safety_ack_flow: bool,
645 /// How this route carries an interleaved `system`/`developer` message.
646 /// `None` derives a safe default from `message_wire_format` (OpenAI/Ollama
647 /// → `Inline`, everything else → `Fold`). See [`SystemMessagePlacement`]
648 /// and [`resolve_system_message_placement`](super::resolve_system_message_placement).
649 pub system_message_placement: Option<SystemMessagePlacement>,
650}
651
652impl Default for Capabilities {
653 fn default() -> Self {
654 Self {
655 native_tools: false,
656 message_wire_format: WireDialect::OpenAiCompat,
657 native_tool_wire_format: "openai".to_string(),
658 defer_loading: false,
659 tool_search: Vec::new(),
660 responses_api: false,
661 hosted_tools: Vec::new(),
662 remote_mcp: false,
663 conversation_state: false,
664 compaction: false,
665 background_mode: false,
666 batch_api: false,
667 batch_wire_format: None,
668 batch_input_mode: None,
669 batch_discount_percent: None,
670 batch_turnaround_hours: None,
671 batch_max_requests: None,
672 batch_max_input_bytes: None,
673 batch_result_retention_days: None,
674 batch_result_ordering: None,
675 batch_partial_failure: None,
676 batch_cancellation: None,
677 batch_security_notes: Vec::new(),
678 batch_operational_notes: Vec::new(),
679 batch_regions: Vec::new(),
680 tool_approval_policy: None,
681 max_tools: None,
682 prompt_caching: false,
683 prompt_cache_ttls: Vec::new(),
684 prompt_cache_min_prefix_tokens: None,
685 cache_breakpoint_style: "none".to_string(),
686 vision: false,
687 audio: false,
688 pdf: false,
689 video: false,
690 files_api_supported: false,
691 file_upload_wire_format: None,
692 structured_output: None,
693 json_schema: None,
694 prefers_xml_scaffolding: false,
695 reserved_tool_call_token: false,
696 prefers_markdown_scaffolding: false,
697 structured_output_mode: "none".to_string(),
698 supports_assistant_prefill: false,
699 prefers_role_developer: false,
700 prefers_xml_tools: false,
701 thinking_block_style: "none".to_string(),
702 live_endpoint_family: None,
703 emits_inline_reasoning: false,
704 thinking_modes: Vec::new(),
705 interleaved_thinking_supported: false,
706 anthropic_beta_features: Vec::new(),
707 vision_supported: false,
708 image_url_input_supported: true,
709 preserve_thinking: false,
710 reasoning_history_wire_field: None,
711 server_parser: "none".to_string(),
712 honors_chat_template_kwargs: false,
713 chat_template_options_field: None,
714 requires_completion_tokens: false,
715 chat_completions_unsupported: false,
716 reasoning_tools_require_responses: false,
717 requires_streaming: false,
718 reasoning_effort_supported: false,
719 reasoning_effort_levels: Vec::new(),
720 reasoning_none_supported: false,
721 max_thinking_budget: None,
722 reasoning_disable_supported: true,
723 reasoning_required_for_tools: false,
724 reasoning_text_promotable: false,
725 reasoning_wire_format: None,
726 seed_supported: true,
727 top_k_supported: true,
728 temperature_supported: true,
729 top_p_supported: true,
730 frequency_penalty_supported: true,
731 presence_penalty_supported: true,
732 stop_supported: true,
733 allowed_tool_choice_modes: Vec::new(),
734 requires_tool_result_adjacency: false,
735 supports_parallel_tool_calls: true,
736 tools_exclude_response_format: false,
737 recommended_endpoint: None,
738 text_tool_wire_format_supported: true,
739 preferred_tool_format: None,
740 tool_mode_parity: None,
741 tool_mode_parity_notes: None,
742 thinking_disable_directive: None,
743 auto_reasoning_overrides: BTreeMap::new(),
744 provider_route_denylist: Vec::new(),
745 openrouter_provider_order: Vec::new(),
746 serving_precision: "unverified".to_string(),
747 computer_use_style: None,
748 screenshot_scaling: None,
749 safety_ack_flow: false,
750 system_message_placement: None,
751 }
752 }
753}