Skip to main content

omni_dev/claude/
backend.rs

1//! Shared AI backend and model resolution.
2//!
3//! This module is the single source of truth for which AI backend an
4//! invocation uses and which model it sends, consumed by both the client
5//! factory ([`crate::claude::client::create_default_claude_client`]) and the
6//! preflight credential check (`crate::utils::preflight`). Before it existed
7//! the two sites duplicated the dispatch switch and had drifted apart
8//! (issue #1118).
9//!
10//! Resolution reads the environment only through an
11//! [`EnvSource`](crate::utils::env::EnvSource) (STYLE-0028); the production
12//! callers pass `&SettingsEnv::load()`, so every variable below can also be
13//! supplied from `~/.omni-dev/settings.json` `env` bundles or profiles.
14//!
15//! Backend selection precedence:
16//!
17//! 1. [`AI_BACKEND_ENV`] (`OMNI_DEV_AI_BACKEND`), set directly or via the
18//!    global `--ai-backend` flag — wins outright, including the value
19//!    `default`, which forces the direct Anthropic API even when a `USE_*`
20//!    flag is set. An unknown value is a hard error.
21//! 2. Legacy flags, first match wins: [`USE_OLLAMA_ENV`] → [`USE_OPENAI_ENV`]
22//!    → [`USE_BEDROCK_ENV`] (each compared against the literal `true`).
23//! 3. Otherwise the direct Anthropic API ([`AiBackend::Default`]).
24//!
25//! Model resolution stops at the first non-empty value: the explicit value
26//! (CLI-independent callers such as MCP tools) → [`MODEL_ENV`]
27//! (`OMNI_DEV_MODEL`, set by the global `--model` flag) → the backend
28//! family's own variables → the registry default for the provider. The
29//! Claude-family variables ([`CLAUDE_MODEL_ENV`], [`CLAUDE_CODE_MODEL_ENV`],
30//! [`ANTHROPIC_MODEL_ENV`]) apply only to Claude-family backends; OpenAI and
31//! Ollama read only their provider variable, so a Claude model id can never
32//! leak into a non-Claude backend.
33
34use anyhow::{anyhow, Result};
35
36use crate::claude::model_config::ModelRegistry;
37use crate::utils::env::EnvSource;
38
39/// Env var selecting the AI backend (`default`, `claude-cli`, `openai`,
40/// `ollama`, `bedrock`); set by the global `--ai-backend` flag.
41pub const AI_BACKEND_ENV: &str = "OMNI_DEV_AI_BACKEND";
42/// Env var carrying the backend-agnostic model override; set by the global
43/// `--model` flag. Outranks every per-family model variable.
44pub const MODEL_ENV: &str = "OMNI_DEV_MODEL";
45/// Env var carrying a `key:value` beta header; set by the global
46/// `--beta-header` flag.
47pub const BETA_HEADER_ENV: &str = "OMNI_DEV_BETA_HEADER";
48/// Highest-precedence Claude-family model variable.
49pub const CLAUDE_MODEL_ENV: &str = "CLAUDE_MODEL";
50/// Claude-family model variable, read after [`CLAUDE_MODEL_ENV`].
51pub const CLAUDE_CODE_MODEL_ENV: &str = "CLAUDE_CODE_MODEL";
52/// Claude-family model variable, read after [`CLAUDE_CODE_MODEL_ENV`].
53pub const ANTHROPIC_MODEL_ENV: &str = "ANTHROPIC_MODEL";
54/// Model variable for the OpenAI backend.
55pub const OPENAI_MODEL_ENV: &str = "OPENAI_MODEL";
56/// Model variable for the Ollama backend.
57pub const OLLAMA_MODEL_ENV: &str = "OLLAMA_MODEL";
58/// Legacy backend-selection flag for OpenAI (`true` to select).
59pub const USE_OPENAI_ENV: &str = "USE_OPENAI";
60/// Legacy backend-selection flag for Ollama (`true` to select).
61pub const USE_OLLAMA_ENV: &str = "USE_OLLAMA";
62/// Legacy backend-selection flag for AWS Bedrock (`true` to select).
63pub const USE_BEDROCK_ENV: &str = "CLAUDE_CODE_USE_BEDROCK";
64/// Env var switching structured JSON-schema output off for the whole
65/// invocation, whatever the model registry says. See
66/// [`resolve_structured_output_disabled`].
67pub const STRUCTURED_OUTPUT_DISABLE_ENV: &str = "OMNI_DEV_STRUCTURED_OUTPUT_DISABLE";
68
69/// Hard fallback when the registry has no default for the `claude` provider.
70///
71/// Keep in sync with `providers.claude.default_model` in
72/// `src/templates/models.yaml`; this only applies when that file fails to load.
73const FALLBACK_CLAUDE_MODEL: &str = "claude-sonnet-5";
74/// Hard fallback when the registry has no default for the `openai` provider.
75const FALLBACK_OPENAI_MODEL: &str = "gpt-5-mini";
76/// Default Ollama model; the registry has no `ollama` provider entry.
77const FALLBACK_OLLAMA_MODEL: &str = "llama2";
78
79/// The AI backend used by commands that invoke an AI model.
80///
81/// One enum serves both the `--ai-backend` CLI flag (via
82/// [`clap::ValueEnum`]) and the resolved dispatch in the client factory and
83/// preflight (STYLE-0019). Selection precedence is documented on
84/// [`resolve_backend`].
85#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
86pub enum AiBackend {
87    /// Direct HTTP to the Anthropic API. Selecting this explicitly overrides
88    /// the legacy `USE_*` env vars.
89    #[value(name = "default")]
90    Default,
91    /// Shell out to the `claude -p` CLI (reuses an existing Claude Code auth
92    /// session). Equivalent to setting `OMNI_DEV_AI_BACKEND=claude-cli`.
93    #[value(name = "claude-cli")]
94    ClaudeCli,
95    /// OpenAI API. Equivalent to setting `OMNI_DEV_AI_BACKEND=openai`
96    /// (legacy selector: `USE_OPENAI=true`).
97    #[value(name = "openai")]
98    OpenAi,
99    /// Local Ollama (or another OpenAI-compatible local server). Equivalent
100    /// to setting `OMNI_DEV_AI_BACKEND=ollama` (legacy: `USE_OLLAMA=true`).
101    #[value(name = "ollama")]
102    Ollama,
103    /// AWS Bedrock with Claude models. Equivalent to setting
104    /// `OMNI_DEV_AI_BACKEND=bedrock` (legacy: `CLAUDE_CODE_USE_BEDROCK=true`).
105    #[value(name = "bedrock")]
106    Bedrock,
107}
108
109impl AiBackend {
110    /// Returns the canonical [`AI_BACKEND_ENV`] value for this backend.
111    pub fn env_value(self) -> &'static str {
112        match self {
113            Self::Default => "default",
114            Self::ClaudeCli => "claude-cli",
115            Self::OpenAi => "openai",
116            Self::Ollama => "ollama",
117            Self::Bedrock => "bedrock",
118        }
119    }
120
121    /// Parses an [`AI_BACKEND_ENV`] value.
122    ///
123    /// Accepts the canonical kebab-case values plus the legacy `claude_cli`
124    /// underscore alias. Returns `None` for anything else.
125    pub fn from_env_value(value: &str) -> Option<Self> {
126        match value {
127            "default" => Some(Self::Default),
128            "claude-cli" | "claude_cli" => Some(Self::ClaudeCli),
129            "openai" => Some(Self::OpenAi),
130            "ollama" => Some(Self::Ollama),
131            "bedrock" => Some(Self::Bedrock),
132            _ => None,
133        }
134    }
135}
136
137/// Returns `var`'s value when it is set and non-empty.
138///
139/// The docs promise resolution "stopping at the first non-empty value", and
140/// treating `VAR=` as unset lets users neutralise an exported variable for a
141/// single invocation.
142fn non_empty_var(env: &impl EnvSource, key: &str) -> Option<String> {
143    env.var(key).filter(|v| !v.is_empty())
144}
145
146/// Resolves which AI backend to use from the environment.
147///
148/// [`AI_BACKEND_ENV`] wins outright when set (including `default`, which
149/// forces the direct Anthropic API even when `USE_*` flags are set); an
150/// unknown value is a hard error listing the valid values. When unset, the
151/// legacy flags apply in order: [`USE_OLLAMA_ENV`] → [`USE_OPENAI_ENV`] →
152/// [`USE_BEDROCK_ENV`], each selecting its backend when equal to `true`.
153/// Otherwise the direct Anthropic API is used.
154pub fn resolve_backend(env: &impl EnvSource) -> Result<AiBackend> {
155    if let Some(raw) = non_empty_var(env, AI_BACKEND_ENV) {
156        return AiBackend::from_env_value(&raw).ok_or_else(|| {
157            anyhow!(
158                "Unknown {AI_BACKEND_ENV} value '{raw}'. \
159                 Valid values: default, claude-cli, openai, ollama, bedrock"
160            )
161        });
162    }
163
164    let flag_true = |key| env.var(key).is_some_and(|v| v == "true");
165    if flag_true(USE_OLLAMA_ENV) {
166        Ok(AiBackend::Ollama)
167    } else if flag_true(USE_OPENAI_ENV) {
168        Ok(AiBackend::OpenAi)
169    } else if flag_true(USE_BEDROCK_ENV) {
170        Ok(AiBackend::Bedrock)
171    } else {
172        Ok(AiBackend::Default)
173    }
174}
175
176/// Resolves whether structured JSON-schema output is switched off for this
177/// invocation, from [`STRUCTURED_OUTPUT_DISABLE_ENV`].
178///
179/// The `output_config.format` request field is otherwise gated per *model*
180/// (`ModelRegistry::supports_structured_output`), which cannot express "this
181/// *endpoint* won't accept the field" — the case a gateway named by
182/// `ANTHROPIC_BEDROCK_BASE_URL` creates when it rejects `output_config`
183/// outright (issue #1561). This is the endpoint-level off-switch: it applies
184/// to every backend
185/// and every model, so a run against such a gateway never sends the field at
186/// all rather than paying one rejected round-trip to discover it.
187///
188/// Truthy values are `1`, `true`, and `yes` (trimmed, case-insensitive);
189/// anything else — including an unset or empty var — leaves structured output
190/// enabled. Read through the injected [`EnvSource`] like every other variable
191/// here, so it can equally be supplied from a `settings.json` `env` bundle or
192/// profile.
193#[must_use]
194pub fn resolve_structured_output_disabled(env: &impl EnvSource) -> bool {
195    env.var(STRUCTURED_OUTPUT_DISABLE_ENV).is_some_and(|v| {
196        let v = v.trim().to_ascii_lowercase();
197        v == "1" || v == "true" || v == "yes"
198    })
199}
200
201/// Resolves the model id for `backend`, stopping at the first non-empty
202/// value.
203///
204/// Chain: `explicit` (callers with their own model parameter, e.g. MCP
205/// tools) → [`MODEL_ENV`] → the backend family's variables
206/// (Claude family: [`CLAUDE_MODEL_ENV`] → [`CLAUDE_CODE_MODEL_ENV`] →
207/// [`ANTHROPIC_MODEL_ENV`]; OpenAI: [`OPENAI_MODEL_ENV`]; Ollama:
208/// [`OLLAMA_MODEL_ENV`]) → the registry default for the provider → a
209/// hard-coded fallback.
210pub fn resolve_model(
211    backend: AiBackend,
212    explicit: Option<&str>,
213    env: &impl EnvSource,
214    registry: &ModelRegistry,
215) -> String {
216    if let Some(model) = explicit.filter(|m| !m.is_empty()) {
217        return model.to_string();
218    }
219    if let Some(model) = non_empty_var(env, MODEL_ENV) {
220        return model;
221    }
222
223    match backend {
224        AiBackend::Default | AiBackend::ClaudeCli | AiBackend::Bedrock => {
225            [CLAUDE_MODEL_ENV, CLAUDE_CODE_MODEL_ENV, ANTHROPIC_MODEL_ENV]
226                .iter()
227                .find_map(|key| non_empty_var(env, key))
228                .unwrap_or_else(|| {
229                    registry
230                        .get_default_model("claude")
231                        .unwrap_or(FALLBACK_CLAUDE_MODEL)
232                        .to_string()
233                })
234        }
235        AiBackend::OpenAi => non_empty_var(env, OPENAI_MODEL_ENV).unwrap_or_else(|| {
236            registry
237                .get_default_model("openai")
238                .unwrap_or(FALLBACK_OPENAI_MODEL)
239                .to_string()
240        }),
241        AiBackend::Ollama => non_empty_var(env, OLLAMA_MODEL_ENV)
242            .unwrap_or_else(|| FALLBACK_OLLAMA_MODEL.to_string()),
243    }
244}
245
246/// Parses a `--beta-header key:value` string into a `(key, value)` tuple.
247pub fn parse_beta_header(s: &str) -> Result<(String, String)> {
248    let (k, v) = s
249        .split_once(':')
250        .ok_or_else(|| anyhow!("Invalid --beta-header format '{s}'. Expected key:value"))?;
251    Ok((k.to_string(), v.to_string()))
252}
253
254/// Resolves the beta header to send with AI API requests.
255///
256/// `explicit` (callers with their own parameter) wins; otherwise
257/// [`BETA_HEADER_ENV`] (set by the global `--beta-header` flag) is parsed as
258/// `key:value`. A set-but-malformed value is a hard error; unset (or empty)
259/// resolves to `None`.
260pub fn resolve_beta_header(
261    explicit: Option<(String, String)>,
262    env: &impl EnvSource,
263) -> Result<Option<(String, String)>> {
264    if explicit.is_some() {
265        return Ok(explicit);
266    }
267    match non_empty_var(env, BETA_HEADER_ENV) {
268        Some(raw) => parse_beta_header(&raw).map(Some),
269        None => Ok(None),
270    }
271}
272
273#[cfg(test)]
274#[allow(clippy::unwrap_used, clippy::expect_used)]
275mod tests {
276    use super::*;
277    use crate::claude::model_config::get_model_registry;
278    use crate::test_support::env::MapEnv;
279
280    #[test]
281    fn env_value_round_trips_for_all_backends() {
282        for backend in [
283            AiBackend::Default,
284            AiBackend::ClaudeCli,
285            AiBackend::OpenAi,
286            AiBackend::Ollama,
287            AiBackend::Bedrock,
288        ] {
289            assert_eq!(
290                AiBackend::from_env_value(backend.env_value()),
291                Some(backend)
292            );
293        }
294    }
295
296    #[test]
297    fn from_env_value_accepts_legacy_underscore_alias() {
298        assert_eq!(
299            AiBackend::from_env_value("claude_cli"),
300            Some(AiBackend::ClaudeCli)
301        );
302    }
303
304    #[test]
305    fn from_env_value_rejects_unknown() {
306        assert_eq!(AiBackend::from_env_value("gemini"), None);
307        assert_eq!(AiBackend::from_env_value(""), None);
308    }
309
310    #[test]
311    fn resolve_backend_defaults_to_direct_api() {
312        assert_eq!(resolve_backend(&MapEnv::new()).unwrap(), AiBackend::Default);
313    }
314
315    #[test]
316    fn resolve_backend_legacy_flags_in_order() {
317        // Ollama beats OpenAI beats Bedrock (matches the pre-#1118 dispatch).
318        let env = MapEnv::new()
319            .with(USE_OLLAMA_ENV, "true")
320            .with(USE_OPENAI_ENV, "true")
321            .with(USE_BEDROCK_ENV, "true");
322        assert_eq!(resolve_backend(&env).unwrap(), AiBackend::Ollama);
323
324        let env = MapEnv::new()
325            .with(USE_OPENAI_ENV, "true")
326            .with(USE_BEDROCK_ENV, "true");
327        assert_eq!(resolve_backend(&env).unwrap(), AiBackend::OpenAi);
328
329        let env = MapEnv::new().with(USE_BEDROCK_ENV, "true");
330        assert_eq!(resolve_backend(&env).unwrap(), AiBackend::Bedrock);
331    }
332
333    #[test]
334    fn resolve_backend_legacy_flags_require_literal_true() {
335        let env = MapEnv::new().with(USE_OLLAMA_ENV, "1");
336        assert_eq!(resolve_backend(&env).unwrap(), AiBackend::Default);
337    }
338
339    #[test]
340    fn resolve_backend_env_var_overrides_legacy_flags() {
341        for (value, expected) in [
342            ("default", AiBackend::Default),
343            ("claude-cli", AiBackend::ClaudeCli),
344            ("openai", AiBackend::OpenAi),
345            ("bedrock", AiBackend::Bedrock),
346        ] {
347            let env = MapEnv::new()
348                .with(AI_BACKEND_ENV, value)
349                .with(USE_OLLAMA_ENV, "true");
350            assert_eq!(resolve_backend(&env).unwrap(), expected, "value {value}");
351        }
352    }
353
354    /// The structured-output off-switch accepts the same truthy vocabulary as
355    /// the request log's flags, and is off unless positively set (#1561).
356    #[test]
357    fn resolve_structured_output_disabled_reads_truthy_values() {
358        for value in ["1", "true", "TRUE", "yes", " true "] {
359            let env = MapEnv::new().with(STRUCTURED_OUTPUT_DISABLE_ENV, value);
360            assert!(
361                resolve_structured_output_disabled(&env),
362                "value {value:?} should disable structured output"
363            );
364        }
365
366        for value in ["", "0", "false", "no", "maybe"] {
367            let env = MapEnv::new().with(STRUCTURED_OUTPUT_DISABLE_ENV, value);
368            assert!(
369                !resolve_structured_output_disabled(&env),
370                "value {value:?} should leave structured output enabled"
371            );
372        }
373
374        assert!(!resolve_structured_output_disabled(&MapEnv::new()));
375    }
376
377    #[test]
378    fn resolve_backend_unknown_value_is_hard_error() {
379        let env = MapEnv::new().with(AI_BACKEND_ENV, "junk");
380        let err = resolve_backend(&env).unwrap_err().to_string();
381        assert!(err.contains("junk"), "unexpected error: {err}");
382        assert!(err.contains("claude-cli"), "unexpected error: {err}");
383    }
384
385    #[test]
386    fn resolve_backend_empty_value_reads_as_unset() {
387        let env = MapEnv::new()
388            .with(AI_BACKEND_ENV, "")
389            .with(USE_OLLAMA_ENV, "true");
390        assert_eq!(resolve_backend(&env).unwrap(), AiBackend::Ollama);
391    }
392
393    #[test]
394    fn resolve_model_explicit_wins_everywhere() {
395        let env = MapEnv::new()
396            .with(MODEL_ENV, "from-omni-dev-model")
397            .with(CLAUDE_MODEL_ENV, "from-claude-model");
398        let model = resolve_model(
399            AiBackend::Default,
400            Some("explicit"),
401            &env,
402            get_model_registry(),
403        );
404        assert_eq!(model, "explicit");
405    }
406
407    #[test]
408    fn resolve_model_omni_dev_model_beats_family_vars() {
409        for backend in [
410            AiBackend::Default,
411            AiBackend::ClaudeCli,
412            AiBackend::OpenAi,
413            AiBackend::Ollama,
414            AiBackend::Bedrock,
415        ] {
416            let env = MapEnv::new()
417                .with(MODEL_ENV, "global-model")
418                .with(CLAUDE_MODEL_ENV, "claude-var")
419                .with(OPENAI_MODEL_ENV, "openai-var")
420                .with(OLLAMA_MODEL_ENV, "ollama-var");
421            assert_eq!(
422                resolve_model(backend, None, &env, get_model_registry()),
423                "global-model",
424                "backend {backend:?}"
425            );
426        }
427    }
428
429    #[test]
430    fn resolve_model_claude_family_chain_order() {
431        let registry = get_model_registry();
432        for backend in [AiBackend::Default, AiBackend::ClaudeCli, AiBackend::Bedrock] {
433            let env = MapEnv::new()
434                .with(CLAUDE_MODEL_ENV, "a")
435                .with(CLAUDE_CODE_MODEL_ENV, "b")
436                .with(ANTHROPIC_MODEL_ENV, "c");
437            assert_eq!(resolve_model(backend, None, &env, registry), "a");
438
439            let env = MapEnv::new()
440                .with(CLAUDE_CODE_MODEL_ENV, "b")
441                .with(ANTHROPIC_MODEL_ENV, "c");
442            assert_eq!(resolve_model(backend, None, &env, registry), "b");
443
444            let env = MapEnv::new().with(ANTHROPIC_MODEL_ENV, "c");
445            assert_eq!(resolve_model(backend, None, &env, registry), "c");
446
447            assert_eq!(
448                resolve_model(backend, None, &MapEnv::new(), registry),
449                "claude-sonnet-5"
450            );
451        }
452    }
453
454    #[test]
455    fn resolve_model_claude_vars_do_not_leak_into_openai_or_ollama() {
456        let env = MapEnv::new()
457            .with(CLAUDE_MODEL_ENV, "claude-opus-4-6")
458            .with(ANTHROPIC_MODEL_ENV, "claude-opus-4-6");
459        let registry = get_model_registry();
460        assert_eq!(
461            resolve_model(AiBackend::OpenAi, None, &env, registry),
462            "gpt-5-mini"
463        );
464        assert_eq!(
465            resolve_model(AiBackend::Ollama, None, &env, registry),
466            "llama2"
467        );
468    }
469
470    #[test]
471    fn resolve_model_provider_vars() {
472        let registry = get_model_registry();
473        let env = MapEnv::new().with(OPENAI_MODEL_ENV, "gpt-4.1");
474        assert_eq!(
475            resolve_model(AiBackend::OpenAi, None, &env, registry),
476            "gpt-4.1"
477        );
478
479        let env = MapEnv::new().with(OLLAMA_MODEL_ENV, "qwen3");
480        assert_eq!(
481            resolve_model(AiBackend::Ollama, None, &env, registry),
482            "qwen3"
483        );
484    }
485
486    #[test]
487    fn resolve_model_skips_empty_values() {
488        // The docs promise "stopping at the first non-empty value".
489        let env = MapEnv::new()
490            .with(CLAUDE_MODEL_ENV, "")
491            .with(CLAUDE_CODE_MODEL_ENV, "b");
492        assert_eq!(
493            resolve_model(AiBackend::Default, None, &env, get_model_registry()),
494            "b"
495        );
496    }
497
498    #[test]
499    fn resolve_beta_header_explicit_wins() {
500        let env = MapEnv::new().with(BETA_HEADER_ENV, "env-key:env-value");
501        let explicit = Some(("k".to_string(), "v".to_string()));
502        let resolved = resolve_beta_header(explicit.clone(), &env).unwrap();
503        assert_eq!(resolved, explicit);
504    }
505
506    #[test]
507    fn resolve_beta_header_from_env() {
508        let env = MapEnv::new().with(BETA_HEADER_ENV, "anthropic-beta:output-128k-2025-02-19");
509        let resolved = resolve_beta_header(None, &env).unwrap();
510        assert_eq!(
511            resolved,
512            Some((
513                "anthropic-beta".to_string(),
514                "output-128k-2025-02-19".to_string()
515            ))
516        );
517    }
518
519    #[test]
520    fn resolve_beta_header_unset_is_none() {
521        assert_eq!(resolve_beta_header(None, &MapEnv::new()).unwrap(), None);
522    }
523
524    #[test]
525    fn resolve_beta_header_malformed_env_is_hard_error() {
526        let env = MapEnv::new().with(BETA_HEADER_ENV, "no-colon-here");
527        let err = resolve_beta_header(None, &env).unwrap_err().to_string();
528        assert!(err.contains("no-colon-here"), "unexpected error: {err}");
529    }
530
531    #[test]
532    fn parse_beta_header_valid() {
533        let (key, value) = parse_beta_header("anthropic-beta:output-128k-2025-02-19").unwrap();
534        assert_eq!(key, "anthropic-beta");
535        assert_eq!(value, "output-128k-2025-02-19");
536    }
537
538    #[test]
539    fn parse_beta_header_multiple_colons() {
540        // Only splits on the first colon
541        let (key, value) = parse_beta_header("key:value:with:colons").unwrap();
542        assert_eq!(key, "key");
543        assert_eq!(value, "value:with:colons");
544    }
545
546    #[test]
547    fn parse_beta_header_missing_colon() {
548        let result = parse_beta_header("no-colon-here");
549        assert!(result.is_err());
550        let err_msg = result.unwrap_err().to_string();
551        assert!(err_msg.contains("no-colon-here"));
552    }
553
554    #[test]
555    fn parse_beta_header_empty_value() {
556        let (key, value) = parse_beta_header("key:").unwrap();
557        assert_eq!(key, "key");
558        assert_eq!(value, "");
559    }
560
561    #[test]
562    fn parse_beta_header_empty_key() {
563        let (key, value) = parse_beta_header(":value").unwrap();
564        assert_eq!(key, "");
565        assert_eq!(value, "value");
566    }
567}