@agxnte/reidx 2.0.3 → 2.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agxnte/reidx",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Terminal-native personal intelligence and coding CLI (agent-first runtime)",
5
5
  "bin": {
6
6
  "reid": "bin/reidx.js"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reidx"
3
- version = "2.0.3"
3
+ version = "2.0.5"
4
4
  description = "Terminal-native personal intelligence and coding CLI (agent-first runtime)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,3 +1,3 @@
1
1
  """ReidX: terminal-native agent-first CLI runtime."""
2
2
 
3
- __version__ = "2.0.3"
3
+ __version__ = "2.0.5"
@@ -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().__len__()} tools")
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())
@@ -19,6 +19,7 @@ class ProviderConfig(BaseModel):
19
19
  base_url: str | None = None
20
20
  api_key: SecretStr | None = None
21
21
  default_model: str = ""
22
+ auth_method: str = "bearer"
22
23
 
23
24
 
24
25
  class PolicyConfig(BaseModel):
@@ -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. E:/leech/Reidchat.json (legacy default — the shared Reidchat file)
14
-
15
- Project-local wins over the shared Reidchat file so a project can bake in a
16
- different backend, permission mode, or provider set without editing the
17
- global file. The env block is authoritative for the process (overrides any
18
- ambient env) this is how ReidX picks up the Reidchat proxy credentials
19
- even when the shell's ANTHROPIC_* vars point somewhere else.
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
 
@@ -29,9 +30,9 @@ from reidx.diagnostics.logger import get_logger
29
30
 
30
31
  log = get_logger("reidx.config.settings")
31
32
 
32
- LEGACY_SETTINGS_PATH = Path("E:/leech/Reidchat.json")
33
33
  GLOBAL_SETTINGS_PATH = app_data_dir() / "settings.json"
34
34
  PROJECT_SETTINGS_FILENAME = "settings.json"
35
+ LEGACY_SETTINGS_PATH = Path.home() / "Reidchat.json"
35
36
 
36
37
 
37
38
  def _walk_upward_for_project_settings(start: Path) -> Path | None:
@@ -62,7 +63,7 @@ def settings_path() -> Path:
62
63
  1. $REIDCHAT_SETTINGS (explicit override)
63
64
  2. project settings.json (walk upward from CWD)
64
65
  3. ~/.reidx/settings.json (global default)
65
- 4. E:/leech/Reidchat.json (legacy shared file)
66
+ 4. ~/Reidchat.json (legacy fallback)
66
67
 
67
68
  Returns the last fallback even if it doesn't exist, so `doctor` can
68
69
  report it as "missing" rather than crashing on a None.
@@ -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,20 @@ 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
- with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as resp:
167
- raw = resp.read().decode("utf-8")
168
- except urllib.error.HTTPError as exc:
169
- err_body = exc.read().decode("utf-8", errors="replace")[:500]
170
- log.error("Anthropic API error %s: %s", exc.code, err_body)
171
- raise RuntimeError(f"Anthropic API error {exc.code}: {err_body}") from exc
172
- except urllib.error.URLError as exc:
173
- log.error("Anthropic connection error: %s", exc)
174
- raise RuntimeError(f"connection error: {exc}") from exc
175
-
176
- return self._parse_response(json.loads(raw), model)
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
+ body = get_json(url, self._headers())
164
+ models: list[str] = []
165
+ for item in body.get("data", []):
166
+ mid = item.get("id", "")
167
+ if mid:
168
+ models.append(mid)
169
+ return sorted(models)
@@ -109,11 +109,7 @@ class OllamaProvider(BaseProvider):
109
109
  )
110
110
 
111
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 []
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
- try:
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", "")
@@ -144,14 +140,37 @@ class OpenAICompatibleProvider(OpenAIProvider):
144
140
  """OpenAI-compatible endpoints (llama.cpp server, LM Studio, vLLM, etc.).
145
141
 
146
142
  Same wire format as OpenAI; only default base_url differs and the API key
147
- is optional. Kept as a subclass so `/providers` can show the kind
148
- separately in the list.
143
+ is optional. Auth method is configurable so providers that use x-api-key
144
+ instead of Bearer are handled correctly. Kept as a subclass so `/providers`
145
+ can show the kind separately in the list.
149
146
  """
150
147
 
151
148
  name = "openai-compatible"
152
149
  DEFAULT_BASE_URL = "http://localhost:8080"
153
150
  DEFAULT_MODEL = "local"
154
151
 
