1#![allow(clippy::struct_excessive_bools)]
15
16use std::path::PathBuf;
17use std::time::Duration;
18
19use clap::builder::{PossibleValuesParser, TypedValueParser};
20use clap::{Subcommand, ValueEnum};
21use lino_arguments::Parser as LinoParser;
22
23use crate::config::{
24 ApiFormat, BuildArgs, Config, ConfigError, RoutingMode, StoragePolicy, UpstreamProvider,
25 default_activitypub_public_key_pem, default_data_dir,
26};
27use crate::subscription::SubscriptionProvider;
28
29mod auth_ops;
30mod client_ops;
31mod configure;
32mod deploy_args;
33mod store_ops;
34mod targets;
35mod value_parsers;
36mod with;
37
38pub use self::auth_ops::{AuthOp, AuthTarget, ImportProvider, ImportTarget, RemoteGh, TlsOp};
39pub use self::client_ops::ClientOp;
40pub use self::configure::ConfigureArgs;
41pub use self::deploy_args::{DeployArgs, UsageArgs};
42pub use self::store_ops::{AccountOp, ProviderOp, TokenOp};
43use self::value_parsers::parse_truthy;
44pub use self::with::{ServerOp, WithArgs, protect_client_arguments};
45
46#[must_use]
59pub fn parse_arguments(arguments: Vec<std::ffi::OsString>) -> Cli {
60 use clap::{CommandFactory as _, FromArgMatches as _};
61
62 let mut command = Cli::command();
63 if names_a_client_launcher(&arguments) {
68 command = command.mut_args(|argument| {
69 if argument.is_global_set() {
70 argument.hide(true)
71 } else {
72 argument
73 }
74 });
75 }
76 let invoked = arguments
83 .first()
84 .map(std::path::Path::new)
85 .and_then(std::path::Path::file_stem)
86 .map_or_else(
87 || "router".to_string(),
88 |name| name.to_string_lossy().into_owned(),
89 );
90 command = substitute_usage_name(command, &invoked);
91 let matches = command.get_matches_from(arguments);
92 Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit())
93}
94
95const OVERRIDDEN_USAGE: [(&[&str], &str); 14] = [
102 (&["configure"], "configure [OPTIONS] <CLIENT>"),
103 (&["clients", "setup"], "clients setup [OPTIONS] <CLIENT>"),
104 (&["clients", "show"], "clients show [OPTIONS] <CLIENT>"),
105 (&["clients", "remove"], "clients remove [OPTIONS] <CLIENT>"),
106 (&["clients", "doctor"], "clients doctor [OPTIONS] <CLIENT>"),
107 (&["tokens", "rotate"], "tokens rotate [OPTIONS] <ID>"),
108 (&["tokens", "revoke"], "tokens revoke [OPTIONS] <ID>"),
109 (&["tokens", "show"], "tokens show [OPTIONS] <ID>"),
110 (
111 &["providers", "add"],
112 "providers add [OPTIONS] --name <NAME> --base-url <BASE_URL>",
113 ),
114 (&["providers", "show"], "providers show [OPTIONS] <NAME>"),
115 (
116 &["providers", "remove"],
117 "providers remove [OPTIONS] <NAME>",
118 ),
119 (
120 &["providers", "import"],
121 "providers import [OPTIONS] <PATH>",
122 ),
123 (
124 &["auth", "import"],
125 "auth import [OPTIONS] [PROVIDER] [DIR]",
126 ),
127 (&["auth", "clear"], "auth clear [OPTIONS] [PROVIDER]"),
128];
129
130fn substitute_usage_name(mut command: clap::Command, invoked: &str) -> clap::Command {
132 for (path, usage) in OVERRIDDEN_USAGE {
133 command = with_subcommand(command, path, &format!("{invoked} {usage}"));
134 }
135 command
136}
137
138fn with_subcommand(command: clap::Command, path: &[&str], usage: &str) -> clap::Command {
140 let Some((head, rest)) = path.split_first() else {
141 return command.override_usage(usage.to_string());
142 };
143 command.mut_subcommand(head, |subcommand| with_subcommand(subcommand, rest, usage))
144}
145
146fn names_a_client_launcher(arguments: &[std::ffi::OsString]) -> bool {
157 arguments
158 .iter()
159 .skip(1)
160 .map(|argument| argument.to_string_lossy().into_owned())
161 .find(|argument| !argument.starts_with('-'))
162 .is_some_and(|argument| argument == "with" || argument == "configure" || argument == "tls")
163}
164
165#[derive(LinoParser)]
167#[command(
172 name = "router",
173 about = "Claude MAX OAuth proxy and token gateway for Anthropic APIs",
174 version
175)]
176pub struct Cli {
177 #[command(subcommand)]
179 pub command: Option<Command>,
180
181 #[arg(long, env = "ROUTER_HOST", default_value = "0.0.0.0", global = true)]
183 pub host: String,
184
185 #[arg(long, env = "ROUTER_PORT", default_value = "8080", global = true)]
187 pub port: u16,
188
189 #[arg(
192 long = "listener",
193 env = "LISTENERS",
194 value_delimiter = ';',
195 global = true,
196 value_name = "ADDR=KIND,TRANSPORT"
197 )]
198 pub listeners: Vec<String>,
199
200 #[arg(long, env = "VERBOSE", global = true, value_parser = parse_truthy)]
202 pub verbose: bool,
203
204 #[arg(long, env = "TOKEN_SECRET", global = true, hide_env_values = true)]
206 pub token_secret: Option<String>,
207
208 #[arg(long, env = "CLAUDE_CODE_HOME", global = true)]
210 pub claude_code_home: Option<String>,
211
212 #[arg(
214 long,
215 env = "UPSTREAM_BASE_URL",
216 default_value = "https://api.anthropic.com",
217 global = true
218 )]
219 pub upstream_base_url: String,
220
221 #[arg(long, env = "UPSTREAM_API_FORMAT", global = true)]
223 pub api_format: Option<String>,
224
225 #[arg(long, env = "ROUTING_MODE", default_value = "direct", global = true)]
227 pub routing_mode: String,
228
229 #[arg(long, env = "STORAGE_POLICY", default_value = "both", global = true)]
231 pub storage_policy: String,
232
233 #[arg(long, env = "DATA_DIR", global = true)]
235 pub data_dir: Option<PathBuf>,
236
237 #[arg(long, value_name = "DIR", global = true)]
245 pub home: Option<PathBuf>,
246
247 #[arg(long, env = "CLAUDE_CLI_BIN", global = true)]
249 pub claude_cli_bin: Option<PathBuf>,
250 #[arg(long, env = "CODEX_CLI_BIN", global = true)]
252 pub codex_cli_bin: Option<PathBuf>,
253
254 #[arg(long, env = "UPSTREAM_PROVIDER", default_value = "auto", global = true)]
257 pub upstream_provider: String,
258
259 #[arg(long, env = "GONKA_PRIVATE_KEY", global = true, hide_env_values = true)]
261 pub gonka_private_key: Option<String>,
262 #[arg(long, env = "GONKA_API_KEY", global = true, hide_env_values = true)]
264 pub gonka_api_key: Option<String>,
265 #[arg(long, env = "GONKA_SOURCE_URL", global = true)]
267 pub gonka_source_url: Option<String>,
268
269 #[arg(long, env = "GONKA_MODEL", default_value = "", global = true)]
271 pub gonka_model: String,
272
273 #[arg(long, env = "ANTHROPIC_BRIDGE_MODEL", global = true)]
276 pub bridge_model: Option<String>,
277
278 #[arg(long, env = "BRIDGE_MODEL_POLICY", global = true)]
281 pub bridge_model_policy: Option<String>,
282
283 #[arg(long, env = "AUDIT_LOG", global = true)]
286 pub audit_log: Option<PathBuf>,
287
288 #[arg(long, env = "REQUEST_LOG", global = true)]
291 pub request_log: Option<PathBuf>,
292
293 #[arg(
298 long,
299 env = "REQUEST_LOG_MAX_BYTES",
300 default_value_t = crate::request_log::DEFAULT_MAX_BYTES,
301 global = true
302 )]
303 pub request_log_max_bytes: u64,
304
305 #[arg(
309 long,
310 env = "REQUEST_LOG_MAX_TOTAL_BYTES",
311 default_value_t = crate::request_log::DEFAULT_MAX_TOTAL_BYTES,
312 global = true
313 )]
314 pub request_log_max_total_bytes: u64,
315
316 #[arg(
319 long,
320 env = "MAX_PROXY_REQUEST_BYTES",
321 default_value_t = crate::config::DEFAULT_MAX_PROXY_REQUEST_BYTES,
322 global = true
323 )]
324 pub max_proxy_request_bytes: usize,
325
326 #[arg(long, env = "CRATER_FORGEFED_INBOX", global = true)]
328 pub crater_forgefed_inbox: Option<String>,
329
330 #[arg(long, env = "CRATER_FORGEFED_ACTOR", global = true)]
332 pub crater_forgefed_actor: Option<String>,
333
334 #[arg(long, env = "CRATER_FORGEFED_TARGET", global = true)]
336 pub crater_forgefed_target: Option<String>,
337
338 #[arg(
340 long,
341 env = "CRATER_POLL_INTERVAL_MS",
342 default_value_t = 1000,
343 global = true
344 )]
345 pub crater_poll_interval_ms: u64,
346
347 #[arg(
349 long,
350 env = "CRATER_POLL_TIMEOUT_SECS",
351 default_value_t = 120,
352 global = true
353 )]
354 pub crater_poll_timeout_secs: u64,
355
356 #[arg(
358 long,
359 env = "OPENAI_COMPATIBLE_PROVIDER_NAME",
360 default_value = "litellm",
361 global = true
362 )]
363 pub openai_compatible_provider_name: String,
364
365 #[arg(
367 long,
368 env = "OPENAI_COMPATIBLE_BASE_URL",
369 default_value = "http://localhost:4000/v1",
370 global = true
371 )]
372 pub openai_compatible_base_url: String,
373
374 #[arg(
377 long,
378 env = "OPENAI_COMPATIBLE_API_KEY",
379 global = true,
380 hide_env_values = true
381 )]
382 pub openai_compatible_api_key: Option<String>,
383
384 #[arg(long, env = "OPENAI_COMPATIBLE_API_KEY_ENV", global = true)]
386 pub openai_compatible_api_key_env: Option<String>,
387
388 #[arg(long, env = "OPENAI_COMPATIBLE_MODEL", global = true)]
390 pub openai_compatible_model: Option<String>,
391
392 #[arg(
394 long,
395 env = "OPENAI_COMPATIBLE_MODELS",
396 value_delimiter = ',',
397 global = true
398 )]
399 pub openai_compatible_models: Vec<String>,
400
401 #[arg(
403 long,
404 env = "OPENAI_COMPATIBLE_SUPPORTED_CLIENTS",
405 value_delimiter = ',',
406 global = true
407 )]
408 pub openai_compatible_supported_clients: Vec<String>,
409
410 #[arg(long, env = "ACTIVITYPUB_ACTOR_BASE_URL", global = true)]
412 pub activitypub_actor_base_url: Option<String>,
413
414 #[arg(long, env = "ACTIVITYPUB_PUBLIC_KEY_PEM", global = true)]
416 pub activitypub_public_key_pem: Option<String>,
417
418 #[arg(
420 long,
421 env = "DISABLE_OPENAI_API",
422 global = true,
423 value_parser = parse_truthy
424 )]
425 pub disable_openai_api: bool,
426
427 #[arg(
429 long,
430 env = "DISABLE_ANTHROPIC_API",
431 global = true,
432 value_parser = parse_truthy
433 )]
434 pub disable_anthropic_api: bool,
435
436 #[arg(
439 long,
440 env = "DISABLE_METRICS",
441 global = true,
442 value_parser = parse_truthy
443 )]
444 pub disable_metrics: bool,
445
446 #[arg(
449 long,
450 env = "INFERENCE_ONLY",
451 global = true,
452 num_args = 0..=1,
453 default_value_t = false,
454 default_missing_value = "true",
455 value_parser = parse_truthy
456 )]
457 pub inference_only: bool,
458
459 #[arg(
461 long,
462 env = "ADDITIONAL_ACCOUNT_DIRS",
463 value_delimiter = ',',
464 global = true
465 )]
466 pub additional_account_dirs: Vec<PathBuf>,
467
468 #[arg(
470 long,
471 env = "ACCOUNT_ROUTING_STRATEGY",
472 default_value = "round-robin",
473 global = true
474 )]
475 pub account_routing_strategy: String,
476
477 #[arg(
479 long,
480 env = "ACCOUNT_COOLDOWN_SECS",
481 default_value_t = 60,
482 global = true
483 )]
484 pub account_cooldown_secs: u64,
485
486 #[arg(
488 long,
489 env = "SESSION_AFFINITY_TTL_SECS",
490 default_value_t = 3600,
491 global = true
492 )]
493 pub session_affinity_ttl_secs: u64,
494
495 #[arg(
497 long,
498 env = "ACCOUNT_REQUEST_LIMITS",
499 value_delimiter = ',',
500 global = true
501 )]
502 pub account_request_limits: Vec<usize>,
503
504 #[arg(
506 long,
507 env = "EXPERIMENTAL_COMPATIBILITY",
508 global = true,
509 value_parser = parse_truthy
510 )]
511 pub experimental_compatibility: bool,
512
513 #[arg(
515 long = "allow-subscription-bridge",
516 env = "SUBSCRIPTION_BRIDGE_OVERRIDES",
517 value_delimiter = ',',
518 global = true
519 )]
520 pub subscription_bridge_overrides: Vec<String>,
521 #[arg(
523 long = "allow-proxied-client",
524 env = "PROXIED_CLIENT_OVERRIDES",
525 value_delimiter = ',',
526 global = true
527 )]
528 pub proxied_client_overrides: Vec<String>,
529 #[arg(long, env = "TOKEN_ADMIN_KEY", global = true, hide_env_values = true)]
531 pub admin_key: Option<String>,
532 #[arg(long, env = "ADMIN_PORT", global = true)]
534 pub admin_port: Option<u16>,
535
536 #[arg(long, env = "ADMIN_HOST", default_value = "127.0.0.1", global = true)]
539 pub admin_host: String,
540
541 #[arg(
543 long,
544 env = "ADMIN_CLAIM_TTL_SECS",
545 default_value_t = crate::admin::DEFAULT_CANDIDATE_TTL_SECS,
546 global = true
547 )]
548 pub admin_claim_ttl_secs: u64,
549
550 #[arg(
561 long,
562 env = "ALLOW_ANONYMOUS_ADMIN",
563 global = true,
564 num_args = 0..=1,
565 default_value_t = false,
566 default_missing_value = "true",
567 value_parser = parse_truthy
568 )]
569 pub allow_anonymous_admin: bool,
570
571 #[arg(
575 long,
576 env = "TELEGRAM_BOT_TOKEN",
577 global = true,
578 hide_env_values = true
579 )]
580 pub telegram_bot_token: Option<String>,
581
582 #[arg(long, env = "VK_BOT_TOKEN", global = true, hide_env_values = true)]
584 pub vk_bot_token: Option<String>,
585
586 #[arg(long, env = "VK_GROUP_ID", global = true)]
589 pub vk_group_id: Option<u64>,
590
591 #[arg(
594 long,
595 env = "CHAT_ADMIN_SECRET_TTL_SECS",
596 default_value_t = crate::chat_admin::DEFAULT_SECRET_TTL_SECS,
597 global = true
598 )]
599 pub chat_admin_secret_ttl_secs: u64,
600
601 #[arg(
604 long,
605 env = "CHAT_ADMIN_RATE_LIMIT_PER_MINUTE",
606 default_value_t = crate::chat_admin::DEFAULT_RATE_LIMIT_PER_MINUTE,
607 global = true
608 )]
609 pub chat_admin_rate_limit_per_minute: u32,
610
611 #[arg(long, env = "MPP_ENABLE", global = true, value_parser = parse_truthy)]
613 pub mpp_enable: bool,
614
615 #[arg(long, env = "MPP_AMOUNT", default_value = "0.00", global = true)]
617 pub mpp_amount: String,
618
619 #[arg(long, env = "MPP_CURRENCY", default_value = "USD", global = true)]
621 pub mpp_currency: String,
622
623 #[arg(long, env = "MPP_RECIPIENT", global = true)]
625 pub mpp_recipient: Option<String>,
626
627 #[arg(long, env = "MPP_METHOD", global = true)]
629 pub mpp_method: Option<String>,
630
631 #[arg(
633 long,
634 env = "DISABLE_LOGIN_API",
635 global = true,
636 value_parser = parse_truthy
637 )]
638 pub disable_login_api: bool,
639
640 #[arg(
642 long,
643 env = "LOGIN_CLI_COMMAND",
644 default_value = "claude",
645 global = true
646 )]
647 pub login_cli_command: String,
648
649 #[arg(long, env = "LOGIN_CLI_ARGS", value_delimiter = ',', global = true)]
651 pub login_cli_args: Vec<String>,
652
653 #[arg(
655 long,
656 env = "LOGIN_SESSION_TTL_SECS",
657 default_value = "900",
658 global = true
659 )]
660 pub login_session_ttl_secs: u64,
661
662 #[arg(long, env = "LOGIN_MAX_SESSIONS", default_value = "4", global = true)]
664 pub login_max_sessions: usize,
665}
666
667#[derive(Debug, Subcommand)]
669pub enum Command {
670 Serve,
672 Tokens {
678 #[command(subcommand)]
679 op: TokenOp,
680 },
681 Accounts {
685 #[command(subcommand)]
686 op: AccountOp,
687 },
688 Providers {
692 #[command(subcommand)]
693 op: ProviderOp,
694 },
695 Clients {
700 #[command(subcommand)]
701 op: ClientOp,
702 },
703 With(WithArgs),
711 Configure(ConfigureArgs),
718 Server {
720 #[command(subcommand)]
721 op: ServerOp,
722 },
723 Auth {
725 #[command(subcommand)]
726 op: AuthOp,
727 },
728 Usage(UsageArgs),
730 Deploy(DeployArgs),
737 Doctor {
744 #[command(flatten)]
745 target: AuthTarget,
746 },
747 Tls {
749 #[command(subcommand)]
750 op: TlsOp,
751 },
752 Logs {
758 #[command(subcommand)]
759 op: LogsOp,
760 },
761}
762
763#[derive(Debug, Subcommand)]
765pub enum LogsOp {
766 Summary {
768 #[arg(long = "token-id", alias = "token", value_name = "HASHED_NAME")]
775 token: Option<String>,
776 #[arg(long)]
778 json: bool,
779 #[command(flatten)]
780 target: AuthTarget,
781 },
782 Anomalies {
786 #[arg(long = "token-id", alias = "token", value_name = "HASHED_NAME")]
788 token: Option<String>,
789 #[arg(long)]
791 json: bool,
792 #[command(flatten)]
793 target: AuthTarget,
794 },
795 Show {
797 correlation_id: String,
798 #[arg(long = "token-id", alias = "token", value_name = "HASHED_NAME")]
800 token: Option<String>,
801 #[command(flatten)]
802 target: AuthTarget,
803 },
804}
805
806#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
808pub enum AuthFlow {
809 #[default]
811 Auto,
812 Device,
814 Code,
816 Loopback,
818 Cli,
820}
821
822pub const CLAUDE_AUTH_FLOWS: [AuthFlow; 3] = [AuthFlow::Auto, AuthFlow::Code, AuthFlow::Cli];
824
825pub const CODEX_AUTH_FLOWS: [AuthFlow; 3] = [AuthFlow::Auto, AuthFlow::Device, AuthFlow::Loopback];
827
828fn auth_flow_parser(flows: &'static [AuthFlow]) -> impl TypedValueParser<Value = AuthFlow> {
829 PossibleValuesParser::new(flows.iter().filter_map(ValueEnum::to_possible_value)).map(|value| {
830 AuthFlow::from_str(&value, false)
831 .unwrap_or_else(|_| unreachable!("possible-values parser returned an unknown flow"))
832 })
833}
834
835impl Cli {
836 pub fn into_config(&self) -> Result<Config, ConfigError> {
838 let port = self.port.to_string();
839 let token_secret = self.token_secret.clone();
840 let process_home = std::env::var_os("HOME")
841 .filter(|home| !home.is_empty())
842 .map_or_else(|| PathBuf::from("/root"), PathBuf::from);
843 let client_home = self.home.clone().unwrap_or_else(|| process_home.clone());
844 let claude_home = self.claude_code_home.clone().unwrap_or_else(|| {
845 client_home
846 .join(SubscriptionProvider::Claude.home_subdir())
847 .to_string_lossy()
848 .into_owned()
849 });
850 let codex_home = if self.home.is_some() {
851 client_home.join(SubscriptionProvider::Codex.home_subdir())
852 } else {
853 SubscriptionProvider::Codex.resolve_home(&process_home.to_string_lossy())
854 };
855 let api_format = self
856 .api_format
857 .as_deref()
858 .map(|value| ApiFormat::from_str_opt(value).ok_or(ConfigError::InvalidApiFormat))
859 .transpose()?;
860 let routing_mode =
861 RoutingMode::from_str_opt(&self.routing_mode).ok_or(ConfigError::InvalidRoutingMode)?;
862 let upstream_provider = UpstreamProvider::from_str_opt(&self.upstream_provider)
863 .ok_or(ConfigError::InvalidUpstreamProvider)?;
864 let storage_policy = StoragePolicy::from_str_opt(&self.storage_policy)
865 .ok_or(ConfigError::InvalidStoragePolicy)?;
866 let account_routing_strategy =
867 crate::accounts::SelectionStrategy::from_str_opt(&self.account_routing_strategy)
868 .ok_or(ConfigError::InvalidAccountRoutingStrategy)?;
869 let data_dir = self.data_dir.clone().unwrap_or_else(default_data_dir);
870 let activitypub_actor_base_url = self
871 .activitypub_actor_base_url
872 .clone()
873 .unwrap_or_else(|| format!("http://{}:{}", self.host, self.port));
874 let crater_actor = self
875 .crater_forgefed_actor
876 .clone()
877 .filter(|value| !value.is_empty())
878 .unwrap_or_else(|| {
879 format!(
880 "{}/api/services/activitypub/actor/code",
881 activitypub_actor_base_url.trim_end_matches('/')
882 )
883 });
884 let crater = crate::crater::CraterConfig::new(
885 self.crater_forgefed_inbox
886 .clone()
887 .filter(|value| !value.is_empty()),
888 &crater_actor,
889 self.crater_forgefed_target
890 .clone()
891 .filter(|value| !value.is_empty()),
892 Duration::from_millis(self.crater_poll_interval_ms),
893 Duration::from_secs(self.crater_poll_timeout_secs),
894 );
895 let activitypub_public_key_pem = self
896 .activitypub_public_key_pem
897 .clone()
898 .unwrap_or_else(default_activitypub_public_key_pem);
899 let openai_compatible = crate::providers::OpenAICompatibleConfig {
900 provider_name: self.openai_compatible_provider_name.clone(),
901 base_url: self.openai_compatible_base_url.clone(),
902 api_key: self
903 .openai_compatible_api_key
904 .clone()
905 .filter(|s| !s.is_empty()),
906 api_key_env: self
907 .openai_compatible_api_key_env
908 .clone()
909 .filter(|s| !s.is_empty()),
910 default_model: self
911 .openai_compatible_model
912 .clone()
913 .filter(|s| !s.is_empty()),
914 models: self.openai_compatible_models.clone(),
915 supported_clients: self.openai_compatible_supported_clients.clone(),
916 };
917 let mut config = Config::build(BuildArgs {
918 host: &self.host,
919 port: &port,
920 token_secret: token_secret.as_deref(),
921 claude_code_home: &claude_home,
922 upstream_base_url: &self.upstream_base_url,
923 verbose: self.verbose,
924 max_proxy_request_bytes: self.max_proxy_request_bytes,
925 api_format,
926 routing_mode,
927 storage_policy,
928 data_dir,
929 claude_cli_bin: self.claude_cli_bin.clone(),
930 codex_cli_bin: self.codex_cli_bin.clone(),
931 upstream_provider,
932 gonka_private_key: self.gonka_private_key.clone().filter(|s| !s.is_empty()),
933 gonka_api_key: self.gonka_api_key.clone().filter(|s| !s.is_empty()),
934 gonka_source_url: self.gonka_source_url.clone().filter(|s| !s.is_empty()),
935 gonka_model: self.gonka_model.clone(),
936 bridge_model: self.bridge_model.clone().filter(|s| !s.is_empty()),
937 bridge_model_policy: self.bridge_model_policy.clone(),
938 audit_log: self
939 .audit_log
940 .as_ref()
941 .map(|p| p.to_string_lossy().into_owned())
942 .filter(|s| !s.is_empty()),
943 crater,
944 openai_compatible,
945 activitypub_actor_base_url,
946 activitypub_public_key_pem,
947 enable_openai_api: !self.disable_openai_api,
948 enable_anthropic_api: !self.disable_anthropic_api,
949 enable_metrics: !self.disable_metrics,
950 inference_only: self.inference_only,
951 listeners: self.listeners.clone(),
952 additional_account_dirs: self.additional_account_dirs.clone(),
953 account_routing_strategy,
954 account_cooldown_secs: self.account_cooldown_secs,
955 session_affinity_ttl_secs: self.session_affinity_ttl_secs,
956 account_request_limits: self.account_request_limits.clone(),
957 experimental_compatibility: self.experimental_compatibility,
958 subscription_bridge_overrides: self.subscription_bridge_overrides.clone(),
959 proxied_client_overrides: self.proxied_client_overrides.clone(),
960 admin_key: self.admin_key.clone().filter(|s| !s.is_empty()),
961 admin_ui: crate::config::admin_ui_config(
962 self.admin_port,
963 &self.admin_host,
964 self.admin_claim_ttl_secs,
965 )?,
966 allow_anonymous_admin: self.allow_anonymous_admin,
967 chat_admin: crate::config::chat_admin_config(
968 self.telegram_bot_token.clone(),
969 self.vk_bot_token.clone(),
970 self.vk_group_id,
971 self.chat_admin_secret_ttl_secs,
972 u64::from(self.chat_admin_rate_limit_per_minute),
973 ),
974 login: crate::login::LoginConfig {
975 enabled: !self.disable_login_api,
976 command: self.login_cli_command.clone(),
977 args: self.login_cli_args.clone(),
978 session_ttl: Duration::from_secs(self.login_session_ttl_secs),
979 max_sessions: self.login_max_sessions,
980 codex_home,
981 ..crate::login::LoginConfig::default()
982 },
983 mpp: crate::mpp::MppConfig {
984 enabled: self.mpp_enable,
985 amount: self.mpp_amount.clone(),
986 currency: self.mpp_currency.clone(),
987 recipient: self.mpp_recipient.clone().unwrap_or_default(),
988 method: self.mpp_method.clone().filter(|s| !s.is_empty()),
989 },
990 })?;
991 config.client_home = client_home;
992 config.isolated_client_home = self.home.is_some();
993 Ok(config)
994 }
995}
996
997#[cfg(test)]
998#[path = "cli_tests.rs"]
999mod tests;