openrouter
This commit is contained in:
@@ -107,12 +107,89 @@ if not shutil.which(CLAUDE_BIN):
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _expand_env_vars(value: str, env: dict[str, str]) -> str:
|
||||
"""Expand $VAR and ${VAR} patterns using values from env dict."""
|
||||
import re
|
||||
|
||||
def replacer(match: re.Match) -> str:
|
||||
var_name = match.group(1) or match.group(2)
|
||||
return env.get(var_name, match.group(0))
|
||||
|
||||
# Match ${VAR} or $VAR
|
||||
return re.sub(r'\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)', replacer, value)
|
||||
|
||||
|
||||
def _load_env_file(path: Path, target_env: dict[str, str]) -> None:
|
||||
"""Parse shell export statements from env file and update target_env."""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("export "):
|
||||
# Parse: export VAR="value" or export VAR=value
|
||||
rest = line[7:] # remove "export "
|
||||
if "=" in rest:
|
||||
key, val = rest.split("=", 1)
|
||||
key = key.strip()
|
||||
# Remove inline comments (everything after unquoted #)
|
||||
val = _strip_shell_comments(val.strip())
|
||||
# Remove quotes if present
|
||||
val = val.strip('"').strip("'")
|
||||
if key and not key.startswith("#"):
|
||||
# Expand shell variables like $OPENROUTER_API_KEY
|
||||
val = _expand_env_vars(val, target_env)
|
||||
target_env[key] = val
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _strip_shell_comments(value: str) -> str:
|
||||
"""Remove shell-style comments from a value string.
|
||||
|
||||
Handles quoted # characters correctly.
|
||||
"""
|
||||
in_single = False
|
||||
in_double = False
|
||||
escaped = False
|
||||
|
||||
for i, char in enumerate(value):
|
||||
if escaped:
|
||||
escaped = False
|
||||
continue
|
||||
if char == '\\':
|
||||
escaped = True
|
||||
continue
|
||||
if char == '"' and not in_single:
|
||||
in_double = not in_double
|
||||
elif char == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
elif char == '#' and not in_single and not in_double:
|
||||
# Comment starts here
|
||||
return value[:i].rstrip()
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _safe_env() -> dict[str, str]:
|
||||
"""Return os.environ minus sensitive/problematic variables."""
|
||||
stripped = {k for k in _ENV_STRIP if k in os.environ}
|
||||
"""Return os.environ minus sensitive/problematic variables.
|
||||
|
||||
Daca exista fisierul .use_openrouter, incarca variabilele din ~/.claude-env.sh
|
||||
"""
|
||||
env = dict(os.environ)
|
||||
|
||||
# Check for OpenRouter toggle
|
||||
semaphore = PROJECT_ROOT / ".use_openrouter"
|
||||
env_file = Path.home() / ".claude-env.sh"
|
||||
if semaphore.exists() and env_file.exists():
|
||||
# Parse and load environment variables from .claude-env.sh
|
||||
_load_env_file(env_file, env)
|
||||
# Keep ANTHROPIC_DEFAULT_*_MODEL vars - Claude CLI uses them to translate
|
||||
# haiku/sonnet/opus aliases to OpenRouter model names
|
||||
|
||||
stripped = {k for k in _ENV_STRIP if k in env}
|
||||
if stripped:
|
||||
_security_log.debug("Stripped env vars from subprocess: %s", stripped)
|
||||
return {k: v for k, v in os.environ.items() if k not in _ENV_STRIP}
|
||||
return {k: v for k, v in env.items() if k not in _ENV_STRIP}
|
||||
|
||||
|
||||
def _load_sessions() -> dict:
|
||||
@@ -242,8 +319,13 @@ def _run_claude(
|
||||
|
||||
if proc.returncode != 0:
|
||||
stdout_tail = "\n".join(text_blocks[-3:]) if text_blocks else ""
|
||||
detail = stderr_output[:500] or stdout_tail[:500]
|
||||
# Check if result_obj has an error
|
||||
result_error = ""
|
||||
if result_obj and result_obj.get("is_error"):
|
||||
result_error = result_obj.get("result", "") or result_obj.get("error", "")
|
||||
detail = stderr_output[:500] or result_error[:500] or stdout_tail[:500]
|
||||
logger.error("Claude CLI stderr: %s", stderr_output[:1000])
|
||||
logger.error("Claude CLI result_obj: %s", result_obj)
|
||||
raise RuntimeError(
|
||||
f"Claude CLI error (exit {proc.returncode}): {detail}"
|
||||
)
|
||||
@@ -385,14 +467,14 @@ def resume_session(
|
||||
If *on_text* is provided, each intermediate Claude text block is passed
|
||||
to the callback as soon as it arrives.
|
||||
"""
|
||||
# Find channel/model for logging
|
||||
# Find channel/model for logging and model selection
|
||||
sessions = _load_sessions()
|
||||
_log_channel = "?"
|
||||
_log_model = "?"
|
||||
_log_model = DEFAULT_MODEL
|
||||
for cid, sess in sessions.items():
|
||||
if sess.get("session_id") == session_id:
|
||||
_log_channel = cid
|
||||
_log_model = sess.get("model", "?")
|
||||
_log_model = sess.get("model", DEFAULT_MODEL)
|
||||
break
|
||||
|
||||
# Wrap external user message with injection protection markers
|
||||
@@ -401,6 +483,7 @@ def resume_session(
|
||||
cmd = [
|
||||
CLAUDE_BIN, "-p", wrapped_message,
|
||||
"--resume", session_id,
|
||||
"--model", _log_model,
|
||||
"--output-format", "stream-json", "--verbose",
|
||||
"--allowedTools", *ALLOWED_TOOLS,
|
||||
]
|
||||
@@ -452,10 +535,15 @@ def send_message(
|
||||
) -> str:
|
||||
"""High-level convenience: auto start or resume based on channel state."""
|
||||
session = get_active_session(channel_id)
|
||||
if session is not None:
|
||||
# Only resume if session has a valid session_id (not a pre-set model placeholder)
|
||||
if session is not None and session.get("session_id"):
|
||||
return resume_session(session["session_id"], message, timeout, on_text=on_text)
|
||||
# Use model from pre-set session if available, otherwise use provided model
|
||||
effective_model = model
|
||||
if session is not None and session.get("model"):
|
||||
effective_model = session["model"]
|
||||
response_text, _session_id = start_session(
|
||||
channel_id, message, model, timeout, on_text=on_text
|
||||
channel_id, message, effective_model, timeout, on_text=on_text
|
||||
)
|
||||
return response_text
|
||||
|
||||
|
||||
Reference in New Issue
Block a user