Fix custom provider identity loss in session persistence

_runtime_model_config persisted the live agent's RESOLVED provider into
the session row's model_config JSON. For any named providers:/
custom_providers: entry, agent.provider is the literal string "custom",
so the entry name was lost (and the api_key is deliberately never
persisted). On session.resume or _reset_session_agent the stored
provider="custom" fed resolve_runtime_provider(requested="custom"),
which cannot match a named entry — the rebuild either raised "No LLM
provider configured" or silently resolved placeholder credentials
against the patched-back base_url.

Persist the REQUESTED/entry identity instead: a new reverse lookup
find_custom_provider_identity(base_url) maps the endpoint URL back to
the canonical custom:<name> menu key. _runtime_model_config stores that
key; _make_agent performs the same recovery for rows persisted before
the fix, falling back to passing the stored base_url as
explicit_base_url so the direct-alias branch still targets the
session's endpoint when no entry matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adalsteinn Helgason
2026-06-13 05:51:05 -07:00
committed by Teknium
co-authored by Claude Fable 5
parent e256f4aae4
commit 643dc82793
4 changed files with 392 additions and 0 deletions
+55
View File
@@ -660,6 +660,61 @@ def has_named_custom_provider(requested_provider: str) -> bool:
return False
def find_custom_provider_identity(base_url: str) -> Optional[str]:
"""Map an endpoint URL back to its canonical ``custom:<name>`` menu key.
Returns the ``custom:<normalized-name>`` slug of the first ``providers:``
/ ``custom_providers:`` entry whose base_url matches, or ``None`` when no
entry owns the URL.
Session persistence stores the agent's *resolved* provider, and for every
named custom endpoint that is the literal string ``"custom"`` — the entry
name is lost, and the api_key is deliberately never persisted. The
endpoint URL is the one durable fact that survives the round-trip, so
this reverse lookup lets persist/rebuild paths recover the entry identity
(and with it key_env/api_key/api_mode resolution via
:func:`_get_named_custom_provider`) instead of failing with
``auth_unavailable`` or silently rebuilding with placeholder credentials.
"""
target = _normalize_base_url_for_match(base_url)
if not target:
return None
try:
config = load_config()
except Exception:
return None
providers = config.get("providers")
if isinstance(providers, dict):
for ep_name, entry in providers.items():
if not isinstance(entry, dict):
continue
entry_url = (
entry.get("api") or entry.get("url") or entry.get("base_url") or ""
)
if _normalize_base_url_for_match(entry_url) == target:
return f"custom:{_normalize_custom_provider_name(str(ep_name))}"
try:
custom_providers = get_compatible_custom_providers(config)
except Exception:
custom_providers = None
for entry in custom_providers or []:
if not isinstance(entry, dict):
continue
name = entry.get("name")
if not isinstance(name, str) or not name.strip():
continue
if _normalize_base_url_for_match(entry.get("base_url")) == target:
return f"custom:{_normalize_custom_provider_name(name)}"
return None
def _normalize_base_url_for_match(value) -> str:
return str(value or "").strip().rstrip("/").lower()
def _custom_provider_request_overrides(custom_provider: Dict[str, Any]) -> Dict[str, Any]:
extra_body = custom_provider.get("extra_body")
if not isinstance(extra_body, dict) or not extra_body: