@agxnte/reidx 2.0.2 → 2.0.4
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 +6 -6
- 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__/models.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/models.py +1 -0
- package/src/reidx/config/settings.py +12 -10
- package/src/reidx/config/storage.py +21 -0
- package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/anthropic.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__/registry.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/anthropic.py +26 -29
- package/src/reidx/provider/base.py +3 -0
- package/src/reidx/provider/ollama.py +14 -1
- package/src/reidx/provider/openai.py +40 -3
- package/src/reidx/provider/registry.py +1 -0
- package/src/reidx/provider/store.py +60 -4
- package/src/reidx/provider_manager/catalog.py +11 -8
- package/src/reidx/provider_manager/keychain.py +20 -3
- package/src/reidx/provider_manager/palette.py +42 -5
- 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 +65 -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)
|
|
@@ -207,7 +207,7 @@ def doctor() -> None:
|
|
|
207
207
|
console.print(f"storage {cfg.storage_root}")
|
|
208
208
|
console.print(f"provider {cfg.default_provider}")
|
|
209
209
|
console.print(f"mode {cfg.policy.default_mode.value}")
|
|
210
|
-
console.print(f"providers {orch.provider.name} (active), {orch.tools.definitions()
|
|
210
|
+
console.print(f"providers {orch.provider.name} (active), {len(orch.tools.definitions())} tools")
|
|
211
211
|
import os
|
|
212
212
|
|
|
213
213
|
has_key = bool(os.environ.get("ANTHROPIC_API_KEY", "").strip())
|
|
@@ -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
|
|
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
|
|
|
@@ -10,13 +10,14 @@ ignored harmlessly.
|
|
|
10
10
|
Path resolution (first hit wins):
|
|
11
11
|
1. $REIDCHAT_SETTINGS (explicit override)
|
|
12
12
|
2. ./settings.json (project-local, when it exists)
|
|
13
|
-
3.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
even when the shell's
|
|
13
|
+
3. ~/.reidx/settings.json (global default)
|
|
14
|
+
4. ~/Reidchat.json (legacy fallback)
|
|
15
|
+
|
|
16
|
+
Project-local wins over the global file so a project can bake in a different
|
|
17
|
+
backend, permission mode, or provider set without editing the global file.
|
|
18
|
+
The env block is authoritative for the process (overrides any ambient env)
|
|
19
|
+
— this is how ReidX picks up proxy credentials even when the shell's
|
|
20
|
+
ANTHROPIC_* vars point somewhere else.
|
|
20
21
|
"""
|
|
21
22
|
from __future__ import annotations
|
|
22
23
|
|
|
@@ -24,13 +25,14 @@ import json
|
|
|
24
25
|
import os
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
|
|
28
|
+
from reidx.config.storage import app_data_dir
|
|
27
29
|
from reidx.diagnostics.logger import get_logger
|
|
28
30
|
|
|
29
31
|
log = get_logger("reidx.config.settings")
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
GLOBAL_SETTINGS_PATH = Path.home() / ".reidx" / "settings.json"
|
|
33
|
+
GLOBAL_SETTINGS_PATH = app_data_dir() / "settings.json"
|
|
33
34
|
PROJECT_SETTINGS_FILENAME = "settings.json"
|
|
35
|
+
LEGACY_SETTINGS_PATH = Path.home() / "Reidchat.json"
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def _walk_upward_for_project_settings(start: Path) -> Path | None:
|
|
@@ -61,7 +63,7 @@ def settings_path() -> Path:
|
|
|
61
63
|
1. $REIDCHAT_SETTINGS (explicit override)
|
|
62
64
|
2. project settings.json (walk upward from CWD)
|
|
63
65
|
3. ~/.reidx/settings.json (global default)
|
|
64
|
-
4.
|
|
66
|
+
4. ~/Reidchat.json (legacy fallback)
|
|
65
67
|
|
|
66
68
|
Returns the last fallback even if it doesn't exist, so `doctor` can
|
|
67
69
|
report it as "missing" rather than crashing on a None.
|
|
@@ -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
|
|
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)
|
|
@@ -11,13 +11,11 @@ Env vars consumed:
|
|
|
11
11
|
"""
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import json
|
|
15
14
|
import os
|
|
16
|
-
import urllib.error
|
|
17
|
-
import urllib.request
|
|
18
15
|
from typing import Any
|
|
19
16
|
|
|
20
17
|
from reidx.diagnostics.logger import get_logger
|
|
18
|
+
from reidx.provider._http import get_json, post_json
|
|
21
19
|
from reidx.provider.base import BaseProvider, Message, ProviderResponse, ToolCall, Usage
|
|
22
20
|
|
|
23
21
|
log = get_logger("reidx.provider.anthropic")
|
|
@@ -25,7 +23,6 @@ log = get_logger("reidx.provider.anthropic")
|
|
|
25
23
|
DEFAULT_BASE_URL = "https://api.anthropic.com"
|
|
26
24
|
API_VERSION = "2023-06-01"
|
|
27
25
|
MAX_TOKENS = 4096
|
|
28
|
-
TIMEOUT_SECONDS = 120
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
class AnthropicProvider(BaseProvider):
|
|
@@ -53,6 +50,12 @@ class AnthropicProvider(BaseProvider):
|
|
|
53
50
|
model = os.environ.get("ANTHROPIC_MODEL", "").strip()
|
|
54
51
|
return cls(api_key=key, base_url=base, default_model=model)
|
|
55
52
|
|
|
53
|
+
def _headers(self) -> dict[str, str]:
|
|
54
|
+
return {
|
|
55
|
+
"x-api-key": self.api_key,
|
|
56
|
+
"anthropic-version": API_VERSION,
|
|
57
|
+
}
|
|
58
|
+
|
|
56
59
|
def _to_anthropic_messages(self, messages: list[Message]) -> tuple[str | None, list[dict]]:
|
|
57
60
|
"""Convert ReidX Messages to Anthropic format. Returns (system, messages)."""
|
|
58
61
|
system: str | None = None
|
|
@@ -62,7 +65,6 @@ class AnthropicProvider(BaseProvider):
|
|
|
62
65
|
system = m.content
|
|
63
66
|
continue
|
|
64
67
|
if m.role == "tool":
|
|
65
|
-
# Tool results -> Anthropic tool_result content block.
|
|
66
68
|
out.append({
|
|
67
69
|
"role": "user",
|
|
68
70
|
"content": [{
|
|
@@ -73,7 +75,6 @@ class AnthropicProvider(BaseProvider):
|
|
|
73
75
|
})
|
|
74
76
|
continue
|
|
75
77
|
if m.role == "assistant" and m.tool_calls:
|
|
76
|
-
# Assistant turn with tool calls -> content blocks.
|
|
77
78
|
blocks: list[dict] = []
|
|
78
79
|
if m.content:
|
|
79
80
|
blocks.append({"type": "text", "text": m.content})
|
|
@@ -86,7 +87,6 @@ class AnthropicProvider(BaseProvider):
|
|
|
86
87
|
})
|
|
87
88
|
out.append({"role": "assistant", "content": blocks})
|
|
88
89
|
continue
|
|
89
|
-
# Plain user or assistant text.
|
|
90
90
|
out.append({"role": m.role, "content": m.content})
|
|
91
91
|
return system, out
|
|
92
92
|
|
|
@@ -150,27 +150,24 @@ class AnthropicProvider(BaseProvider):
|
|
|
150
150
|
if anthropic_tools:
|
|
151
151
|
payload["tools"] = anthropic_tools
|
|
152
152
|
|
|
153
|
-
body = json.dumps(payload).encode("utf-8")
|
|
154
153
|
url = f"{self.base_url}/v1/messages"
|
|
155
|
-
req = urllib.request.Request(
|
|
156
|
-
url,
|
|
157
|
-
data=body,
|
|
158
|
-
headers={
|
|
159
|
-
"x-api-key": self.api_key,
|
|
160
|
-
"anthropic-version": API_VERSION,
|
|
161
|
-
"content-type": "application/json",
|
|
162
|
-
},
|
|
163
|
-
method="POST",
|
|
164
|
-
)
|
|
165
154
|
try:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
155
|
+
body = post_json(url, payload, self._headers())
|
|
156
|
+
except RuntimeError:
|
|
157
|
+
log.exception("Anthropic API request failed")
|
|
158
|
+
raise
|
|
159
|
+
return self._parse_response(body, model)
|
|
160
|
+
|
|
161
|
+
def fetch_models(self) -> list[str]:
|
|
162
|
+
url = f"{self.base_url}/v1/models"
|
|
163
|
+
try:
|
|
164
|
+
body = get_json(url, self._headers())
|
|
165
|
+
except RuntimeError:
|
|
166
|
+
log.debug("failed to fetch models from %s", url)
|
|
167
|
+
return []
|
|
168
|
+
models: list[str] = []
|
|
169
|
+
for item in body.get("data", []):
|
|
170
|
+
mid = item.get("id", "")
|
|
171
|
+
if mid:
|
|
172
|
+
models.append(mid)
|
|
173
|
+
return sorted(models)
|
|
@@ -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,19 +125,56 @@ 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.).
|
|
131
145
|
|
|
132
146
|
Same wire format as OpenAI; only default base_url differs and the API key
|
|
133
|
-
is optional.
|
|
134
|
-
|
|
147
|
+
is optional. Auth method is configurable so providers that use x-api-key
|
|
148
|
+
instead of Bearer are handled correctly. Kept as a subclass so `/providers`
|
|
149
|
+
can show the kind separately in the list.
|
|
135
150
|
"""
|
|
136
151
|
|
|
137
152
|
name = "openai-compatible"
|
|
138
153
|
DEFAULT_BASE_URL = "http://localhost:8080"
|
|
139
154
|
DEFAULT_MODEL = "local"
|
|
140
155
|
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
api_key: str,
|
|
159
|
+
base_url: str = "",
|
|
160
|
+
default_model: str = "",
|
|
161
|
+
auth_method: str = "bearer",
|
|
162
|
+
) -> None:
|
|
163
|
+
self.auth_method = auth_method
|
|
164
|
+
super().__init__(api_key=api_key, base_url=base_url, default_model=default_model)
|
|
165
|
+
|
|
166
|
+
def _headers(self) -> dict[str, str]:
|
|
167
|
+
h = {}
|
|
168
|
+
if not self.api_key:
|
|
169
|
+
return h
|
|
170
|
+
if self.auth_method == "x-api-key":
|
|
171
|
+
h["x-api-key"] = self.api_key
|
|
172
|
+
elif self.auth_method == "none":
|
|
173
|
+
pass
|
|
174
|
+
else:
|
|
175
|
+
h["authorization"] = f"Bearer {self.api_key}"
|
|
176
|
+
return h
|
|
177
|
+
|
|
141
178
|
|
|
142
179
|
def _json_dump(obj) -> str: # type: ignore[no-untyped-def]
|
|
143
180
|
import json
|
|
@@ -79,6 +79,7 @@ def default_registry(config: Config) -> ProviderRegistry:
|
|
|
79
79
|
base_url=pc.base_url or "",
|
|
80
80
|
api_key=pc.api_key.get_secret_value() if pc.api_key else "",
|
|
81
81
|
default_model=pc.default_model,
|
|
82
|
+
auth_method=pc.auth_method,
|
|
82
83
|
))
|
|
83
84
|
except (ValueError, TypeError):
|
|
84
85
|
log.warning("provider '%s' (kind=%s) configured but could not be built; skipping", name, kind)
|
|
@@ -39,6 +39,7 @@ class ProviderRecord:
|
|
|
39
39
|
base_url: str = ""
|
|
40
40
|
api_key: str = ""
|
|
41
41
|
default_model: str = ""
|
|
42
|
+
auth_method: str = "bearer"
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def build_provider(record: ProviderRecord) -> BaseProvider:
|
|
@@ -60,6 +61,7 @@ def build_provider(record: ProviderRecord) -> BaseProvider:
|
|
|
60
61
|
api_key=record.api_key,
|
|
61
62
|
base_url=record.base_url,
|
|
62
63
|
default_model=record.default_model,
|
|
64
|
+
auth_method=record.auth_method,
|
|
63
65
|
)
|
|
64
66
|
if kind == "ollama":
|
|
65
67
|
return OllamaProvider(
|
|
@@ -125,11 +127,28 @@ class ProviderStore:
|
|
|
125
127
|
return True
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
def validate_provider(record: ProviderRecord) -> tuple[bool, str]:
|
|
131
|
+
try:
|
|
132
|
+
provider = build_provider(record)
|
|
133
|
+
except (ValueError, TypeError) as exc:
|
|
134
|
+
return False, str(exc)
|
|
135
|
+
_keyless_kinds = ("ollama", "openai-compatible")
|
|
136
|
+
if not record.api_key and record.kind not in _keyless_kinds:
|
|
137
|
+
return False, "API key required for this provider kind"
|
|
138
|
+
if not record.api_key:
|
|
139
|
+
return True, "no key required"
|
|
140
|
+
try:
|
|
141
|
+
models = provider.fetch_models()
|
|
142
|
+
except Exception:
|
|
143
|
+
return False, "could not connect to provider"
|
|
144
|
+
if models:
|
|
145
|
+
return True, f"ok ({len(models)} models available)"
|
|
146
|
+
if record.kind == "ollama":
|
|
147
|
+
return True, "connected"
|
|
148
|
+
return False, "key rejected or no models returned"
|
|
149
|
+
|
|
150
|
+
|
|
128
151
|
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
152
|
added: list[str] = []
|
|
134
153
|
store = ProviderStore(storage_root)
|
|
135
154
|
for record in store.list():
|
|
@@ -139,3 +158,40 @@ def load_into(registry: ProviderRegistry, storage_root: Path) -> list[str]:
|
|
|
139
158
|
except (ValueError, TypeError):
|
|
140
159
|
log.exception("skipping provider %s (kind=%s): failed to build", record.name, record.kind)
|
|
141
160
|
return added
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def load_from_database(registry: ProviderRegistry, storage_root: Path) -> list[str]:
|
|
164
|
+
from reidx.provider_manager.database import ProviderDatabase
|
|
165
|
+
|
|
166
|
+
added: list[str] = []
|
|
167
|
+
db = ProviderDatabase(storage_root)
|
|
168
|
+
for sp in db.list_providers():
|
|
169
|
+
if registry.has(sp.name):
|
|
170
|
+
continue
|
|
171
|
+
api_key = sp.decrypted_api_key()
|
|
172
|
+
record = ProviderRecord(
|
|
173
|
+
name=sp.name,
|
|
174
|
+
kind=sp.kind,
|
|
175
|
+
base_url=sp.base_url,
|
|
176
|
+
api_key=api_key,
|
|
177
|
+
default_model=sp.default_model,
|
|
178
|
+
auth_method=sp.auth_method,
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
provider = build_provider(record)
|
|
182
|
+
except (ValueError, TypeError):
|
|
183
|
+
log.warning("skipping provider %s (kind=%s): failed to build", sp.name, sp.kind)
|
|
184
|
+
continue
|
|
185
|
+
if not record.default_model:
|
|
186
|
+
try:
|
|
187
|
+
models = provider.fetch_models()
|
|
188
|
+
except Exception:
|
|
189
|
+
models = []
|
|
190
|
+
if models:
|
|
191
|
+
provider.default_model = models[0]
|
|
192
|
+
sp.default_model = models[0]
|
|
193
|
+
db.save_provider(sp)
|
|
194
|
+
log.info("auto-fetched model for %s: %s", sp.name, models[0])
|
|
195
|
+
registry.register(sp.name, provider)
|
|
196
|
+
added.append(sp.name)
|
|
197
|
+
return added
|
|
@@ -113,8 +113,8 @@ _BUILTIN: list[ProviderDefinition] = [
|
|
|
113
113
|
default_model="auto", aliases=[], auth_method="none", icon="🚀",
|
|
114
114
|
),
|
|
115
115
|
ProviderDefinition(
|
|
116
|
-
id="azure-openai", name="Azure OpenAI", description="OpenAI models via Azure cloud",
|
|
117
|
-
kind="openai", base_url="", default_model="gpt-4o-mini",
|
|
116
|
+
id="azure-openai", name="Azure OpenAI", description="OpenAI models via Azure cloud (set base URL to your endpoint)",
|
|
117
|
+
kind="openai", base_url="https://api.openai.com", default_model="gpt-4o-mini",
|
|
118
118
|
aliases=["azure"], icon="☁️",
|
|
119
119
|
),
|
|
120
120
|
ProviderDefinition(
|
|
@@ -178,8 +178,9 @@ _BUILTIN: list[ProviderDefinition] = [
|
|
|
178
178
|
default_model="jamba-1.5-large", aliases=[], icon="🃏",
|
|
179
179
|
),
|
|
180
180
|
ProviderDefinition(
|
|
181
|
-
id="databricks", name="Databricks", description="Serving endpoints for fine-tuned models",
|
|
182
|
-
kind="openai-compatible", base_url="
|
|
181
|
+
id="databricks", name="Databricks", description="Serving endpoints for fine-tuned models (set base URL to your workspace)",
|
|
182
|
+
kind="openai-compatible", base_url="https://dbc-xxxxxxxx.cloud.databricks.com/serving-endpoints",
|
|
183
|
+
default_model="databricks-dbrx-instruct",
|
|
183
184
|
aliases=[], icon="🧱",
|
|
184
185
|
),
|
|
185
186
|
ProviderDefinition(
|
|
@@ -205,7 +206,7 @@ _BUILTIN: list[ProviderDefinition] = [
|
|
|
205
206
|
ProviderDefinition(
|
|
206
207
|
id="lambda", name="Lambda Labs", description="GPU cloud for AI training and inference",
|
|
207
208
|
kind="openai-compatible", base_url="https://api.lambdalabs.com/v1",
|
|
208
|
-
default_model="", aliases=["lambda-labs"], icon="λ",
|
|
209
|
+
default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=["lambda-labs"], icon="λ",
|
|
209
210
|
),
|
|
210
211
|
ProviderDefinition(
|
|
211
212
|
id="kluster", name="Kluster AI", description="Distributed GPU network for inference",
|
|
@@ -234,7 +235,8 @@ _BUILTIN: list[ProviderDefinition] = [
|
|
|
234
235
|
),
|
|
235
236
|
ProviderDefinition(
|
|
236
237
|
id="cloudflare-ai", name="Cloudflare Workers AI", description="Serverless AI inference at the edge",
|
|
237
|
-
kind="openai-compatible", base_url="
|
|
238
|
+
kind="openai-compatible", base_url="https://api.cloudflare.com/client/v4",
|
|
239
|
+
default_model="@cf/meta/llama-3.1-70b-instruct",
|
|
238
240
|
aliases=["cf-ai"], icon="orange",
|
|
239
241
|
),
|
|
240
242
|
ProviderDefinition(
|
|
@@ -245,7 +247,7 @@ _BUILTIN: list[ProviderDefinition] = [
|
|
|
245
247
|
ProviderDefinition(
|
|
246
248
|
id="baseten", name="Baseten", description="Model deployment and serving platform",
|
|
247
249
|
kind="openai-compatible", base_url="https://inference.baseten.co/v1",
|
|
248
|
-
default_model="", aliases=[], icon="🏗️",
|
|
250
|
+
default_model="meta-llama/Meta-Llama-3.1-70B-Instruct", aliases=[], icon="🏗️",
|
|
249
251
|
),
|
|
250
252
|
ProviderDefinition(
|
|
251
253
|
id="anyscale", name="Anyscale", description="Ray-based scalable model serving",
|
|
@@ -264,7 +266,8 @@ _BUILTIN: list[ProviderDefinition] = [
|
|
|
264
266
|
),
|
|
265
267
|
ProviderDefinition(
|
|
266
268
|
id="watsonx", name="IBM Watsonx", description="Enterprise AI platform with Granite models",
|
|
267
|
-
kind="openai-compatible", base_url="
|
|
269
|
+
kind="openai-compatible", base_url="https://us-south.ml.cloud.ibm.com",
|
|
270
|
+
default_model="ibm/granite-3-8b-instruct",
|
|
268
271
|
aliases=["ibm"], icon="🔷",
|
|
269
272
|
),
|
|
270
273
|
ProviderDefinition(
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
|
+
import getpass
|
|
5
|
+
import hashlib
|
|
4
6
|
import os
|
|
7
|
+
import socket
|
|
5
8
|
import sys
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
|
|
@@ -70,6 +73,21 @@ if _IS_WINDOWS:
|
|
|
70
73
|
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
|
71
74
|
|
|
72
75
|
|
|
76
|
+
def _machine_key() -> bytes:
|
|
77
|
+
parts = [
|
|
78
|
+
getpass.getuser(),
|
|
79
|
+
socket.gethostname(),
|
|
80
|
+
str(Path.home()),
|
|
81
|
+
sys.platform,
|
|
82
|
+
]
|
|
83
|
+
raw = "|".join(parts).encode("utf-8")
|
|
84
|
+
return hashlib.pbkdf2_hmac("sha256", raw, b"reidx-keychain-v1", 100000, dklen=32)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _xor_cipher(data: bytes, key: bytes) -> bytes:
|
|
88
|
+
return bytes(data[i] ^ key[i % len(key)] for i in range(len(data)))
|
|
89
|
+
|
|
90
|
+
|
|
73
91
|
def encrypt(plaintext: str) -> str:
|
|
74
92
|
if not plaintext:
|
|
75
93
|
return ""
|
|
@@ -77,8 +95,7 @@ def encrypt(plaintext: str) -> str:
|
|
|
77
95
|
if _IS_WINDOWS:
|
|
78
96
|
encrypted = _dpapi_encrypt(raw)
|
|
79
97
|
else:
|
|
80
|
-
|
|
81
|
-
encrypted = raw
|
|
98
|
+
encrypted = _xor_cipher(raw, _machine_key())
|
|
82
99
|
return base64.b64encode(encrypted).decode("ascii")
|
|
83
100
|
|
|
84
101
|
|
|
@@ -89,7 +106,7 @@ def decrypt(ciphertext: str) -> str:
|
|
|
89
106
|
if _IS_WINDOWS:
|
|
90
107
|
decrypted = _dpapi_decrypt(raw)
|
|
91
108
|
else:
|
|
92
|
-
decrypted = raw
|
|
109
|
+
decrypted = _xor_cipher(raw, _machine_key())
|
|
93
110
|
return decrypted.decode("utf-8")
|
|
94
111
|
|
|
95
112
|
|
|
@@ -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
|
|
@@ -609,13 +628,12 @@ class ProviderPalette:
|
|
|
609
628
|
existing.default_model = model
|
|
610
629
|
existing.auth_method = auth
|
|
611
630
|
if api_key:
|
|
631
|
+
from reidx.provider_manager import keychain
|
|
612
632
|
existing.keys.append(StoredKey(
|
|
613
633
|
id=uuid.uuid4().hex[:12],
|
|
614
634
|
label="Default",
|
|
615
|
-
encrypted_key=
|
|
635
|
+
encrypted_key=keychain.encrypt(api_key),
|
|
616
636
|
))
|
|
617
|
-
from reidx.provider_manager import keychain
|
|
618
|
-
existing.keys[-1].encrypted_key = keychain.encrypt(api_key)
|
|
619
637
|
if existing.active_key_id is None:
|
|
620
638
|
existing.active_key_id = existing.keys[-1].id
|
|
621
639
|
self.db.save_provider(existing)
|
|
@@ -647,10 +665,19 @@ class ProviderPalette:
|
|
|
647
665
|
if not self.current_provider:
|
|
648
666
|
return
|
|
649
667
|
name = self.current_provider.name
|
|
668
|
+
sp = self.current_provider
|
|
669
|
+
record = ProviderRecord(
|
|
670
|
+
name=name, kind=sp.kind, base_url=sp.base_url,
|
|
671
|
+
api_key=api_key, default_model=sp.default_model,
|
|
672
|
+
)
|
|
673
|
+
ok, msg = validate_provider(record)
|
|
674
|
+
if not ok:
|
|
675
|
+
self._show_message(f"Key validation failed: {msg}", self.MANAGE)
|
|
676
|
+
return
|
|
650
677
|
self.db.add_key(name, label, api_key)
|
|
651
678
|
self.current_provider = self.db.get_provider(name)
|
|
652
679
|
self._register_current()
|
|
653
|
-
self._show_message(f"Added key '{label}'", self.MANAGE)
|
|
680
|
+
self._show_message(f"Added key '{label}' ({msg})", self.MANAGE)
|
|
654
681
|
|
|
655
682
|
def _do_delete_key(self, key_id: str, label: str) -> str:
|
|
656
683
|
if not self.current_provider:
|
|
@@ -680,8 +707,18 @@ class ProviderPalette:
|
|
|
680
707
|
record = ProviderRecord(
|
|
681
708
|
name=sp.name, kind=sp.kind, base_url=sp.base_url,
|
|
682
709
|
api_key=api_key, default_model=sp.default_model,
|
|
710
|
+
auth_method=sp.auth_method,
|
|
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,49 @@ _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
|
+
existing.auth_method = record.auth_method
|
|
506
|
+
if record.default_model:
|
|
507
|
+
existing.default_model = record.default_model
|
|
508
|
+
db.save_provider(existing)
|
|
509
|
+
else:
|
|
510
|
+
sp = StoredProvider(
|
|
511
|
+
name=record.name, kind=record.kind, base_url=record.base_url,
|
|
512
|
+
default_model=record.default_model, auth_method=record.auth_method,
|
|
513
|
+
)
|
|
514
|
+
if record.api_key:
|
|
515
|
+
k = StoredKey(
|
|
516
|
+
id=uuid.uuid4().hex[:12],
|
|
517
|
+
label="Default",
|
|
518
|
+
encrypted_key=keychain.encrypt(record.api_key),
|
|
519
|
+
)
|
|
520
|
+
sp.keys.append(k)
|
|
521
|
+
sp.active_key_id = k.id
|
|
522
|
+
db.save_provider(sp)
|
|
523
|
+
|
|
524
|
+
|
|
479
525
|
def _handle_providers(orchestrator: Orchestrator) -> None:
|
|
480
526
|
store = _providers_store(orchestrator)
|
|
481
527
|
persisted = store.list()
|
|
@@ -507,12 +553,28 @@ def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
|
|
|
507
553
|
api_key = parts[3] if len(parts) > 3 else ""
|
|
508
554
|
model = parts[4] if len(parts) > 4 else ""
|
|
509
555
|
record = ProviderRecord(name=name, kind=kind, base_url=base_url, api_key=api_key, default_model=model)
|
|
556
|
+
if api_key:
|
|
557
|
+
ok, msg = validate_provider(record)
|
|
558
|
+
if not ok:
|
|
559
|
+
render.print_error(f"key validation failed: {msg}")
|
|
560
|
+
return
|
|
561
|
+
render.print_info(f"key validated: {msg}")
|
|
510
562
|
try:
|
|
511
563
|
provider = build_provider(record)
|
|
512
564
|
except ValueError as exc:
|
|
513
565
|
render.print_error(f"failed to build provider: {exc}")
|
|
514
566
|
return
|
|
567
|
+
if not model and api_key:
|
|
568
|
+
try:
|
|
569
|
+
models = provider.fetch_models()
|
|
570
|
+
except Exception:
|
|
571
|
+
models = []
|
|
572
|
+
if models:
|
|
573
|
+
provider.default_model = models[0]
|
|
574
|
+
record.default_model = models[0]
|
|
575
|
+
render.print_info(f"auto-detected model: {models[0]}")
|
|
515
576
|
_providers_store(orchestrator).save(record)
|
|
577
|
+
_save_to_database(orchestrator, record)
|
|
516
578
|
if orchestrator.providers is not None:
|
|
517
579
|
orchestrator.providers.register(name, provider)
|
|
518
580
|
render.print_info(f"connected provider '{name}' ({kind}) → {base_url or '(default)'}")
|