Skip to content

Insights: treat the model's cold start as a wait, not a failure - #52

Merged
eddietejeda merged 4 commits into
mainfrom
insights-cold-start
Oct 17, 2026
Merged

eddietejeda merged 4 commits into
mainfrom
insights-cold-start

Conversation

@eddietejeda

@eddietejeda eddietejeda commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

On hototel.com both the chat and the takeaways run failed with "the analysis did not finish". Server logs: Error code: 503 - {'error': 'no upstreams available'} from the Modal endpoint. The model container had scaled to zero; Modal's endpoint rejects with 503 while a new one boots, and that first request is what starts it (per Modal's Servers docs). The boot itself was also reading weights off the volume at ~60MB/s and would have been killed by the 15-minute startup timeout.

The model stays scale-to-zero on purpose: it runs only after someone signs in, for as long as they keep using it.

  • A successful sign-in wakes the model in the background, so it is booting while the person looks at the dashboard. A failed sign-in wakes nothing.
  • /api/insights/status probes /health (which also wakes it) and reports modelState (ready / starting), cached a few seconds. Only a 503 or no connection means starting; an OpenAI-compatible host without /health (404) counts as up.
  • A chat while the model is cold is a 503 with starting: true; the page shows "Starting the model…", polls status every 10s (up to 20 min) and resends the question.
  • The takeaways thread waits for ready before running; the card says "starting the model first".
  • Modal class: startup_timeout 30 min and --safetensors-load-strategy prefetch for the volume's 9P mount. Redeployed; a cold start now measures 184s.

Tests: three new cases in test_insights.py; all suites pass.

The Modal endpoint scales to zero and answers 503 "no upstreams available"
while a container boots; the first refused request is what starts it. Both
the chat and the takeaways run reported that as "the analysis did not
finish". Now opening the page probes /health to wake the model, status
reports modelState, a chat during the boot is a 503 with starting=true that
the page waits out (polling status, up to eight minutes) before resending,
and the takeaways thread waits for ready before it runs.
@eddietejeda
eddietejeda requested a review from a team as a code owner October 17, 2026 03:31
@eddietejeda
eddietejeda requested review from anoop-narang and removed request for a team October 17, 2026 03:31
Comment thread server/insights.py Outdated
def chat(self, database_id, org_name, history):
"""`history` is the validated (role, content) list. Returns the JSON
the page renders: the answer plus the queries that produced it."""
if self.model_state() != "ready":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking. Remove this pre-check, or treat only a probe 503 as "starting". The chat must still send the real request when /health fails for any other reason.

_probe returns False on every failure: a 404, a 401, a DNS error, a timeout, or a 5xx other than 503. chat then raises ModelStarting and never calls the model. The comment at server/insights.py:40 says HOTOTEL_INSIGHTS_BASE_URL accepts "any OpenAI-compatible endpoint". Most such endpoints (OpenAI, OpenRouter, a vLLM behind a path-rewriting proxy) have no /health beside /v1.

Failure scenario: an operator points HOTOTEL_INSIGHTS_BASE_URL at https://api.openai.com/v1. /health answers 404, so modelState is always starting. Every chat returns 503 starting: true. The page waits 8 minutes, then shows "the model did not start". The takeaways thread also waits 8 minutes in wait_ready and caches an error. Before this PR, the same configuration worked.

