utim-cli 2.0.4__tar.gz → 2.1.0__tar.gz
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.
- {utim_cli-2.0.4 → utim_cli-2.1.0}/CHANGELOG.md +22 -0
- {utim_cli-2.0.4/utim_cli.egg-info → utim_cli-2.1.0}/PKG-INFO +4 -1
- {utim_cli-2.0.4 → utim_cli-2.1.0}/pyproject.toml +11 -1
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/__init__.py +1 -1
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_version.py +1 -1
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/agent.py +5 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/client_utils.py +82 -4
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/config.py +150 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/orchestrator.py +65 -13
- utim_cli-2.1.0/utim_cli/server/attribution.py +62 -0
- utim_cli-2.1.0/utim_cli/server/audit_log.py +165 -0
- utim_cli-2.1.0/utim_cli/server/captcha.py +206 -0
- utim_cli-2.1.0/utim_cli/server/cli_auth.py +399 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/db.py +1188 -1049
- utim_cli-2.1.0/utim_cli/server/docs_md/SECRET_PROVISIONING.md +79 -0
- utim_cli-2.1.0/utim_cli/server/provision_build.py +184 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/router.py +78 -7
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/auth_routes.py +849 -777
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/completion_routes.py +1400 -1384
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/security_routes.py +507 -427
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/situational_scoring.py +278 -271
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/publish_dialog.py +6 -6
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/utim.py +2 -2
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/vector_memory.py +8 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0/utim_cli.egg-info}/PKG-INFO +4 -1
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli.egg-info/SOURCES.txt +6 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli.egg-info/requires.txt +4 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/LICENSE +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/MANIFEST.in +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/README.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/setup.cfg +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/setup.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_check_obj.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_find_mcp.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_find_plan.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_find_tool.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_patch_multimodal.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/_test_regex.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/auth.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/backup.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/billing.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/constants.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/doctor.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/harbor.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/local_db.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/logger.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/reflection.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/report.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/history.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/models.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/server.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/share.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/state.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tools.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/utilities.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli/workspace.py +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.0.4 → utim_cli-2.1.0}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0] - 2026-08-05
|
|
4
|
+
|
|
5
|
+
### ⚡ Performance
|
|
6
|
+
- **Added `UTIM_LITE_MODE=1` for low-spec PCs**: Skip loading heavy ML models (ChromaDB, sentence-transformers, MiniLM embeddings, local Qwen/torch download). CLI now starts instantly with no model-download lag. Enabled by default for users on `bin\utimlite.cmd`.
|
|
7
|
+
- **New `utimlite` launcher** alongside `utim` (full mode) — `bin\utimlite.cmd` sets `UTIM_LITE_MODE=1` and runs the same app with a lightweight backend.
|
|
8
|
+
- **Heavy dependencies are now opt-in**: `chromadb` + `sentence-transformers` moved out of core deps into the new `memory` extra (`pip install "utim-cli[memory]"`). Core install is now 11 lightweight deps only.
|
|
9
|
+
|
|
10
|
+
### 🛠️ Fixes
|
|
11
|
+
- **Fixed Parasail `400 'Expecting value: char 200'` error**: Python's `json.dumps` emits `NaN`/`Infinity` which strict providers (Parasail) reject with a JSON parse error. Added recursive `_sanitize_json()` in `client_utils.py` and wired it into `proxy_openrouter_request()` so every outbound LLM payload is now strict-JSON-safe (non-finite floats → `null`).
|
|
12
|
+
- **Fixed `Wrong color format 'dim'` crash in publish dialog** (carried from 2.0.4).
|
|
13
|
+
|
|
14
|
+
### 🔐 Security Hardening
|
|
15
|
+
- **CLI signature middleware** with HMAC-SHA256 per-build secrets, install-id binding, and atomic nonce consumption (replay-attack resistant).
|
|
16
|
+
- **Pluggable CAPTCHA** (reCAPTCHA / Turnstile / hCaptcha) on `/auth/*` endpoints.
|
|
17
|
+
- **OpenRouter attribution centralized** via `OPENROUTER_HEADERS` and propagated to all client entry points (`proxy_openrouter_request`, `orchestrator.py` streaming path, `agent.py` ReActAgent, `server/routes/completion_routes.py` primary `/completions` server path, `server/router.py`, `security_routes.py`).
|
|
18
|
+
- **Audit logging** with automatic PII/sensitive-data sanitization.
|
|
19
|
+
- **Body-stream caching middleware** + atomic nonce DB updates bound to `(install_id, ip)`.
|
|
20
|
+
- **`provision_build.py`** for release-time secret generation/rotation.
|
|
21
|
+
- **Hardened CORS**: replaced permissive `*.utim.dev` regex with explicit allowlist (`www.utim.dev`, `api.utim.dev`).
|
|
22
|
+
- **Request size limits**: 4MB default, 16MB marketplace; **web search sandboxing** (64KB response cap, 500-char query limit, host allow-list).
|
|
23
|
+
- **Fixed abort-then-print bug** in `orchestrator.abort()`: removed premature `self.cancel_event.clear()` that re-enabled stream printing after abort — event now stays set until turn-reset paths clear it.
|
|
24
|
+
|
|
3
25
|
## [2.0.4] - 2026-08-05
|
|
4
26
|
|
|
5
27
|
### 🛠️ Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -57,6 +57,9 @@ Requires-Dist: mcp>=0.1.0; (platform_system != "Linux" or (platform_machine != "
|
|
|
57
57
|
Requires-Dist: openai>=1.30.0; (platform_system != "Linux" or (platform_machine != "aarch64" and platform_machine != "arm64" and platform_machine != "armv7l" and platform_machine != "armv8l" and platform_machine != "arm" and platform_machine != "armv7")) and extra == "full"
|
|
58
58
|
Provides-Extra: legacy-agent
|
|
59
59
|
Requires-Dist: openai>=1.30.0; extra == "legacy-agent"
|
|
60
|
+
Provides-Extra: memory
|
|
61
|
+
Requires-Dist: chromadb>=0.4.0; extra == "memory"
|
|
62
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "memory"
|
|
60
63
|
|
|
61
64
|
# UTIM CLI Agent: Enterprise Coder Assistant
|
|
62
65
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "2.0
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -75,6 +75,16 @@ full = [
|
|
|
75
75
|
legacy-agent = [
|
|
76
76
|
"openai>=1.30.0",
|
|
77
77
|
]
|
|
78
|
+
# Heavy / memory-heavy features — intentionally NOT in core dependencies.
|
|
79
|
+
# Installing these downloads several hundred MB (chromadb + sentence-transformers
|
|
80
|
+
# + the MiniLM model) and can lag low-spec PCs. The CLI works fully without them:
|
|
81
|
+
# - Set UTIM_LITE_MODE=1 to force lightweight fallbacks (deterministic mock
|
|
82
|
+
# embeddings, no local Qwen/torch download).
|
|
83
|
+
# - Use `pip install "utim-cli[memory]"` on machines that can afford it.
|
|
84
|
+
memory = [
|
|
85
|
+
"chromadb>=0.4.0",
|
|
86
|
+
"sentence-transformers>=2.2.0",
|
|
87
|
+
]
|
|
78
88
|
|
|
79
89
|
[project.scripts]
|
|
80
90
|
utim = "utim_cli.utim:main_cli_entry"
|
|
@@ -61,6 +61,11 @@ class ReActAgent:
|
|
|
61
61
|
self.client = OpenAI(
|
|
62
62
|
base_url="https://openrouter.ai/api/v1",
|
|
63
63
|
api_key=api_key or "sk-fake-key-for-init",
|
|
64
|
+
default_headers={
|
|
65
|
+
"HTTP-Referer": "https://utim.dev",
|
|
66
|
+
"X-Title": "UTIM CLI Agent",
|
|
67
|
+
"User-Agent": "UTIM-CLI/2.0 (+https://utim.dev)",
|
|
68
|
+
},
|
|
64
69
|
)
|
|
65
70
|
self.messages = [{"role": "system", "content": system_prompt}]
|
|
66
71
|
self.start_time = None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
+
import math
|
|
3
4
|
import requests
|
|
4
5
|
from utim_cli.config import config
|
|
5
6
|
|
|
@@ -37,6 +38,32 @@ def get_server_url() -> str:
|
|
|
37
38
|
return "https://api.utim.dev"
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def _sanitize_json(obj):
|
|
42
|
+
"""Recursively scrub a payload for values that break strict JSON parsers.
|
|
43
|
+
|
|
44
|
+
Python's json.dumps emits NaN/Infinity as literal `NaN` / `Infinity`,
|
|
45
|
+
which is NOT valid JSON — strict providers (e.g. Parasail) reject the
|
|
46
|
+
whole request with 'Expecting value' at the exact offending character.
|
|
47
|
+
This replaces non-finite floats with None (safe in API payloads) and
|
|
48
|
+
drops anything that cannot round-trip through JSON.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(obj, dict):
|
|
51
|
+
return {k: _sanitize_json(v) for k, v in obj.items()
|
|
52
|
+
if isinstance(k, (str, int, float, bool)) or k is None}
|
|
53
|
+
if isinstance(obj, (list, tuple)):
|
|
54
|
+
return [_sanitize_json(v) for v in obj]
|
|
55
|
+
if isinstance(obj, float):
|
|
56
|
+
return obj if math.isfinite(obj) else None
|
|
57
|
+
if isinstance(obj, (str, int, bool)) or obj is None:
|
|
58
|
+
return obj
|
|
59
|
+
# Anything else (bytes, objects, sets…) can't survive JSON serialisation.
|
|
60
|
+
try:
|
|
61
|
+
json.dumps(obj)
|
|
62
|
+
return obj
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
40
67
|
# ─── Model catalog cache ──────────────────────────────────────────────────────
|
|
41
68
|
_catalog_cache: dict | None = None
|
|
42
69
|
_catalog_cache_ts: float = 0.0
|
|
@@ -84,6 +111,10 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
84
111
|
"""Routes LLM request to UTIM server /completions, or falls back to direct OpenRouter."""
|
|
85
112
|
if timeout is None:
|
|
86
113
|
timeout = (15, 900)
|
|
114
|
+
# Scrub payload for NaN/Infinity/non-JSON types BEFORE it leaves the
|
|
115
|
+
# machine. Providers like Parasail do a strict parse and reject the whole
|
|
116
|
+
# request (400 'Expecting value') if even one value is malformed.
|
|
117
|
+
json_data = _sanitize_json(json_data)
|
|
87
118
|
# Ensure environment variables are loaded (orchestrator handles this, but tools.py is independent)
|
|
88
119
|
_cwd_env = os.path.join(os.getcwd(), ".env")
|
|
89
120
|
if os.path.isfile(_cwd_env):
|
|
@@ -98,19 +129,63 @@ def proxy_openrouter_request(json_data: dict, stream: bool = False, timeout=None
|
|
|
98
129
|
llm_key = os.getenv("OPENROUTER_API_KEY")
|
|
99
130
|
|
|
100
131
|
if llm_key:
|
|
132
|
+
# Direct path to OpenRouter — must always carry canonical attribution
|
|
133
|
+
# headers so OpenRouter shows "UTIM CLI Agent" (NOT "unknown") in its
|
|
134
|
+
# analytics. The user-agent and install-id are added by the global
|
|
135
|
+
# session patcher in config.py.
|
|
101
136
|
headers = {
|
|
102
137
|
"Authorization": f"Bearer {llm_key}",
|
|
103
138
|
"Content-Type": "application/json",
|
|
104
139
|
"HTTP-Referer": "https://utim.dev",
|
|
105
140
|
"X-Title": "UTIM CLI Agent",
|
|
141
|
+
"User-Agent": "UTIM-CLI/2.0 (+https://utim.dev)",
|
|
106
142
|
}
|
|
107
|
-
return requests.post(
|
|
143
|
+
return requests.post(
|
|
144
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
145
|
+
json=json_data,
|
|
146
|
+
headers=headers,
|
|
147
|
+
stream=stream or json_data.get("stream"),
|
|
148
|
+
timeout=timeout,
|
|
149
|
+
verify=config.verify_ssl,
|
|
150
|
+
)
|
|
108
151
|
|
|
109
152
|
if api_key:
|
|
153
|
+
# Path via UTIM server. We attach CLI identity headers (version,
|
|
154
|
+
# install-id) so the server can correlate calls and (when enforcement
|
|
155
|
+
# is on) verify the request signature. The session-level UA patch in
|
|
156
|
+
# config.py adds the User-Agent too.
|
|
157
|
+
try:
|
|
158
|
+
from utim_cli._version import VERSION as _CLI_VERSION
|
|
159
|
+
except Exception:
|
|
160
|
+
_CLI_VERSION = "unknown"
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
from utim_cli.config import get_install_id
|
|
164
|
+
_INSTALL_ID = get_install_id()
|
|
165
|
+
except Exception:
|
|
166
|
+
_INSTALL_ID = ""
|
|
167
|
+
|
|
110
168
|
headers = {
|
|
111
169
|
"X-API-Key": api_key,
|
|
112
|
-
"Content-Type": "application/json"
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"X-UTIM-CLI-Version": _CLI_VERSION,
|
|
172
|
+
"X-UTIM-Install-ID": _INSTALL_ID,
|
|
173
|
+
"User-Agent": f"UTIM-CLI/{_CLI_VERSION} (+https://utim.dev)",
|
|
113
174
|
}
|
|
175
|
+
|
|
176
|
+
# Optional: attach CLI signature if the server is enforcing
|
|
177
|
+
# signatures. The signing flow (challenge → sign) is implemented in
|
|
178
|
+
# config.py's global session patcher so it works for every request,
|
|
179
|
+
# not just this one. If unavailable, we still send the headers above
|
|
180
|
+
# so the server can at least identify the caller.
|
|
181
|
+
try:
|
|
182
|
+
from utim_cli.config import sign_request_for_server
|
|
183
|
+
signed = sign_request_for_server(method="POST", path="/completions", body=json_data)
|
|
184
|
+
if signed:
|
|
185
|
+
headers.update(signed)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
114
189
|
payload = {
|
|
115
190
|
"messages": json_data.get("messages"),
|
|
116
191
|
"model_id": json_data.get("model"),
|
|
@@ -237,7 +312,10 @@ def parse_xml_tool_calls(content: str) -> tuple[str | None, list[dict]]:
|
|
|
237
312
|
import re
|
|
238
313
|
import uuid
|
|
239
314
|
tool_calls = []
|
|
240
|
-
|
|
315
|
+
# Match BOTH <tool_call> and the pipe-delimited <|tool_call|> variant.
|
|
316
|
+
# Some models emit <|tool_call|> (Claude/Ollama style); the old regex only
|
|
317
|
+
# matched <tool_call>, so those calls leaked raw into chat as text.
|
|
318
|
+
tool_call_pattern = re.compile(r'<\|?tool_call\|?>([\s\S]*?)<\|?/tool_call\|?>')
|
|
241
319
|
|
|
242
320
|
matches = list(tool_call_pattern.finditer(content))
|
|
243
321
|
if not matches:
|
|
@@ -506,4 +584,4 @@ def update_session_usage(
|
|
|
506
584
|
entry["tools_credits"] = entry.get("tools_credits", 0.0) + credits_used
|
|
507
585
|
|
|
508
586
|
all_sessions[eff_id] = entry
|
|
509
|
-
_write_all_usage(all_sessions)
|
|
587
|
+
_write_all_usage(all_sessions)
|
|
@@ -3,10 +3,160 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
import pathlib
|
|
5
5
|
import shutil
|
|
6
|
+
import time
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
6
9
|
from typing import Any, Dict, Optional
|
|
7
10
|
|
|
8
11
|
logger = logging.getLogger("utim.config")
|
|
9
12
|
|
|
13
|
+
|
|
14
|
+
# ── CLI install identity & request signing ───────────────────────────────────
|
|
15
|
+
# Used by client_utils.py to attach X-UTIM-* headers to every server-bound
|
|
16
|
+
# request and (when the server enforces signing) to compute X-UTIM-CLI-Signature.
|
|
17
|
+
# The server defines the canonical string in utim_cli/server/cli_auth.py.
|
|
18
|
+
|
|
19
|
+
_INSTALL_ID: str = ""
|
|
20
|
+
_HMAC_SECRET: bytes = b"" # populated lazily from environment / config
|
|
21
|
+
_LAST_CHALLENGE_FETCH: float = 0.0
|
|
22
|
+
_CHALLENGE_CACHE: dict = {"nonce": "", "expires_at": 0}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_install_id() -> str:
|
|
26
|
+
"""Read or generate the persistent install ID (uuid4 stored on disk)."""
|
|
27
|
+
global _INSTALL_ID
|
|
28
|
+
if _INSTALL_ID:
|
|
29
|
+
return _INSTALL_ID
|
|
30
|
+
try:
|
|
31
|
+
uid_path = get_utim_dir() / "install_id"
|
|
32
|
+
if uid_path.exists():
|
|
33
|
+
_INSTALL_ID = uid_path.read_text(encoding="utf-8").strip() or _generate_install_id()
|
|
34
|
+
else:
|
|
35
|
+
_INSTALL_ID = _generate_install_id()
|
|
36
|
+
uid_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
uid_path.write_text(_INSTALL_ID, encoding="utf-8")
|
|
38
|
+
except Exception:
|
|
39
|
+
_INSTALL_ID = _generate_install_id()
|
|
40
|
+
return _INSTALL_ID
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _generate_install_id() -> str:
|
|
44
|
+
import uuid
|
|
45
|
+
return f"inst-{uuid.uuid4().hex}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_install_id() -> str:
|
|
49
|
+
"""Public accessor — returns the per-install UUID."""
|
|
50
|
+
return _load_install_id()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_cli_version() -> str:
|
|
54
|
+
try:
|
|
55
|
+
from utim_cli._version import VERSION # type: ignore
|
|
56
|
+
return VERSION
|
|
57
|
+
except Exception:
|
|
58
|
+
return "unknown"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_hmac_secret() -> bytes:
|
|
62
|
+
"""Return the build-embedded HMAC secret used to sign requests.
|
|
63
|
+
|
|
64
|
+
Source priority:
|
|
65
|
+
1. UTIM_CLI_HMAC_SECRET environment variable (dev / override)
|
|
66
|
+
2. Persisted config value (set by the CLI on first launch from a baked-in default)
|
|
67
|
+
3. A stable derived default — sufficient for the server to log but NOT to
|
|
68
|
+
authenticate (server treats unknown secrets as "unsigned legacy client")
|
|
69
|
+
"""
|
|
70
|
+
global _HMAC_SECRET
|
|
71
|
+
if _HMAC_SECRET:
|
|
72
|
+
return _HMAC_SECRET
|
|
73
|
+
env_secret = os.environ.get("UTIM_CLI_HMAC_SECRET", "").strip()
|
|
74
|
+
if env_secret:
|
|
75
|
+
_HMAC_SECRET = env_secret.encode("utf-8")
|
|
76
|
+
return _HMAC_SECRET
|
|
77
|
+
try:
|
|
78
|
+
stored = config.get("cli_hmac_secret")
|
|
79
|
+
if stored:
|
|
80
|
+
_HMAC_SECRET = stored.encode("utf-8")
|
|
81
|
+
return _HMAC_SECRET
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
# Stable fallback derived from install_id — server rejects but client code
|
|
85
|
+
# still works without raising. The server logs these as "unsigned legacy".
|
|
86
|
+
_HMAC_SECRET = hashlib.sha256(("utim-legacy:" + get_install_id()).encode("utf-8")).digest()
|
|
87
|
+
return _HMAC_SECRET
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _fetch_challenge(server_url: str) -> dict:
|
|
91
|
+
"""Ask the server for a fresh nonce. Cached for 30s to avoid spamming."""
|
|
92
|
+
global _LAST_CHALLENGE_FETCH, _CHALLENGE_CACHE
|
|
93
|
+
now = time.time()
|
|
94
|
+
if _CHALLENGE_CACHE.get("nonce") and _CHALLENGE_CACHE.get("expires_at", 0) > now + 5:
|
|
95
|
+
if (now - _LAST_CHALLENGE_FETCH) < 30:
|
|
96
|
+
return _CHALLENGE_CACHE
|
|
97
|
+
try:
|
|
98
|
+
import requests as _req
|
|
99
|
+
r = _req.get(
|
|
100
|
+
f"{server_url.rstrip('/')}/auth/cli-challenge",
|
|
101
|
+
headers={
|
|
102
|
+
"X-UTIM-Install-ID": get_install_id(),
|
|
103
|
+
"User-Agent": f"UTIM-CLI/{get_cli_version()} (+https://utim.dev)",
|
|
104
|
+
},
|
|
105
|
+
timeout=5,
|
|
106
|
+
)
|
|
107
|
+
if r.status_code == 200:
|
|
108
|
+
data = r.json()
|
|
109
|
+
_CHALLENGE_CACHE = {
|
|
110
|
+
"nonce": data.get("nonce", ""),
|
|
111
|
+
"expires_at": int(data.get("expires_at", 0)),
|
|
112
|
+
"enforcement_enabled": bool(data.get("enforcement_enabled", False)),
|
|
113
|
+
}
|
|
114
|
+
_LAST_CHALLENGE_FETCH = now
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
return _CHALLENGE_CACHE
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def sign_request_for_server(method: str, path: str, body: Any) -> dict:
|
|
121
|
+
"""Build the X-UTIM-* signature headers for a server-bound request.
|
|
122
|
+
|
|
123
|
+
Returns {} silently if signing is not available (no network, no secret).
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
from utim_cli.client_utils import get_server_url
|
|
127
|
+
server_url = get_server_url()
|
|
128
|
+
except Exception:
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
challenge = _fetch_challenge(server_url)
|
|
132
|
+
nonce = (challenge or {}).get("nonce") or ""
|
|
133
|
+
if not nonce:
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
if body is None:
|
|
137
|
+
body_bytes = b""
|
|
138
|
+
elif isinstance(body, (bytes, bytearray)):
|
|
139
|
+
body_bytes = bytes(body)
|
|
140
|
+
else:
|
|
141
|
+
try:
|
|
142
|
+
body_bytes = json.dumps(body, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
143
|
+
except Exception:
|
|
144
|
+
body_bytes = str(body).encode("utf-8")
|
|
145
|
+
|
|
146
|
+
body_hash = hashlib.sha256(body_bytes).hexdigest()
|
|
147
|
+
install_id = get_install_id()
|
|
148
|
+
canonical = f"{method.upper()}\n{path}\n{nonce}\n{install_id}\n{body_hash}"
|
|
149
|
+
|
|
150
|
+
secret = get_hmac_secret()
|
|
151
|
+
signature = hmac.new(secret, canonical.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"X-UTIM-Nonce": nonce,
|
|
155
|
+
"X-UTIM-CLI-Signature": signature,
|
|
156
|
+
"X-UTIM-CLI-Version": get_cli_version(),
|
|
157
|
+
"X-UTIM-Install-ID": install_id,
|
|
158
|
+
}
|
|
159
|
+
|
|
10
160
|
# ── SSL session patch ─────────────────────────────────────────────────────────
|
|
11
161
|
# Keeps a reference to the true original so toggling works correctly in both
|
|
12
162
|
# directions without chaining patches on top of each other.
|
|
@@ -490,6 +490,10 @@ _qwen_downloading = False
|
|
|
490
490
|
def _trigger_qwen_download_bg():
|
|
491
491
|
"""Trigger background download and caching of Qwen local model if not already loaded or downloading."""
|
|
492
492
|
global _qwen_model, _qwen_tokenizer, _qwen_downloading
|
|
493
|
+
# LITE MODE: never download/load the local Qwen model (torch + ~1GB RAM).
|
|
494
|
+
# Falls back to the cloud API path (proxy_openrouter_request) instead.
|
|
495
|
+
if os.environ.get("UTIM_LITE_MODE", "0") in ("1", "true", "TRUE", "yes"):
|
|
496
|
+
return
|
|
493
497
|
if _qwen_model is None and not _qwen_downloading:
|
|
494
498
|
import sys
|
|
495
499
|
if "pytest" in sys.modules:
|
|
@@ -758,16 +762,28 @@ def _get_dynamic_directives(is_compressed: bool, messages: Optional[List[Dict]]
|
|
|
758
762
|
if not enabled_tools:
|
|
759
763
|
return "### TOOL STATUS: ALL TOOLS DISABLED\nAll tools are disabled. You may only respond in text."
|
|
760
764
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
765
|
+
if is_compressed:
|
|
766
|
+
# Short, token-efficient directives for steady-state turns. The full
|
|
767
|
+
# version is only injected on iteration 0 (first turn).
|
|
768
|
+
directives = [
|
|
769
|
+
"### CORE DIRECTIVES:",
|
|
770
|
+
"1. **Targeted Search**: Use `grep_search` for symbols/errors; `read_file` with symbol_name or line ranges — no un-sliced large reads.",
|
|
771
|
+
"2. **Images**: If the prompt includes an image path (.png/.jpg/.jpeg), `read_file` it immediately on Turn 1 with that exact path.",
|
|
772
|
+
"3. **Planning**: Invoke `plan_project` for non-trivial tasks/new features/refactors before implementing.",
|
|
773
|
+
"4. **Think-Create-Verify**: Execute tools directly (no preamble). Verify edits with `run_command` (build/test).",
|
|
774
|
+
"5. **Output**: Native function-calling only — no raw JSON, <think> tags, or tool markup in text. Finish with a concise summary.",
|
|
775
|
+
]
|
|
776
|
+
else:
|
|
777
|
+
directives = [
|
|
778
|
+
"### CORE DIRECTIVES:",
|
|
779
|
+
"1. **Targeted Search**: Use `grep_search` to locate exact symbol definitions, imports, or error text across codebase files.",
|
|
780
|
+
"2. **Scoped File Reading**: Use `read_file` with `symbol_name` or line ranges (`start_line`/`end_line`) to inspect target function/class definitions. Avoid reading un-sliced large files.",
|
|
781
|
+
"3. **Images & Screenshots**: If the prompt includes an image or screenshot path (.png, .jpg, .jpeg, etc.), execute `read_file` immediately on Turn 1 with that exact path. Absolute file paths anywhere on the system are valid — do not question file locations.",
|
|
782
|
+
"4. **Project & Feature Planning**: Proactively invoke `plan_project` (with `plan_part` set to 'design', 'architecture', 'security', 'database', 'testing', 'deployment', or 'features') for non-trivial tasks, new features, system refactors, or new application builds before implementing.",
|
|
783
|
+
"5. **Think-Create-Verify**: Execute tools directly without conversational preambles or text planning. Verify edits with `run_command` (compile/build/test).",
|
|
784
|
+
"6. **Execution**: Invoke native function-calling schemas directly. Never output raw JSON, <think> tags, or conversational text before tool calls.",
|
|
785
|
+
"7. **Output & Summary**: Be concise, professional, and clear. Upon finishing, provide a comprehensive summary of completion.",
|
|
786
|
+
]
|
|
771
787
|
# NOTE: 'Iteration Budget' directive intentionally removed.
|
|
772
788
|
# Iteration limits are now enforced dynamically (window-aware) at runtime
|
|
773
789
|
# via milestone warnings, not hardcoded into the system prompt.
|
|
@@ -1278,8 +1294,12 @@ class Orchestrator:
|
|
|
1278
1294
|
abort_active_command()
|
|
1279
1295
|
except Exception:
|
|
1280
1296
|
pass
|
|
1281
|
-
#
|
|
1282
|
-
|
|
1297
|
+
# NOTE: We deliberately do NOT clear cancel_event here. The stream
|
|
1298
|
+
# reader thread may still be draining a socket with buffered chunks,
|
|
1299
|
+
# and the render loops check `cancel_event.is_set()` before printing.
|
|
1300
|
+
# Clearing it here would let those buffered chunks print after abort.
|
|
1301
|
+
# The event is cleared at the START of the next turn (see the
|
|
1302
|
+
# `_run`/turn-reset paths that call self.cancel_event.clear()).
|
|
1283
1303
|
|
|
1284
1304
|
|
|
1285
1305
|
# LLM calling
|
|
@@ -1755,10 +1775,22 @@ class Orchestrator:
|
|
|
1755
1775
|
current_model,
|
|
1756
1776
|
is_primary=current_is_primary and not override_model,
|
|
1757
1777
|
)
|
|
1778
|
+
# Attribution headers — OpenRouter shows "UTIM CLI Agent"
|
|
1779
|
+
# in its activity log ONLY when HTTP-Referer + X-Title are
|
|
1780
|
+
# present on every outbound call. Without them the app
|
|
1781
|
+
# shows as "unknown".
|
|
1782
|
+
try:
|
|
1783
|
+
from utim_cli.server.attribution import OPENROUTER_HEADERS as _OR_ATTR
|
|
1784
|
+
except Exception:
|
|
1785
|
+
_OR_ATTR = {
|
|
1786
|
+
"HTTP-Referer": "https://utim.dev",
|
|
1787
|
+
"X-Title": "UTIM CLI Agent",
|
|
1788
|
+
}
|
|
1758
1789
|
_headers = {
|
|
1759
1790
|
"Authorization": f"Bearer {_endpoint_key}",
|
|
1760
1791
|
"Content-Type": "application/json",
|
|
1761
1792
|
"User-Agent": _browser_ua,
|
|
1793
|
+
**_OR_ATTR,
|
|
1762
1794
|
}
|
|
1763
1795
|
request_payload = payload
|
|
1764
1796
|
else:
|
|
@@ -1805,6 +1837,9 @@ class Orchestrator:
|
|
|
1805
1837
|
was_cut_off = False
|
|
1806
1838
|
_stream_finish_reason: str = ""
|
|
1807
1839
|
_stream_completion_tokens: int = 0
|
|
1840
|
+
# Buffers raw <tool_call>/<|tool_call|> XML streamed as text
|
|
1841
|
+
# so it never renders in the live chat output.
|
|
1842
|
+
_tool_xml_buf: list = [""]
|
|
1808
1843
|
|
|
1809
1844
|
try:
|
|
1810
1845
|
# Dynamic thinking phases — cycle through contextual messages
|
|
@@ -1938,7 +1973,18 @@ class Orchestrator:
|
|
|
1938
1973
|
if t == "content_delta":
|
|
1939
1974
|
text = chunk.get("text", "")
|
|
1940
1975
|
final_content += text
|
|
1941
|
-
|
|
1976
|
+
# Never print raw tool-call XML tokens live. If the
|
|
1977
|
+
# model emits <tool_call>/<|tool_call|> as text (some
|
|
1978
|
+
# models do), we buffer it out of the visible stream
|
|
1979
|
+
# so schemas don't leak into chat; it's parsed into
|
|
1980
|
+
# real tool calls at end-of-stream (see below).
|
|
1981
|
+
_looks_like_tool_xml = (
|
|
1982
|
+
"<tool_call" in text or "</tool_call" in text
|
|
1983
|
+
or "<|tool_call" in text or "</|tool_call" in text
|
|
1984
|
+
)
|
|
1985
|
+
if _looks_like_tool_xml:
|
|
1986
|
+
_tool_xml_buf[0] += text
|
|
1987
|
+
elif not silent and text and not self.cancel_event.is_set():
|
|
1942
1988
|
display_buf += text
|
|
1943
1989
|
_line_buf += text
|
|
1944
1990
|
if "\n" in _line_buf:
|
|
@@ -2139,6 +2185,12 @@ class Orchestrator:
|
|
|
2139
2185
|
# Content was already streamed live — just add trailing newline
|
|
2140
2186
|
self.console.print()
|
|
2141
2187
|
|
|
2188
|
+
# Merge any buffered tool-call XML back into final_content so
|
|
2189
|
+
# parse_xml_tool_calls (which now handles <|tool_call|>) can
|
|
2190
|
+
# turn it into real tool calls instead of leaked chat text.
|
|
2191
|
+
if _tool_xml_buf[0]:
|
|
2192
|
+
final_content += _tool_xml_buf[0]
|
|
2193
|
+
|
|
2142
2194
|
clean_content = re.sub(
|
|
2143
2195
|
r"<think(?:ing)?>.*?</think(?:ing)?>", "", final_content, flags=re.DOTALL
|
|
2144
2196
|
).strip()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenRouter Attribution Helper
|
|
3
|
+
=============================
|
|
4
|
+
|
|
5
|
+
Centralised injection of the HTTP-Referer / X-Title / User-Agent headers that
|
|
6
|
+
OpenRouter uses to identify the calling app. OpenRouter shows "UTIM CLI Agent"
|
|
7
|
+
in its logs only when these are present on every outbound call. Previously
|
|
8
|
+
some code paths (the server's /completions proxy, the CLI's direct fallback)
|
|
9
|
+
were inconsistent — leading to the app showing as "unknown" in analytics.
|
|
10
|
+
|
|
11
|
+
This module exposes:
|
|
12
|
+
|
|
13
|
+
OPENROUTER_HEADERS : dict of headers every OpenRouter-bound request MUST carry
|
|
14
|
+
attach_openrouter_headers(headers=None) -> dict
|
|
15
|
+
attach_openrouter_headers_to_session(session)
|
|
16
|
+
install_urllib3_user_agent() # one-time patch for clients that bypass `requests`
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
# ── Constants ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
OPENROUTER_HEADERS = {
|
|
25
|
+
"HTTP-Referer": "https://utim.dev",
|
|
26
|
+
"X-Title": "UTIM CLI Agent",
|
|
27
|
+
"User-Agent": "UTIM-CLI/2.0 (+https://utim.dev)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def attach_openrouter_headers(headers: Optional[dict] = None) -> dict:
|
|
32
|
+
"""Return a new dict with the canonical OpenRouter attribution headers merged in.
|
|
33
|
+
|
|
34
|
+
Caller-supplied headers take precedence over the defaults (so a test harness
|
|
35
|
+
can still override), except HTTP-Referer and X-Title which are forced to
|
|
36
|
+
the canonical values (otherwise OpenRouter shows the app as 'unknown').
|
|
37
|
+
"""
|
|
38
|
+
merged = dict(headers or {})
|
|
39
|
+
for key, val in OPENROUTER_HEADERS.items():
|
|
40
|
+
merged[key] = val
|
|
41
|
+
return merged
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def attach_openrouter_headers_to_session(session) -> None:
|
|
45
|
+
"""Mutate a requests.Session in place to always carry the headers."""
|
|
46
|
+
session.headers.update(OPENROUTER_HEADERS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def install_urllib3_user_agent() -> None:
|
|
50
|
+
"""Patch urllib3's default User-Agent pool header. Useful for code that
|
|
51
|
+
goes through the `httpx` or `urllib3` lower layers and bypasses `requests`.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
import urllib3
|
|
55
|
+
urllib3.util.make_headers( # noqa: F841 - intentional side-effect
|
|
56
|
+
user_agent=OPENROUTER_HEADERS["User-Agent"]
|
|
57
|
+
)
|
|
58
|
+
# Note: urllib3 doesn't actually store this globally; this is a stub
|
|
59
|
+
# for the rare direct-urllib3 caller. Most code uses `requests` and
|
|
60
|
+
# should be patched via attach_openrouter_headers_to_session().
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|