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:
kshitijk4poor
2026-09-29 23:25:26 +05:30
parent ebf1be6527
commit bf87ae620e
8 changed files with 67 additions and 8 deletions
+1 -1
View File
@@ -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]:
+2 -1
View File
@@ -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,
+5 -2
View File
@@ -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
+3
View File
@@ -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"),
):
+4 -2
View File
@@ -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
+9 -1
View File
@@ -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": ""