Conversation
OllamaLLMProvider built its chat-model list in the constructor, and AIServiceManager builds every provider, so every Jupyter server start imported the ollama SDK and called the Ollama host whatever the configured provider was. A Claude-only user paid the import, one connection attempt, and a "Failed to update supported Ollama models" warning on each start. The list is now built on first access of the chat_models property, which the capabilities response and the readiness preflight already read, and the update-provider-models route still refreshes it explicitly. Be precise about the saving: the marginal import is around 37ms once Jupyter has loaded httpx and pydantic, so an interactive user pays it back on the first capabilities GET. The real wins are headless servers, servers whose users never open the NBI frontend, and the stalled-host case below. An Ollama-configured user still enumerates during startup through get_chat_model, which is what that user needs. Deferring alone would have made the worst case worse, so the requests are bounded too. ollama passes timeout=None to httpx, so a host that drops packets rather than refusing them waits out the OS connect timeout, 75s on macOS, and the capabilities handler reading this property is synchronous: left unbounded, that wait moves off startup and onto the event-loop thread serving every other request in the process. Each request now carries a 5s bound, deliberately generous because /api/show is a metadata read that a loaded host still answers slowly and a model whose metadata times out drops out of the list entirely. A 6s wall-clock budget covers the enumeration as a whole, because the per-request bound alone scales with the model count: 20 models against a host that answers the listing and then stalls measured 40s before the budget and 10s after, and the warning now names how many models were left out. Building into a local list and rebinding once, only on success, keeps a reader on another thread from serializing a half-built list, since readiness reads this property on a pool thread while a capabilities GET reads it on the event loop, and keeps the models the dropdown already had when a refresh fails. The tests pin ordering rather than a call count, because "one call in total" held for the constructor version too: not enumerated after construction, enumerated on first access, cached thereafter. The ollama stub implements both the module-level and Client surfaces so enumeration cannot hide on the one it omits, and the suite covers the timeout value, both embedding families, a per-model failure not costing the rest of the list, budget truncation, recovery through the explicit refresh, and last-good retention when a refresh fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#370 took
litellm,openai, andanthropicout of the server-extension import path. Ollama is the one provider SDK still loading eagerly:OllamaLLMProvider.__init__callsupdate_chat_model_list(), and sinceAIServiceManager.__init__builds every provider, each Jupyter server start imports theollamaSDK and calls the Ollama host whatever the configured provider is. A Claude-only user pays an import, a connection attempt, and aFailed to update supported Ollama modelswarning on every start, which is the tail end of what #368 reported.This continues that work and adds a bound on the requests, which turned out to matter more than the deferral.
Solution
The chat-model list is now built on first access of the
chat_modelsproperty, which the capabilities response and the readiness preflight already read, and theupdate-provider-modelsroute still refreshes it explicitly.Being precise about what that buys, since the obvious framing overstates it:
import ollamastandalone; the marginal cost once Jupyter has loadedhttpxandpydanticis ~35ms./capabilitiesGET, sincechat_model_idsiterates every registered provider. On a healthy host this is close to a wash for that user.update_models_from_configresolves the persisted selection throughget_chat_model. That is what that user needs, and it means the startup-latency benefit is specifically for users of other providers.Deferring on its own would have made the worst case worse, so the enumeration is now bounded:
ollamapassestimeout=Noneto httpx, so a host that drops packets rather than refusing them waits out the OS connect timeout, measured at 75s on macOS.GetCapabilitiesHandler.getis a synchronous handler, so unbounded that wait moves off startup and onto the event-loop thread serving every other request in the process, freezing a live server instead of slowing a boot./api/showis a metadata read that a loaded host still answers slowly, and a model whose metadata times out drops out of the list entirely./api/showapiece). 20 models against a host that answers the listing then stalls measured 40s with only per-request bounds and 10s with the budget, and the warning names how many models were left out.Testing
pytest tests/ --ignore=tests/test_claude_client.py: 1762 passed.tsc --noEmit,eslint,stylelint,prettierclean;jest: 423 passed.New coverage in
tests/test_lazy_provider_imports.py, alongside #370's existing import test. The tests pin ordering rather than a call count, because "one call in total" is also true of the constructor version: not enumerated after construction, enumerated on first access, cached thereafter. Theollamastub implements both the module-level andClientsurfaces so enumeration cannot hide on the one it omits. Also covered: the timeout value (a timeout that merely exists could be an hour), both entries ofOLLAMA_EMBEDDING_FAMILIES, two chat families so a hardcoded context-window key cannot pass, a per-model metadata failure not costing the rest of the list, budget truncation and its warning, recovery through the explicit refresh, and last-good retention when a refresh fails. Verified by mutation: these fail against the pre-change provider, and the manager-construction test fails against a constructor that enumerates inline.No Playwright run: the change touches no TypeScript,
LabIconregistration, shell-area iteration, or@jupyterlab/*runtime call. The only user-visible surface is the settings dropdown's contents, so I exercised that path in-process instead and confirmed a stubbed two-model host produces the expectedchat_model_idsrow and thatget_chat_modelround-trips.Risks and follow-ups
/api/tagsand then stalls on every/api/show, the list can come back empty rather than partial (measured: 0 of 20 models, in 10s). That is bounded and logged with a count, and the explicit refresh retries, but it is a real behavior difference from the old unbounded call, which would eventually populate.chat_model_idsiterates providers unfiltered, sodisabled_providersdoes not suppress this enumeration for an admin who disabled Ollama;docs/admin-guide.mdadvertises anollama.base_urlconfig block that nothing in the tree consumes; and the two new constants have noNBI_*env override, thoughclaude.pyhas an idiom for that if it is wanted.Closes #427