minionsai 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,19 +66,49 @@ KNOWN_PROVIDER_PREFIXES = {
66
66
  "kimi",
67
67
  "kimi-coding",
68
68
  "minimax",
69
+ "minimax-cn",
69
70
  "mistral",
70
71
  "mistralai",
71
72
  "moonshotai",
72
73
  "nous",
74
+ "nvidia",
73
75
  "ollama",
74
76
  "ollama-cloud",
77
+ "opencode-go",
78
+ "opencode-zen",
75
79
  "openrouter",
76
80
  "qwen",
77
81
  "x-ai",
78
82
  "xai",
83
+ "xiaomi",
79
84
  "z-ai",
80
85
  "zai",
81
86
  }
87
+ PORTAL_PROVIDERS = {"nous", "opencode-zen", "opencode-go", "nvidia"}
88
+ LOCAL_SERVER_PROVIDERS = {
89
+ "lmstudio",
90
+ "lm-studio",
91
+ "ollama",
92
+ "llamacpp",
93
+ "llama-cpp",
94
+ "vllm",
95
+ "tabby",
96
+ "tabbyapi",
97
+ "koboldcpp",
98
+ "textgen",
99
+ "localai",
100
+ }
101
+ CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"
102
+ RUNTIME_MANAGED_PROVIDER_PREFIXES = {
103
+ "copilot",
104
+ "copilot-acp",
105
+ "google-gemini-cli",
106
+ "minimax-oauth",
107
+ "nous",
108
+ "openai-codex",
109
+ "qwen-oauth",
110
+ }
111
+ MODEL_RUNTIME_OVERRIDE_KEYS = ("base_url", "api_key", "api", "api_mode")
82
112
 
83
113
  _AGENT_DIR: Path | None = None
84
114
  _IMPORTS_READY = False
@@ -315,15 +345,23 @@ def _set_defaults(request: dict[str, Any]) -> dict[str, Any]:
315
345
  if isinstance(raw_model, str) and raw_model.strip():
316
346
  model_val = raw_model.strip()
317
347
  parsed = _parse_provider_model(model_val)
348
+ previous_provider = string_or_none(cfg["model"].get("provider"))
318
349
  if parsed:
319
350
  provider_hint, bare_model = parsed
320
351
  if provider_hint and not _provider_hint_is_available(provider_hint):
321
352
  _raise_invalid_provider(provider_hint)
322
353
  cfg["model"]["default"] = bare_model
323
354
  cfg["model"]["provider"] = provider_hint
355
+ if (
356
+ provider_hint != previous_provider
357
+ or provider_hint in RUNTIME_MANAGED_PROVIDER_PREFIXES
358
+ ):
359
+ _clear_model_runtime_overrides(cfg["model"])
324
360
  else:
325
361
  cfg["model"]["default"] = model_val
326
362
  cfg["model"].pop("provider", None)
363
+ if previous_provider and not previous_provider.startswith("custom"):
364
+ _clear_model_runtime_overrides(cfg["model"])
327
365
 
328
366
  if "reasoningEffort" in request:
329
367
  normalized = _normalize_reasoning(request["reasoningEffort"])
@@ -339,6 +377,11 @@ def _set_defaults(request: dict[str, Any]) -> dict[str, Any]:
339
377
  return _defaults_from_config(cfg)
340
378
 
341
379
 
380
+ def _clear_model_runtime_overrides(model_cfg: dict[str, Any]) -> None:
381
+ for key in MODEL_RUNTIME_OVERRIDE_KEYS:
382
+ model_cfg.pop(key, None)
383
+
384
+
342
385
  def _defaults_from_config(cfg: dict[str, Any] | None = None) -> dict[str, Any]:
343
386
  cfg = cfg if cfg is not None else _load_config()
344
387
  model_cfg = _model_section(cfg)
@@ -411,11 +454,39 @@ def _custom_provider_models(entry: dict[str, Any]) -> list[str]:
411
454
 
412
455
  def _parse_provider_model(raw: str) -> tuple[str, str] | None:
413
456
  if raw.startswith("@") and ":" in raw:
414
- provider, model = raw[1:].split(":", 1)
457
+ inner = raw[1:]
458
+ provider, model = inner.rsplit(":", 1)
459
+ if provider.startswith("custom:") and provider.count(":") >= 2:
460
+ slug_rest = provider[len("custom:"):]
461
+ if not _custom_slug_rest_looks_like_host_port(slug_rest):
462
+ provider, extra = provider.rsplit(":", 1)
463
+ model = f"{extra}:{model}"
464
+ elif provider not in KNOWN_PROVIDER_PREFIXES and not provider.startswith("custom:"):
465
+ provider, model = inner.split(":", 1)
415
466
  return provider, model
416
467
  return None
417
468
 
418
469
 
