feat(models): add gpt-6.1-sol (OpenRouter + direct OpenAI API)
OpenAI released GPT-6.1 Sol on 2026-09-29. It is live on OpenRouter (openai/gpt-6.1-sol, -pro) and the direct API; not yet on Nous Portal or on Codex OAuth for our test account. - Catalog: OPENROUTER_MODELS + openai-api picker; kept off the nous list via _OPENROUTER_ONLY until the Portal serves it. Manifest regenerated. - Context: 1.05M direct API, 272K Codex fallback; 272K compaction auto-raise on the Codex route. - Pricing: $2/$10, cached input $0.10 (5%, not 6 Sol's 10%), cache write $2.50, 2x/1.5x above 272K; -pro aliased to the base row. - Reasoning: none/minimal return 400 on 6.1 Sol (unlike 6 Sol), so it uses Astra's low..max ladder without Astra's account gating. OpenRouter's catalog wrongly lists 'none' for it, so the OpenRouter profile omits a disable when the OpenAI ladder has no 'none'.
This commit is contained in:
@@ -620,7 +620,7 @@ def _is_codex_gpt54_or_gpt55(model: Optional[str], provider: Optional[str] = Non
|
||||
return "900k" not in bare
|
||||
return bare == "gpt-daybreak-blue-latest" or any(
|
||||
bare == fam or bare.startswith(fam + "-") or bare.startswith(fam + ".")
|
||||
for fam in ("gpt-5.4", "gpt-5.5", "gpt-5.6", "gpt-6-sol", "gpt-6-luna"))
|
||||
for fam in ("gpt-5.4", "gpt-5.5", "gpt-5.6", "gpt-6-sol", "gpt-6.1-sol", "gpt-6-luna"))
|
||||
|
||||
|
||||
def _codex_route_bare_model(model: Optional[str], provider: Optional[str]) -> Optional[str]:
|
||||
|
||||
@@ -298,6 +298,7 @@ DEFAULT_CONTEXT_LENGTHS = {
|
||||
# its own branch). 5.4-nano/-mini are 400k, not 1.05M; gpt-5.3-codex-spark is
|
||||
# Codex-OAuth-only and listed so "gpt-5" (400k) doesn't win.
|
||||
"gpt-6-astra": 1050000, # also matches -pro (verified live on OpenRouter)
|
||||
"gpt-6.1-sol": 1050000, # -pro too (OpenAI model page + OpenRouter live 2026-09-29)
|
||||
"gpt-6-sol": 1050000, "gpt-6-luna": 1050000, # -pro too (OpenRouter live 2026-09-22)
|
||||
"gpt-5.6-luna": 1050000, "gpt-5.6-terra": 1050000, "gpt-5.6-sol": 1050000, "gpt-5.5": 1050000,
|
||||
"gpt-5.4-nano": 400000, "gpt-5.4-mini": 400000, "gpt-5.4": 1050000,
|
||||
@@ -1687,7 +1688,7 @@ def _query_anthropic_context_length(model: str, base_url: str, api_key: Any) ->
|
||||
# Codex OAuth `context_window` values (what Codex enforces — lower than the direct API for the same
|
||||
# slugs). Fallback when the live probe fails; longest-key-first. gpt-5.3-codex-spark is listed so "gpt-5.3-codex" doesn't win.
|
||||
_CODEX_OAUTH_CONTEXT_FALLBACK: Dict[str, int] = {
|
||||
"gpt-6-astra": 272_000, "gpt-6-sol": 272_000, "gpt-6-luna": 272_000,
|
||||
"gpt-6-astra": 272_000, "gpt-6.1-sol": 272_000, "gpt-6-sol": 272_000, "gpt-6-luna": 272_000,
|
||||
"gpt-5.1-codex-max": 272_000, "gpt-5.1-codex-mini": 272_000, "gpt-5.3-codex": 272_000,
|
||||
"gpt-5.3-codex-spark": 128_000, "gpt-5.2-codex": 272_000, "gpt-5.4-mini": 272_000,
|
||||
"gpt-5.6-sol": 272_000, "gpt-5.6-terra": 272_000, "gpt-5.6-luna": 272_000, "gpt-daybreak-blue-latest": 272_000,
|
||||
|
||||
@@ -37,6 +37,9 @@ CODEX_ASTRA_EFFORTS: tuple[str, ...] = ("low", "medium", "high", "xhigh", "max")
|
||||
ASTRA_MODEL_IDS: frozenset[str] = frozenset({"gpt-6-astra", "gpt-6-astra-900k"})
|
||||
#: GPT-6 Sol/Terra/Luna (the 5.6 successors; ``-pro``/``-900k``/dated snapshots share the prefix).
|
||||
GPT6_TIER_PREFIXES: tuple[str, ...] = ("gpt-6-sol", "gpt-6-luna")
|
||||
#: GPT-6.1 Sol takes Astra's ``low..max`` ladder (``none`` 400s, live 2026-09-29) without Astra's
|
||||
#: account gating, so it stays in the static catalogs.
|
||||
NO_DISABLE_TIER_PREFIXES: tuple[str, ...] = ("gpt-6.1-sol",)
|
||||
DAYBREAK_MODEL_IDS: frozenset[str] = frozenset(
|
||||
{"gpt-daybreak-blue-latest", "gpt-daybreak-blue-latest-900k"}
|
||||
)
|
||||
@@ -96,9 +99,9 @@ def is_astra_model(model: Optional[str]) -> bool:
|
||||
|
||||
def codex_supported_efforts(model: Optional[str]) -> tuple[str, ...]:
|
||||
"""Supported effort set for an OpenAI/Codex Responses model."""
|
||||
if is_astra_model(model):
|
||||
return CODEX_ASTRA_EFFORTS
|
||||
bare = (model or "").strip().lower().rsplit("/", 1)[-1]
|
||||
if is_astra_model(model) or bare.startswith(NO_DISABLE_TIER_PREFIXES):
|
||||
return CODEX_ASTRA_EFFORTS
|
||||
return (
|
||||
CODEX_GPT56_EFFORTS
|
||||
if "gpt-5.6" in bare or bare.startswith(GPT6_TIER_PREFIXES) or bare in DAYBREAK_MODEL_IDS
|
||||
|
||||
@@ -276,6 +276,8 @@ _OFFICIAL_DOCS_PRICING[("openai", "gpt-6-astra")] = _snap(
|
||||
# Terra has no published model page yet, so it deliberately has no row.
|
||||
for _slug, _inp, _out, _read, _write, _inp_above, _out_above, _read_above, _write_above in (
|
||||
("gpt-6-sol", "2.00", "10.00", "0.20", "2.50", "4.00", "15.00", "0.40", "5.00"),
|
||||
# 6.1 Sol: same input/output as 6 Sol, but cached input is 0.05x input (not 0.10x).
|
||||
("gpt-6.1-sol", "2.00", "10.00", "0.10", "2.50", "4.00", "15.00", "0.20", "5.00"),
|
||||
("gpt-6-luna", "0.10", "0.50", "0.01", "0.125", "0.20", "0.75", "0.02", "0.25"),
|
||||
):
|
||||
_OFFICIAL_DOCS_PRICING[("openai", _slug)] = _snap(
|
||||
@@ -323,6 +325,7 @@ for _provider, _alias, _canonical in (
|
||||
*((("openai", f"{m}-{suffix}", m)
|
||||
for m in ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-sol", "gpt-6-luna")
|
||||
for suffix in ("pro", "900k"))),
|
||||
("openai", "gpt-6.1-sol-pro", "gpt-6.1-sol"), # no -900k: not verified above 272K on Codex
|
||||
("google", "gemini-3.1-pro-preview", "gemini-3.1-pro"),
|
||||
("google", "gemini-3.1-flash-lite-preview", "gemini-3.1-flash-lite"),
|
||||
):
|
||||
|
||||
@@ -34,7 +34,7 @@ OPENROUTER_MODELS: list[tuple[str, str]] = [
|
||||
"anthropic/claude-sonnet-5.5", "anthropic/claude-sonnet-5", "anthropic/claude-haiku-4.5", "openai/gpt-6-astra",
|
||||
"openai/gpt-6-astra-fast", "openai/gpt-6-astra-flex", "openai/gpt-6-astra-pro", "openai/gpt-6-astra-pro-fast",
|
||||
"openai/gpt-6-astra-pro-flex",
|
||||
"openai/gpt-6-sol", "openai/gpt-6-sol-pro",
|
||||
"openai/gpt-6.1-sol", "openai/gpt-6.1-sol-pro", "openai/gpt-6-sol", "openai/gpt-6-sol-pro",
|
||||
"openai/gpt-6-luna", "openai/gpt-6-luna-pro",
|
||||
"openai/gpt-5.5", "openai/gpt-5.5-pro", "openai/gpt-5.4-mini", "google/gemini-3.1-pro-preview",
|
||||
"google/gemini-3.8-flash", "google/gemini-3.7-flash", "x-ai/grok-4.7", "x-ai/grok-4.6",
|
||||
@@ -59,6 +59,8 @@ _OPENROUTER_ONLY = {
|
||||
"anthropic/claude-opus-5-fast", "anthropic/claude-opus-4.8-fast", "meta/muse-spark-1.2",
|
||||
"meta/muse-spark-1.2-contributor", "meta/muse-spark-1.3", "meta/muse-spark-1.3-contributor", "openrouter/pareto-code",
|
||||
"stealth/union-alpha",
|
||||
# Not on the Portal catalog yet (2026-09-29, launch day); drop once /v1/models lists them.
|
||||
"openai/gpt-6.1-sol", "openai/gpt-6.1-sol-pro",
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +168,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||
# Used by /model counts and provider_model_ids fallback when /v1/models is unavailable.
|
||||
"openai": list(_OPENAI_CHAT_MODELS),
|
||||
"openai-api": [
|
||||
"gpt-6-sol", "gpt-6-sol-pro", "gpt-6-luna", "gpt-6-luna-pro",
|
||||
"gpt-6.1-sol", "gpt-6.1-sol-pro", "gpt-6-sol", "gpt-6-sol-pro", "gpt-6-luna", "gpt-6-luna-pro",
|
||||
"gpt-5.6-sol", "gpt-5.6-sol-pro", "gpt-5.6-terra", "gpt-5.6-terra-pro", "gpt-5.6-luna",
|
||||
"gpt-5.6-luna-pro", "gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano",
|
||||
"gpt-5-mini", "gpt-5.3-codex", "gpt-4.1", "gpt-4o", "gpt-4o-mini",
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Any
|
||||
|
||||
from agent.portal_tags import get_affinity_scope, get_conversation_context
|
||||
from agent.prompt_cache_scope import GROK_AGGREGATOR_MODEL_PREFIXES, is_fork_cache_scope
|
||||
from agent.reasoning_effort import codex_supported_efforts
|
||||
from agent.transports.codex import _cache_scope_from_session_id
|
||||
from providers import register_provider
|
||||
from providers.base import ProviderProfile
|
||||
@@ -87,8 +88,11 @@ class OpenRouterProfile(ProviderProfile):
|
||||
# A reasoning-mandatory route 400s on a disable ("Reasoning is
|
||||
# mandatory for this endpoint and cannot be disabled") — omit
|
||||
# the field and let the model think, same as the Nous profile.
|
||||
# OpenRouter's catalog lists ``none`` for openai/gpt-6.1-sol, but upstream 400s on it
|
||||
# (live 2026-09-29), so the OpenAI ladder in agent.reasoning_effort wins over the catalog.
|
||||
if disabled:
|
||||
return None if caps.get("mandatory") else cfg
|
||||
no_disable = (model or "").startswith("openai/") and "none" not in codex_supported_efforts(model)
|
||||
return None if caps.get("mandatory") or no_disable else cfg
|
||||
clamped = clamp_reasoning_effort_to_supported(
|
||||
effort, caps.get("supported_efforts")
|
||||
)
|
||||
|
||||
@@ -40,3 +40,41 @@ def test_gpt6_tiers_share_the_codex_900k_contract_with_56():
|
||||
_compression_threshold_for_model("gpt-5.6-sol", provider="openai-codex")
|
||||
assert _compression_threshold_for_model(f"{base}-900k", provider="openai-codex") is None
|
||||
assert codex_supported_efforts(f"openai/{base}") == CODEX_GPT56_EFFORTS
|
||||
|
||||
|
||||
def test_gpt61_sol_takes_astra_ladder_without_astra_gating():
|
||||
"""``none`` 400s on gpt-6.1-sol (live 2026-09-29), but it is not account-gated like Astra."""
|
||||
from agent.reasoning_effort import CODEX_ASTRA_EFFORTS, is_astra_model
|
||||
|
||||
for slug in ("gpt-6.1-sol", "openai/gpt-6.1-sol-pro", "gpt-6.1-sol-2026-09-29"):
|
||||
assert codex_supported_efforts(slug) == CODEX_ASTRA_EFFORTS, slug
|
||||
assert not is_astra_model(slug), slug
|
||||
assert "none" in codex_supported_efforts("gpt-6-sol")
|
||||
|
||||
|
||||
def test_openrouter_omits_disable_the_openai_ladder_rejects(monkeypatch):
|
||||
"""OpenRouter's catalog advertises ``none`` for both Sol generations; only 6.1 must omit the disable."""
|
||||
import hermes_cli.models_reasoning_caps as caps_mod
|
||||
from providers import get_provider_profile
|
||||
|
||||
monkeypatch.setattr(caps_mod, "openrouter_model_reasoning_capabilities", lambda model: {
|
||||
"supports_reasoning": True, "mandatory": False,
|
||||
"supported_efforts": ["max", "xhigh", "high", "medium", "low", "none"]})
|
||||
p = get_provider_profile("openrouter")
|
||||
off = {"enabled": False}
|
||||
body, _ = p.build_api_kwargs_extras(reasoning_config=off, supports_reasoning=True, model="openai/gpt-6.1-sol")
|
||||
assert "reasoning" not in body
|
||||
body, _ = p.build_api_kwargs_extras(reasoning_config=off, supports_reasoning=True, model="openai/gpt-6-sol")
|
||||
assert body["reasoning"] == off
|
||||
|
||||
|
||||
def test_gpt61_sol_resolves_context_and_pricing_like_its_tier():
|
||||
from agent.model_metadata import DEFAULT_CONTEXT_LENGTHS, _CODEX_OAUTH_CONTEXT_FALLBACK
|
||||
from agent.usage_pricing import _OFFICIAL_DOCS_PRICING
|
||||
|
||||
assert DEFAULT_CONTEXT_LENGTHS["gpt-6.1-sol"] == DEFAULT_CONTEXT_LENGTHS["gpt-6-sol"]
|
||||
assert _CODEX_OAUTH_CONTEXT_FALLBACK["gpt-6.1-sol"] == _CODEX_OAUTH_CONTEXT_FALLBACK["gpt-6-sol"]
|
||||
base = _OFFICIAL_DOCS_PRICING[("openai", "gpt-6.1-sol")]
|
||||
assert _OFFICIAL_DOCS_PRICING[("openai", "gpt-6.1-sol-pro")] is base
|
||||
assert base.cache_read_cost_per_million == base.input_cost_per_million / 20 # 5%, not 6 Sol's 10%
|
||||
assert not is_codex_900k_base("gpt-6.1-sol") # not verified above 272K on Codex
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updated_at": "2026-09-28T19:38:29Z",
|
||||
"updated_at": "2026-09-29T17:51:00Z",
|
||||
"metadata": {
|
||||
"source": "hermes-agent repo",
|
||||
"docs": "https://hermes-agent.nousresearch.com/docs/reference/model-catalog"
|
||||
@@ -76,6 +76,14 @@
|
||||
"id": "openai/gpt-6-astra-pro-flex",
|
||||
"description": "0.5x price, flex tier"
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-6.1-sol",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-6.1-sol-pro",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-6-sol",
|
||||
"description": ""
|
||||
|
||||
Reference in New Issue
Block a user