feat(myah): Catalog capability passthrough + /aux-resolved endpoint - #7
Conversation
Shape change: curated_models list[str] → list[{id, name, capabilities?}].
6 real ModelCapabilities fields only (supports_tools, supports_vision,
supports_reasoning, context_window, max_output_tokens, model_family).
Capabilities omitted when get_model_capabilities returns None or raises.
Myah marker added for upstream triage.
Refs T3-1001
Returns the effective resolved provider/model per aux task using the same _resolve_task_provider_model + _resolve_auto logic Hermes uses at aux call time. Enables Myah's settings UI to show users the exact model each task will use instead of the ambiguous 'auto' placeholder. Refs T3-1001
Review: Hermes PR 7 — Catalog capability passthrough + /aux-resolved endpointVerdict: Approve / merge when CI clears. What this PR doesTwo additions to 1.
Rolling-deploy safe: platform's 2. Proxies
4 new tests cover capability passthrough: happy path, CI status
Minor observations (not blockers)
Ready to merge as a dependency of myah PR 47. |
…ner + Sentry/admin moves (#24) * chore(hermes): drop unused _get_adapter_for_platform() helper Verified zero callers via repo-wide grep. Function was a leftover from earlier dispatch refactoring. Drop candidate #2 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.1). * chore(hermes): drop os.environ CHAT_ID writer (concurrency bug) Per-request mutation of process-wide environment, racing with concurrent requests. The TODO comment confirmed this was a known defect awaiting migration. set_session_vars() in session_context.py is the contextvar-safe replacement. Drop candidate #5 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.2). * chore(hermes): catch up MAX_REQUEST_BYTES to upstream (10MB) Verified via git blame: the 1MB value at line 88 was authored by Teknium upstream at commit 80cc27e (2026-03-24) and later bumped to 10MB upstream. Fork has the older value due to drift, not a Myah security decision. Drop candidate #6 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.3). * chore(hermes): drop /health Myah credential-validation extension The MYAH_ADAPTER_ENABLED-gated credential check was specific to the fork-bundled adapter and goes away with the plugin-based OSS architecture. /health/detailed (line 832 below) provides rich runtime status; 'hermes doctor' covers credential validation. Drop candidate #4 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.4). * chore(hermes): drop cronjob_tools origin-unresolved WARNING Pure debug logging — the function returns None either way; the WARNING was added for diagnostic purposes during a 2025-era cron delivery bug. Function behavior unchanged. Drop candidate #7 from spec 2026-05-06 §2.3 (Tier 2A Task 2A.1.5). * feat(plugin): move myah_overrides.py from hermes_cli/ into plugin Provider catalog augmentation (230 LOC) was Myah-only data living in an upstream-counterpart directory. Moves it into the pip-installed plugin at myah_hermes_plugin/myah_admin/myah_overrides.py and updates the two known importers (plugins/myah-admin/dashboard/_providers.py and the plugin tests). After this commit, hermes_cli/ contains zero Myah-only files — one fewer cardinal-counterpart file to keep marker hygiene on. Implements Tier 2A Task 2A.6 from spec 2026-05-06 §3. * feat(plugin): move Sentry init from upstream into plugin register() (Task 2A.7 + drop #3) Previously gateway/platforms/api_server.py:76-82 had an inline 'try: from logging_setup import setup_sentry; setup_sentry(); except ImportError: pass' Myah marker block. logging_setup.py was Myah-specific (agent-container Sentry init + SentryHook adapter for agent.telemetry.TelemetryHook protocol) but lived in an upstream-counterpart top-level path. This commit: - Renames logging_setup.py -> myah_hermes_plugin/sentry_init.py. - Calls sentry_init.setup_sentry() from the plugin's register() so hosted-Myah agent containers still get Sentry on gateway boot. - Drops the inline init Myah-marker block from api_server.py. Idempotent: setup_sentry() returns silently when SENTRY_DSN_AGENT is unset (the OSS-user case). Closes drop candidate #3 from spec 2026-05-06 §2.3 (deferred from Tier 2A Task 2A.1 to 2A.7 because it depended on having a plugin-side sentry_init.py to call into). Implements Tier 2A Task 2A.7 from spec 2026-05-06 §3. * feat(plugin): add pre_gateway_dispatch hook (Tier 2A Task 2A.4) Replaces the skip_user_authorization semantics that PR #20 removed. Adds myah_hermes_plugin/myah_platform/pre_dispatch_hook.py with a single hook callback that returns {'action': 'allow'} for Myah-platform messages and None (passthrough) for everything else. Architectural note: returning 'allow' does NOT bypass the gateway-level user-allowlist check at gateway/run.py:3655. Auth bypass for Myah's single-tenant deployment is handled by allow_all_env=MYAH_ALLOW_ALL_USERS on the platform registration. The hook exists as a documented choke point for future Myah-specific routing logic (rate limiting, silent ingest, content rewrite) that must NOT live in upstream gateway/run.py. Wired via ctx.register_hook('pre_gateway_dispatch', ...) in the plugin's register() — guarded with hasattr(ctx, 'register_hook') so older PluginContext versions don't fail. Implements Tier 2A Task 2A.4 from spec 2026-05-06 §3. * feat(plugin): vendor _dispatch_approval_notify + register_gateway_notify Plugin owns its own copy of the notify-dispatch chain. Without it, the plugin's vendored request_action_confirmation has no transport to the platform adapter and the agent silently auto-approves. Variadic dispatch (1/2/3-arity callbacks) matches upstream's gateway/run.py:_dispatch_approval_notify behavior at lines 334-460. Uses threading.Lock for the registry to match upstream's thread-safe semantics in tools/approval.py. Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.0. * feat(plugin): vendor request_action_confirmation + _action_queues registry Plugin owns its own action confirmation primitives mirroring upstream's tools/approval.py:request_action_confirmation (sync, threading.Event-based). The sync contract is required because cronjob_tools._execute() runs in a tool-runner threadpool and blocks on event.wait(timeout) — an asyncio version would force rewriting the cron tool. Two of the six tests intentionally fail until Task 2A.2.2 lands the plugin's cron_tool.py shadow. Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.1. * feat(plugin): shadow upstream cronjob_tools.py with plugin-owned cron tool Verbatim copy of upstream's tools/cronjob_tools.py, with the single change of importing request_action_confirmation from the plugin-vendored myah_hermes_plugin.cron_approval. Tool registration uses the same 'cronjob' name as upstream so last-writer-wins on import order shadows upstream's handler with the plugin's. All 6 tests in test_cron_approval.py now pass (the two cron_tool sanity checks added in 2A.2.1 had been failing pending this commit). Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.2. * refactor(plugin): adapter.py uses plugin-vendored approval primitives Move 4 import sites in adapter.py from upstream's tools.approval to the plugin's vendored modules: - resolve_action_confirmation → myah_hermes_plugin.cron_approval - resolve_action_confirmation_by_session → myah_hermes_plugin.cron_approval - unregister_gateway_notify (×2) → myah_hermes_plugin.dispatcher Update mock targets in test_myah_confirm_dispatch.py to follow the new import paths. Comment block in send_action_confirmation updated to reference the plugin's cron_approval module. resolve_gateway_approval (legacy terminal-command approval) stays on tools.approval — it's outside Tier 2A's cron-approval scope. Implements docs/superpowers/plans/2026-05-06-myah-oss-completion-2a-plugin-stock-upstream.md Task 2A.2.3. * feat(plugin): adapter standalone aiohttp runner on MYAH_GATEWAY_PORT Tier 2A Task 2A.3 — collapse the previous hosted/standalone split. The plugin's MyahAdapter now ALWAYS owns its own aiohttp AppRunner + TCPSite via the new MyahStandaloneRunner helper, removing the dependency on upstream's gateway/platforms/api_server.py register_pre_setup_hook + get_shared_app. This is a one-way door for hosted Myah: once shipped, hosted Myah uses the standalone runner forever. See spec docs/superpowers/specs/2026-05-06-myah-oss-completion-design.md §3 Task 2A.3 for the rationale. Default port is now 8643 via MYAH_GATEWAY_PORT (was 8642 hard-coded). Port resolution still respects config.extra.port and MYAH_ADAPTER_PORT overrides for backward compat with existing yaml. Tests cover ephemeral binding (port=0 → OS-assigned), live HTTP route dispatch, and env-var resolution fallback. Zero new failures in the gateway/tools test directories vs baseline (55 pre-existing failures unchanged). * test: fix Tier 2A regressions surfaced by full CI suite The PR's local verification only ran tests/gateway/ + tests/tools/ — three tests in tests/cron/, tests/plugins/, and tests/tools/test_resolve_path.py broke under the full CI run. 1. tests/cron/test_origin_capture_log.py — DELETED. Task 2A.1.5 dropped the WARNING that this entire 'Bug E' file was designed to verify. The file's purpose is gone with the warning; keeping the happy-path companion test would just duplicate coverage that lives elsewhere. 2. tests/plugins/test_myah_admin_providers.py::test_list_providers_against_real_catalog — import switched to pytest.importorskip(). Task 2A.6 moved myah_overrides.py from hermes_cli/ into the plugin distribution, but the hermes Tests workflow only runs 'uv pip install -e ".[all,dev]"' against the core package — it never installs the plugin. Local dev runs (where the plugin is editable-installed) still exercise the real-catalog assertion; CI now skips it cleanly. The plugin's own pytest suite covers the catalog path independently. 3. tests/tools/test_resolve_path.py — added isolated_resolve_state fixture that clears tools.file_tools._file_ops_cache and tools.terminal_tool._active_environments before the two tests that rely on falling through to TERMINAL_CWD. Adding the new plugin modules to the suite shifted xdist worker assignment in a way that exposed pre-existing pollution from other tests leaving a 'default' task_id entry in those module-level dicts. The fixture is per-test (monkeypatch.setattr) so it doesn't disrupt other tests. * test: gentler test_resolve_path isolation (don't rebind module attrs) Previous fix used monkeypatch.setattr to swap _file_ops_cache and _active_environments with empty dicts. This unintentionally caused test_file_state_registry tests to fail on Linux CI under xdist — rebinding the module attribute discarded entries other tests had written under non-'default' keys, and the cross-worker timing exposed the leak. The replacement fixture mutates the existing dicts in place — only pops the 'default' key (which is what _resolve_path reads when no explicit task_id is passed) and restores the prior value after the test. No module-attribute rebinding, no impact on entries other tests created. * ci: trigger re-run to confirm test flakiness Recent runs showed 3 new tests intermittently failing on Linux CI that pass locally and pass in isolation: - test_concurrent_inserts_settle_at_cap (30s timeout) - test_modal_sandbox_fixes::TestToolResolution::test_terminal_tool_present - test_modal_sandbox_fixes::TestToolResolution::test_terminal_and_file_toolsets_resolve_all_tools This empty commit re-runs CI to confirm whether they're deterministic regressions or pre-existing test-ordering flakes exposed by the changed test count from this PR.
Closes Issue #7 from the OSS follow-up spec. Plugin-side replacement for the fork's get_structured_callbacks polymorphic dispatch in gateway/run.py:_run_agent. Three coordinated pieces: 1. pre_llm_call hook (runtime_extensions/streaming_callbacks.py) that mutates AIAgent callback attributes just-in-time before each LLM call. Resolves session_key from chat_id via the adapter's _chat_id_session_keys map; finds the cached agent in runner._agent_cache (private-attribute pattern; same risk class as _session_model_overrides used elsewhere in the plugin). 2. Marks _native_streaming_used per session so send() knows. 3. Duplicate-send suppression in MyahAdapter.send() — drops the gateway's final adapter.send(chat_id, full_response) call after streaming completes, preventing the message from duplicating. CI guards verify pre_llm_call is in VALID_HOOKS, GatewayRunner has _agent_cache, and AIAgent has the four callback attributes. Removable when upstream U-CB PR lands. Until then, structured streaming on stock vanilla matches the fork's behaviour. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase F.
…nect (#27) * feat(plugin): MYAH_USER_ID bootstrap via platform /whoami in OSS mode Hosted Myah injects MYAH_USER_ID per-container at spawn time. OSS self-hosted users have no spawner — without this bootstrap they would have to manually copy their user_id from the platform UI to ~/.hermes/.env or cron deliveries would fail with HTTP 400. The plugin's register(ctx) now calls _bootstrap_user_id() which: - Idempotent: no-op if MYAH_USER_ID already set (hosted case). - Skips silently if MYAH_PLATFORM_BASE_URL or bearer is missing. - Calls GET /api/v1/myah/whoami with the platform bearer token. - On success: populates MYAH_USER_ID in os.environ. - On any error: logs at WARNING level and continues (chat still works without cron-webhook delivery). Bearer alias fallback: MYAH_PLATFORM_BEARER, MYAH_AGENT_BEARER_TOKEN, or MYAH_AGENT_TOKEN — any one is acceptable, matching the env-var naming in agent/Dockerfile.stock and ~/.hermes/.env templates. Test: 9 tests in test_user_id_bootstrap.py cover idempotency, missing config, HTTP 4xx/network errors, empty response, alias env vars, trailing-slash URL handling. Refs: Phase 8.2 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * feat(plugin): F4 secret-capture global callback registration (Phase 5.1) Vanilla upstream's tools/skills_tool exposes set_secret_capture_callback(fn) — a single global slot for the secret-prompt callback, invoked when a tool needs the user to provide a secret value. The fork carries hosted-Myah-specific session-keyed wiring inside _run_agent's closure; on stock vanilla that wiring doesn't exist, so without this commit the plugin's _secret_capture_callback bound method never fires and secret prompts silently auto-skip. This commit registers a global wrapper at plugin register-time: 1. tools.skills_tool.set_secret_capture_callback(_global_secret_callback) takes the vanilla-shaped (name, prompt, metadata) call. 2. _global_secret_callback consults adapter._LATEST_ADAPTER (a late-bound module attr set by MyahAdapter.__init__) plus the session-key contextvar (from tools.approval) to resolve the right stream_id. 3. Delegates to adapter._secret_capture_callback(name, prompt, metadata, stream_id) — which already does the SSE event push and threading.Event blocking dance. No adapter-side changes needed. Single-adapter / single-process assumption (single-tenant OSS or per-user agent container in hosted Myah). Multi-tenant in-process would need a contextvar-keyed lookup; out of scope for v1. Failure modes (all silent + logged at WARNING): - No active adapter → auto-skip with success=true, skipped=true. - No active stream → adapter still called with empty stream_id; adapter handles via its existing 'No stream' branch. - tools.skills_tool unavailable → ImportError swallowed, no-op. Test: 6 tests in test_secret_capture_wiring.py cover all branches. All 310 plugin tests still pass. Refs: Phase 5.1 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * feat(plugin): F7 per-server MCP disconnect via direct private-attribute access Vanilla upstream's tools/mcp_tool exposes shutdown_mcp_servers() which tears down ALL configured MCP servers at once. The Myah admin UI needs per-server teardown so a user can remove one MCP connection (e.g. a misbehaving server) while leaving the others alive. Vanilla does not expose a public per-server disconnect API. The implementation accesses upstream's private state directly: - _servers: Dict[str, MCPServerTask] (upstream/main:tools/mcp_tool.py:1607) - _lock: threading.Lock (line 1969 — sync, NOT asyncio) - MCPServerTask.shutdown(self) (line 1568 — async coro) - _run_on_mcp_loop(coro, timeout=...) (line 2042 — cross-loop bridge) Per AGENTS.md interpretation (see runtime_extensions/__init__.py docstring): direct attribute access is normal Python, NOT modifying core files on disk. Same pattern adapter.py:get_session_override_direct already uses for runner._session_model_overrides. Behavior: - Acquire the threading.Lock (sync — never use asyncio.acquire). - Look up _servers[name]; return False if missing. - Call MCPServerTask.shutdown() coro through _run_on_mcp_loop bridge with caller-supplied timeout (default 15s). - Swallow shutdown exceptions — the user wants the server gone, don't block removal on a hung close. - Pop from _servers and return True. Robustness: every getattr falls back to None and degrades to a no-op-with-False return on upstream API drift. Two CI guard tests catch the rename at plugin-CI time: - test_upstream_state_present: assert _servers / _lock / _run_on_mcp_loop attributes still exist. - test_upstream_lock_is_threading_not_asyncio: assert lock works in a sync 'with' block (asyncio.Lock would error). Test: 7 tests in test_mcp_disconnect.py cover happy path, missing server, missing private state, exception during shutdown, custom timeout propagation, and the two CI guards. Refs: Phase 5.2 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * docs(plugin): release v1.1.0 with OSS bootstrap, F4 secret wiring, F7 MCP disconnect CHANGELOG.md gains: - New 'Known limitations' section at the top documenting F5 BOOT.md unavailability on stock vanilla upstream + plugin, and the single-tenant assumption of /whoami. - v1.1.0 entry documenting Phase 8.2 (user_id bootstrap), Phase 5.1 (secret-capture global callback), Phase 5.2 (MCP disconnect helper). - Test count update: 333 plugin tests (310 prior + 23 new). pyproject.toml version bumped 1.0.0 → 1.1.0 (additive features, no breaking changes per semver). Refs: Phase 6 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md * docs(plugin): document F6 cron-on-vanilla limitation and resolution paths Vanilla cron/scheduler.py:_deliver_result does NOT call runtime_adapter.build_delivery_metadata(...) — that polymorphic hook is fork-only (Tier 2B Task 2B.4). Result on stock vanilla + plugin: cron jobs run and write to ~/.hermes/cron/output/, but do not appear in chat history. Two resolution paths documented (neither shipped in v1.1.0): 1. Upstream PR U-CRON adding the polymorphic call to vanilla. 2. Plugin-side cron output-dir watcher (~150 LOC, no upstream PR). The hosted Myah deployment is unaffected — it uses the fork build with Tier 2B's polymorphic hook, so cron deliveries reach chat normally. Refs: Phase 3 of docs/superpowers/plans/2026-05-09-myah-no-fork-vendoring.md (documented as known limitation rather than shipping the contentious runtime monkey-patch on cron.scheduler._deliver_result) * chore(plugin): INFO log model switch flow for OSS Issue #2 Adds three logger.info calls in _handle_message_endpoint's model override block: pre-switch (raw_input + current state), post-switch (success/new_model/target_provider/error_message), and post-write (confirmed override). Lets us reproduce why OSS model picks fail to take effect and triage the right fix. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase B.1. * fix(plugin): OSS graceful fallback when model name unresolvable When MYAH_DEPLOYMENT_MODE=oss and the platform-supplied model is not in any of the user's hermes credentials (e.g. user.default_model is openai/gpt-4o-mini but hermes pool is openrouter+opencode-go), fall through to the agent's existing default rather than 400-failing the entire chat turn. Hosted mode behaviour is unchanged (multi-tenant: a 400 surfaces an auth/config bug to the user). OSS is single-tenant: the user wants their message to go through with whatever model the agent has. The session override is cleared either way, so subsequent turns re-attempt resolution. Closes Issue #2 from the OSS follow-up spec. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase B.4. * feat(plugin): bootstrap user.default_model from hermes config Extends _bootstrap_user_id() to read default_model from /whoami's OSS response and POST it to the platform's /api/v1/users/user/default-model endpoint. Idempotent and best-effort: logs and continues on any failure, never blocking the bootstrap. Closes Issue #5 plugin side from the OSS follow-up spec. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase C.4. * feat(plugin): F6 cron->chat output watcher for stock vanilla Closes Issue #6 from the OSS follow-up spec without requiring an upstream PR or core modification. The watcher polls the stable ~/.hermes/cron/output/{job_id}/*.md convention every 2s, reads job metadata via vanilla cron.jobs.load_jobs(), and POSTs to the platform's existing /api/v1/processes/webhook/run-complete. Trade-off: tool_calls_log is None because the on-disk .md doesn't carry it. Cron AG-UI render_* artifacts deliver as plain Markdown (accepted degradation per spec). Started lazily from pre_gateway_dispatch hook to avoid Python 3.12+ asyncio.get_event_loop() deprecation. Silent no-op when MYAH_PLATFORM_BASE_URL is unset. Filtered to origin.platform == 'myah' so telegram/discord cron jobs are unaffected. Includes a CI guard that catches upstream API drift on cron.jobs.OUTPUT_DIR and load_jobs. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase E. * chore(plugin): add _chat_id_session_keys + _native_streaming_used state Prepares MyahAdapter for the Phase F streaming workaround. New fields are wired up but have no consumer yet — adding them in a separate commit lets the next commits be focused. Includes defensive cleanup in the hosted-mode failure path so the map doesn't leak entries when model-override fails. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase F.1. * feat(plugin): F-streaming pre_llm_call hook for stock vanilla Closes Issue #7 from the OSS follow-up spec. Plugin-side replacement for the fork's get_structured_callbacks polymorphic dispatch in gateway/run.py:_run_agent. Three coordinated pieces: 1. pre_llm_call hook (runtime_extensions/streaming_callbacks.py) that mutates AIAgent callback attributes just-in-time before each LLM call. Resolves session_key from chat_id via the adapter's _chat_id_session_keys map; finds the cached agent in runner._agent_cache (private-attribute pattern; same risk class as _session_model_overrides used elsewhere in the plugin). 2. Marks _native_streaming_used per session so send() knows. 3. Duplicate-send suppression in MyahAdapter.send() — drops the gateway's final adapter.send(chat_id, full_response) call after streaming completes, preventing the message from duplicating. CI guards verify pre_llm_call is in VALID_HOOKS, GatewayRunner has _agent_cache, and AIAgent has the four callback attributes. Removable when upstream U-CB PR lands. Until then, structured streaming on stock vanilla matches the fork's behaviour. Refs: docs/superpowers/specs/2026-05-10-myah-oss-followup-issues.md Phase F. * fix(plugin): lazy gateway_runner self-discovery via _gateway_runner_ref `GatewayRunner._create_adapter` sets `adapter.gateway_runner` only for built-in adapters (Discord, Webhook at gateway/run.py:4620/4730). For plugin-registered platforms it returns the adapter from `platform_registry.create_adapter` directly without setting the backref, so MyahAdapter.gateway_runner stays None forever. This silently disabled three flows: 1. Phase B model override block (_handle_message_endpoint) — every model pick from the platform was dropped, agent always used hermes default. 2. Per-message model attribution (_dispatch_message finally) — _attribution_model/_attribution_provider stayed empty. 3. Phase F pre_llm_call streaming hook — early-returned at runner-None. Diagnostic confirmed during E2E: [myah-modelswitch] DIAGNOSTIC entry _override_model='moonshotai/kimi-k2.6' runner_is_none=True gateway_runner_id=None Add `MyahAdapter._resolve_runner()` that reads the upstream-exposed module weakref `gateway.run._gateway_runner_ref` (populated by GatewayRunner.__init__ at run.py:1206) when the fast-path attribute is unset. Cache the resolved runner on `self.gateway_runner` for next call. Same direct-attribute access pattern Tier 2B established for `_session_model_overrides` and `_agent_cache` access. Replace three reader sites — Phase B override, per-message attribution, admin route registration — with `self._resolve_runner()`. Update the `pre_llm_call` hook in `streaming_callbacks.py` similarly; keep a defensive fallback to `getattr(adapter, 'gateway_runner', None)` for older plugin builds. Works on both this branch and stock vanilla pip-installed hermes — both expose the same `_gateway_runner_ref` symbol. Hosted Myah behavior is unchanged because the fast-path returns the externally-set attribute when present. Verified E2E by sending a curl POST to /myah/v1/message with model='moonshotai/kimi-k2.6'; observed all three modelswitch INFO logs fire, agent's auxiliary client used `provider=openrouter model=moonshotai/kimi-k2.6`. Refs: e2e-output/report.md ISSUE-001 * fix(plugin): add /myah/v1/admin/config endpoint for hermes config readback Phase C.5's `fetch_hermes_default_model` previously hit the hermes dashboard at :9119/api/config which requires `HERMES_WEB_SESSION_TOKEN` — a token the OSS `setup-myah-oss.sh` script doesn't generate. Result: the dashboard returned 401, the helper silently returned None, the platform's /whoami returned `default_model: null`, and no sync ever fired. User stuck with open-webui's gpt-4o-mini default. Hosted Myah dodged this because the per-user-container spawner injects both tokens. OSS users have to opt into dashboard auth manually, which is invisible / poorly documented. Replace the dashboard call with a new plugin-side endpoint `GET /myah/v1/admin/config` that: - Reads `get_hermes_home() / 'config.yaml'` in-process - Returns only the model block (provider/default/base_url/api_mode) — no API keys, no plugin settings - Auths via MYAH_ADAPTER_AUTH_KEY (same bearer as every other /myah/v1/admin/* endpoint) which OSS sets to empty (allow-all) and hosted spawner sets to the per-user token The platform's `fetch_hermes_default_model` rewires to call this new endpoint on the adapter port (8643 OSS, per-user-container port hosted) instead of the dashboard. Auth simplifies: same bearer everywhere, no separate session token needed. Drops the broken plugin-side POST to /api/v1/users/user/default-model (that endpoint requires JWT auth via get_verified_user — the plugin only has the agent bearer). The platform's /whoami now performs the sync itself directly via Users.update_user_by_id — single-tenant OSS makes this safe; multi-tenant hosted doesn't reach this path because of the deployment_mode gate. Tests: - 7 new tests in tests/test_runtime_admin_config.py covering happy path, missing file, malformed YAML, partial model block, auth on/off - 4 new tests in platform test_myah.py covering the sync behavior - E2E verified: /whoami returns hermes default; DB updates to match; deliberate user choices (anything not gpt-4o-mini) are preserved Refs: e2e-output/report.md ISSUE-004 * fix(plugin): add /myah/v1/admin/providers endpoint with credential status Phase C.2's catalog short-circuit and the planned auto-import path both need a way to enumerate hermes-configured providers from the platform. The existing dashboard endpoint at /api/plugins/myah-admin/providers returns the raw catalog without a 'has_credential' field — callers had no way to distinguish 'provider available in UI' from 'user actually configured this'. The dashboard also requires HERMES_WEB_SESSION_TOKEN which OSS users don't have. New plugin-side admin endpoint: GET /myah/v1/admin/providers -> {"providers": [{"id": ..., "has_credential": bool, ...}, ...]} Calls the dashboard's _build_catalog() in-process (same plugin pip package) and enriches each entry with has_credential computed from: 1. auth.json['credential_pool'] — the canonical hermes-configured provider set (covers both api_key + oauth via 'hermes auth') 2. auth.json['providers'] — legacy OAuth-token storage 3. os.environ[<env_var>] — api_key fallback for providers configured via .env (e.g. OPENROUTER_API_KEY) Auth uses the standard adapter auth_key (MYAH_AGENT_BEARER_TOKEN in both OSS and hosted) — no separate session token needed. E2E verified: against the user's hermes (6 configured providers: openrouter, opencode-go, zai, openai-codex, copilot, xiaomi), the endpoint returns 36 total providers with exactly those 6 flagged as has_credential=True. Tests added (7 in tests/test_runtime_admin_providers.py): api_key via env, oauth via auth.json.providers, credential_pool path, all-three empty, response shape, catalog failure, auth on/off. Refs: e2e-output/report.md ISSUE-003 * fix(plugin): surface inline error when gateway suppresses failed response When the agent's LLM call fails (e.g. provider 402 / fallback exhausted / network timeout), the agent's `run_conversation` returns: { final_response: 'API call failed after 3 retries: ...', failed: True } But the gateway's `_run_agent` at gateway/run.py:14701-14720 constructs a NEW response dict that DOES NOT preserve the `failed` field. The downstream suppression check at gateway/run.py:15326 then sees `failed=None` (falsy), treats the run as successful, and combined with `native_streamed=True` (set optimistically when our structured callbacks were wired) suppresses the call to `adapter.send()`. Net result: user sees 'Thinking...' forever — no token streamed, no error rendered, no indication anything happened. Backend log shows 'Suppressing normal final send for session ... (streamed=False previewed=False native_streamed=True)' with no follow-up Send. We can't fix the upstream bug (don't-touch-hermes-core constraint). Plugin-side workaround: track whether `stream_delta` actually fired for each session. In `_dispatch_message`'s finally block (right before emitting `run.completed`), if no stream_delta ever fired for this session, emit a generic 'LLM call did not produce a response' error as a synthetic `message.delta` event. The frontend renders this inline so the user sees SOMETHING instead of staring at an empty Thinking spinner. Three coordinated changes: 1. MyahAdapter._stream_delta_invoked: new per-session set tracking actual stream_delta callback invocations. 2. get_structured_callbacks: stream_delta wrapper marks the session on first call. 3. _dispatch_message finally: emit error message.delta when the set doesn't contain this session_key by the time the run ends. Also adds a `post_llm_call` hook in streaming_callbacks.py for cases where the agent's run_conversation completes normally and the post-call hook can surface the actual response text. The hook is defensive and complements the _dispatch_message catch-all. E2E verified: triggered openrouter 402 fallback via curl POST to /myah/v1/message with model=moonshotai/kimi-k2.6; SSE stream now contains a message.delta event with the user-visible error string followed by run.completed. Browser test confirmed the error renders inline in the chat instead of empty Thinking spinner. Tests added (5 in test_streaming_callbacks.py): post_llm_call success path, no-op when stream fired, ignores other platforms, ignores empty response, no-op when no active stream, post_llm_call in VALID_HOOKS CI guard. Refs: e2e-output/report.md (new finding — surfaced during user manual testing after the original 4 ISSUE fixes landed) * fix(plugin): slash commands no longer trigger false-positive warning The previous gateway-suppression-bug workaround checked `session_key in _stream_delta_invoked` to decide whether to emit the fallback warning. `_stream_delta_invoked` is marked inside the `stream_delta_callback` wrapper in `get_structured_callbacks` — which only fires for LLM streaming output. Slash commands like `/model` deliver their response a different way: hermes' gateway processes the command in-process and calls `adapter.send(chat_id, response_text)`. `send()` pushes a `message.delta` event via `_push_event_sync` directly — never touching the stream_delta callback. So `_stream_delta_invoked` stays empty for slash commands, and the finally block fired the warning even after a valid response had just been delivered. User-visible bug: chat showed the /model picker output with the warning text appended at the end: Current: gpt-5.4-mini on OpenAI Codex ... /model <name> --global — persist⚠️ The agent's LLM call did not produce a response. This usually means... Fix: add `_stream_had_content`, a per-stream_id set that's marked whenever ANY `message.delta` event passes through `_push_event_sync`. Unified tracking across: - stream_delta_callback wrapper (LLM streaming output) - send() live-chat path (slash commands, agent reply) - send() cron-delivery live-preview - post_llm_call hook synthetic emission The `_dispatch_message` finally block now checks `stream_id not in self._stream_had_content` instead of `session_key not in self._stream_delta_invoked`. False-positive eliminated; real failure case still triggers the warning. E2E verified: - /model command: SSE stream contains exactly one message.delta with the picker text, NO warning appended - Failure case (kimi via openrouter 402): warning still emitted as expected Tests added (4 in TestStreamHadContent): - message.delta push marks the set - non-delta events (reasoning/status/tool/run.*) don't mark - unknown stream_id is a graceful no-op - repeated pushes are idempotent (set semantics) Found via user manual testing — chat log analysis showed the warning appended to a successful /model response. Refs: e2e-output/report.md (follow-up to ISSUE-001..004 fixes)
Two Myah-specific additions to the gateway platform adapter, both shipped as Myah marker blocks.
Changes
_build_catalogcapability passthrough (1ff4124e,f74a6a2a):_build_catalogpreviously emittedcurated_modelsas a plainlist[str]. Myah's settings UI needs capability metadata to filter model dropdowns (e.g. only show vision-capable models for the vision aux task). This restructures the list tolist[{id, name, capabilities?}]by callingget_model_capabilities(provider, model_id)per model.A new
_build_model_entry(provider, model_id)helper wraps the lookup:get_model_capabilitiesreturnsNone→ entry hasidandnameonly (key absent, not None)get_model_capabilitiesraises → log warning with f-string, omitcapabilities, catalog still returnsModelCapabilitiesfields only:supports_tools,supports_vision,supports_reasoning,context_window,max_output_tokens,model_familyThe import of
get_model_capabilitiesis deferred inside the helper, matching the existing deferred-import pattern in_build_catalog.Rolling-deploy safe: the platform consumer accepts both old-string and new-dict
curated_modelsshapes during a mixed-replica window./myah/api/config/aux-resolvedendpoint (44abeb00):New
GET /myah/api/config/aux-resolvedhandler that iterates every aux task inDEFAULT_CONFIG.auxiliaryand runs the same resolution chain Hermes uses at aux call time (_resolve_task_provider_model+ Step 1 of_resolve_auto). Returns:{ "title_generation": {"provider": "openrouter", "model": "google/gemini-3-flash-preview", "source": "config"}, "vision": {"provider": "anthropic", "model": "claude-opus-4.6", "source": "auto-main"}, ... }sourcevalues:config,config-base-url,auto-main,auto-chain,unresolved.The Myah platform proxies this at
GET /api/v1/agent/config/aux-resolvedand uses it to show users the exact model each background task will route to in the settings UI, replacing the ambiguous "auto" placeholder.Tests
4 new tests in
tests/gateway/test_myah_management.pycovering capability passthrough: happy path, None return, exception handling, and shape assertion. 114 total tests pass.Compatibility
No changes to any shared upstream file. Both additions are wrapped in Myah markers. The
curated_modelsshape change is a breaking change at the API level, mitigated on the consumer side.Refs T3-1001