152
+ def __init__(
153
+ self,
154
+ api_key: str,
155
+ base_url: str = "",
156
+ default_model: str = "",
157
+ auth_method: str = "bearer",
158
+ ) -> None:
159
+ self.auth_method = auth_method
160
+ super().__init__(api_key=api_key, base_url=base_url, default_model=default_model)
161
+
162
+ def _headers(self) -> dict[str, str]:
163
+ h = {}
164
+ if not self.api_key:
165
+ return h
166
+ if self.auth_method == "x-api-key":
167
+ h["x-api-key"] = self.api_key
168
+ elif self.auth_method == "none":
169
+ pass
170
+ else:
171
+ h["authorization"] = f"Bearer {self.api_key}"
172
+ return h
173
+
155
174
 
156
175
  def _json_dump(obj) -> str: # type: ignore[no-untyped-def]
157
176
  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(
@@ -130,12 +132,15 @@ def validate_provider(record: ProviderRecord) -> tuple[bool, str]:
130
132
  provider = build_provider(record)
131
133
  except (ValueError, TypeError) as exc:
132
134
  return False, str(exc)
133
- if not record.api_key and record.kind != "ollama":
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:
134
139
  return True, "no key required"
135
140
  try:
136
141
  models = provider.fetch_models()
137
- except Exception:
138
- return False, "could not connect to provider"
142
+ except Exception as exc:
143
+ return False, f"could not connect to provider: {exc}"
139
144
  if models:
140
145
  return True, f"ok ({len(models)} models available)"
141
146
  if record.kind == "ollama":
@@ -170,6 +175,7 @@ def load_from_database(registry: ProviderRegistry, storage_root: Path) -> list[s
170
175
  base_url=sp.base_url,
171
176
  api_key=api_key,
172
177
  default_model=sp.default_model,
178
+ auth_method=sp.auth_method,
173
179
  )
174
180
  try:
175
181
  provider = build_provider(record)
@@ -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="", default_model="databricks-dbrx-instruct",
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="", default_model="@cf/meta/llama-3.1-70b-instruct",
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="", default_model="ibm/granite-3-8b-instruct",
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
- log.warning("DPAPI not available on this platform; storing key without OS encryption")
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
 
@@ -606,6 +606,7 @@ 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:
@@ -628,13 +629,12 @@ class ProviderPalette:
628
629
  existing.default_model = model
629
630
  existing.auth_method = auth
630
631
  if api_key:
632
+ from reidx.provider_manager import keychain
631
633
  existing.keys.append(StoredKey(
632
634
  id=uuid.uuid4().hex[:12],
633
635
  label="Default",
634
- encrypted_key="",
636
+ encrypted_key=keychain.encrypt(api_key),
635
637
  ))
636
- from reidx.provider_manager import keychain
637
- existing.keys[-1].encrypted_key = keychain.encrypt(api_key)
638
638
  if existing.active_key_id is None:
639
639
  existing.active_key_id = existing.keys[-1].id
640
640
  self.db.save_provider(existing)
@@ -670,6 +670,7 @@ class ProviderPalette:
670
670
  record = ProviderRecord(
671
671
  name=name, kind=sp.kind, base_url=sp.base_url,
672
672
  api_key=api_key, default_model=sp.default_model,
673
+ auth_method=sp.auth_method,
673
674
  )
674
675
  ok, msg = validate_provider(record)
675
676
  if not ok:
@@ -708,6 +709,7 @@ class ProviderPalette:
708
709
  record = ProviderRecord(
709
710
  name=sp.name, kind=sp.kind, base_url=sp.base_url,
710
711
  api_key=api_key, default_model=sp.default_model,
712
+ auth_method=sp.auth_method,
711
713
  )
712
714
  provider = build_provider(record)
713
715
  if not sp.default_model:
@@ -502,13 +502,14 @@ def _save_to_database(orchestrator: Orchestrator, record: ProviderRecord) -> Non
502
502
  existing.active_key_id = k.id
503
503
  existing.kind = record.kind
504
504
  existing.base_url = record.base_url
505
+ existing.auth_method = record.auth_method
505
506
  if record.default_model:
506
507
  existing.default_model = record.default_model
507
508
  db.save_provider(existing)
508
509
  else:
509
510
  sp = StoredProvider(
510
511
  name=record.name, kind=record.kind, base_url=record.base_url,
511
- default_model=record.default_model,
512
+ default_model=record.default_model, auth_method=record.auth_method,
512
513
  )
513
514
  if record.api_key:
514
515
  k = StoredKey(