gateway-model-list-envelope-mismatch
Done. The repo wasn't present in this environment, but the deployed Hermes gateway was reachable, so I diagnosed against the real endpoint and produced a self-contained, runnable fix.
The live gateway at <ip-address>:8642/v1/models answers exactly the OpenAI envelope:
{"object": "list", "data": [{"id": "Hermes Agent", "object": "model", "created": 1789684185, "owned_by": "hermes", "permission": [], "root": "Hermes Agent", "parent": null}]}
The mirrored internal/hermes decoder only understood a bare array or {"models":[...]}, so it returned missing "models" field, and the caller swallowed that into a silent default fallback.
/workspace/SOLUTION.md — the full write-up: root-cause analysis, exact fix, verification results, and the generalisable rule./workspace/solution/ — runnable Go reference implementation:modellist.go — decodeList accepting bare array → models → data with explicit precedence; typed DecodeError.Reason; observable fallback logging (source= / reason=)testdata/v1_models.live.json — the exact captured live payload (sha256 9971f78f…b9468a)cmd/capture/main.go — fetches the real endpoint and writes the fixture verbatimmodellist_test.go — live-payload test, shape table, precedence pin, reason-label tests, and a regression test proving the legacy mirrored decoder fails on the live payloadgo vet ./... && go test ./... -v
--- PASS: TestDecodeList_LiveGatewayPayload
--- PASS: TestDecodeList_AllAcceptedShapes
--- PASS: TestDecodeList_Precedence
--- PASS: TestDecodeList_ErrorsCarryReason
--- PASS: TestLegacyMirroredDecoder_RejectsLivePayload
--- PASS: TestListModels_FallsBackObservably
PASS
I also re-ran cmd/capture against the live gateway and confirmed the captured fixture shape is identical (only the volatile created timestamp differs).
Key rule: a response-shape claim copied from a sibling client is a hypothesis, not a fact — capture the live payload, make it the fixture, pin precedence with a test, and make the failure observable via a source/reason log line.
# Evidence - Problem class: gateway-model-list-envelope-mismatch - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T22:30:54.486Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A new HTTP client method was written by mirroring a sibling client in the same repo that decodes the same logical endpoint (internal/hermes decodeList: bare JSON array, or an object wrapping the list under a key named models). Every unit test used hand-rolled fixtures in the mirrored client own tolerant shape, so the suite was green while the method failed 100% against the DEPLOYED endpoint, which answers the OpenAI-style envelope {\"object\":\"list\",\"data\":[...]}. The failure was silent by design: the caller treats any decode error as catalog-unreachable and falls back to a previous default, so the feature shipped inert with no error anywhere. Fix shape: accept every shape the real endpoint may use (bare array, then named-key, then data), pin the precedence explicitly with a test, and add the EXACT captured live payload as a fixture rather than a hand-written near-miss. Generalisable rule: a response-shape claim copied from a sibling client is a hypothesis, not a fact - capture the live payload from the real endpoint and make it the fixture, and make sure the failure mode is loud or at least observable (a debug log line carrying the source/reason label is what let the foreman tell catalog_error from unknown_model in the live probe).", "environment": "hermes-canopy canopyd -> Hermes gateway api_server /v1/models", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gateway-model-list-envelope-mismatch", "provider": "openrouter", "solved_at": "2026-09-17T22:30:54.486Z", "version": ""}Done. The repo wasn't present in this environment, but the deployed Hermes gateway was reachable, so I diagnosed against the real endpoint and produced a self-contained, runnable fix.
The live gateway at <ip-address>:8642/v1/models answers exactly the OpenAI envelope:
{"object": "list", "data": [{"id": "Hermes Agent", "object": "model", "created": 1789684185, "owned_by": "hermes", "permission": [], "root": "Hermes Agent", "parent": null}]}
The mirrored internal/hermes decoder only understood a bare array or {"models":[...]}, so it returned missing "models" field, and the caller swallowed that into a silent default fallback.
/workspace/SOLUTION.md — the full write-up: root-cause analysis, exact fix, verification results, and the generalisable rule./workspace/solution/ — runnable Go reference implementation:modellist.go — decodeList accepting bare array → models → data with explicit precedence; typed DecodeError.Reason; observable fallback logging (source= / reason=)testdata/v1_models.live.json — the exact captured live payload (sha256 9971f78f…b9468a)cmd/capture/main.go — fetches the real endpoint and writes the fixture verbatimmodellist_test.go — live-payload test, shape table, precedence pin, reason-label tests, and a regression test proving the legacy mirrored decoder fails on the live payloadgo vet ./... && go test ./... -v
--- PASS: TestDecodeList_LiveGatewayPayload
--- PASS: TestDecodeList_AllAcceptedShapes
--- PASS: TestDecodeList_Precedence
--- PASS: TestDecodeList_ErrorsCarryReason
--- PASS: TestLegacyMirroredDecoder_RejectsLivePayload
--- PASS: TestListModels_FallsBackObservably
PASS
I also re-ran cmd/capture against the live gateway and confirmed the captured fixture shape is identical (only the volatile created timestamp differs).
Key rule: a response-shape claim copied from a sibling client is a hypothesis, not a fact — capture the live payload, make it the fixture, pin precedence with a test, and make the failure observable via a source/reason log line.
# Evidence - Problem class: gateway-model-list-envelope-mismatch - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T22:30:54.486Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A new HTTP client method was written by mirroring a sibling client in the same repo that decodes the same logical endpoint (internal/hermes decodeList: bare JSON array, or an object wrapping the list under a key named models). Every unit test used hand-rolled fixtures in the mirrored client own tolerant shape, so the suite was green while the method failed 100% against the DEPLOYED endpoint, which answers the OpenAI-style envelope {\"object\":\"list\",\"data\":[...]}. The failure was silent by design: the caller treats any decode error as catalog-unreachable and falls back to a previous default, so the feature shipped inert with no error anywhere. Fix shape: accept every shape the real endpoint may use (bare array, then named-key, then data), pin the precedence explicitly with a test, and add the EXACT captured live payload as a fixture rather than a hand-written near-miss. Generalisable rule: a response-shape claim copied from a sibling client is a hypothesis, not a fact - capture the live payload from the real endpoint and make it the fixture, and make sure the failure mode is loud or at least observable (a debug log line carrying the source/reason label is what let the foreman tell catalog_error from unknown_model in the live probe).", "environment": "hermes-canopy canopyd -> Hermes gateway api_server /v1/models", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "gateway-model-list-envelope-mismatch", "provider": "openrouter", "solved_at": "2026-09-17T22:30:54.486Z", "version": ""}