@agxnte/reidx 2.0.2 → 2.0.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/reidx/__init__.py +1 -1
- package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/commands.py +5 -5
- package/src/reidx/config/__init__.py +3 -0
- package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/storage.cpython-312.pyc +0 -0
- package/src/reidx/config/loader.py +2 -1
- package/src/reidx/config/settings.py +2 -1
- package/src/reidx/config/storage.py +21 -0
- package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/provider/_http.py +13 -0
- package/src/reidx/provider/base.py +3 -0
- package/src/reidx/provider/ollama.py +14 -1
- package/src/reidx/provider/openai.py +15 -1
- package/src/reidx/provider/store.py +54 -4
- package/src/reidx/provider_manager/palette.py +39 -2
- package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/ui/app.py +4 -2
- package/src/reidx/ui/commands.py +64 -3
package/README.md
CHANGED
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/reidx/__init__.py
CHANGED
|
Binary file
|
|
@@ -16,9 +16,11 @@ from reidx import __version__
|
|
|
16
16
|
from reidx.automation.exec import exec_run
|
|
17
17
|
from reidx.config.loader import ConfigLoader
|
|
18
18
|
from reidx.config.models import Config
|
|
19
|
+
from reidx.config.storage import storage_root
|
|
19
20
|
from reidx.deepreid import format_markdown, run_deepreid, save_deepreid_result
|
|
20
21
|
from reidx.diagnostics.logger import get_logger
|
|
21
22
|
from reidx.provider.registry import default_registry
|
|
23
|
+
from reidx.provider.store import load_from_database
|
|
22
24
|
from reidx.provider.store import load_into as load_stored_providers
|
|
23
25
|
from reidx.runtime.orchestrator import Orchestrator
|
|
24
26
|
from reidx.tools import default_registry as tools_registry
|
|
@@ -97,11 +99,9 @@ def _main(
|
|
|
97
99
|
def build_orchestrator(config: Config | None = None) -> Orchestrator:
|
|
98
100
|
cfg = config or ConfigLoader().load()
|
|
99
101
|
providers = default_registry(cfg)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# session (that's a session setting, not a config change). Fall back to stub
|
|
104
|
-
# if the configured default isn't registered.
|
|
102
|
+
root = cfg.storage_root or storage_root()
|
|
103
|
+
load_stored_providers(providers, root)
|
|
104
|
+
load_from_database(providers, root)
|
|
105
105
|
default_name = cfg.default_provider if providers.has(cfg.default_provider) else "stub"
|
|
106
106
|
provider = providers.get(default_name)
|
|
107
107
|
return Orchestrator(cfg, provider, tools_registry(), providers=providers)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from reidx.config.loader import ConfigLoader
|
|
2
2
|
from reidx.config.models import Config, PolicyConfig, ProviderConfig, default_config
|
|
3
|
+
from reidx.config.storage import app_data_dir, storage_root
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
"Config",
|
|
6
7
|
"ConfigLoader",
|
|
7
8
|
"PolicyConfig",
|
|
8
9
|
"ProviderConfig",
|
|
10
|
+
"app_data_dir",
|
|
9
11
|
"default_config",
|
|
12
|
+
"storage_root",
|
|
10
13
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,11 +11,12 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
from reidx.config.models import Config, default_config
|
|
13
13
|
from reidx.config.settings import apply_settings_env, read_reidx_block
|
|
14
|
+
from reidx.config.storage import storage_root
|
|
14
15
|
from reidx.diagnostics.logger import get_logger
|
|
15
16
|
|
|
16
17
|
log = get_logger("reidx.config")
|
|
17
18
|
|
|
18
|
-
GLOBAL_DIR =
|
|
19
|
+
GLOBAL_DIR = storage_root()
|
|
19
20
|
PROJECT_DIR = Path(".reidx")
|
|
20
21
|
CONFIG_FILENAME = "config.json"
|
|
21
22
|
|
|
@@ -24,12 +24,13 @@ import json
|
|
|
24
24
|
import os
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
+
from reidx.config.storage import app_data_dir
|
|
27
28
|
from reidx.diagnostics.logger import get_logger
|
|
28
29
|
|
|
29
30
|
log = get_logger("reidx.config.settings")
|
|
30
31
|
|
|
31
32
|
LEGACY_SETTINGS_PATH = Path("E:/leech/Reidchat.json")
|
|
32
|
-
GLOBAL_SETTINGS_PATH =
|
|
33
|
+
GLOBAL_SETTINGS_PATH = app_data_dir() / "settings.json"
|
|
33
34
|
PROJECT_SETTINGS_FILENAME = "settings.json"
|
|
34
35
|
|
|
35
36
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def app_data_dir() -> Path:
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
base = os.environ.get("APPDATA")
|
|
11
|
+
if base:
|
|
12
|
+
return Path(base) / "Reid"
|
|
13
|
+
return Path.home() / ".reidx"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def storage_root() -> Path:
|
|
17
|
+
return app_data_dir()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def settings_file() -> Path:
|
|
21
|
+
return app_data_dir() / "settings.json"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -27,3 +27,16 @@ def post_json(url: str, payload: dict, headers: dict[str, str], timeout: int = T
|
|
|
27
27
|
except urllib.error.URLError as exc:
|
|
28
28
|
raise RuntimeError(f"connection error: {exc}") from exc
|
|
29
29
|
return json.loads(raw)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_json(url: str, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
|
|
33
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
34
|
+
try:
|
|
35
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
36
|
+
raw = resp.read().decode("utf-8")
|
|
37
|
+
except urllib.error.HTTPError as exc:
|
|
38
|
+
err_body = exc.read().decode("utf-8", errors="replace")[:500]
|
|
39
|
+
raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
|
|
40
|
+
except urllib.error.URLError as exc:
|
|
41
|
+
raise RuntimeError(f"connection error: {exc}") from exc
|
|
42
|
+
return json.loads(raw)
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from reidx.diagnostics.logger import get_logger
|
|
12
|
-
from reidx.provider._http import post_json
|
|
12
|
+
from reidx.provider._http import get_json, post_json
|
|
13
13
|
from reidx.provider.base import BaseProvider, Message, ProviderResponse, ToolCall, Usage
|
|
14
14
|
|
|
15
15
|
log = get_logger("reidx.provider.ollama")
|
|
@@ -107,3 +107,16 @@ class OllamaProvider(BaseProvider):
|
|
|
107
107
|
),
|
|
108
108
|
stop_reason=body.get("done_reason", "stop"),
|
|
109
109
|
)
|
|
110
|
+
|
|
111
|
+
def fetch_models(self) -> list[str]:
|
|
112
|
+
try:
|
|
113
|
+
body = get_json(f"{self.base_url}/api/tags", {})
|
|
114
|
+
except RuntimeError:
|
|
115
|
+
log.debug("failed to fetch models from ollama")
|
|
116
|
+
return []
|
|
117
|
+
models: list[str] = []
|
|
118
|
+
for item in body.get("models", []):
|
|
119
|
+
name = item.get("name", "")
|
|
120
|
+
if name:
|
|
121
|
+
models.append(name)
|
|
122
|
+
return sorted(models)
|
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from reidx.diagnostics.logger import get_logger
|
|
17
|
-
from reidx.provider._http import post_json
|
|
17
|
+
from reidx.provider._http import get_json, post_json
|
|
18
18
|
from reidx.provider.base import BaseProvider, Message, ProviderResponse, ToolCall, Usage
|
|
19
19
|
|
|
20
20
|
log = get_logger("reidx.provider.openai")
|
|
@@ -125,6 +125,20 @@ class OpenAIProvider(BaseProvider):
|
|
|
125
125
|
raise
|
|
126
126
|
return self._parse(body, model)
|
|
127
127
|
|
|
128
|
+
def fetch_models(self) -> list[str]:
|
|
129
|
+
url = f"{self.base_url}/v1/models"
|
|
130
|
+
try:
|
|
131
|
+
body = get_json(url, self._headers())
|
|
132
|
+
except RuntimeError:
|
|
133
|
+
log.debug("failed to fetch models from %s", url)
|
|
134
|
+
return []
|
|
135
|
+
models: list[str] = []
|
|
136
|
+
for item in body.get("data", []):
|
|
137
|
+
mid = item.get("id", "")
|
|
138
|
+
if mid:
|
|
139
|
+
models.append(mid)
|
|
140
|
+
return sorted(models)
|
|
141
|
+
|
|
128
142
|
|
|
129
143
|
class OpenAICompatibleProvider(OpenAIProvider):
|
|
130
144
|
"""OpenAI-compatible endpoints (llama.cpp server, LM Studio, vLLM, etc.).
|
|
@@ -125,11 +125,25 @@ class ProviderStore:
|
|
|
125
125
|
return True
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def validate_provider(record: ProviderRecord) -> tuple[bool, str]:
|
|
129
|
+
try:
|
|
130
|
+
provider = build_provider(record)
|
|
131
|
+
except (ValueError, TypeError) as exc:
|
|
132
|
+
return False, str(exc)
|
|
133
|
+
if not record.api_key and record.kind != "ollama":
|
|
134
|
+
return True, "no key required"
|
|
135
|
+
try:
|
|
136
|
+
models = provider.fetch_models()
|
|
137
|
+
except Exception:
|
|
138
|
+
return False, "could not connect to provider"
|
|
139
|
+
if models:
|
|
140
|
+
return True, f"ok ({len(models)} models available)"
|
|
141
|
+
if record.kind == "ollama":
|
|
142
|
+
return True, "connected"
|
|
143
|
+
return False, "key rejected or no models returned"
|
|
144
|
+
|
|
145
|
+
|
|
128
146
|
def load_into(registry: ProviderRegistry, storage_root: Path) -> list[str]:
|
|
129
|
-
"""Register every persisted provider into `registry`. Returns the list of
|
|
130
|
-
names successfully registered. Skips (with a warning) any record whose
|
|
131
|
-
kind is unknown or fails to construct — the app must not crash on a
|
|
132
|
-
single bad entry."""
|
|
133
147
|
added: list[str] = []
|
|
134
148
|
store = ProviderStore(storage_root)
|
|
135
149
|
for record in store.list():
|
|
@@ -139,3 +153,39 @@ def load_into(registry: ProviderRegistry, storage_root: Path) -> list[str]:
|
|
|
139
153
|
except (ValueError, TypeError):
|
|
140
154
|
log.exception("skipping provider %s (kind=%s): failed to build", record.name, record.kind)
|
|
141
155
|
return added
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def load_from_database(registry: ProviderRegistry, storage_root: Path) -> list[str]:
|
|
159
|
+
from reidx.provider_manager.database import ProviderDatabase
|
|
160
|
+
|
|
161
|
+
added: list[str] = []
|
|
162
|
+
db = ProviderDatabase(storage_root)
|
|
163
|
+
for sp in db.list_providers():
|
|
164
|
+
if registry.has(sp.name):
|
|
165
|
+
continue
|
|
166
|
+
api_key = sp.decrypted_api_key()
|
|
167
|
+
record = ProviderRecord(
|
|
168
|
+
name=sp.name,
|
|
169
|
+
kind=sp.kind,
|
|
170
|
+
base_url=sp.base_url,
|
|
171
|
+
api_key=api_key,
|
|
172
|
+
default_model=sp.default_model,
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
provider = build_provider(record)
|
|
176
|
+
except (ValueError, TypeError):
|
|
177
|
+
log.warning("skipping provider %s (kind=%s): failed to build", sp.name, sp.kind)
|
|
178
|
+
continue
|
|
179
|
+
if not record.default_model:
|
|
180
|
+
try:
|
|
181
|
+
models = provider.fetch_models()
|
|
182
|
+
except Exception:
|
|
183
|
+
models = []
|
|
184
|
+
if models:
|
|
185
|
+
provider.default_model = models[0]
|
|
186
|
+
sp.default_model = models[0]
|
|
187
|
+
db.save_provider(sp)
|
|
188
|
+
log.info("auto-fetched model for %s: %s", sp.name, models[0])
|
|
189
|
+
registry.register(sp.name, provider)
|
|
190
|
+
added.append(sp.name)
|
|
191
|
+
return added
|
|
@@ -9,7 +9,7 @@ from typing import Any
|
|
|
9
9
|
from prompt_toolkit.buffer import Buffer
|
|
10
10
|
|
|
11
11
|
from reidx.diagnostics.logger import get_logger
|
|
12
|
-
from reidx.provider.store import ProviderRecord, build_provider
|
|
12
|
+
from reidx.provider.store import ProviderRecord, build_provider, validate_provider
|
|
13
13
|
from reidx.provider_manager.catalog import (
|
|
14
14
|
ProviderDefinition,
|
|
15
15
|
all_providers,
|
|
@@ -602,6 +602,25 @@ class ProviderPalette:
|
|
|
602
602
|
auth = d.get("auth_method", "bearer")
|
|
603
603
|
api_key = d.get("api_key", "").strip()
|
|
604
604
|
|
|
605
|
+
if api_key:
|
|
606
|
+
record = ProviderRecord(
|
|
607
|
+
name=name, kind=kind, base_url=base_url,
|
|
608
|
+
api_key=api_key, default_model=model,
|
|
609
|
+
)
|
|
610
|
+
ok, msg = validate_provider(record)
|
|
611
|
+
if not ok:
|
|
612
|
+
self._show_message(f"Key validation failed: {msg}", self.WIZARD)
|
|
613
|
+
self.wizard_step_idx = len(WIZARD_STEPS) - 1
|
|
614
|
+
return
|
|
615
|
+
if not model:
|
|
616
|
+
try:
|
|
617
|
+
provider = build_provider(record)
|
|
618
|
+
models = provider.fetch_models()
|
|
619
|
+
except Exception:
|
|
620
|
+
models = []
|
|
621
|
+
if models:
|
|
622
|
+
model = models[0]
|
|
623
|
+
|
|
605
624
|
existing = self.db.get_provider(name)
|
|
606
625
|
if existing:
|
|
607
626
|
existing.kind = kind
|
|
@@ -647,10 +666,19 @@ class ProviderPalette:
|
|
|
647
666
|
if not self.current_provider:
|
|
648
667
|
return
|
|
649
668
|
name = self.current_provider.name
|
|
669
|
+
sp = self.current_provider
|
|
670
|
+
record = ProviderRecord(
|
|
671
|
+
name=name, kind=sp.kind, base_url=sp.base_url,
|
|
672
|
+
api_key=api_key, default_model=sp.default_model,
|
|
673
|
+
)
|
|
674
|
+
ok, msg = validate_provider(record)
|
|
675
|
+
if not ok:
|
|
676
|
+
self._show_message(f"Key validation failed: {msg}", self.MANAGE)
|
|
677
|
+
return
|
|
650
678
|
self.db.add_key(name, label, api_key)
|
|
651
679
|
self.current_provider = self.db.get_provider(name)
|
|
652
680
|
self._register_current()
|
|
653
|
-
self._show_message(f"Added key '{label}'", self.MANAGE)
|
|
681
|
+
self._show_message(f"Added key '{label}' ({msg})", self.MANAGE)
|
|
654
682
|
|
|
655
683
|
def _do_delete_key(self, key_id: str, label: str) -> str:
|
|
656
684
|
if not self.current_provider:
|
|
@@ -682,6 +710,15 @@ class ProviderPalette:
|
|
|
682
710
|
api_key=api_key, default_model=sp.default_model,
|
|
683
711
|
)
|
|
684
712
|
provider = build_provider(record)
|
|
713
|
+
if not sp.default_model:
|
|
714
|
+
try:
|
|
715
|
+
models = provider.fetch_models()
|
|
716
|
+
except Exception:
|
|
717
|
+
models = []
|
|
718
|
+
if models:
|
|
719
|
+
provider.default_model = models[0]
|
|
720
|
+
sp.default_model = models[0]
|
|
721
|
+
self.db.save_provider(sp)
|
|
685
722
|
self.orchestrator.providers.register(sp.name, provider)
|
|
686
723
|
except (ValueError, TypeError):
|
|
687
724
|
log.exception("failed to register provider %s", sp.name)
|
|
Binary file
|
package/src/reidx/ui/app.py
CHANGED
|
@@ -458,8 +458,10 @@ class ChatApp:
|
|
|
458
458
|
if self._palette is not None:
|
|
459
459
|
return
|
|
460
460
|
from pathlib import Path
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
|
|
462
|
+
from reidx.config.storage import storage_root as get_storage_root
|
|
463
|
+
root = self.orchestrator.config.storage_root or get_storage_root()
|
|
464
|
+
db = ProviderDatabase(Path(root))
|
|
463
465
|
self._palette = ProviderPalette(
|
|
464
466
|
db=db,
|
|
465
467
|
orchestrator=self.orchestrator,
|
package/src/reidx/ui/commands.py
CHANGED
|
@@ -10,15 +10,22 @@ from, so they can't drift out of sync.
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import uuid
|
|
14
14
|
|
|
15
15
|
from rich.console import Group
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
|
|
19
|
+
from reidx.config.storage import storage_root
|
|
19
20
|
from reidx.goals.models import Goal, GoalNodeKind, GoalStatus
|
|
20
21
|
from reidx.policy.models import PermissionMode
|
|
21
|
-
from reidx.provider.store import
|
|
22
|
+
from reidx.provider.store import (
|
|
23
|
+
SUPPORTED_KINDS,
|
|
24
|
+
ProviderRecord,
|
|
25
|
+
ProviderStore,
|
|
26
|
+
build_provider,
|
|
27
|
+
validate_provider,
|
|
28
|
+
)
|
|
22
29
|
from reidx.runtime.orchestrator import Orchestrator
|
|
23
30
|
from reidx.ui import render
|
|
24
31
|
from reidx.ui.theme import APP_NAME, BOX, PRIMARY
|
|
@@ -472,10 +479,48 @@ _BUILTIN_PROVIDER_NAMES = ("stub", "anthropic")
|
|
|
472
479
|
|
|
473
480
|
|
|
474
481
|
def _providers_store(orchestrator: Orchestrator) -> ProviderStore:
|
|
475
|
-
root = orchestrator.config.storage_root or (
|
|
482
|
+
root = orchestrator.config.storage_root or storage_root()
|
|
476
483
|
return ProviderStore(root)
|
|
477
484
|
|
|
478
485
|
|
|
486
|
+
def _save_to_database(orchestrator: Orchestrator, record: ProviderRecord) -> None:
|
|
487
|
+
from reidx.provider_manager import keychain
|
|
488
|
+
from reidx.provider_manager.database import ProviderDatabase, StoredKey, StoredProvider
|
|
489
|
+
|
|
490
|
+
root = orchestrator.config.storage_root or storage_root()
|
|
491
|
+
db = ProviderDatabase(root)
|
|
492
|
+
existing = db.get_provider(record.name)
|
|
493
|
+
if existing:
|
|
494
|
+
if record.api_key:
|
|
495
|
+
k = StoredKey(
|
|
496
|
+
id=uuid.uuid4().hex[:12],
|
|
497
|
+
label="Default",
|
|
498
|
+
encrypted_key=keychain.encrypt(record.api_key),
|
|
499
|
+
)
|
|
500
|
+
existing.keys.append(k)
|
|
501
|
+
if existing.active_key_id is None:
|
|
502
|
+
existing.active_key_id = k.id
|
|
503
|
+
existing.kind = record.kind
|
|
504
|
+
existing.base_url = record.base_url
|
|
505
|
+
if record.default_model:
|
|
506
|
+
existing.default_model = record.default_model
|
|
507
|
+
db.save_provider(existing)
|
|
508
|
+
else:
|
|
509
|
+
sp = StoredProvider(
|
|
510
|
+
name=record.name, kind=record.kind, base_url=record.base_url,
|
|
511
|
+
default_model=record.default_model,
|
|
512
|
+
)
|
|
513
|
+
if record.api_key:
|
|
514
|
+
k = StoredKey(
|
|
515
|
+
id=uuid.uuid4().hex[:12],
|
|
516
|
+
label="Default",
|
|
517
|
+
encrypted_key=keychain.encrypt(record.api_key),
|
|
518
|
+
)
|
|
519
|
+
sp.keys.append(k)
|
|
520
|
+
sp.active_key_id = k.id
|
|
521
|
+
db.save_provider(sp)
|
|
522
|
+
|
|
523
|
+
|
|
479
524
|
def _handle_providers(orchestrator: Orchestrator) -> None:
|
|
480
525
|
store = _providers_store(orchestrator)
|
|
481
526
|
persisted = store.list()
|
|
@@ -507,12 +552,28 @@ def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
|
|
|
507
552
|
api_key = parts[3] if len(parts) > 3 else ""
|
|
508
553
|
model = parts[4] if len(parts) > 4 else ""
|
|
509
554
|
record = ProviderRecord(name=name, kind=kind, base_url=base_url, api_key=api_key, default_model=model)
|
|
555
|
+
if api_key:
|
|
556
|
+
ok, msg = validate_provider(record)
|
|
557
|
+
if not ok:
|
|
558
|
+
render.print_error(f"key validation failed: {msg}")
|
|
559
|
+
return
|
|
560
|
+
render.print_info(f"key validated: {msg}")
|
|
510
561
|
try:
|
|
511
562
|
provider = build_provider(record)
|
|
512
563
|
except ValueError as exc:
|
|
513
564
|
render.print_error(f"failed to build provider: {exc}")
|
|
514
565
|
return
|
|
566
|
+
if not model and api_key:
|
|
567
|
+
try:
|
|
568
|
+
models = provider.fetch_models()
|
|
569
|
+
except Exception:
|
|
570
|
+
models = []
|
|
571
|
+
if models:
|
|
572
|
+
provider.default_model = models[0]
|
|
573
|
+
record.default_model = models[0]
|
|
574
|
+
render.print_info(f"auto-detected model: {models[0]}")
|
|
515
575
|
_providers_store(orchestrator).save(record)
|
|
576
|
+
_save_to_database(orchestrator, record)
|
|
516
577
|
if orchestrator.providers is not None:
|
|
517
578
|
orchestrator.providers.register(name, provider)
|
|
518
579
|
render.print_info(f"connected provider '{name}' ({kind}) → {base_url or '(default)'}")
|