Merge pull request #2388 from NousResearch/hermes/hermes-31d7db3b
fix(provider): prevent Anthropic fallback from inheriting non-Anthropic base_url + fix(update): reset on stash conflict
This commit is contained in:
commit
55510cbad2
4 changed files with 83 additions and 17 deletions
|
|
@ -654,16 +654,20 @@ def _try_anthropic() -> Tuple[Optional[Any], Optional[str]]:
|
||||||
if not token:
|
if not token:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Allow base URL override from config.yaml model.base_url
|
# Allow base URL override from config.yaml model.base_url, but only
|
||||||
|
# when the configured provider is anthropic — otherwise a non-Anthropic
|
||||||
|
# base_url (e.g. Codex endpoint) would leak into Anthropic requests.
|
||||||
base_url = _ANTHROPIC_DEFAULT_BASE_URL
|
base_url = _ANTHROPIC_DEFAULT_BASE_URL
|
||||||
try:
|
try:
|
||||||
from hermes_cli.config import load_config
|
from hermes_cli.config import load_config
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
model_cfg = cfg.get("model")
|
model_cfg = cfg.get("model")
|
||||||
if isinstance(model_cfg, dict):
|
if isinstance(model_cfg, dict):
|
||||||
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
|
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
if cfg_base_url:
|
if cfg_provider == "anthropic":
|
||||||
base_url = cfg_base_url
|
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
|
||||||
|
if cfg_base_url:
|
||||||
|
base_url = cfg_base_url
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2559,12 +2559,29 @@ def _restore_stashed_changes(
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
if restore.returncode != 0:
|
|
||||||
|
# Check for unmerged (conflicted) files — can happen even when returncode is 0
|
||||||
|
unmerged = subprocess.run(
|
||||||
|
git_cmd + ["diff", "--name-only", "--diff-filter=U"],
|
||||||
|
cwd=cwd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
has_conflicts = bool(unmerged.stdout.strip())
|
||||||
|
|
||||||
|
if restore.returncode != 0 or has_conflicts:
|
||||||
|
# Reset the working tree so Hermes is runnable with the updated code
|
||||||
|
subprocess.run(
|
||||||
|
git_cmd + ["reset", "--hard", "HEAD"],
|
||||||
|
cwd=cwd,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
print("✗ Update pulled new code, but restoring local changes failed.")
|
print("✗ Update pulled new code, but restoring local changes failed.")
|
||||||
if restore.stdout.strip():
|
if restore.stdout.strip():
|
||||||
print(restore.stdout.strip())
|
print(restore.stdout.strip())
|
||||||
if restore.stderr.strip():
|
if restore.stderr.strip():
|
||||||
print(restore.stderr.strip())
|
print(restore.stderr.strip())
|
||||||
|
print("The working tree has been reset to a clean state.")
|
||||||
print("Your changes are still preserved in git stash.")
|
print("Your changes are still preserved in git stash.")
|
||||||
print(f"Resolve manually with: git stash apply {stash_ref}")
|
print(f"Resolve manually with: git stash apply {stash_ref}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
|
|
@ -363,9 +363,14 @@ def resolve_runtime_provider(
|
||||||
"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
|
"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
|
||||||
"run 'claude setup-token', or authenticate with 'claude /login'."
|
"run 'claude setup-token', or authenticate with 'claude /login'."
|
||||||
)
|
)
|
||||||
# Allow base URL override from config.yaml model.base_url
|
# Allow base URL override from config.yaml model.base_url, but only
|
||||||
|
# when the configured provider is anthropic — otherwise a non-Anthropic
|
||||||
|
# base_url (e.g. Codex endpoint) would leak into Anthropic requests.
|
||||||
model_cfg = _get_model_config()
|
model_cfg = _get_model_config()
|
||||||
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
|
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
|
cfg_base_url = ""
|
||||||
|
if cfg_provider == "anthropic":
|
||||||
|
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
|
||||||
base_url = cfg_base_url or "https://api.anthropic.com"
|
base_url = cfg_base_url or "https://api.anthropic.com"
|
||||||
return {
|
return {
|
||||||
"provider": "anthropic",
|
"provider": "anthropic",
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path,
|
||||||
calls.append((cmd, kwargs))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["diff", "--name-only"]:
|
||||||
|
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "list"]:
|
if cmd[1:3] == ["stash", "list"]:
|
||||||
return SimpleNamespace(stdout="stash@{1} abc123\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="stash@{1} abc123\n", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "drop"]:
|
if cmd[1:3] == ["stash", "drop"]:
|
||||||
|
|
@ -81,8 +83,9 @@ def test_restore_stashed_changes_prompts_before_applying(monkeypatch, tmp_path,
|
||||||
|
|
||||||
assert restored is True
|
assert restored is True
|
||||||
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
|
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
|
||||||
assert calls[1][0] == ["git", "stash", "list", "--format=%gd %H"]
|
assert calls[1][0] == ["git", "diff", "--name-only", "--diff-filter=U"]
|
||||||
assert calls[2][0] == ["git", "stash", "drop", "stash@{1}"]
|
assert calls[2][0] == ["git", "stash", "list", "--format=%gd %H"]
|
||||||
|
assert calls[3][0] == ["git", "stash", "drop", "stash@{1}"]
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "Restore local changes now? [Y/n]" in out
|
assert "Restore local changes now? [Y/n]" in out
|
||||||
assert "restored on top of the updated codebase" in out
|
assert "restored on top of the updated codebase" in out
|
||||||
|
|
@ -117,6 +120,8 @@ def test_restore_stashed_changes_applies_without_prompt_when_disabled(monkeypatc
|
||||||
calls.append((cmd, kwargs))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["diff", "--name-only"]:
|
||||||
|
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "list"]:
|
if cmd[1:3] == ["stash", "list"]:
|
||||||
return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "drop"]:
|
if cmd[1:3] == ["stash", "drop"]:
|
||||||
|
|
@ -129,8 +134,9 @@ def test_restore_stashed_changes_applies_without_prompt_when_disabled(monkeypatc
|
||||||
|
|
||||||
assert restored is True
|
assert restored is True
|
||||||
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
|
assert calls[0][0] == ["git", "stash", "apply", "abc123"]
|
||||||
assert calls[1][0] == ["git", "stash", "list", "--format=%gd %H"]
|
assert calls[1][0] == ["git", "diff", "--name-only", "--diff-filter=U"]
|
||||||
assert calls[2][0] == ["git", "stash", "drop", "stash@{0}"]
|
assert calls[2][0] == ["git", "stash", "list", "--format=%gd %H"]
|
||||||
|
assert calls[3][0] == ["git", "stash", "drop", "stash@{0}"]
|
||||||
assert "Restore local changes now?" not in capsys.readouterr().out
|
assert "Restore local changes now?" not in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -152,6 +158,8 @@ def test_restore_stashed_changes_keeps_going_when_stash_entry_cannot_be_resolved
|
||||||
calls.append((cmd, kwargs))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["diff", "--name-only"]:
|
||||||
|
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "list"]:
|
if cmd[1:3] == ["stash", "list"]:
|
||||||
return SimpleNamespace(stdout="stash@{0} def456\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="stash@{0} def456\n", stderr="", returncode=0)
|
||||||
raise AssertionError(f"unexpected command: {cmd}")
|
raise AssertionError(f"unexpected command: {cmd}")
|
||||||
|
|
@ -161,10 +169,9 @@ def test_restore_stashed_changes_keeps_going_when_stash_entry_cannot_be_resolved
|
||||||
restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
|
restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
|
||||||
|
|
||||||
assert restored is True
|
assert restored is True
|
||||||
assert calls == [
|
assert calls[0] == (["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True})
|
||||||
(["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True}),
|
assert calls[1] == (["git", "diff", "--name-only", "--diff-filter=U"], {"cwd": tmp_path, "capture_output": True, "text": True})
|
||||||
(["git", "stash", "list", "--format=%gd %H"], {"cwd": tmp_path, "capture_output": True, "text": True, "check": True}),
|
assert calls[2] == (["git", "stash", "list", "--format=%gd %H"], {"cwd": tmp_path, "capture_output": True, "text": True, "check": True})
|
||||||
]
|
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "couldn't find the stash entry to drop" in out
|
assert "couldn't find the stash entry to drop" in out
|
||||||
assert "stash was left in place" in out
|
assert "stash was left in place" in out
|
||||||
|
|
@ -181,6 +188,8 @@ def test_restore_stashed_changes_keeps_going_when_drop_fails(monkeypatch, tmp_pa
|
||||||
calls.append((cmd, kwargs))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["diff", "--name-only"]:
|
||||||
|
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "list"]:
|
if cmd[1:3] == ["stash", "list"]:
|
||||||
return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
|
return SimpleNamespace(stdout="stash@{0} abc123\n", stderr="", returncode=0)
|
||||||
if cmd[1:3] == ["stash", "drop"]:
|
if cmd[1:3] == ["stash", "drop"]:
|
||||||
|
|
@ -192,7 +201,7 @@ def test_restore_stashed_changes_keeps_going_when_drop_fails(monkeypatch, tmp_pa
|
||||||
restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
|
restored = hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
|
||||||
|
|
||||||
assert restored is True
|
assert restored is True
|
||||||
assert calls[2][0] == ["git", "stash", "drop", "stash@{0}"]
|
assert calls[3][0] == ["git", "stash", "drop", "stash@{0}"]
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "couldn't drop the saved stash entry" in out
|
assert "couldn't drop the saved stash entry" in out
|
||||||
assert "drop failed" in out
|
assert "drop failed" in out
|
||||||
|
|
@ -208,6 +217,10 @@ def test_restore_stashed_changes_exits_cleanly_when_apply_fails(monkeypatch, tmp
|
||||||
calls.append((cmd, kwargs))
|
calls.append((cmd, kwargs))
|
||||||
if cmd[1:3] == ["stash", "apply"]:
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
return SimpleNamespace(stdout="conflict output\n", stderr="conflict stderr\n", returncode=1)
|
return SimpleNamespace(stdout="conflict output\n", stderr="conflict stderr\n", returncode=1)
|
||||||
|
if cmd[1:3] == ["diff", "--name-only"]:
|
||||||
|
return SimpleNamespace(stdout="hermes_cli/main.py\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["reset", "--hard"]:
|
||||||
|
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||||
raise AssertionError(f"unexpected command: {cmd}")
|
raise AssertionError(f"unexpected command: {cmd}")
|
||||||
|
|
||||||
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
|
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
|
||||||
|
|
@ -219,7 +232,34 @@ def test_restore_stashed_changes_exits_cleanly_when_apply_fails(monkeypatch, tmp
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "Your changes are still preserved in git stash." in out
|
assert "Your changes are still preserved in git stash." in out
|
||||||
assert "git stash apply abc123" in out
|
assert "git stash apply abc123" in out
|
||||||
assert calls == [(["git", "stash", "apply", "abc123"], {"cwd": tmp_path, "capture_output": True, "text": True})]
|
assert "working tree has been reset to a clean state" in out
|
||||||
|
# Verify reset --hard was called to clean up conflict markers
|
||||||
|
reset_calls = [c for c, _ in calls if c[1:3] == ["reset", "--hard"]]
|
||||||
|
assert len(reset_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_restore_stashed_changes_resets_when_unmerged_files_detected(monkeypatch, tmp_path, capsys):
|
||||||
|
"""Even if stash apply returns 0, conflict markers must be cleaned up."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_run(cmd, **kwargs):
|
||||||
|
calls.append((cmd, kwargs))
|
||||||
|
if cmd[1:3] == ["stash", "apply"]:
|
||||||
|
return SimpleNamespace(stdout="applied\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["diff", "--name-only"]:
|
||||||
|
return SimpleNamespace(stdout="cli.py\n", stderr="", returncode=0)
|
||||||
|
if cmd[1:3] == ["reset", "--hard"]:
|
||||||
|
return SimpleNamespace(stdout="", stderr="", returncode=0)
|
||||||
|
raise AssertionError(f"unexpected command: {cmd}")
|
||||||
|
|
||||||
|
monkeypatch.setattr(hermes_main.subprocess, "run", fake_run)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match="1"):
|
||||||
|
hermes_main._restore_stashed_changes(["git"], tmp_path, "abc123", prompt_user=False)
|
||||||
|
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "working tree has been reset to a clean state" in out
|
||||||
|
assert "git stash apply abc123" in out
|
||||||
|
|
||||||
|
|
||||||
def test_stash_local_changes_if_needed_raises_when_stash_ref_missing(monkeypatch, tmp_path):
|
def test_stash_local_changes_if_needed_raises_when_stash_ref_missing(monkeypatch, tmp_path):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue