{"repo":"NousResearch/hermes-agent","public_id":"5718","display_label":"GPT-5 custom endpoints ignoring explicit api_mode setting","summary":"Reports and fixes around custom GPT-5 provider endpoints not respecting an explicitly configured api_mode, defaulting to codex_responses instead of chat_completions. Includes proposed fixes and a salvage attempt for a related prior change.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":2,"attempt_prs":0},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":10548},"resolution":{"state":"partially_resolved","by":"pr-10548,pr-115738"},"freshness":{"state":"verified","last_verified_at":"2026-09-19T16:27:09+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.30799999999999994},"fix_edges":[{"issue_number":10473,"pr_number":10548,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":33062,"pr_number":115738,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":81932,"pr_number":115738,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":5718,"title":"openai-codex provider should allow configurable api_mode (chat_completions vs codex_responses)","url":null,"state":"closed"},{"kind":"issue","number":10473,"title":"[Bug]: custom GPT-5 endpoint is forced to codex_responses, ignoring explicit chat_completions","url":null,"state":"closed"},{"kind":"issue","number":33062,"title":"[Bug]: Custom GPT-5 providers can ignore explicit api_mode","url":null,"state":"closed"},{"kind":"pr","number":10548,"title":"fix: respect explicit api_mode for custom GPT-5 endpoints","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","run_agent.py","tests/gateway/test_background_process_notifications.py"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -486,9 +486,14 @@ def _parse_session_key(session_key: str) -> \"dict | None\":\n     \"\"\"Parse a session key into its component parts.\n \n     Session keys follow the format\n-    ``agent:main:{platform}:{chat_type}:{chat_id}[:{thread_id}[:{user_id}]]``.\n+    ``agent:main:{platform}:{chat_type}:{chat_id}[:{extra}...]``.\n     Returns a dict with ``platform``, ``chat_type``, ``chat_id``, and\n     optionally ``thread_id`` keys, or None if the key doesn't match.\n+\n+    The 6th element is only returned as ``thread_id`` for chat types where\n+    it is unambiguous (``dm`` and ``thread``).  For group/channel sessions\n+    the suffix may be a user_id (per-user isolation) rather than a\n+    thread_id, so we leave ``thread_id`` out to avoid mis-routing.\n     \"\"\"\n     parts = session_key.split(\":\")\n     if len(parts) >= 5 and parts[0] == \"agent\" and parts[1] == \"main\":","@@ -497,7 +502,7 @@ def _parse_session_key(session_key: str) -> \"dict | None\":\n             \"chat_type\": parts[3],\n             \"chat_id\": parts[4],\n         }\n-        if len(parts) > 5:\n+        if len(parts) > 5 and parts[3] in (\"dm\", \"thread\"):\n             result[\"thread_id\"] = parts[5]\n         return result\n     return None"]},{"file":"run_agent.py","hunks":["@@ -721,8 +721,11 @@ def __init__(\n         # Responses there. ACP runtimes are excluded: CopilotACPClient\n         # handles its own routing and does not implement the Responses API\n         # surface.\n+        # When api_mode was explicitly provided, respect it — the user\n+        # knows what their endpoint supports (#10473).\n         if (\n-            self.api_mode == \"chat_completions\"\n+            api_mode is None\n+            and self.api_mode == \"chat_completions\"\n             and self.provider != \"copilot-acp\"\n             and not str(self.base_url or \"\").lower().startswith(\"acp://copilot\")\n             and not str(self.base_url or \"\").lower().startswith(\"acp+tcp://\")"]},{"file":"tests/gateway/test_background_process_notifications.py","hunks":["@@ -383,15 +383,27 @@ def test_parse_session_key_valid():\n \n \n def test_parse_session_key_with_extra_parts():\n-    \"\"\"Thread ID (6th part) is extracted; further parts are ignored.\"\"\"\n+    \"\"\"6th part in a group key may be a user_id, not a thread_id — omit it.\"\"\"\n     result = _parse_session_key(\"agent:main:discord:group:chan123:thread456\")\n-    assert result == {\"platform\": \"discord\", \"chat_type\": \"group\", \"chat_id\": \"chan123\", \"thread_id\": \"thread456\"}\n+    assert result == {\"platform\": \"discord\", \"chat_type\": \"group\", \"chat_id\": \"chan123\"}\n \n \n def test_parse_session_key_with_user_id_part():\n-    \"\"\"7th part (user_id) is ignored — only up to thread_id is extracted.\"\"\"\n-    result = _parse_session_key(\"agent:main:telegram:group:chat1:thread42:user99\")\n-    assert result == {\"platform\": \"telegram\", \"chat_type\": \"group\", \"chat_id\": \"chat1\", \"thread_id\": \"thread42\"}\n+    \"\"\"Group keys with per-user isolation have user_id as 6th part — don't return as thread_id.\"\"\"\n+    result = _parse_session_key(\"agent:main:telegram:group:chat1:user99\")\n+    assert result == {\"platform\": \"telegram\", \"chat_type\": \"group\", \"chat_id\": \"chat1\"}\n+\n+\n+def test_parse_session_key_dm_with_thread():\n+    \"\"\"DM keys use parts[5] as thread_id unambiguously.\"\"\"\n+    result = _parse_session_key(\"agent:main:telegram:dm:chat1:topic42\")\n+    assert result == {\"platform\": \"telegram\", \"chat_type\": \"dm\", \"chat_id\": \"chat1\", \"thread_id\": \"topic42\"}\n+\n+\n+def test_parse_session_key_thread_chat_type():\n+    \"\"\"Thread-typed keys use parts[5] as thread_id unambiguously.\"\"\"\n+    result = _parse_session_key(\"agent:main:discord:thread:chan1:thread99\")\n+    assert result == {\"platform\": \"discord\", \"chat_type\": \"thread\", \"chat_id\": \"chan1\", \"thread_id\": \"thread99\"}\n \n \n def test_parse_session_key_too_short():\n"]}]},{"kind":"pr","number":115738,"title":"fix(fallback): entries naming a providers block keep its declared transport on activation (#33062, #81932)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/chat_completion_helpers.py","tests/agent/test_fallback_api_mode_preservation.py","website/docs/integrations/providers.md"]}],"trust":{"default_tier":"verified","includes_suggested":false}}