nexo-brain 7.9.34 → 7.10.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.34",
3
+ "version": "7.10.0",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,9 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.9.34` is the current packaged-runtime line. Patch release with two fixes. First, the email monitor's header parser was dropping any email whose RFC822 headers came back as ``email.header.Header`` instances (Q-encoded utf-8 / quoted-printable, common when sender names or subjects contain non-ASCII). ``msg.get("Message-ID").strip()`` raised ``TypeError: 'Header' object is not subscriptable``, the exception was swallowed at DEBUG, and the email was discarded silently operators only noticed when Nero stopped replying. Every ``msg.get(...)`` now goes through ``_decode_header`` (which decodes Q-encoding AND coerces to ``str``), and the failure log is lifted from DEBUG to WARNING so a future regression cannot drop emails silently. Second, the PreToolUse Guardian gate emitted JSON ``permissionDecision: deny`` on a hard-mode block but exit 0 — terminal Claude Code occasionally proceeded with the next tool anyway because the JSON deny channel was being dropped or out-of-order delivered mid-tool-loop. Hard blocks now also write the structured Guardian reason to stderr and exit 2, the documented PreToolUse blocking exit. Belt-and-suspenders enforcement: the model receives the same Guardian reason through both channels and self-corrects instead of blindly retrying.
21
+ Version `7.10.0` is the current packaged-runtime line. Minor release **removes the LLM proxy override path that 7.9.28 7.9.34 introduced**. Background: 7.9.28 added two opt-in files at `~/.nexo/config/llm_endpoint.json` and `~/.nexo/config/auth_provider.json` that let a third-party orchestrator (NEXO Desktop) redirect every Anthropic SDK call from Brain to a custom proxy and resolve the bearer via a local helper, with concrete model names translated to wire aliases (`nexo-max`, `nexo-high`, `nexo-medium`, `nexo-low`, `nexo-mini`) and an `Idempotency-Key` per request. NEXO Desktop's commercial model has changed: Desktop is now a wrapper over the user's own Claude Code subscription (Max / Pro), with a separate Desktop licence. Brain calls go directly to `api.anthropic.com` using the user's existing OAuth (the one stored under `~/.claude/` and consumed by Claude Code spawns) or a plain `ANTHROPIC_API_KEY`. There is no NEXO bearer, no NEXO proxy, no NEXO credit accounting in this codebase. Every proxy symbol is gone from `call_model_raw.py` and `agent_runner.py`; the proxy-specific tests and `docs/api/override-files.md` are removed; any pre-existing override files on disk are simply ignored from this release forward.
22
+
23
+ Previously in `7.9.34`: two fixes — the email monitor's header parser was dropping any email whose RFC822 headers came back as `email.header.Header` instances (Q-encoded utf-8 / quoted-printable). Every `msg.get(...)` now goes through `_decode_header`, and the failure log is lifted from DEBUG to WARNING. The PreToolUse Guardian gate hardens hard blocks with stderr + exit 2 enforcement so terminal Claude cannot ignore the deny channel mid-tool-loop.
22
24
 
23
25
  Previously in `7.9.33`: adds ``usedforsecurity=False`` to the SHA-1 call that derives a filesystem-safe checkpoint filename from the email's Message-ID, so Bandit's B324 audit no longer fails the publish workflow on a non-security usage. The ``v7.9.32`` git tag is preserved for traceability but no npm release ever shipped for it; ``nexo-brain@7.9.33`` is the first release that carries the 7.9.32 email-recovery checkpoints.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.9.34",
3
+ "version": "7.10.0",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -375,68 +375,6 @@ def _codex_config_path() -> Path:
375
375
  return Path.home() / ".codex" / "config.toml"
376
376
 
377
377
 
378
- def _apply_llm_endpoint_override(env: dict) -> dict:
379
- """Redirect the child Anthropic-compatible CLI to the configured proxy
380
- when Brain is in override mode (``~/.nexo/config/llm_endpoint.json``
381
- present). Standalone runs leave ``env`` untouched, so Brain libre keeps
382
- hitting ``api.anthropic.com`` directly with whatever ``ANTHROPIC_API_KEY``
383
- the operator already had configured.
384
-
385
- Security guarantee: if override mode is active but ``auth_provider.json``
386
- is missing, malformed, or the helper command fails to produce a
387
- bearer, the helper does NOT inject ``ANTHROPIC_BASE_URL`` either.
388
- Otherwise the spawned CLI would inherit the operator's real
389
- ``ANTHROPIC_API_KEY`` (a ``sk-ant-...`` key) from the parent process
390
- and send it as the bearer to the custom proxy, leaking the real
391
- Anthropic credential to a third party. Fail-closed: the override is
392
- skipped completely, and the CLI either runs against
393
- ``api.anthropic.com`` with the operator's real key (if also present
394
- as standalone fallback) or fails locally.
395
-
396
- The contract is symmetric with what ``call_model_raw.py`` does for SDK
397
- direct calls: same files, same precedence, same alias system. The CLI
398
- child reads ``ANTHROPIC_BASE_URL`` (Anthropic SDK convention) and
399
- ``ANTHROPIC_API_KEY`` from the spawned environment.
400
-
401
- No-op (and silent) when ``call_model_raw`` is unavailable for any
402
- reason; the headless surface should never block on this helper.
403
- """
404
- try:
405
- from call_model_raw import (
406
- is_override_mode,
407
- resolve_api_base_url,
408
- resolve_auth_token,
409
- )
410
- except Exception:
411
- return env
412
- try:
413
- if not is_override_mode():
414
- return env
415
- bearer = resolve_auth_token()
416
- if not bearer:
417
- # auth_provider.json missing or failed in override mode.
418
- # Do NOT redirect to the proxy with a stale operator key — see
419
- # the docstring above. Skip the override entirely and let the
420
- # spawn either run standalone or fail explicitly elsewhere.
421
- sys.stderr.write(
422
- "[brain] llm_endpoint override active but auth_provider "
423
- "produced no bearer; skipping CLI env injection to avoid "
424
- "leaking a real ANTHROPIC_API_KEY to the proxy.\n"
425
- )
426
- return env
427
- base_url = resolve_api_base_url()
428
- if base_url:
429
- env["ANTHROPIC_BASE_URL"] = base_url
430
- env["ANTHROPIC_API_KEY"] = bearer
431
- except Exception:
432
- # Override is best-effort: a misconfigured override file must not
433
- # crash an automation run that would otherwise have worked in
434
- # standalone. The SDK direct path already surfaces config errors
435
- # via ClassifierUnavailableError; the CLI path stays defensive.
436
- pass
437
- return env
438
-
439
-
440
378
  def _headless_env(env: dict | None = None) -> dict:
441
379
  merged = os.environ.copy()
442
380
  if env:
@@ -445,7 +383,7 @@ def _headless_env(env: dict | None = None) -> dict:
445
383
  merged["NEXO_AUTOMATION"] = "1"
446
384
  merged.pop("CLAUDECODE", None)
447
385
  merged.pop("CLAUDE_CODE", None)
448
- return _apply_llm_endpoint_override(merged)
386
+ return merged
449
387
 
450
388
 
451
389
  def _load_client_bootstrap_prompt(client: str) -> str:
@@ -666,7 +604,6 @@ def run_automation_interactive(
666
604
  launch_env = os.environ.copy()
667
605
  if env:
668
606
  launch_env.update(env)
669
- launch_env = _apply_llm_endpoint_override(launch_env)
670
607
  cwd_path = Path(_interactive_target_cwd(target))
671
608
 
672
609
  # Best-effort resonance lookup — interactive sessions do not swap the
@@ -1107,21 +1044,7 @@ def run_automation_prompt(
1107
1044
 
1108
1045
  bare_api_key = ""
1109
1046
  if resolved_bare:
1110
- # In override mode the bearer was already injected into
1111
- # run_env by _apply_llm_endpoint_override (proxy token, not
1112
- # the operator's raw Anthropic key). Reuse it instead of
1113
- # asking the keychain helper for a real Anthropic key — the
1114
- # proxy expects its own bearer and would reject the real one.
1115
- override_bearer = run_env.get("ANTHROPIC_API_KEY", "").strip() if run_env else ""
1116
- try:
1117
- from call_model_raw import is_override_mode as _is_override_mode
1118
- _override_active = _is_override_mode()
1119
- except Exception:
1120
- _override_active = False
1121
- if _override_active and override_bearer:
1122
- bare_api_key = override_bearer
1123
- else:
1124
- bare_api_key = _resolve_anthropic_api_key()
1047
+ bare_api_key = _resolve_anthropic_api_key()
1125
1048
  if not bare_api_key:
1126
1049
  # Silent fallback: we would rather take the slower path
1127
1050
  # than force the caller to fail-closed on an env quirk.
@@ -41,11 +41,7 @@ gap.
41
41
  from __future__ import annotations
42
42
 
43
43
  import json
44
- import logging
45
44
  import os
46
- import subprocess
47
- import sys
48
- import uuid
49
45
  from pathlib import Path
50
46
 
51
47
 
@@ -69,246 +65,6 @@ _OPENAI_KEY_PATHS = (
69
65
  Path.home() / ".codex" / "auth.json",
70
66
  )
71
67
 
72
- # ---------------------------------------------------------------------------
73
- # Optional override files (~/.nexo/config/)
74
- # ---------------------------------------------------------------------------
75
- # Two forward-compatible JSON files let third-party orchestrators (such as an
76
- # Anthropic-compatible proxy) redirect the LLM endpoint and delegate token
77
- # resolution to a local helper. Pattern is analogous to git's `core.editor`
78
- # and `credential.helper`.
79
- #
80
- # ~/.nexo/config/llm_endpoint.json
81
- # {
82
- # "version": 1,
83
- # "anthropic_base_url": "https://my-proxy.example.com/api/proxy"
84
- # }
85
- #
86
- # ~/.nexo/config/auth_provider.json
87
- # {
88
- # "version": 1,
89
- # "command": "/path/to/auth-helper",
90
- # "args": ["--for", "anthropic"],
91
- # "timeout_sec": 5
92
- # }
93
- #
94
- # If neither file exists the caller falls back to standalone behaviour:
95
- # direct call to api.anthropic.com using ANTHROPIC_API_KEY from environment
96
- # or filesystem. NEXO Brain's open-source distribution is unaffected.
97
-
98
- def _resolve_brain_config_dir() -> Path:
99
- """Honour ``NEXO_HOME`` so tests, devcontainers and non-default
100
- installs (Maria iMac, Codex sandboxes, etc.) hit the right
101
- ``config/`` directory. Resolved at every call so a process that
102
- sets ``NEXO_HOME`` after this module is imported still picks up
103
- the right path on the next request — relevant for LaunchAgent
104
- crons that rely on env exported by their wrapper script. Falls
105
- back to ``~/.nexo/config/``."""
106
- nexo_home = os.environ.get("NEXO_HOME", "").strip()
107
- if nexo_home:
108
- return Path(nexo_home).expanduser() / "config"
109
- return Path.home() / ".nexo" / "config"
110
-
111
-
112
- # Tests monkeypatch this attribute to redirect overrides to a tmp dir.
113
- # Production code MUST NOT read this directly — use ``_brain_config_dir()``.
114
- # Default ``None`` lets ``_brain_config_dir()`` fall through to the live
115
- # ``_resolve_brain_config_dir()`` so call-time NEXO_HOME changes are honoured.
116
- _BRAIN_CONFIG_DIR: Path | None = None
117
-
118
-
119
- def _brain_config_dir() -> Path:
120
- """Production-side resolver. Honours the test monkeypatch hook above
121
- when set, otherwise resolves from the live environment on every call."""
122
- if _BRAIN_CONFIG_DIR is not None:
123
- return _BRAIN_CONFIG_DIR
124
- return _resolve_brain_config_dir()
125
-
126
-
127
- _SUPPORTED_OVERRIDE_VERSION = 1
128
- _LLM_ENDPOINT_FILENAME = "llm_endpoint.json"
129
- _AUTH_PROVIDER_FILENAME = "auth_provider.json"
130
- _DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
131
- _DEFAULT_AUTH_PROVIDER_TIMEOUT = 5
132
-
133
- # Internal map: (concrete_model, effort) -> wire alias accepted by an
134
- # Anthropic-compatible proxy. ONLY consulted when override mode is active.
135
- # Standalone mode never reads this map and keeps using the concrete model.
136
- #
137
- # Add entries here in lockstep with new tiers added to resonance_tiers.json.
138
- # Failing fast on an unmapped (model, effort) is preferable to letting the
139
- # proxy reject the request with a 400 — the operator gets a clear local
140
- # error instead of a remote one.
141
- _CONCRETE_TO_ALIAS: dict[tuple[str, str], str] = {
142
- ("claude-opus-4-7[1m]", "max"): "nexo-max",
143
- ("claude-opus-4-7[1m]", "xhigh"): "nexo-high",
144
- ("claude-opus-4-7[1m]", "high"): "nexo-medium",
145
- ("claude-opus-4-7[1m]", "medium"): "nexo-low",
146
- ("claude-haiku-4-5-20251001", ""): "nexo-mini",
147
- }
148
-
149
-
150
- def _read_versioned_config(filename: str) -> dict | None:
151
- """Load a versioned override file from the Brain config directory.
152
-
153
- Calls ``_brain_config_dir()`` on every invocation so a process that
154
- sets ``NEXO_HOME`` after importing the module picks up the new path
155
- immediately. Tests can monkeypatch ``_BRAIN_CONFIG_DIR`` to redirect
156
- to a tmp dir.
157
-
158
- Returns the dict iff the file exists, parses as JSON and declares
159
- ``version: 1``. Any other case (missing, malformed, unsupported version)
160
- returns None and emits a stderr warning so operators can see why the
161
- override was ignored. Never raises.
162
- """
163
- path = _brain_config_dir() / filename
164
- try:
165
- if not path.is_file():
166
- return None
167
- cfg = json.loads(path.read_text())
168
- except (OSError, json.JSONDecodeError) as exc:
169
- sys.stderr.write(
170
- f"[brain] failed to read override {filename}: {exc}; ignoring\n"
171
- )
172
- return None
173
- if not isinstance(cfg, dict):
174
- sys.stderr.write(
175
- f"[brain] override {filename} is not a JSON object; ignoring\n"
176
- )
177
- return None
178
- version = cfg.get("version", 0)
179
- if version != _SUPPORTED_OVERRIDE_VERSION:
180
- sys.stderr.write(
181
- f"[brain] override {filename} version {version!r} not supported "
182
- f"(expected {_SUPPORTED_OVERRIDE_VERSION}); ignoring\n"
183
- )
184
- return None
185
- return cfg
186
-
187
-
188
- def resolve_api_base_url() -> str:
189
- """Return the Anthropic API base URL.
190
-
191
- Resolution order:
192
- 1) ``~/.nexo/config/llm_endpoint.json`` with ``anthropic_base_url``.
193
- 2) ``NEXO_LLM_ENDPOINT`` env var.
194
- 3) Default ``https://api.anthropic.com`` (standalone).
195
- """
196
- cfg = _read_versioned_config(_LLM_ENDPOINT_FILENAME)
197
- if cfg:
198
- url = str(cfg.get("anthropic_base_url", "") or "").strip()
199
- if url:
200
- return url
201
- env_url = os.environ.get("NEXO_LLM_ENDPOINT", "").strip()
202
- if env_url:
203
- return env_url
204
- return _DEFAULT_ANTHROPIC_BASE_URL
205
-
206
-
207
- def _override_force_disabled() -> bool:
208
- # Internal escape hatch used by the test suite and by maintainers when
209
- # they need to validate a regression against the upstream Anthropic API
210
- # without renaming the override files on disk. Intentionally undocumented
211
- # outside the source so that the canonical override-mode contract stays
212
- # purely file-driven for everybody else.
213
- raw = os.environ.get("NEXO_RAW_ANTHROPIC", "").strip().lower()
214
- return raw in ("1", "true", "yes", "on")
215
-
216
-
217
- def is_override_mode() -> bool:
218
- """True iff a valid ``llm_endpoint.json`` is present and selects a custom
219
- base URL. The override gate is the file (not an env var) so that
220
- env-only configurations remain transparent to standalone callers."""
221
- if _override_force_disabled():
222
- return False
223
- cfg = _read_versioned_config(_LLM_ENDPOINT_FILENAME)
224
- if not cfg:
225
- return False
226
- url = str(cfg.get("anthropic_base_url", "") or "").strip()
227
- return bool(url)
228
-
229
-
230
- def _resolve_auth_provider_token() -> str:
231
- """Resolve the bearer token strictly from ``auth_provider.json``.
232
-
233
- Returns the trimmed stdout of the configured command on success.
234
- Returns ``""`` if the file is absent, malformed, or the command
235
- times out / fails / exits non-zero / produces empty stdout. Never
236
- falls back to environment or filesystem keys; that decision is
237
- made by the caller based on whether override mode is active.
238
- """
239
- cfg = _read_versioned_config(_AUTH_PROVIDER_FILENAME)
240
- if not cfg:
241
- return ""
242
- cmd = str(cfg.get("command", "") or "").strip()
243
- if not cmd:
244
- return ""
245
- args_raw = cfg.get("args", []) or []
246
- args = [str(a) for a in args_raw if isinstance(a, (str, int, float))]
247
- try:
248
- timeout_sec = int(cfg.get("timeout_sec", _DEFAULT_AUTH_PROVIDER_TIMEOUT))
249
- except (TypeError, ValueError):
250
- timeout_sec = _DEFAULT_AUTH_PROVIDER_TIMEOUT
251
- try:
252
- result = subprocess.run(
253
- [cmd, *args],
254
- capture_output=True,
255
- text=True,
256
- timeout=timeout_sec,
257
- check=False,
258
- )
259
- except subprocess.TimeoutExpired as exc:
260
- # Learning #294: subprocess timeouts must be captured explicitly so
261
- # the operator sees the helper hung instead of a generic
262
- # "auth missing" downstream.
263
- sys.stderr.write(
264
- f"[brain] auth_provider command timed out after {timeout_sec}s: "
265
- f"{exc}\n"
266
- )
267
- return ""
268
- except (FileNotFoundError, PermissionError, OSError) as exc:
269
- sys.stderr.write(f"[brain] auth_provider command failed: {exc}\n")
270
- return ""
271
- if result.returncode != 0:
272
- stderr_excerpt = (result.stderr or "").strip()[:200]
273
- sys.stderr.write(
274
- f"[brain] auth_provider command exit={result.returncode}: "
275
- f"{stderr_excerpt}\n"
276
- )
277
- return ""
278
- return (result.stdout or "").strip()
279
-
280
-
281
- def resolve_auth_token() -> str:
282
- """Return the bearer token to use against the resolved base URL.
283
-
284
- The resolution depends on whether override mode is active:
285
-
286
- * **Override mode** (``llm_endpoint.json`` valid): the token MUST
287
- come from ``auth_provider.json``. Falling back to
288
- ``ANTHROPIC_API_KEY`` (a real ``sk-ant-...`` key bound to the
289
- operator's Anthropic account) and sending it as the bearer to a
290
- third-party proxy would leak that credential. If the helper
291
- command fails or is not configured, returns ``""`` so the caller
292
- raises ``ClassifierUnavailableError``.
293
- * **Standalone mode** (no override file): cascade
294
- ``auth_provider.json`` → ``ANTHROPIC_API_KEY`` env →
295
- ``~/.claude/anthropic-api-key.txt`` → ``~/.nexo/config/anthropic-api-key.txt``.
296
- The legacy fallbacks exist so an operator that scripted bearer
297
- resolution via the helper can still rely on the env var when
298
- Brain is not redirected anywhere.
299
- """
300
- if is_override_mode():
301
- # Strict: the bearer must come from the configured helper. If
302
- # the helper is missing or fails, refuse to authenticate rather
303
- # than leak a real Anthropic key to a custom proxy.
304
- return _resolve_auth_provider_token()
305
-
306
- # Standalone: helper first (if scripted), env/files otherwise.
307
- helper_token = _resolve_auth_provider_token()
308
- if helper_token:
309
- return helper_token
310
- return _resolve_anthropic_key()
311
-
312
68
 
313
69
  def _resolve_anthropic_key() -> str:
314
70
  env_key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
@@ -382,91 +138,28 @@ def _extract_openai_text(response) -> str:
382
138
  return ""
383
139
 
384
140
 
385
- def _resolve_override_alias(model: str, effort: str) -> str:
386
- """In override mode the proxy speaks aliases, not concrete model names.
387
- Translate ``(model, effort)`` into the wire alias the proxy validates.
388
- Unmapped pairs fail-closed: better to surface a local config error than
389
- let the proxy reject the request remotely.
390
- """
391
- key = (model, effort)
392
- alias = _CONCRETE_TO_ALIAS.get(key)
393
- if not alias:
394
- raise ClassifierUnavailableError(
395
- f"override mode: no alias mapped for (model={model!r}, "
396
- f"effort={effort!r}); update _CONCRETE_TO_ALIAS in call_model_raw.py"
397
- )
398
- return alias
399
-
400
-
401
141
  def _call_anthropic_raw(
402
142
  *,
403
143
  prompt: str,
404
144
  system: str | None,
405
145
  model: str,
406
- effort: str,
407
146
  max_tokens: int,
408
147
  temperature: float,
409
148
  stop_sequences: list[str],
410
149
  timeout: float,
411
- idempotency_key: str | None = None,
412
150
  ) -> str:
413
151
  try:
414
152
  import anthropic # type: ignore
415
153
  except ImportError as exc:
416
154
  raise ClassifierUnavailableError(f"anthropic SDK missing: {exc}") from exc
417
155
 
418
- override = is_override_mode()
419
- if override:
420
- # Proxy mode. The Anthropic SDK distinguishes:
421
- # api_key=... -> header "X-Api-Key: <value>" (Anthropic-style)
422
- # auth_token=... -> header "Authorization: Bearer <value>" (OAuth-style)
423
- # NEXO Desktop and any compatible proxy parse the standard
424
- # "Authorization: Bearer" header, so we MUST pass the resolved
425
- # bearer through ``auth_token`` — passing it as ``api_key`` would
426
- # send "X-Api-Key" which the proxy would reject with 401.
427
- wire_model = _resolve_override_alias(model, effort)
428
- base_url = resolve_api_base_url()
429
- bearer = resolve_auth_token()
430
- if not bearer:
431
- raise ClassifierUnavailableError(
432
- "anthropic override: no bearer resolved from auth_provider.json; "
433
- "override mode requires a configured auth helper to avoid leaking "
434
- "a real ANTHROPIC_API_KEY to a custom proxy"
435
- )
436
- # The SDK ``__init__`` resolves ``api_key`` from
437
- # ``ANTHROPIC_API_KEY`` whenever the kwarg is ``None`` (the
438
- # parameter default). It then sends BOTH ``X-Api-Key`` (from the
439
- # env-resolved api_key) and ``Authorization: Bearer`` (from
440
- # auth_token) on every request. A custom proxy would see and
441
- # potentially log the operator's real ``sk-ant-...`` key. Passing
442
- # ``api_key=""`` does not fix it either: the SDK's auth_headers
443
- # check is ``if api_key is None`` (strict ``is``, not falsy), so
444
- # the empty string still produces an ``X-Api-Key:`` header.
445
- # Solution: pop the env var around the constructor call so the
446
- # SDK records ``api_key=None`` and skips the X-Api-Key header
447
- # entirely. Then restore the original env so we don't break
448
- # other code paths in the same Python process.
449
- _saved_anthropic_env = os.environ.pop("ANTHROPIC_API_KEY", None)
450
- try:
451
- client = anthropic.Anthropic(
452
- auth_token=bearer,
453
- base_url=base_url,
454
- timeout=timeout,
455
- )
456
- finally:
457
- if _saved_anthropic_env is not None:
458
- os.environ["ANTHROPIC_API_KEY"] = _saved_anthropic_env
459
- else:
460
- # Standalone: behaviour identical to pre-V11. No override, no alias
461
- # translation, no extra headers — direct hit to api.anthropic.com.
462
- wire_model = model
463
- api_key = _resolve_anthropic_key()
464
- if not api_key:
465
- raise ClassifierUnavailableError("anthropic: no ANTHROPIC_API_KEY found")
466
- client = anthropic.Anthropic(api_key=api_key, timeout=timeout)
156
+ api_key = _resolve_anthropic_key()
157
+ if not api_key:
158
+ raise ClassifierUnavailableError("anthropic: no ANTHROPIC_API_KEY found")
159
+ client = anthropic.Anthropic(api_key=api_key, timeout=timeout)
467
160
 
468
161
  kwargs: dict = {
469
- "model": wire_model,
162
+ "model": model,
470
163
  "max_tokens": max_tokens,
471
164
  "temperature": temperature,
472
165
  "messages": [{"role": "user", "content": prompt}],
@@ -482,21 +175,6 @@ def _call_anthropic_raw(
482
175
  if system:
483
176
  kwargs["system"] = system
484
177
 
485
- if override:
486
- # Idempotency-Key: opaque per-request token. The proxy dedups on
487
- # (token_id + idempotency_key) for 24h, so network-level retries
488
- # do not double-bill the user. The caller is encouraged to pass
489
- # an explicit ``idempotency_key`` and reuse it across application-
490
- # level retries (e.g. enforcement_classifier retrying after a
491
- # ClassifierUnavailableError) so the proxy treats the second
492
- # attempt as a duplicate of the first instead of a brand-new
493
- # billable request. If the caller omits it we generate a fresh
494
- # UUID4, which still covers SDK-level transparent retries since
495
- # the SDK reuses the same ``kwargs`` across them.
496
- if idempotency_key is None:
497
- idempotency_key = uuid.uuid4().hex
498
- kwargs["extra_headers"] = {"Idempotency-Key": idempotency_key}
499
-
500
178
  try:
501
179
  response = client.messages.create(**kwargs)
502
180
  except anthropic.APITimeoutError as exc:
@@ -576,7 +254,6 @@ def call_model_raw(
576
254
  stop_sequences: list[str] | None = None,
577
255
  timeout: float = 10.0,
578
256
  system: str | None = None,
579
- idempotency_key: str | None = None,
580
257
  ) -> str:
581
258
  """Run a single short LLM completion for enforcement-class classification.
582
259
 
@@ -605,16 +282,6 @@ def call_model_raw(
605
282
  ``ClassifierUnavailableError``.
606
283
  timeout — per-request timeout in seconds. Default 10.0.
607
284
  system — optional system prompt. Default None (provider default).
608
- idempotency_key — optional opaque token attached as
609
- ``Idempotency-Key`` header in override mode. Reuse
610
- the same value across application-level retries
611
- (e.g. when the caller catches
612
- ``ClassifierUnavailableError`` and tries again)
613
- so the proxy treats the retry as a duplicate of
614
- the first request and does not double-bill.
615
- Ignored in standalone mode. If omitted in
616
- override mode, a fresh UUID4 is generated which
617
- still covers transparent SDK-level retries.
618
285
 
619
286
  Returns the raw text response, trimmed. The CALLER is responsible for
620
287
  parsing yes/no — the "triple reinforcement" (prompt strict, max_tokens
@@ -635,9 +302,7 @@ def call_model_raw(
635
302
  if stop_sequences is not None:
636
303
  # Anthropic API: ``each stop sequence must contain
637
304
  # non-whitespace`` (HTTP 400). Surface the configuration error
638
- # locally instead of letting Anthropic 400 the request — and,
639
- # in override mode, instead of letting the proxy translate that
640
- # 400 into a misleading ``503 all_providers_down``.
305
+ # locally instead of letting Anthropic 400 the request.
641
306
  invalid = [
642
307
  repr(s) for s in stop_sequences
643
308
  if not isinstance(s, str) or not s.strip()
@@ -668,7 +333,7 @@ def call_model_raw(
668
333
  raise ClassifierUnavailableError("automation_backend=none")
669
334
 
670
335
  try:
671
- model, effort = resolve_model_and_effort(
336
+ model, _effort = resolve_model_and_effort(
672
337
  caller=caller,
673
338
  backend=backend,
674
339
  explicit_tier=tier,
@@ -687,12 +352,10 @@ def call_model_raw(
687
352
  prompt=prompt,
688
353
  system=system,
689
354
  model=model,
690
- effort=effort,
691
355
  max_tokens=max_tokens,
692
356
  temperature=temperature,
693
357
  stop_sequences=stop_sequences,
694
358
  timeout=timeout,
695
- idempotency_key=idempotency_key,
696
359
  )
697
360
  if backend == CLIENT_CODEX:
698
361
  return _call_openai_raw(
@@ -711,7 +374,4 @@ def call_model_raw(
711
374
  __all__ = [
712
375
  "call_model_raw",
713
376
  "ClassifierUnavailableError",
714
- "is_override_mode",
715
- "resolve_api_base_url",
716
- "resolve_auth_token",
717
377
  ]