470
+ def _custom_slug_rest_looks_like_host_port(rest: str) -> bool:
471
+ rest = str(rest or "").strip()
472
+ if ":" not in rest:
473
+ return False
474
+ host, port_s = rest.rsplit(":", 1)
475
+ if not host or ":" in host or not port_s.isdigit():
476
+ return False
477
+ if not (1 <= int(port_s) <= 65535):
478
+ return False
479
+ try:
480
+ import ipaddress
481
+
482
+ ipaddress.ip_address(host)
483
+ return True
484
+ except ValueError:
485
+ pass
486
+ host_l = host.lower()
487
+ return host_l == "localhost" or "." in host
488
+
489
+
419
490
  def _provider_hint_is_available(provider: str) -> bool:
420
491
  if provider.startswith("custom:"):
421
492
  return True
@@ -495,6 +566,36 @@ def _groups_have_model(groups: dict[str, list[dict[str, Any]]], model_id: str) -
495
566
  )
496
567
 
497
568
 
569
+ def _is_local_server_provider(provider: str | None) -> bool:
570
+ provider_l = str(provider or "").strip().lower()
571
+ if provider_l in LOCAL_SERVER_PROVIDERS:
572
+ return True
573
+ if provider_l.startswith("custom:"):
574
+ return provider_l.removeprefix("custom:") in LOCAL_SERVER_PROVIDERS
575
+ return False
576
+
577
+
578
+ def _base_url_points_at_local_server(base_url: str | None) -> bool:
579
+ if not base_url:
580
+ return False
581
+ try:
582
+ import ipaddress
583
+ from urllib.parse import urlparse
584
+
585
+ host = (urlparse(base_url).hostname or "").lower()
586
+ if host in {"localhost", "ip6-localhost", "ip6-loopback"}:
587
+ return True
588
+ if not host:
589
+ return False
590
+ try:
591
+ addr = ipaddress.ip_address(host)
592
+ except ValueError:
593
+ return False
594
+ return addr.is_loopback or addr.is_private or addr.is_link_local
595
+ except Exception:
596
+ return False
597
+
598
+
498
599
  def _model_option_id(provider: str | None, model_id: str, active_provider: str | None) -> str:
499
600
  if not provider or provider == active_provider:
500
601
  return model_id
@@ -620,6 +721,7 @@ def _resolve_model_provider(requested_model: str | None, cfg: dict[str, Any] | N
620
721
  model_cfg = _model_section(cfg)
621
722
  config_provider = string_or_none(model_cfg.get("provider"))
622
723
  config_base_url = string_or_none(model_cfg.get("base_url"))
724
+ config_provider_l = (config_provider or "").lower()
623
725
  default_model = string_or_none(model_cfg.get("default"))
624
726
  model_id = (requested_model or default_model or "").strip()
625
727
 
@@ -643,17 +745,26 @@ def _resolve_model_provider(requested_model: str | None, cfg: dict[str, Any] | N
643
745
  if "/" in model_id:
644
746
  prefix, bare = model_id.split("/", 1)
645
747
  prefix_normalized = prefix.lower()
646
- if config_provider == "openrouter":
748
+ if config_provider_l == "openrouter":
647
749
  return model_id, "openrouter", config_base_url
648
- if config_provider and prefix_normalized == config_provider:
649
- return bare, config_provider, config_base_url
650
- if config_provider in {"nous", "opencode-zen", "opencode-go"}:
750
+ if config_provider_l in PORTAL_PROVIDERS:
651
751
  return model_id, config_provider, config_base_url
752
+ if config_provider and prefix_normalized == config_provider_l:
753
+ return bare, config_provider, config_base_url
754
+ if (
755
+ config_provider_l == "openai-codex"
756
+ and (config_base_url or "").rstrip("/") == CODEX_BASE_URL
757
+ and prefix_normalized in KNOWN_PROVIDER_PREFIXES
758
+ and prefix_normalized != config_provider_l
759
+ ):
760
+ return model_id, "openrouter", None
652
761
  if config_base_url:
762
+ if _is_local_server_provider(config_provider) or _base_url_points_at_local_server(config_base_url):
763
+ return model_id, config_provider, config_base_url
653
764
  if prefix_normalized in KNOWN_PROVIDER_PREFIXES:
654
765
  return bare, config_provider, config_base_url
655
766
  return model_id, config_provider, config_base_url
656
- if prefix_normalized in KNOWN_PROVIDER_PREFIXES and prefix_normalized != config_provider:
767
+ if prefix_normalized in KNOWN_PROVIDER_PREFIXES and prefix_normalized != config_provider_l:
657
768
  return model_id, "openrouter", None
658
769
 
659
770
  return model_id, config_provider, config_base_url
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minionsai",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Mission Control for Hermes Agent — a Kanban board for autonomous agent tasks",
5
5
  "license": "MIT",
6
6
  "type": "module",