Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,4 @@ tests/launch_*.py
uv.lock
# Superpowers smoke runs: raw agent output + local paths, share sanitized excerpts instead
smoke_results/
.codegraph/
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ include = ["skillopt", "skillopt.*", "skillopt_sleep", "skillopt_sleep.*", "skil
line-length = 120
target-version = "py310"

[tool.pytest.ini_options]
markers = ["slow: opt-in tests that invoke the real Hermes CLI or network"]

[tool.ruff.lint]
select = ["E", "F", "I", "W"]
ignore = ["E501"]
88 changes: 88 additions & 0 deletions skillopt/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from skillopt.model import azure_openai as _openai
from skillopt.model import claude_backend as _claude
from skillopt.model import codex_backend as _codex
from skillopt.model import hermes_backend as _hermes
from skillopt.model import minimax_backend as _minimax
from skillopt.model import openai_compatible_backend as _openai_compat
from skillopt.model import qwen_backend as _qwen
Expand Down Expand Up @@ -71,6 +72,10 @@ def set_backend(name: str | None) -> str:
set_optimizer_backend("openai_compatible")
set_target_backend("openai_compatible")
return "openai_compatible"
if normalized in {"hermes", "hermes_chat"}:
set_optimizer_backend("hermes_chat")
set_target_backend("hermes_chat")
return "hermes_chat"
raise ValueError(f"Unsupported legacy backend: {name!r}")


Expand All @@ -94,6 +99,8 @@ def get_backend_name() -> str:
return "cursor_exec"
if optimizer == "openai_compatible" and target == "openai_compatible":
return "openai_compatible"
if optimizer == "hermes_chat" and target == "hermes_chat":
return "hermes_chat"
return f"{optimizer}+{target}"


Expand Down Expand Up @@ -154,6 +161,15 @@ def chat_optimizer(
stage=stage,
timeout=timeout,
)
if get_optimizer_backend() == "hermes_chat":
return _hermes.chat_optimizer(
system=system,
user=user,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
timeout=timeout,
)
return _openai.chat_optimizer(
system=system,
user=user,
Expand Down Expand Up @@ -212,6 +228,15 @@ def chat_target(
reasoning_effort=reasoning_effort,
timeout=timeout,
)
if get_target_backend() == "hermes_chat":
return _hermes.chat_target(
system=system,
user=user,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
timeout=timeout,
)
if not is_target_chat_backend():
raise NotImplementedError(
"chat_target is only supported with target_backend=openai_chat, claude_chat, qwen_chat, minimax_chat, "
Expand Down Expand Up @@ -298,6 +323,17 @@ def chat_optimizer_messages(
return_message=return_message,
timeout=timeout,
)
if get_optimizer_backend() == "hermes_chat":
return _hermes.chat_optimizer_messages(
messages=messages,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
tools=tools,
tool_choice=tool_choice,
return_message=return_message,
timeout=timeout,
)
return _openai.chat_optimizer_messages(
messages=messages,
max_completion_tokens=max_completion_tokens,
Expand Down Expand Up @@ -369,6 +405,17 @@ def chat_target_messages(
return_message=return_message,
timeout=timeout,
)
if get_target_backend() == "hermes_chat":
return _hermes.chat_target_messages(
messages=messages,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
tools=tools,
tool_choice=tool_choice,
return_message=return_message,
timeout=timeout,
)
if not is_target_chat_backend():
raise NotImplementedError(
"chat_target_messages is only supported with target_backend=openai_chat, claude_chat, qwen_chat, "
Expand Down Expand Up @@ -400,6 +447,23 @@ def chat_messages_with_deployment(
return_message: bool = False,
timeout: int | None = None,
) -> tuple[Any, dict]:
if get_optimizer_backend() == "hermes_chat" and get_target_backend() == "hermes_chat":
# Route to Hermes only when BOTH backends are hermes_chat. When only
# one side is hermes_chat (dual-backend scenario) the function routes
# to OpenAI, which handles both sides via the generic OpenAI backend.
# A deployment-level ``role`` parameter would be cleaner but requires
# a broader API change — see the sibling function below.
return _hermes.chat_messages_with_deployment(
deployment=deployment,
messages=messages,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
tools=tools,
tool_choice=tool_choice,
return_message=return_message,
timeout=timeout,
)
return _openai.chat_messages_with_deployment(
deployment=deployment,
messages=messages,
Expand All @@ -424,6 +488,18 @@ def chat_with_deployment(
reasoning_effort: str | None = None,
timeout: int | None = None,
) -> tuple[str, dict]:
if get_optimizer_backend() == "hermes_chat" and get_target_backend() == "hermes_chat":
# Route to Hermes only when BOTH backends are hermes_chat. Same
# rationale as chat_messages_with_deployment above.
return _hermes.chat_with_deployment(
deployment=deployment,
system=system,
user=user,
max_completion_tokens=max_completion_tokens,
retries=retries,
stage=stage,
timeout=timeout,
)
return _openai.chat_with_deployment(
deployment=deployment,
system=system,
Expand Down Expand Up @@ -493,6 +569,17 @@ def get_token_summary() -> dict:
summary[stage]["prompt_tokens"] += values["prompt_tokens"]
summary[stage]["completion_tokens"] += values["completion_tokens"]
summary[stage]["total_tokens"] += values["total_tokens"]
hermes_summary = _hermes.get_token_summary()
for stage, values in hermes_summary.items():
if stage == "_total":
continue
if stage not in summary:
summary[stage] = values
continue
summary[stage]["calls"] += values["calls"]
summary[stage]["prompt_tokens"] += values["prompt_tokens"]
summary[stage]["completion_tokens"] += values["completion_tokens"]
summary[stage]["total_tokens"] += values["total_tokens"]
total = {
"calls": 0,
"prompt_tokens": 0,
Expand All @@ -517,6 +604,7 @@ def reset_token_tracker() -> None:
_minimax.reset_token_tracker()
_openai_compat.reset_token_tracker()
_codex.reset_token_tracker()
_hermes.reset_token_tracker()


def configure_azure_openai(
Expand Down
27 changes: 23 additions & 4 deletions skillopt/model/backend_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ def set_optimizer_backend(backend: str) -> None:
"minimax_chat",
"openai_compatible",
"codex_exec",
"hermes_chat",
}:
raise ValueError(
f"Unsupported optimizer backend: {OPTIMIZER_BACKEND!r}. "
"Supported values are 'openai_chat', 'claude_chat', 'qwen_chat', 'minimax_chat', "
"'openai_compatible', and 'codex_exec'."
"'openai_compatible', 'codex_exec', and 'hermes_chat'."
)
os.environ["OPTIMIZER_BACKEND"] = OPTIMIZER_BACKEND

Expand All @@ -74,11 +75,21 @@ def get_optimizer_backend() -> str:
def set_target_backend(backend: str) -> None:
global TARGET_BACKEND
TARGET_BACKEND = normalize_backend_name(backend or "openai_chat")
if TARGET_BACKEND not in {"openai_chat", "claude_chat", "qwen_chat", "minimax_chat", "openai_compatible", "codex_exec", "claude_code_exec", "cursor_exec"}:
if TARGET_BACKEND not in {
"openai_chat",
"claude_chat",
"qwen_chat",
"minimax_chat",
"openai_compatible",
"codex_exec",
"claude_code_exec",
"cursor_exec",
"hermes_chat",
}:
raise ValueError(
f"Unsupported target backend: {TARGET_BACKEND!r}. "
"Supported values are 'openai_chat', 'claude_chat', 'qwen_chat', 'minimax_chat', "
"'openai_compatible', 'codex_exec', 'claude_code_exec', and 'cursor_exec'."
"'openai_compatible', 'codex_exec', 'claude_code_exec', 'cursor_exec', and 'hermes_chat'."
)
os.environ["TARGET_BACKEND"] = TARGET_BACKEND

Expand All @@ -99,11 +110,19 @@ def is_optimizer_chat_backend() -> bool:
"minimax_chat",
"openai_compatible",
"codex_exec",
"hermes_chat",
}


def is_target_chat_backend() -> bool:
return TARGET_BACKEND in {"openai_chat", "claude_chat", "qwen_chat", "minimax_chat", "openai_compatible"}
return TARGET_BACKEND in {
"openai_chat",
"claude_chat",
"qwen_chat",
"minimax_chat",
"openai_compatible",
"hermes_chat",
}


def configure_codex_exec(
Expand Down
3 changes: 3 additions & 0 deletions skillopt/model/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"qwen_chat": "Qwen/Qwen3.5-4B",
"minimax_chat": "MiniMax-M2.7",
"openai_compatible": "gpt-4o-mini",
"hermes_chat": "hermes",
}

_BACKEND_ALIASES = {
Expand All @@ -53,6 +54,8 @@
"openai_compatible_chat": "openai_compatible",
"openai-compatible": "openai_compatible",
"compat": "openai_compatible",
"hermes": "hermes_chat",
"hermes_chat": "hermes_chat",
}


Expand Down
Loading