Skip to content

Docs audit — reference (api, headers, environment variables, glossary) — 2026-08-16 #68

Description

@SebConejo

Audited: the reference topic in scoped mode: the API, headers, environment-variables and glossary pages.

Window: the full reference surface at platform main (633455b98, 2026-08-14), plus every merged PR touching it over the last 60 days (#2540, #2566, #2678, #2565, #2485, #2668, #2569, #2623/#2628, #2697, #2539, #2578, and the historical #1407 that removed the 424 sentinel). Docs at 3d0a691.

Pages audited (4): reference/api.mdx, reference/headers.mdx, reference/environment-variables.mdx, reference/glossary.mdx. reference/telemetry.mdx belongs to the request-logs topic and was left out of scope.

10 findings: WRONG x7, MISSING-CASE x3. Four parked backlog items were consumed by this run.

Findings


1 · [WRONG] The rate-limit section says a self-hosted instance allows 100 requests a minute per harness and that two settings change that. The real cap is 200 a minute per workspace, and no setting moves it, on Cloud or self-hosted.

Page: https://manifest.build/docs/reference/api (Rate limits) and https://manifest.build/docs/reference/environment-variables (Network and security, the two rate-limit rows)

What actually happens:

  1. Your client sends requests through the gateway.
  2. Manifest allows 200 requests per minute per workspace, 500 requests per minute per IP address, and 10 requests in flight at once. The numbers are the same on Cloud and self-hosted.
  3. Cross one of them and the request fails with 429: M201 for the workspace cap, M202 for the IP cap, M203 for the concurrency cap.
  4. On self-hosted you raise the two rate-limit settings and restart. The gateway behaves exactly as before. Those two settings throttle the dashboard's own API calls, not the gateway.
  5. Your Cloud plan changes your monthly request quota. It does not change any per-minute cap.

Options:

  1. Rewrite the rate-limit section with the real per-minute and concurrency caps, stated as identical on Cloud and self-hosted and not configurable, and rescope the two settings in the environment-variable reference to the dashboard's own API calls. Recommended.
  2. Delete the override sentence, link the three rate-limit error pages for the numbers, and rescope the two settings only.
  3. Leave.
Evidence (for the agent)
  • reference/api.mdx:212 — "Self-hosted instances default to 100 requests per 60 seconds per harness. Override with THROTTLE_TTL and THROTTLE_LIMIT (Environment variables)."
  • reference/api.mdx:214 — "Cloud rate limits are tied to your plan and shown in the dashboard."
  • reference/environment-variables.mdx:38-39 — "THROTTLE_TTL | 60000 | Rate-limit window in milliseconds" and "THROTTLE_LIMIT | 100 | Max requests per window per harness".
  • Code: proxy.controller.ts:119,123@Controller('v1') carries @SkipThrottle(), so the Nest throttler never sees /v1/*.
  • Code: app.module.ts:78-81THROTTLE_TTL/THROTTLE_LIMIT feed ThrottlerModule.forRoot only, governing the Nest-routed controllers under /api/v1/**. The tracker is the client, not the harness key.
  • Code: proxy-rate-limiter.ts:4-9 — the real gateway caps are hardcoded module constants: window 60_000, RATE_MAX_REQUESTS = 200 per tenant (M201 at :47), IP_RATE_MAX_REQUESTS = 500 per IP (M202 at :72), CONCURRENCY_MAX = 10 in flight (M203 at :85). Checked in order at proxy.controller.ts:378-381. The file reads no process.env and no ConfigService.
  • Code: plan-limits.ts:9,11-16 — what varies by Cloud plan is the monthly request quota (Free 10 000, Pro unlimited), not any per-minute cap.
  • Not a re-raise: issue Docs audit — error catalog (M-codes) — 2026-08-14 #58 finding 2 (still open) says the caps are unconfigurable, on errors/M201-M203.mdx. This is the opposite-side defect, on the pages that tell users the knob exists.
  • Impact: a self-hoster hitting M201 or M203 raises the documented setting, restarts, and nothing changes.

2 · [WRONG] Three pages say an exhausted fallback chain returns 424. Manifest returns the status the first model's provider gave, and marks the exhaustion with a response header.

Page: https://manifest.build/docs/reference/api (Errors, the status table and the note below it), https://manifest.build/docs/reference/headers (Response headers, fallback only, the exhausted row) and https://manifest.build/docs/reference/glossary (Fallback chain)

What actually happens:

  1. Your request goes to the primary model and fails.
  2. Manifest walks the rest of the chain. Every model fails.
  3. You receive the status the primary model's provider returned, for example 500 or 429. Never 424.
  4. The response carries the header that marks the chain as exhausted, set to true. That header is the only reliable signal.
  5. The body keeps the provider's own error code when the provider sent one, so there is no single code to match on either.

Options:

  1. Replace the 424 claim on all three pages with header-based detection, and ship it together with the routing-page fix in issue Docs audit — llm gateway (routing, tiers, fallbacks) — 2026-08-09 #53. Recommended.
  2. Fix the three reference pages now and leave the routing page to Docs audit — llm gateway (routing, tiers, fallbacks) — 2026-08-09 #53.
  3. Leave until Docs audit — llm gateway (routing, tiers, fallbacks) — 2026-08-09 #53 is decided.
Evidence (for the agent)
  • reference/api.mdx:6 — frontmatter keyword "424 fallback exhausted".
  • reference/api.mdx:204 — "| 424 | Fallback chain exhausted (all configured models failed) |".
  • reference/api.mdx:208 — "Status 424 is the only one that does not trigger a fallback. Manifest returns it itself when the chain is exhausted, so re-routing it would loop forever."
  • reference/headers.mdx:47 — "| X-Manifest-Fallback-Exhausted | Set to true. Response status is 424. |"
  • reference/glossary.mdx:53 — "When all of them fail, Manifest returns HTTP 424 with X-Manifest-Fallback-Exhausted: true."
  • Code: repo-wide grep for 424 / FAILED_DEPENDENCY across packages/*/src at 633455b98 returns zero production hits. The sentinel was removed by PR #1407 (12ede3c7d, 2026-03-30) and a spec now asserts the opposite (__tests__/proxy-fallback.service.spec.ts:1848-1879).
  • Code: proxy.service.ts:1258,1265 — the exhausted forward is rebuilt with status: primaryStatus, the primary provider's real status; proxy-response-handler.ts:403-405 sets that status then X-Manifest-Fallback-Exhausted: true.
  • Code: fallback-status-codes.ts:1-3 — the fallback trigger is any status >= 400 with no exclusions, so api.mdx:208's "the only one that does not trigger a fallback" describes a rule that does not exist.
  • Caveat for whoever writes the fix: the body's code is providerCode ?? 'fallback_exhausted' (proxy-response-handler.ts:410). When the primary provider supplied its own code, fallback_exhausted never appears. Do not write "the code is always fallback_exhausted".
  • Page-echo, not a re-raise: the same concept was raised for llm-gateway.mdx:6,97,138 as issue Docs audit — llm gateway (routing, tiers, fallbacks) — 2026-08-09 #53 finding 2 (still open). Different pages, named here.
  • Consumes the parked backlog line of 2026-08-10 on reference/api.mdx:6,204,208, extended to the headers and glossary pages, which the parked line did not list.

3 · [WRONG] The error example shows a numeric code and a limit-exceeded type. No Manifest response contains either, and a spend hard limit arrives as 200, not 429.

Page: https://manifest.build/docs/reference/api (Errors, the example response and the status table)

What actually happens:

  1. You copy the example and branch on the numeric code, or on the limit-exceeded type.
  2. Manifest puts a text code, or nothing, in the code field, and keeps the numeric status in a separate field. A number never lands in the code field.
  3. The type is always one of the standard error types a provider uses, such as an authentication error or a rate-limit error. Limit exceeded is a routing reason, not a type.
  4. When a spend hard limit trips, Manifest answers 200 (M200) and puts the limit message in the assistant turn. Your error handling never runs.
  5. A 429 means a gateway cap (M201, M202, M203) or a rate limit the provider itself returned. The gateway ones carry a message and a type, with no code field at all.

Options:

  1. Replace the example with a real error body (the shape a tool or SDK client gets on a bad key is the most common) and rewrite the 429 row for the gateway rate limits and upstream provider rate limits, with a separate line saying a hard limit arrives as 200. Recommended.
  2. Drop the example and point to the error catalog for the shapes.
  3. Leave.
Evidence (for the agent)
  • reference/api.mdx:191-197 — the only error example on the page: {"error":{"message":"Limit exceeded: cost usage ($1.23) exceeds $1.00 per day","type":"limit_exceeded","code":429}}.
  • reference/api.mdx:205 — "| 429 | Hard limit hit (M200), or Manifest rate limit tripped |".
  • Code: proxy-response-handler.ts:189-224 — the canonical envelope builder emits code: string | null alongside a separate numeric status field.
  • Code: proxy-error-sanitizer.ts:78-82code passes through errorField(), which returns null for anything that is not a non-empty string. A provider sending "code": 429 yields code: null.
  • Code: proxy-error-sanitizer.ts:17-22,169-175type is drawn from the closed set invalid_request_error | authentication_error | permission_error | rate_limit_error | server_error; the Anthropic-mode overloaded_error/api_error branch is proxy-response-handler.ts:207-214. limit_exceeded is a routing reason (proxy.service.ts:260), never an error type.
  • Code: M200 is delivered as an HTTP 200 assistant-message stub to every client (proxy-friendly-response.ts:89,118, emitted at proxy.service.ts:260).
  • Code: Manifest's own 429s come only from M201/M202/M203, shaped {message, type:'rate_limit_error'} with no code key (proxy.controller.ts:775). A provider's own 429 is forwarded with its real status and the full envelope (proxy-response-handler.ts:308-310), so the row must cover both sources.
  • Real pairs for the replacement example: auth on a tool client is type: 'auth_error', code: 'manifest_auth' (proxy-exception.filter.ts:146); Free-plan quota is type: 'insufficient_quota', code: 'PLAN_LIMIT_REQUESTS' (proxy-exception.filter.ts:120).
  • Page-echo, not a re-raise: the M200-to-429 half echoes issue Docs audit — llm gateway (routing, tiers, fallbacks) — 2026-08-09 #53 finding 3 (llm-gateway.mdx:142) and issue Docs audit — error catalog (M-codes) — 2026-08-14 #58 finding 1 (errors.mdx:43, errors/M200.mdx:4,7), both still open. Different page, named here.
  • Consumes the parked backlog line of 2026-08-10 on reference/api.mdx:194-195.

4 · [WRONG] The headers page says the tier response header can carry a custom tier's name. It never carries a name, and requests Manifest answers by itself report values the page says cannot occur.

Page: https://manifest.build/docs/reference/headers (Response headers (every request), the tier row and the reason row)

What actually happens:

  1. You create a custom tier on your own header key and value, then send that header.
  2. Manifest routes the request to the tier's model.
  3. The tier response header comes back with a fixed generic value. The reason header is what tells you a header match happened.
  4. You compare the header against your tier's name, see no match, and start debugging a rule that works.
  5. When Manifest answers without calling a provider (a spend block, no provider connected, a model it cannot serve), the tier, model and provider headers all report Manifest's own values. A log pipeline keyed on the documented values files those as real model calls.

Options:

  1. Correct both rows with the values a client can actually receive, and add one sentence: the tier's name appears on the request in the dashboard, never in a header. Recommended.
  2. Drop the custom-tier-name claim and mark both lists non-exhaustive without enumerating.
  3. Leave.
Evidence (for the agent)
  • reference/headers.mdx:27 — "| X-Manifest-Tier | The tier that handled the request: default, a custom tier's name, or direct | default |".
  • reference/headers.mdx:30 — "| X-Manifest-Reason | Why that route was picked: default, header-match, or direct | header-match |".
  • Code: resolve.service.ts:325-335 — a header-tier match returns tier: 'standard' and reason: 'header-match'; header_tier_name is recorded to the database and never emitted as a header.
  • Code: proxy-response-handler.ts:163-171buildMetaHeaders copies meta.tier verbatim. No branch can produce a tier name.
  • Code: proxy-friendly-response.ts:59-63 — Manifest-blocked responses emit tier: 'simple', model: 'manifest', provider: 'manifest', with reasons limit_exceeded / no_provider / model_not_available (proxy.service.ts:260,1428,1438), through the same header builder (proxy.controller.ts:406).
  • Code: scoring/types.ts:39-51 — the reason union also contains scored, tool_detected, large_context, short_message, momentum, ambiguous, heartbeat, specificity, formal_logic_override.
  • Constraint on the fix: do not introduce the complexity-routing tier vocabulary into the docs. That feature is being retired in-product (RoutingDeprecationNotice.tsx:4) and no page mentions it. Document only the values a client can receive.
  • Open point: the reason list above is "confirmed to occur", not proven exhaustive. Option 2 exists partly for that reason.

5 · [WRONG] The chat completions section says the body is forwarded verbatim and that every standard OpenAI field passes through. Manifest rewrites the body for the resolved provider and drops the fields that provider does not accept.

Page: https://manifest.build/docs/reference/api (Chat completions, the paragraph under the request example)

What actually happens:

  1. You send a routed request with an OpenAI-only field, such as the option that asks for usage totals inside the stream, or the reasoning-effort setting.
  2. Routing lands on an Anthropic or Google model.
  3. Manifest converts the payload to that provider's shape, renames the token-limit parameter where the provider expects another name, and drops the fields that only OpenAI and OpenRouter accept.
  4. You get a normal answer with no usage totals and no reasoning control. No warning, no error.
  5. Manifest also merges the model parameters saved on your harness into every routed attempt, so a value you never sent can reach the provider.

Options:

  1. Replace the sentence with what Manifest does to the body (adapts it to the resolved provider, drops unsupported fields, renames parameters, merges harness defaults) and list the fields that survive only on OpenAI and OpenRouter routes. Recommended.
  2. Say that Manifest adapts the body to the resolved provider, with no list.
  3. Leave.
Evidence (for the agent)
  • reference/api.mdx:53 — "The body is forwarded verbatim to the resolved provider, with model rewritten to the actual model ID. All standard OpenAI fields (temperature, max_tokens, tools, tool_choice, response_format, stream, etc.) pass through."
  • Code: provider-client-converters.ts:98-107 — the stripped set is store, metadata, service_tier, stream_options, modalities, audio, prediction, reasoning_effort, dropped for every provider outside the passthrough set {openai, openrouter} (:113, applied at :343,364-372). reasoning_effort is re-admitted for xai and deepseek only (:368-371).
  • Code: provider-client-converters.ts:122,130,348-349,360-363,374-384max_tokens is rewritten to max_completion_tokens and back depending on the target; DeepSeek max_tokens is clamped to 8192 (:116,387).
  • Code: provider-client.ts:724,729 — prompt-cache markers are injected; :572 (Google), :611 (Anthropic) and :647 (Responses) convert the payload whole to the target's shape.
  • Code: proxy.service.ts:304-317,328-338 — per-harness model-parameter defaults are merged into every attempt that is not an explicit-model override.
  • Impact: the page names stream as passing through, which makes the sibling stream_options drop maximally surprising. The user debugs the provider instead of the proxy.

6 · [WRONG] The row for the listening interface tells you to open it up for LAN access. On the bundled Docker install that changes nothing: the published port in the compose file decides who reaches the server.

Page: https://manifest.build/docs/reference/environment-variables (Network and security, the row for the listening interface)

What actually happens:

  1. You want another machine on your network to reach your install.
  2. You follow the row, set the listening interface to all interfaces, and restart. The container already listened on all interfaces.
  3. The other machine still cannot connect, because the compose file publishes the port on the loopback address only.
  4. The host binding lives on a commented line in the install's environment file. Uncomment it, set your address, restart, and the machine connects.
  5. The other direction breaks too: you restrict exposure by hand-editing the compose file as the row says, then run the upgrade command, which rewrites that file and silently drops your restriction.

Options:

  1. Rewrite the row for what the variable does on a non-Docker launch, point Docker users at the host-binding variable in the install's environment file, and add that variable's own row in the same PR as the issue Docs audit — self-hosting with Docker — 2026-08-12 #56 fix. Recommended.
  2. Keep the bind description only and let the self-hosted page own LAN exposure.
  3. Leave until Docs audit — self-hosting with Docker — 2026-08-12 #56 lands, then fix both pages together.
Evidence (for the agent)
  • reference/environment-variables.mdx:32 — "| BIND_ADDRESS | 127.0.0.1 | Interface the server binds to. Set to 0.0.0.0 for LAN access. The Docker image already sets 0.0.0.0; restrict a container with the compose ports line instead |".
  • Code: docker/Dockerfile:78ENV BIND_ADDRESS=0.0.0.0. The shipped container already binds every interface, which is why setting the variable is a no-op. The row's second clause is true and makes the first clause dead.
  • Code: docker/docker-compose.yml:50 — the publish line is "${HOST_BIND_ADDRESS:-127.0.0.1}:${HOST_PORT:-${PORT:-2099}}:${PORT:-2099}". LAN reachability is decided there.
  • Code: docker/.env.example:28,31 — the upgrade-safe knobs shipped to users are the commented HOST_BIND_ADDRESS and HOST_PORT lines.
  • Code: app.config.ts:22 — the 127.0.0.1 default is real, but only for a non-Docker launch.
  • install.sh --upgrade atomically replaces docker-compose.yml and never touches .env (evidence recorded with issue Docs audit — self-hosting with Docker — 2026-08-12 #56), so the hand-edit the row recommends is wiped by the documented upgrade path.
  • Page-echo, not a re-raise: same defect class as issue Docs audit — self-hosting with Docker — 2026-08-12 #56 finding 1 (self-hosted.mdx:208, still open), on a different page.
  • Backlog line of 2026-08-12 (missing rows for the compose host-mapping variables) is CLEARED as written, not raised: no .mdx page mentions either variable today, so nothing is used-but-undefined and the coherence invariant holds. Option 1 adds the row and pre-satisfies the invariant when the Docs audit — self-hosting with Docker — 2026-08-12 #56 fix introduces the variable into prose.

7 · [WRONG] The request-header table marks the Anthropic API version header as required and says Manifest forwards it. Manifest requires nothing and forwards nothing: it sets the version it sends upstream itself.

Page: https://manifest.build/docs/reference/headers (Request headers, the row for the Anthropic API version) and https://manifest.build/docs/reference/api (Anthropic messages, the example request)

What actually happens:

  1. You call the Anthropic-format endpoint and leave the version header out.
  2. Manifest accepts the request and routes it. Nothing checks that header on the way in. A missing key still fails with 401; a missing version header does not.
  3. Manifest sets the version value on every call it makes to Anthropic.
  4. You send a different version string. Manifest ignores it and sends its own.

Options:

  1. Reclassify the row as accepted and ignored: Anthropic SDKs send it on their own, Manifest does not require it, and Manifest chooses the version it sends upstream. Keep the example request unchanged. Recommended.
  2. Delete the row and remove the header from the example request.
  3. Leave.
Evidence (for the agent)
  • reference/headers.mdx:17 — "| anthropic-version | 2023-06-01 | Required for /v1/messages. Forwarded to the upstream. |".
  • reference/api.mdx:61 — the Anthropic curl example sends -H "anthropic-version: 2023-06-01".
  • Code: a grep for anthropic-version across packages/backend/src returns outbound injection sites only, all with the hardcoded value '2023-06-01': provider-endpoints.ts:105,114,122, subscription-clients.ts:59, custom-provider.service.ts:445, provider-model-fetcher.service.ts:883,921. No inbound validator exists anywhere.
  • Contract test: contracts/gateway/tests/routes.test.mjs:85-95 posts to /v1/messages with only Content-Type: application/json and asserts a 401 auth error, not a 400 for the missing header (request helper :10-31, assertion :33-52).
  • Consumes the parked backlog line of 2026-08-10 on reference/headers.mdx:17, which asked exactly this question.

8 · [MISSING-CASE] The glossary says a real model ID goes straight to that provider and skips tiers. A matching custom-tier header wins over the model ID, and the request runs on the tier's model.

Page: https://manifest.build/docs/reference/glossary (Direct routing, and Custom tier) and https://manifest.build/docs/reference/headers (Request headers, the sentence under the table)

What actually happens:

  1. You configure a custom tier on your own header key and value.
  2. Your client sends that header along with a concrete model ID, because most SDKs require a model name in the body.
  3. Manifest checks the tier header first. It matches, so the request goes to the tier's model. The ID you sent is never resolved.
  4. The tier response header reports a header match, not direct routing, and your Requests page shows the tier's model with its cost.

Options:

  1. Add the precedence to Direct routing and Custom tier in the glossary, and to the routing sentence on the headers page: a matching custom-tier header takes precedence over an explicit model ID. Recommended.
  2. Add the precedence to the glossary only, and leave the headers page as it is.
  3. Leave.
Evidence (for the agent)
  • reference/glossary.mdx:45 — "Sending a real model ID (one that GET /v1/models lists) instead of auto. Manifest forwards the request straight to that provider and skips tiers and fallbacks."
  • reference/headers.mdx:21 — "The custom tier header is the only one that changes routing: send the header you configured on a tier and the request goes to that tier's model."
  • Code: proxy.service.ts:903-905 sends any non-auto model to resolveExplicitModel, which calls resolveHeaderTier at :976-978 and returns immediately on a match, before the catalog lookup. The rationale is written at :957-959: the operator's rule is a deliberate override, and the SDK's model field is mandatory, so most harnesses send a name they cannot change.
  • Code: resolve.service.ts:325-335 — that match then reports tier: 'standard' and reason: 'header-match', which is finding 4's other half.
  • The rest of the glossary sentence is correct: direct routing does set fallback_routes: null (proxy.service.ts:1063) and does bypass scoring (:1057-1072).
  • Impact: the user pays for the tier's model while believing they pinned another one.

9 · [MISSING-CASE] The status table and the "every request" header heading assume one kind of client. Streaming and chat clients receive 200 with the message in the assistant reply, and a request rejected before a provider is picked carries no routing headers.

Page: https://manifest.build/docs/reference/api (Errors, the status table, and Response headers) and https://manifest.build/docs/reference/headers (Response headers (every request), the heading)

What actually happens:

  1. Claude Code, or any client that streams or asks for an event stream, sends a request with a bad key or an exhausted quota.
  2. Manifest answers 200 and puts the explanation in the assistant turn. The 401 and 402 rows never reach that client, so its error handling stays silent.
  3. A tool or SDK client that does not stream gets the statuses the table lists.
  4. Auth, quota and rate-limit responses carry no routing headers, for either kind of client. Manifest rejects those before routing runs.
  5. Several Manifest replies (no provider connected, a blocked spend limit, a model it cannot serve) answer 200 to every client and appear in no row of the table.

Options:

  1. Add one sentence above the table (streaming and chat clients receive 200 with the message inside the assistant turn; the statuses below are what tool and SDK callers see) and retitle the headers section so it stops promising every request. Recommended.
  2. Do that, and add the missing rows: 400 for a rejected body, 503 and 504 for transport failure and timeout.
  3. Leave.
Evidence (for the agent)
  • reference/api.mdx:200-206 — the status table (401, 402, 424, 429, 5xx), introduced at :188 by "The proxy returns a standard JSON error envelope:".
  • reference/api.mdx:218 — "Every response carries routing headers so your client can see which model and tier handled the request".
  • reference/headers.mdx:23 — the heading "## Response headers (every request)".
  • Code: proxy-exception.filter.ts:38-44isChatRenderingClient is true when body.stream === true or Accept contains text/event-stream. Those callers get HTTP 200 with the message in the assistant turn (:131-149) instead of 401 for auth or 402 for quota.
  • Code: M100, M101, M102, M200 and M302 return HTTP 200 to every client (proxy.service.ts:260,411-416,1428,1438; proxy-friendly-response.ts:89,118) and appear in no row of the table.
  • Code: proxy-response-handler.ts:309,404,545,861 — the header builder runs only on success, SSE init, provider-error and exhausted paths. Auth 401s, quota 402s, rate-limit 429s and internal errors carry no X-Manifest-* headers at all.
  • Page-echo, not a re-raise: the chat-versus-tool split was raised for errors.mdx:21 against :55,:71 as issue Docs audit — error catalog (M-codes) — 2026-08-14 #58 finding 3 (still open). Different page, lower severity here since this is the API reference rather than the error catalog.

10 · [MISSING-CASE] The model-listing section says any model ID missing from the list returns M302. A provider-qualified ID still routes when one of your connected providers serves it, even before the list learns it.

Page: https://manifest.build/docs/reference/api (Listing models, the paragraph under the example)

What actually happens:

  1. A provider ships a new model. Your model list has not picked it up yet.
  2. You send that model ID with its provider prefix.
  3. Manifest works out which connected provider owns it and forwards the request. The provider decides whether the model exists.
  4. M302 comes back only when no connected provider can serve the ID.

Options:

  1. Soften the sentence: a model ID that no connected provider can serve returns M302. Recommended.
  2. Spell out the nuance: a provider-qualified ID can route before the list learns it.
  3. Leave the simplification, whose only cost is a user not trying a new model ID.
Evidence (for the agent)
  • reference/api.mdx:80 — "If you send a model ID that is not in this harness-specific list, Manifest returns M302: Model not available."
  • Code: proxy.service.ts:961-964, verbatim comment: "When discovery has not learned the model yet, a provider-qualified or provider-inferable ID may still route through credentials enabled on this harness; the provider is the authority on whether that model exists." Then at :966: "Returns null when no unambiguous connected provider applies." The routing itself is resolveConnectedExplicitModel (:990-999); M302 is raised only in that null case (:1438).
  • Page-echo, not a re-raise: third page carrying this concept, after llm-gateway.mdx:89 (issue Docs audit — llm gateway (routing, tiers, fallbacks) — 2026-08-09 #53 finding 4, still open) and self-hosted.mdx (issue Docs audit — introduction (getting started) — 2026-08-07 #49 finding 3, accepted). Severity is lowest of this batch: the failure mode is a user under-using a feature.

Cleared during this audit (recorded so they are not re-derived)
  • The model-list query parameters for capabilities and cost (api.mdx:82-178) are correct on every falsifiable point: exact-string gating, USD per million tokens, per-field and whole-object omission when unknown, auto never carrying metadata, created: 0, -subscription IDs directly routable (proxy.controller.ts:104-116,150-168,190,194; openai-model-capabilities.ts:4,10-14,17-34).
  • X-Manifest-Confidence and X-Manifest-Specificity are set but absent from the headers table (proxy-response-handler.ts:168,173-175). An unlisted extra header misleads nobody. Acceptable curation.
  • DB_POOL_MAX / AUTH_DB_POOL_MAX documented as 30/10 against a real 10/5: re-verified today (app.config.ts:38, auth.instance.ts:33), unchanged since issue Docs audit — 2026-08-07 (PRs #2693–#2697) #48 finding 8 recorded it. Still open there, not re-raised here.
  • The gateway-tester CORS variable rows (environment-variables.mdx:33,37, cited as 32/36 before the page shifted a line) are already open as issue Docs audit — 2026-08-07 (PRs #2693–#2697) #48 finding 10. Not re-raised. A sweep for every other internal codename across the four pages returned zero hits, so there is no new leak.
  • STREAM_IDLE_TIMEOUT_MS is a real variable absent from the page, but it appears in no user-facing example file for the Docker install, so nothing is used-but-undefined.
  • AUTOFIX_TIMEOUT_MS default 10000 is correct (autofix.module.ts:19,49).
  • REQUEST_RECORDING_RETENTION_DAYS default 365 is correct for this page's audience: the code default resolves to 365 on any non-billing install (app.config.ts:58-63), and the page is self-hosted-only by its own note.
  • PORT 2099, NODE_ENV production, BETTER_AUTH_URL and the recordings path: the code defaults differ, but the stated values are the bundled compose file's effective ones, which is the install this page documents. Same call already recorded on 2026-08-12 for the self-hosting page.
  • api.mdx:26 sends users to the Harnesses page for a key. The key lives one level in, at the harness's Settings tab (Settings.tsx:247-311). The entry point is right and the retired /agents path is not used, so nobody lands on a dead screen. Imprecise, not misleading.
  • The glossary describing only Default and Custom tiers: complexity routing exists in code but is being retired in-product, and no page documents it. Narrating it would break "less beats confusing".
  • headers.mdx:19 on the session key is true as written. The unstated cliff (omitting it disables routing momentum and provider cache affinity, proxy-session-scope.ts:22-37) would require introducing a concept no page defines, and no user acts wrongly on the current sentence.
  • headers.mdx:16 marking the content type required: no explicit inbound validator, but body parsing is content-type driven (main.ts:206-207), so it is required in practice. Correct as written.
  • api.mdx:184 on streaming and fallback matches the warm-up peek before commit and the no-mid-stream-retry rule (proxy.service.ts:546,589-594; proxy.controller.ts:740-748).
  • glossary.mdx:53 "Up to 5 models" is true (routing.dto.ts:201-202,212-213). The uncapped legacy header-tier payload is an API-layer edge the dashboard never produces.
  • Glossary entries for harness, request, attempt, Autofix and the three auth types all match the platform, and "Autofix" is spelled without a hyphen on all four pages.
Parked for other topics (not findings here)
  • llm-gateway.mdx:52 — the custom-tiers section never says a matching header also overrides an explicit model in the body (proxy.service.ts:970-975). Same gap as finding 8, on the routing topic's page. Both fixes should use the same sentence.
  • self-hosted.mdx:321 — "Everything else is optional: rate limits, provider timeouts, ..." implies a self-hoster can configure gateway rate limits by environment variable. None reaches the gateway. Same root cause as finding 1, on the self-hosting topic's page.

Reply with the finding number and the chosen option
(e.g. "3 → option 2", or "3: other idea, let's discuss").
PRs are then made manually with the agent, never by the harness.
Rejected findings: reply "N → reject" and they will never be re-raised.

Metadata

Metadata

Assignees

No one assigned

    Labels

    docs-syncFiled by the docs-sync harnessmissing-caseA user situation is not coveredwrongDocs say it incorrectly

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions