@agxnte/reidx 2.0.4 → 2.0.6
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/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/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/anthropic.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 +75 -42
- package/src/reidx/provider/anthropic.py +1 -5
- package/src/reidx/provider/ollama.py +1 -5
- package/src/reidx/provider/openai.py +1 -5
- package/src/reidx/provider/store.py +16 -6
- package/src/reidx/provider_manager/palette.py +39 -5
- package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/ui/commands.py +11 -4
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/reidx/__init__.py
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,42 +1,75 @@
|
|
|
1
|
-
"""Shared HTTP helper for provider clients — stdlib urllib, no extra deps."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
"""Shared HTTP helper for provider clients — stdlib urllib, no extra deps."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import ssl
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
|
|
10
|
+
TIMEOUT_SECONDS = 120
|
|
11
|
+
|
|
12
|
+
_SSL_CONTEXT: ssl.SSLContext | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _build_ssl_context() -> ssl.SSLContext:
|
|
16
|
+
insecure = os.environ.get("REIDX_INSECURE", "").strip().lower() in ("1", "true", "yes", "on")
|
|
17
|
+
if insecure:
|
|
18
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
19
|
+
ctx.check_hostname = False
|
|
20
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
21
|
+
return ctx
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import certifi
|
|
25
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
26
|
+
except ImportError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
ctx = ssl.create_default_context()
|
|
30
|
+
try:
|
|
31
|
+
ctx.load_default_certs(ssl.Purpose.SERVER_AUTH)
|
|
32
|
+
except ssl.SSLError:
|
|
33
|
+
pass
|
|
34
|
+
return ctx
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ssl_ctx() -> ssl.SSLContext:
|
|
38
|
+
global _SSL_CONTEXT
|
|
39
|
+
if _SSL_CONTEXT is None:
|
|
40
|
+
_SSL_CONTEXT = _build_ssl_context()
|
|
41
|
+
return _SSL_CONTEXT
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def post_json(url: str, payload: dict, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
|
|
45
|
+
"""POST a JSON payload, return the parsed JSON response.
|
|
46
|
+
|
|
47
|
+
Raises RuntimeError on HTTP or network errors — providers surface those
|
|
48
|
+
to the agent loop, which turns them into tool-result errors rather than
|
|
49
|
+
crashing the turn.
|
|
50
|
+
"""
|
|
51
|
+
body = json.dumps(payload).encode("utf-8")
|
|
52
|
+
hdrs = {"content-type": "application/json", **headers}
|
|
53
|
+
req = urllib.request.Request(url, data=body, headers=hdrs, method="POST")
|
|
54
|
+
try:
|
|
55
|
+
with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx()) as resp:
|
|
56
|
+
raw = resp.read().decode("utf-8")
|
|
57
|
+
except urllib.error.HTTPError as exc:
|
|
58
|
+
err_body = exc.read().decode("utf-8", errors="replace")[:500]
|
|
59
|
+
raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
|
|
60
|
+
except urllib.error.URLError as exc:
|
|
61
|
+
raise RuntimeError(f"connection error: {exc}") from exc
|
|
62
|
+
return json.loads(raw)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_json(url: str, headers: dict[str, str], timeout: int = TIMEOUT_SECONDS) -> dict:
|
|
66
|
+
req = urllib.request.Request(url, headers=headers, method="GET")
|
|
67
|
+
try:
|
|
68
|
+
with urllib.request.urlopen(req, timeout=timeout, context=_ssl_ctx()) as resp:
|
|
69
|
+
raw = resp.read().decode("utf-8")
|
|
70
|
+
except urllib.error.HTTPError as exc:
|
|
71
|
+
err_body = exc.read().decode("utf-8", errors="replace")[:500]
|
|
72
|
+
raise RuntimeError(f"HTTP {exc.code}: {err_body}") from exc
|
|
73
|
+
except urllib.error.URLError as exc:
|
|
74
|
+
raise RuntimeError(f"connection error: {exc}") from exc
|
|
75
|
+
return json.loads(raw)
|
|
@@ -160,11 +160,7 @@ class AnthropicProvider(BaseProvider):
|
|
|
160
160
|
|
|
161
161
|
def fetch_models(self) -> list[str]:
|
|
162
162
|
url = f"{self.base_url}/v1/models"
|
|
163
|
-
|
|
164
|
-
body = get_json(url, self._headers())
|
|
165
|
-
except RuntimeError:
|
|
166
|
-
log.debug("failed to fetch models from %s", url)
|
|
167
|
-
return []
|
|
163
|
+
body = get_json(url, self._headers())
|
|
168
164
|
models: list[str] = []
|
|
169
165
|
for item in body.get("data", []):
|
|
170
166
|
mid = item.get("id", "")
|
|
@@ -109,11 +109,7 @@ class OllamaProvider(BaseProvider):
|
|
|
109
109
|
)
|
|
110
110
|
|
|
111
111
|
def fetch_models(self) -> list[str]:
|
|
112
|
-
|
|
113
|
-
body = get_json(f"{self.base_url}/api/tags", {})
|
|
114
|
-
except RuntimeError:
|
|
115
|
-
log.debug("failed to fetch models from ollama")
|
|
116
|
-
return []
|
|
112
|
+
body = get_json(f"{self.base_url}/api/tags", {})
|
|
117
113
|
models: list[str] = []
|
|
118
114
|
for item in body.get("models", []):
|
|
119
115
|
name = item.get("name", "")
|
|
@@ -127,11 +127,7 @@ class OpenAIProvider(BaseProvider):
|
|
|
127
127
|
|
|
128
128
|
def fetch_models(self) -> list[str]:
|
|
129
129
|
url = f"{self.base_url}/v1/models"
|
|
130
|
-
|
|
131
|
-
body = get_json(url, self._headers())
|
|
132
|
-
except RuntimeError:
|
|
133
|
-
log.debug("failed to fetch models from %s", url)
|
|
134
|
-
return []
|
|
130
|
+
body = get_json(url, self._headers())
|
|
135
131
|
models: list[str] = []
|
|
136
132
|
for item in body.get("data", []):
|
|
137
133
|
mid = item.get("id", "")
|
|
@@ -127,7 +127,10 @@ class ProviderStore:
|
|
|
127
127
|
return True
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
def validate_provider(
|
|
130
|
+
def validate_provider(
|
|
131
|
+
record: ProviderRecord,
|
|
132
|
+
skip_verify: bool = False,
|
|
133
|
+
) -> tuple[bool, str]:
|
|
131
134
|
try:
|
|
132
135
|
provider = build_provider(record)
|
|
133
136
|
except (ValueError, TypeError) as exc:
|
|
@@ -137,15 +140,22 @@ def validate_provider(record: ProviderRecord) -> tuple[bool, str]:
|
|
|
137
140
|
return False, "API key required for this provider kind"
|
|
138
141
|
if not record.api_key:
|
|
139
142
|
return True, "no key required"
|
|
143
|
+
if skip_verify:
|
|
144
|
+
return True, "verification skipped"
|
|
140
145
|
try:
|
|
141
146
|
models = provider.fetch_models()
|
|
142
|
-
except
|
|
143
|
-
|
|
147
|
+
except RuntimeError as exc:
|
|
148
|
+
msg = str(exc)
|
|
149
|
+
if msg.startswith("HTTP 401") or msg.startswith("HTTP 403"):
|
|
150
|
+
return False, f"authentication failed ({msg})"
|
|
151
|
+
if msg.startswith("HTTP "):
|
|
152
|
+
return False, f"provider error: {msg}"
|
|
153
|
+
return False, f"{msg} (use --skip-verify to save anyway)"
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
return False, f"unexpected error: {exc} (use --skip-verify to save anyway)"
|
|
144
156
|
if models:
|
|
145
157
|
return True, f"ok ({len(models)} models available)"
|
|
146
|
-
|
|
147
|
-
return True, "connected"
|
|
148
|
-
return False, "key rejected or no models returned"
|
|
158
|
+
return True, "connected (no models endpoint or empty list)"
|
|
149
159
|
|
|
150
160
|
|
|
151
161
|
def load_into(registry: ProviderRegistry, storage_root: Path) -> list[str]:
|
|
@@ -606,11 +606,18 @@ class ProviderPalette:
|
|
|
606
606
|
record = ProviderRecord(
|
|
607
607
|
name=name, kind=kind, base_url=base_url,
|
|
608
608
|
api_key=api_key, default_model=model,
|
|
609
|
+
auth_method=auth,
|
|
609
610
|
)
|
|
610
611
|
ok, msg = validate_provider(record)
|
|
611
612
|
if not ok:
|
|
612
|
-
self.
|
|
613
|
-
self.
|
|
613
|
+
self._confirm_text = f"Key validation failed: {msg}\nSave anyway?"
|
|
614
|
+
self._pending_action = lambda: self._commit_wizard_provider(
|
|
615
|
+
name, kind, base_url, model, auth, api_key, forced_msg=msg,
|
|
616
|
+
)
|
|
617
|
+
self.screen = self.CONFIRM
|
|
618
|
+
self.selected_index = 0
|
|
619
|
+
self._scroll_offset = 0
|
|
620
|
+
self._invalidate()
|
|
614
621
|
return
|
|
615
622
|
if not model:
|
|
616
623
|
try:
|
|
@@ -621,6 +628,18 @@ class ProviderPalette:
|
|
|
621
628
|
if models:
|
|
622
629
|
model = models[0]
|
|
623
630
|
|
|
631
|
+
self._commit_wizard_provider(name, kind, base_url, model, auth, api_key)
|
|
632
|
+
|
|
633
|
+
def _commit_wizard_provider(
|
|
634
|
+
self,
|
|
635
|
+
name: str,
|
|
636
|
+
kind: str,
|
|
637
|
+
base_url: str,
|
|
638
|
+
model: str,
|
|
639
|
+
auth: str,
|
|
640
|
+
api_key: str,
|
|
641
|
+
forced_msg: str = "",
|
|
642
|
+
) -> str:
|
|
624
643
|
existing = self.db.get_provider(name)
|
|
625
644
|
if existing:
|
|
626
645
|
existing.kind = kind
|
|
@@ -659,7 +678,9 @@ class ProviderPalette:
|
|
|
659
678
|
self.wizard_data = {}
|
|
660
679
|
self.wizard_step_idx = 0
|
|
661
680
|
self.input_is_password = False
|
|
662
|
-
|
|
681
|
+
note = " (unverified)" if forced_msg else ""
|
|
682
|
+
self._close(f"Saved provider '{name}' ({kind}){note}")
|
|
683
|
+
return f"Saved provider '{name}'"
|
|
663
684
|
|
|
664
685
|
def _do_add_key(self, label: str, api_key: str) -> None:
|
|
665
686
|
if not self.current_provider:
|
|
@@ -669,15 +690,28 @@ class ProviderPalette:
|
|
|
669
690
|
record = ProviderRecord(
|
|
670
691
|
name=name, kind=sp.kind, base_url=sp.base_url,
|
|
671
692
|
api_key=api_key, default_model=sp.default_model,
|
|
693
|
+
auth_method=sp.auth_method,
|
|
672
694
|
)
|
|
673
695
|
ok, msg = validate_provider(record)
|
|
674
|
-
if
|
|
675
|
-
self.
|
|
696
|
+
if ok:
|
|
697
|
+
self._commit_key(label, api_key, msg)
|
|
676
698
|
return
|
|
699
|
+
self._confirm_text = f"Key validation failed: {msg}\nSave anyway?"
|
|
700
|
+
self._pending_action = lambda: self._commit_key(label, api_key, "saved (unverified)")
|
|
701
|
+
self.screen = self.CONFIRM
|
|
702
|
+
self.selected_index = 0
|
|
703
|
+
self._scroll_offset = 0
|
|
704
|
+
self._invalidate()
|
|
705
|
+
|
|
706
|
+
def _commit_key(self, label: str, api_key: str, msg: str) -> str:
|
|
707
|
+
if not self.current_provider:
|
|
708
|
+
return "Error: no provider"
|
|
709
|
+
name = self.current_provider.name
|
|
677
710
|
self.db.add_key(name, label, api_key)
|
|
678
711
|
self.current_provider = self.db.get_provider(name)
|
|
679
712
|
self._register_current()
|
|
680
713
|
self._show_message(f"Added key '{label}' ({msg})", self.MANAGE)
|
|
714
|
+
return f"Added key '{label}'"
|
|
681
715
|
|
|
682
716
|
def _do_delete_key(self, key_id: str, label: str) -> str:
|
|
683
717
|
if not self.current_provider:
|
|
Binary file
|
package/src/reidx/ui/commands.py
CHANGED
|
@@ -536,10 +536,17 @@ def _handle_providers(orchestrator: Orchestrator) -> None:
|
|
|
536
536
|
|
|
537
537
|
|
|
538
538
|
def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
|
|
539
|
-
|
|
539
|
+
skip_verify = False
|
|
540
|
+
tokens = arg.split()
|
|
541
|
+
parts: list[str] = []
|
|
542
|
+
for tok in tokens:
|
|
543
|
+
if tok in ("--skip-verify", "--skip", "--no-verify"):
|
|
544
|
+
skip_verify = True
|
|
545
|
+
else:
|
|
546
|
+
parts.append(tok)
|
|
540
547
|
if len(parts) < 3:
|
|
541
548
|
render.print_error(
|
|
542
|
-
"usage: /connect <name> <kind> <base_url> [api_key] [model] "
|
|
549
|
+
"usage: /connect <name> <kind> <base_url> [api_key] [model] [--skip-verify] "
|
|
543
550
|
f"(kind: {'|'.join(SUPPORTED_KINDS)})"
|
|
544
551
|
)
|
|
545
552
|
return
|
|
@@ -554,7 +561,7 @@ def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
|
|
|
554
561
|
model = parts[4] if len(parts) > 4 else ""
|
|
555
562
|
record = ProviderRecord(name=name, kind=kind, base_url=base_url, api_key=api_key, default_model=model)
|
|
556
563
|
if api_key:
|
|
557
|
-
ok, msg = validate_provider(record)
|
|
564
|
+
ok, msg = validate_provider(record, skip_verify=skip_verify)
|
|
558
565
|
if not ok:
|
|
559
566
|
render.print_error(f"key validation failed: {msg}")
|
|
560
567
|
return
|
|
@@ -564,7 +571,7 @@ def _handle_connect(orchestrator: Orchestrator, arg: str) -> None:
|
|
|
564
571
|
except ValueError as exc:
|
|
565
572
|
render.print_error(f"failed to build provider: {exc}")
|
|
566
573
|
return
|
|
567
|
-
if not model and api_key:
|
|
574
|
+
if not model and api_key and not skip_verify:
|
|
568
575
|
try:
|
|
569
576
|
models = provider.fetch_models()
|
|
570
577
|
except Exception:
|