Suggested fix:

  • In chat, drop the gate. _run already turns a client 503 into ModelStarting through _is_cold_start, and that 503 is the request that wakes Modal.
  • In _probe, return "starting" only for HTTPError with code == 503, or for a connection error. Treat 404 as ready, so endpoints without /health are not blocked.
  • Add a test where /health returns 404 and model_state() returns ready.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6de0ce0: the gate in chat is gone (_run's 503 -> ModelStarting is the only path), _probe returns starting only for HTTPError 503 or a connection failure, and test_the_engine_tells_a_cold_start_from_a_failure now runs a local /health that answers 404 (-> ready) and 503 (-> starting).

Comment thread server/server.py Outdated
if self.insights:
# opening the page is the moment to wake a scaled-to-zero
# model, so it is booting while the person reads or types
self.insights.warm()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Remove the warm() call (not blocking). The next line calls self.insights.status(), which calls model_state() synchronously in the same request. When the cache has expired, warm() and status() each send a separate /health probe at the same time. model_state does not deduplicate a probe that is already running. The status request still blocks up to the 10-second probe timeout either way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed warm() and its call in 6de0ce0; status() probes synchronously and that probe wakes the endpoint.

Comment thread server/insights.py Outdated
# -- the endpoint's cold start -------------------------------------------

def model_state(self):
"""'ready', 'starting' or 'ready' for a hosted API that is always up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: Fix the docstring wording (not blocking). "'ready', 'starting' or 'ready'" lists ready twice. Write "'ready' or 'starting'; always 'ready' for a hosted API".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring fixed in 6de0ce0.

Comment thread server/insights.py Outdated
# a background thread can afford to wait out a cold start; the
# page shows "starting the model" meanwhile
if not self.wait_ready():
raise ModelStarting()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Give this exception a message (not blocking). ModelStarting() has no message, so the except block at line 498 logs error: takeaways for <id>: with no cause. An operator cannot tell an 8-minute cold-start timeout from other failures. Use raise ModelStarting(f"model not ready after {MODEL_START_TIMEOUT}s").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6de0ce0: ModelStarting carries the timeout message, and the in-run case carries the client error text.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Blocking Issues

  • server/insights.py:453: chat refuses to run whenever the /health probe fails, for any reason. _probe returns False on 404, 401, DNS errors, timeouts and every 5xx. The code documents HOTOTEL_INSIGHTS_BASE_URL as "any OpenAI-compatible endpoint" (server/insights.py:40). An endpoint with no /health beside /v1 reports starting forever. On such an endpoint, every chat returns 503 starting: true, and the takeaways thread waits 8 minutes before it caches an error.

Action Required

  • Remove the model_state() gate in chat. _run already turns a client 503 into ModelStarting through _is_cold_start.
  • In _probe, return not-ready only for HTTP 503 or a connection error. Treat other responses, such as 404, as ready.
  • Add a test where /health answers 404 and model_state() returns ready.

…ute wait

A cold boot read the 57GB of weights off the volume at ~60MB/s (a minute per
shard, 16 shards) and the 15-minute startup timeout killed it, so the page
only ever saw 503s. The class now allows 30 minutes and loads the shards
with vLLM's parallel prefetch strategy, which the loader only turns on by
itself for NFS/Lustre, not the volume's 9P mount. The server and the page
wait up to 20 minutes for "ready" to match.
Comment thread server/static/insights.js Outdated
if (d.status === 'pending') {
meta.textContent = 'analysing the last 30 days, usually one to three minutes';
meta.textContent = d.modelState === 'starting'
? 'starting the model first, two to three minutes'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: Change "two to three minutes" to "usually a few minutes" (not blocking). This commit raised the wait to 20 minutes and changed STARTING_NOTE to match, but the takeaways card still promises two to three minutes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to 'usually a few minutes' in 6de0ce0.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Blocking Issues

  • server/insights.py:453 and server/insights.py:355: The blocking issue from the previous review (thread #4301117480) is still open. Commit b6d096c did not change the code. chat still refuses to send the request when model_state() is not ready. _probe still returns False on every failure, including a 404 from an endpoint that has no /health. When HOTOTEL_INSIGHTS_BASE_URL points at an OpenAI-compatible API without /health, every chat returns 503 starting: true. The page then waits 20 minutes, which is longer than the 8-minute wait in the previous commit, before it reports a failure. The takeaways thread waits the same 20 minutes in wait_ready and then caches an error.

Action Required

  • Remove the model_state() gate in chat. _run already turns a client 503 into ModelStarting.
  • In _probe, return not-ready only for an HTTPError with code 503 or for a connection error. Treat other HTTP codes, such as 404, as ready.
  • Add a test where /health returns 404 and model_state() returns ready.

…ys asks

Review fixes. The chat no longer gates on the probe: _run turns the
endpoint's 503 into ModelStarting, and that request is what wakes Modal.
The probe reports "starting" only for a 503 or a connection failure, so
an OpenAI-compatible host without /health (404) is treated as up. The
separate warm() call went: status() probes synchronously already.
ModelStarting carries a message so the log says why a run gave up.
claude[bot]
claude Bot previously approved these changes Oct 17, 2026
The model is deliberately not kept on around the clock. A successful
sign-in starts its cold boot in the background, so it is minutes along by
the time the person opens Insights, and it still scales to zero ten
minutes after the last request. A failed sign-in wakes nothing.
@eddietejeda
eddietejeda merged commit 924f181 into main Oct 17, 2026
2 checks passed
@eddietejeda
eddietejeda deleted the insights-cold-start branch October 17, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant