@agxnte/reidx 2.0.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agxnte/reidx",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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.4"
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.4"
@@ -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,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
- 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
+ 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)
@@ -144,14 +144,37 @@ class OpenAICompatibleProvider(OpenAIProvider):
144
144
  """OpenAI-compatible endpoints (llama.cpp server, LM Studio, vLLM, etc.).
145
145
 
146
146
  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.
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.
149
150
  """
150
151
 
151
152
  name = "openai-compatible"
152
153
  DEFAULT_BASE_URL = "http://localhost:8080"
153
154
  DEFAULT_MODEL = "local"
154
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
+
155
178
 
156
179
  def _json_dump(obj) -> str: # type: ignore[no-untyped-def]
157
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(
@@ -130,7 +132,10 @@ 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()
@@ -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
 
@@ -628,13 +628,12 @@ class ProviderPalette:
628
628
  existing.default_model = model
629
629
  existing.auth_method = auth
630
630
  if api_key:
631
+ from reidx.provider_manager import keychain
631
632
  existing.keys.append(StoredKey(
632
633
  id=uuid.uuid4().hex[:12],
633
634
  label="Default",
634
- encrypted_key="",
635
+ encrypted_key=keychain.encrypt(api_key),
635
636
  ))
636
- from reidx.provider_manager import keychain
637
- existing.keys[-1].encrypted_key = keychain.encrypt(api_key)
638
637
  if existing.active_key_id is None:
639
638
  existing.active_key_id = existing.keys[-1].id
640
639
  self.db.save_provider(existing)
@@ -708,6 +707,7 @@ class ProviderPalette:
708
707
  record = ProviderRecord(
709
708
  name=sp.name, kind=sp.kind, base_url=sp.base_url,
710
709
  api_key=api_key, default_model=sp.default_model,
710
+ auth_method=sp.auth_method,
711
711
  )
712
712
  provider = build_provider(record)
713
713
  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(