mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
Merge upstream main into feat/hermes-relay-shared-metrics
This commit is contained in:
commit
147e451cc8
8 changed files with 89 additions and 2 deletions
|
|
@ -1156,6 +1156,10 @@ def init_agent(
|
|||
elif base_url_host_matches(effective_base, "chatgpt.com"):
|
||||
from agent.auxiliary_client import _codex_cloudflare_headers
|
||||
client_kwargs["default_headers"] = _codex_cloudflare_headers(api_key)
|
||||
elif base_url_host_matches(effective_base, "x.ai"):
|
||||
from tools.xai_http import hermes_xai_default_headers
|
||||
|
||||
client_kwargs["default_headers"] = hermes_xai_default_headers()
|
||||
elif "default_headers" not in client_kwargs:
|
||||
# Fall back to profile.default_headers for providers that
|
||||
# declare custom headers (e.g. Kimi User-Agent on non-kimi.com
|
||||
|
|
|
|||
|
|
@ -2957,7 +2957,13 @@ def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str
|
|||
return None, None
|
||||
api_key, base_url = resolved
|
||||
logger.debug("Auxiliary client: xAI OAuth (%s via Responses API)", model)
|
||||
real_client = _create_openai_client(api_key=api_key, base_url=base_url)
|
||||
from tools.xai_http import hermes_xai_default_headers
|
||||
|
||||
real_client = _create_openai_client(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
default_headers=hermes_xai_default_headers(),
|
||||
)
|
||||
return CodexAuxiliaryClient(real_client, model), model
|
||||
|
||||
|
||||
|
|
@ -5085,6 +5091,10 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False):
|
|||
async_kwargs["default_headers"] = {"User-Agent": "claude-code/0.1.0"}
|
||||
elif base_url_host_matches(sync_base_url, "integrate.api.nvidia.com"):
|
||||
async_kwargs["default_headers"] = build_nvidia_nim_headers(sync_base_url)
|
||||
elif base_url_host_matches(sync_base_url, "x.ai"):
|
||||
from tools.xai_http import hermes_xai_default_headers
|
||||
|
||||
async_kwargs["default_headers"] = hermes_xai_default_headers()
|
||||
else:
|
||||
# Fall back to profile.default_headers for providers that declare
|
||||
# client-level headers on their ProviderProfile (e.g. attribution
|
||||
|
|
@ -5708,6 +5718,10 @@ def resolve_provider_client(
|
|||
))
|
||||
elif base_url_host_matches(base_url, "integrate.api.nvidia.com"):
|
||||
headers.update(build_nvidia_nim_headers(base_url))
|
||||
elif base_url_host_matches(base_url, "x.ai"):
|
||||
from tools.xai_http import hermes_xai_default_headers
|
||||
|
||||
headers.update(hermes_xai_default_headers())
|
||||
else:
|
||||
# Fall back to profile.default_headers for providers that declare
|
||||
# client-level attribution headers on their profile (e.g. GMI
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { type AppendMessage, AssistantRuntimeProvider, ExportedMessageRepository, type ThreadMessage } from '@assistant-ui/react'
|
||||
import {
|
||||
type AppendMessage,
|
||||
AssistantRuntimeProvider,
|
||||
ExportedMessageRepository,
|
||||
type ThreadMessage
|
||||
} from '@assistant-ui/react'
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
|
|
@ -105,6 +110,7 @@ function Harness({ onEdit }: { onEdit: (message: AppendMessage) => Promise<void>
|
|||
describe('edit send arrow — macOS click gesture (blur races cancel)', () => {
|
||||
it('sends without an uncaught "Composer is not available" when the arrow-click blurs the editor', async () => {
|
||||
const uncaught: unknown[] = []
|
||||
|
||||
const onUncaught = (err: unknown) => {
|
||||
uncaught.push(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ const HasTooltipProvider = React.createContext(false)
|
|||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
// Radix's "skip" grace: after one tip opens, every trigger touched within
|
||||
// this window opens INSTANTLY, delay bypassed. Its 300ms default meant a
|
||||
// cursor sweeping the chrome still flashed a trail of tips despite the
|
||||
// hover delay. Zero it so each tip independently honors `delayDuration`.
|
||||
skipDelayDuration = 0,
|
||||
// Tips are labels, not interactive surfaces. Hoverable content + Radix's
|
||||
// pointer-grace bridge is what leaves tips stuck open — especially over
|
||||
// Electron `-webkit-app-region: drag` chrome where pointermove never fires
|
||||
|
|
@ -28,6 +33,7 @@ function TooltipProvider({
|
|||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
disableHoverableContent={disableHoverableContent}
|
||||
skipDelayDuration={skipDelayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""xAI (Grok) provider profile."""
|
||||
|
||||
from hermes_cli import __version__ as _HERMES_VERSION
|
||||
from providers import register_provider
|
||||
from providers.base import ProviderProfile
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ xai = ProviderProfile(
|
|||
env_vars=("XAI_API_KEY",),
|
||||
base_url="https://api.x.ai/v1",
|
||||
auth_type="api_key",
|
||||
default_headers={"User-Agent": f"Hermes-Agent/{_HERMES_VERSION}"},
|
||||
)
|
||||
|
||||
register_provider(xai)
|
||||
|
|
|
|||
|
|
@ -5086,6 +5086,11 @@ class AIAgent:
|
|||
self._client_kwargs["default_headers"] = _codex_cloudflare_headers(
|
||||
self._client_kwargs.get("api_key", "")
|
||||
)
|
||||
elif base_url_host_matches(base_url, "x.ai"):
|
||||
# Cover both provider=xai and provider=xai-oauth (api.x.ai).
|
||||
from tools.xai_http import hermes_xai_default_headers
|
||||
|
||||
self._client_kwargs["default_headers"] = hermes_xai_default_headers()
|
||||
else:
|
||||
# No URL-specific headers — check profile.default_headers before clearing.
|
||||
_ph_headers = None
|
||||
|
|
|
|||
|
|
@ -159,6 +159,46 @@ def test_gmi_base_url_picks_up_profile_user_agent(mock_openai):
|
|||
assert headers["User-Agent"].startswith("HermesAgent/")
|
||||
|
||||
|
||||
@patch("run_agent.OpenAI")
|
||||
def test_xai_base_url_applies_hermes_user_agent(mock_openai):
|
||||
"""Direct xAI chat must send Hermes-Agent/* instead of OpenAI/Python."""
|
||||
mock_openai.return_value = MagicMock()
|
||||
agent = AIAgent(
|
||||
api_key="test-key",
|
||||
base_url="https://api.x.ai/v1",
|
||||
model="grok-4",
|
||||
provider="xai",
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=True,
|
||||
)
|
||||
|
||||
agent._apply_client_headers_for_base_url("https://api.x.ai/v1")
|
||||
|
||||
headers = agent._client_kwargs["default_headers"]
|
||||
assert headers["User-Agent"].startswith("Hermes-Agent/")
|
||||
|
||||
|
||||
@patch("run_agent.OpenAI")
|
||||
def test_xai_oauth_base_url_applies_hermes_user_agent(mock_openai):
|
||||
"""xai-oauth uses the same api.x.ai host and must get the Hermes UA."""
|
||||
mock_openai.return_value = MagicMock()
|
||||
agent = AIAgent(
|
||||
api_key="oauth-token",
|
||||
base_url="https://api.x.ai/v1",
|
||||
model="grok-4",
|
||||
provider="xai-oauth",
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=True,
|
||||
)
|
||||
|
||||
agent._apply_client_headers_for_base_url("https://api.x.ai/v1")
|
||||
|
||||
headers = agent._client_kwargs["default_headers"]
|
||||
assert headers["User-Agent"].startswith("Hermes-Agent/")
|
||||
|
||||
|
||||
@patch("run_agent.OpenAI")
|
||||
def test_unknown_base_url_clears_default_headers(mock_openai):
|
||||
mock_openai.return_value = MagicMock()
|
||||
|
|
|
|||
|
|
@ -97,6 +97,16 @@ def hermes_xai_user_agent() -> str:
|
|||
return f"Hermes-Agent/{__version__}"
|
||||
|
||||
|
||||
def hermes_xai_default_headers() -> Dict[str, str]:
|
||||
"""Default headers for OpenAI-SDK and raw HTTP clients talking to xAI.
|
||||
|
||||
Replaces the OpenAI Python SDK's identifying ``User-Agent: OpenAI/Python …``
|
||||
so chat/completions and Responses traffic is attributed as Hermes Agent,
|
||||
matching the direct HTTP integrations (search, TTS, STT, image, video).
|
||||
"""
|
||||
return {"User-Agent": hermes_xai_user_agent()}
|
||||
|
||||
|
||||
def _load_config_section(section_name: str) -> Dict[str, Any]:
|
||||
"""Return a top-level Hermes config section as a dict, or empty."""
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue