I Read the Source of the Job Bot I Have Been Running. Two Bugs, One File, Same Root Cause.
Both bugs come from the same place: an import graph that does not agree with the runtime dispatch, and capability detection written as a hardcoded allow-list. If you support more than one LLM provider, you have probably written at least one of them.
I have been running an open source LinkedIn auto-applier for months. This week I finally read its AI layer properly, and found two bugs sitting in the same file — both from the same root cause, and both of a kind that shows up in nearly every codebase that tries to support more than one LLM provider.
Neither bug is exotic. That is the point. If you have written an app that speaks to OpenAI and something else, you have probably written at least one of them.
The shape of the problem
The bot supports three providers: OpenAI, DeepSeek and Gemini. The obvious way to build that, and the way this codebase did, is one module per provider:
modules/ai/openaiConnections.py
modules/ai/deepseekConnections.py
modules/ai/geminiConnections.pyEach exports its own trio of functions — create a client, extract skills, answer a question — and the caller picks:
if ai_provider.lower() == "openai":
answer = ai_answer_question(...)
elif ai_provider.lower() == "deepseek":
answer = deepseek_answer_question(...)
elif ai_provider.lower() == "gemini":
answer = gemini_answer_question(...)It reads fine. The dispatch is explicit, each provider's quirks stay in its own file, and adding a fourth is a copy-paste away. This is the design most projects land on first, and it has two failure modes baked into it.
Bug one: the abstraction leaks at import time
The dispatch above is careful. It only calls Gemini's code when Gemini is selected. But the imports at the top of the file were not:
from modules.ai.openaiConnections import ...
from modules.ai.deepseekConnections import ...
from modules.ai.geminiConnections import ...And the first line of the Gemini module is:
import google.generativeai as genaiSo every user, on every run, imports Google's SDK. Set your provider to DeepSeek, never touch Gemini, and you still crash on startup if that package is missing:
ModuleNotFoundError: No module named 'google'The runtime dispatch is provider-agnostic. The import graph is not, and the import graph runs first. Your careful if/elif never gets a chance.
The cost is not only the crash. google-generativeai pulls in about two dozen transitive packages — gRPC, protobuf, cryptography, the Google API client — and an OpenAI-only user installs all of it to satisfy an import they will never execute.
The fix is to make the imports agree with the dispatch that already exists:
if use_AI:
selected = ai_provider.lower()
if selected in ("openai", "deepseek"):
from modules.ai.openaiConnections import ...
if selected == "deepseek":
from modules.ai.deepseekConnections import ...
if selected == "gemini":
from modules.ai.geminiConnections import ...The trap inside the fix
Look at that first condition again. Why does DeepSeek import the OpenAI module?
Because DeepSeek speaks the OpenAI wire protocol, so its module borrows OpenAI's client and — crucially — the shutdown path reuses OpenAI's teardown function directly:
elif ai_provider.lower() == "deepseek":
ai_close_openai_client(aiClient)Gate openaiConnections on provider == "openai" alone, which is the natural way to write it, and you fix the crash while breaking DeepSeek's shutdown with a NameError that only fires when the program exits — the single worst place for a new bug to hide.
The general lesson: when you make an import conditional, the question is not "which module declares this name." It is "which code paths reach this name." Those are different questions, and only the second one is answered by grepping every call site.
Bug two: capability detection as a hardcoded list
Four lines further down the same file, a second bug, unreported. Whether a model accepts a temperature parameter was decided like this:
def model_supports_temperature(model_name: str) -> bool:
return model_name in ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo", "gpt-4o", "gpt-4o-mini"]And the caller:
if model_supports_temperature(llm_model):
params["temperature"] = temperatureAn allow-list of five names. Every other model gets no temperature at all — silently. No warning, no log line. The setting simply evaporates.
I can reconstruct why this was written. OpenAI's reasoning models reject temperature outright, so someone hit that error and enumerated the models they knew worked. Reasonable in the moment, wrong as a policy, because it fails in the direction nobody checks: quietly, for models nobody tested.
The damage is visible in the project's own config file, which recommends running local models:
llm_model = "" # Examples: "gpt-3.5-turbo", "gpt-4o", "llama-3.2-3b-instruct",
# "qwen3:latest", "gemini-pro", "deepseek-llm:latest"Three of those run through LM Studio or Ollama, all accept temperature, and none appear on the allow-list. The project documented a configuration it then ignored. Same for gpt-4.1, and for any pinned snapshot like gpt-4o-2024-08-06, because a date suffix makes the string miss.
Enumerate the exceptions, not the rule

The set of models that reject temperature is small, well-known and slow-moving: OpenAI's reasoning families. The set that accepts it is unbounded — every OpenAI release, every local model anyone ever loads. Enumerating the unbounded set is a losing game.
model = model_name.lower().strip()
model = model.rsplit("/", 1)[-1] # "openai/o3-mini" → "o3-mini"
for prefix in ("o1", "o3", "o4", "gpt-5"):
if model == prefix or model.startswith(prefix + "-"):
return False
return TrueTwo details do real work there.
Match the family, not the letters. A bare startswith("o1") also matches omni-model. Requiring either an exact match or a family- prefix keeps the boundary at a token, not a character.
Choose which way to be wrong. The two errors are not symmetric. Sending temperature to a model that rejects it is a hard API failure. Omitting it for a model that accepts it falls back to a sane default. So anything ambiguous should fall on the omit side — gpt-5-chat-latest gets treated as a reasoning model, and the cost is a default temperature rather than a crash.
That asymmetry is worth stating out loud whenever you write a capability check, because it decides the shape of the code. Here it is the difference between a deny-list and an allow-list.
What the maintainer did instead
The interesting epilogue: while I was fixing this on the community branch, the maintainer had already rebuilt the whole AI layer on main using LangChain and LangGraph. Three sibling modules collapsed into one connections.py, and init_chat_model handles provider selection.
Both of my bugs disappear under that design, not because anyone fixed them, but because the design has no place to put them. There is one import path and one client abstraction, so there is no set of sibling modules to import unconditionally and no per-provider capability list to fall behind.
That is the honest argument for a framework here. Not that it is faster or cleverer, but that it deletes a category of bug by removing the structure the bug lived in. The cost is a real dependency and someone else's opinion about how a chat model should look — which, for a project whose entire AI surface is "answer this question, extract these skills", is a genuine trade rather than an obvious win.
Both bugs were still live on the branch that actually accepts contributions, which is where the fixes went.
If you are building a multi-provider LLM layer
- Import lazily, or import one thing. Either gate provider imports on configuration, or route everything through a single abstraction. A module-level import of every provider makes each one's dependencies mandatory for all users.
- Do not enumerate what works. Enumerate what breaks. The working set grows with every model release; the broken set is short and changes slowly.
- Decide which failure you prefer. Loud errors and silent defaults are not equally bad. Push ambiguity toward the recoverable one.
- Watch for shared helpers across providers. The moment DeepSeek borrows OpenAI's client teardown, "which provider is selected" stops being the same question as "which module do I need."
- Log when you drop a setting. If the code decides to ignore a configured
temperature, say so once. Neither of these bugs would have survived a single log line.
The last one is the cheapest and the one I would fix first. A silent no-op is a bug that never gets reported, because from the outside it looks exactly like the feature working.