mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
feat(agent): add Upstage Solar as a model provider
Adds Upstage Solar as a bundled model-provider plugin. Solar exposes an OpenAI-compatible chat-completions endpoint at https://api.upstage.ai/v1, so the generic chat_completions transport handles request/response/streaming/tool calls — the profile is the core integration. Provider registration (Upstage isn't in models.dev, so each registry that does not auto-wire from the plugin layer needs an explicit entry — same pattern as nvidia/gmi): - plugins/model-providers/upstage/: UpstageProfile + plugin.yaml. Picker default and offline catalog list only the agentic Solar Pro models, led by `solar-pro` (rolling alias for the latest Pro). default_aux_model empty so aux tasks use the main model. `solar` alias. UPSTAGE_BASE_URL overrides the host. - hermes_cli/providers.py: HERMES_OVERLAYS + label + `solar` alias, so resolve_provider_full('upstage') resolves (without this, an explicit `provider: upstage` in config was dropped and fell through to auto-detect). - hermes_cli/auth.py: PROVIDER_REGISTRY entry + `solar` alias, so `hermes doctor` / resolve_provider recognise upstage (the static-registry path the lazy profile-extension doesn't reliably cover at validation time). - hermes_cli/models.py: CANONICAL_PROVIDERS entry places Upstage Solar in the curated picker order (above the auto-appended `custom`). - agent/model_metadata.py: context-window fallbacks (/v1/models omits context_length); `solar-pro` carries the 128K Pro context as the catch-all. Reasoning: UpstageProfile.build_api_kwargs_extras wires Solar's top-level `reasoning_effort` (low|medium|high; xhigh/max→high). Reasoning-capable families are solar-pro* and solar-open*; solar-mini/syn-pro never receive it. Defaults ON at medium when unset (matches the /reasoning "medium (default)" label); `/reasoning none` disables; explicit/saved settings are honored. No reasoning_content echo handling needed (unlike DeepSeek/Kimi). Web dashboard: - web/src/pages/EnvPage.tsx: add an "Upstage Solar" provider group so UPSTAGE_API_KEY / UPSTAGE_BASE_URL appear under LLM Providers (not "Other"). Docs/tests: - .env.example: documents UPSTAGE_API_KEY / UPSTAGE_BASE_URL. - tests: profile wiring, reasoning_effort mapping (pro/open/mini, efforts, disabled, default-on), provider-resolver regression (resolve_provider_full / get_provider / solar alias / overlay), `solar-pro` default. Testing: pytest tests/providers tests/plugins/model_providers tests/hermes_cli/test_upstage_provider.py tests/run_agent/test_provider_parity.py tests/hermes_cli/test_api_key_providers.py; ruff clean. Verified end-to-end: `hermes doctor` shows "Upstage Solar", and live chat works via both `--provider upstage` and `--provider solar`. Reasoning wire format per https://console.upstage.ai/api/docs/for-agents/raw. Platforms tested: macOS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
77d5b2d573
commit
20502b407c
10 changed files with 402 additions and 0 deletions
106
plugins/model-providers/upstage/__init__.py
Normal file
106
plugins/model-providers/upstage/__init__.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""Upstage Solar provider profile."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from providers import register_provider
|
||||
from providers.base import ProviderProfile
|
||||
|
||||
|
||||
# Model-name markers for Solar families that accept ``reasoning_effort``.
|
||||
# Substring match (not startswith) so dated variants like
|
||||
# ``solar-pro3-250127`` and ``solar-open-...`` are covered too.
|
||||
_REASONING_MODEL_MARKERS = ("solar-pro", "solar-open")
|
||||
|
||||
# When the user hasn't picked a reasoning effort, Hermes passes
|
||||
# reasoning_config=None. Solar's own server default is "minimal" (reasoning
|
||||
# off), which is the wrong default for an agentic workload. We default reasoning
|
||||
# ON at this effort — matching the "medium (default)" that Hermes' /reasoning
|
||||
# panel shows for an unset config, so the displayed default and the real wire
|
||||
# value agree. An explicit saved setting or a `/reasoning <level>` change is
|
||||
# always honored over this default; `/reasoning none` disables it.
|
||||
_DEFAULT_REASONING_EFFORT = "medium"
|
||||
|
||||
|
||||
def _model_supports_reasoning(model: str | None) -> bool:
|
||||
"""Solar reasoning-capable models.
|
||||
|
||||
The Solar Pro family (``solar-pro``, ``solar-pro2``, ``solar-pro3`` and
|
||||
dated variants like ``solar-pro3-250127``) and the Solar Open family
|
||||
(``solar-open*``) accept ``reasoning_effort``. ``solar-mini`` / ``syn-pro``
|
||||
ignore the parameter, so we don't send it.
|
||||
"""
|
||||
m = (model or "").strip().lower()
|
||||
return any(marker in m for marker in _REASONING_MODEL_MARKERS)
|
||||
|
||||
|
||||
class UpstageProfile(ProviderProfile):
|
||||
"""Upstage Solar — top-level ``reasoning_effort`` control.
|
||||
|
||||
Solar Pro/Open expose reasoning through a top-level ``reasoning_effort``
|
||||
field (``minimal`` | ``low`` | ``medium`` | ``high``), mirroring OpenAI's
|
||||
shape. Unlike DeepSeek/Kimi it does NOT require echoing ``reasoning_content``
|
||||
back on later turns, so only the request field needs wiring. We emit at most
|
||||
``low`` | ``medium`` | ``high`` — the explicit values both Solar Pro 2 and
|
||||
Pro 3 accept.
|
||||
|
||||
Default-on: Solar's own server default is ``minimal`` (off), but for an
|
||||
agentic workload we default reasoning ON (``_DEFAULT_REASONING_EFFORT``)
|
||||
when the user hasn't picked an effort. The user can still set any level or
|
||||
turn it off with ``/reasoning none``.
|
||||
"""
|
||||
|
||||
def build_api_kwargs_extras(
|
||||
self, *, reasoning_config: dict | None = None, model: str | None = None, **context
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
top_level: dict[str, Any] = {}
|
||||
|
||||
# solar-mini / syn-pro ignore reasoning_effort — send nothing.
|
||||
if not _model_supports_reasoning(model):
|
||||
return {}, top_level
|
||||
|
||||
# Unset (reasoning_config is None) → default reasoning ON for agents.
|
||||
if not reasoning_config or not isinstance(reasoning_config, dict):
|
||||
return {}, {"reasoning_effort": _DEFAULT_REASONING_EFFORT}
|
||||
|
||||
# Explicitly disabled (`/reasoning none`) → omit the field so Solar
|
||||
# applies its own default (minimal = off).
|
||||
if reasoning_config.get("enabled") is False:
|
||||
return {}, top_level
|
||||
|
||||
# Map Hermes' effort vocabulary onto Solar's accepted set. xhigh/max
|
||||
# collapse to high (Solar's strongest). minimal → off (omit). An
|
||||
# enabled request with no recognised effort uses the default effort.
|
||||
effort = (reasoning_config.get("effort") or "").strip().lower()
|
||||
mapped = {
|
||||
"minimal": None,
|
||||
"low": "low",
|
||||
"medium": "medium",
|
||||
"high": "high",
|
||||
"xhigh": "high",
|
||||
"max": "high",
|
||||
}.get(effort, _DEFAULT_REASONING_EFFORT)
|
||||
|
||||
if mapped:
|
||||
top_level["reasoning_effort"] = mapped
|
||||
return {}, top_level
|
||||
|
||||
|
||||
upstage = UpstageProfile(
|
||||
name="upstage",
|
||||
aliases=("solar",),
|
||||
display_name="Upstage Solar",
|
||||
description="Upstage (Solar API)",
|
||||
signup_url="https://console.upstage.ai/api-keys",
|
||||
env_vars=("UPSTAGE_API_KEY", "UPSTAGE_BASE_URL"),
|
||||
base_url="https://api.upstage.ai/v1",
|
||||
auth_type="api_key",
|
||||
# default_aux_model left empty → auxiliary side tasks use the main model.
|
||||
# entry [0] is the setup default. solar-pro is a rolling alias for the
|
||||
# latest Solar Pro, so the default tracks the current flagship.
|
||||
fallback_models=(
|
||||
"solar-pro",
|
||||
"solar-pro3",
|
||||
),
|
||||
)
|
||||
|
||||
register_provider(upstage)
|
||||
5
plugins/model-providers/upstage/plugin.yaml
Normal file
5
plugins/model-providers/upstage/plugin.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
name: upstage-provider
|
||||
kind: model-provider
|
||||
version: 1.0.0
|
||||
description: Upstage (Solar API)
|
||||
author: Upstage AI
|
||||
Loading…
Add table
Add a link
Reference in a new issue