Troubleshooting

On this page

ImportError: install <sdk>: pip install <sdk>

You've instantiated a provider interface without its backing SDK installed. Install exactly what the message says — see the per-provider dependency table. Note that importing the llm package or the specific interface class never fails this way — only constructing an instance does, since each SDK import lives inside __init__.

ValueError: Either 'prompt' or 'messages' must be provided.

Raised by all seven interfaces' internal _build_messages/_build_contents helper when you call generate/generate_async/astream without supplying either argument — but whether it reaches you as a raised exception or gets folded into the normal error return depends on the specific method, since on most call paths the helper is invoked inside that method's own try/except:

Providergenerate()generate_async()astream()
OpenAI / Anthropic / Groq / AWS Bedrockcaught → {"error": "..."}caught → {"error": "..."}caught → yields "Error: ..." string
Mistralcaught → "Error: ..." stringcaught → "Error: ..." stringcaught → yields "Error: ..." string
Geminiraises uncaught (checked before the retry loop's try/except)caught → "Error: ..." stringdelegates to generate_async, so yields "Error: ..." string
Ollamaraises uncaught (dedicated except ValueError: raise bypasses the generic handler)raises uncaught (same guard)yields "⚠️ Ollama stream error: ..." string if aiohttp is installed; raises uncaught if it isn't (the ImportError fallback calls generate() directly, which itself raises)

If you need to branch on "did the user forget to pass a prompt," don't assume this always shows up as a caught exception or always as a returned value — check both, or validate your own inputs before calling.

ValueError from a messages-list positional call gone wrong

messages is the first positional parameter on generate/generate_async/astream for every provider (it comes before prompt in every signature). If you call llm.generate("some text") positionally, "some text" is bound to messages, not prompt — every provider's _build_messages/_build_contents helper will then treat that string as if it were a messages list. Depending on the provider this either raises immediately with a confusing error from deep inside the SDK (since a raw string isn't a valid messages list), or — for MistralInterface, whose helper just does if messages is not None: return messages — silently forwards the raw string on to the API call, which will then fail with an SDK-level error unrelated to ValueError at all. Always call with keyword arguments: llm.generate(prompt="...") or llm.generate(messages=[...]).

RuntimeError: asyncio.run() cannot be called from a running event loop

Happens if you call validate_connection(async_mode=True) from code that's already inside a running event loop (e.g. inside an async def that's being awaited). validate_connection's async path uses asyncio.run(...) internally, which cannot be nested. Call validate_connection() (sync, async_mode=False, the default) from async code instead, or await self.generate_async(...)/await self.validate_connection_equivalent logic directly rather than going through the sync helper.

A generate() call returns a dict instead of text (OpenAI / Anthropic / Groq )

This is the documented general error path for these four interfaces — on any exception (including a missing prompt/messages), they return {"error": str(e)} instead of raising. Check for this before using the result:

Python · 5 lines
result = llm.generate(prompt="...")
if isinstance(result, dict):
    print("Error:", result["error"])
else:
    print(result)

Since tools support was added, a dict result is no longer necessarily an error on any of the seven interfaces, including the three below that otherwise represent errors as plain strings. If you pass tools, a successful tool call also returns a dict — shaped {"content", "tool_calls", "stop_reason"}, with no "error" key. Distinguish the two by key rather than by type alone:

Python · 8 lines
result = llm.generate(prompt="...", tools=tools)
if isinstance(result, dict):
    if "tool_calls" in result:
        ...  # successful tool call
    elif "error" in result:
        ...  # failure
else:
    ...  # plain-text answer, no tool called

See Tool Calling for the full return-shape contract.

A generate() call returns a string starting with "Error:" or "❌" (Gemini / Mistral / Ollama)

These three interfaces represent most failures as plain strings rather than a distinguishable object — check with something like result.startswith(("Error", "⚠️", "❌", "⏱️")) if you need to branch on failure, or rely on validate_connection() for a structured success/reason check instead. Exception: GeminiInterface.generate (sync) and OllamaInterface.generate/generate_async raise ValueError uncaught specifically for a missing prompt/messages rather than returning a string for that particular failure — see the table above. A second exception, specific to tools: on all three of these interfaces, a successful tool call still returns the unified {"content", "tool_calls", "stop_reason"} dict, not a string — so code that only checks for an error-prefixed string needs an isinstance(result, dict) / "tool_calls" in result check as well when tools is passed, same as the other four providers above.

OllamaInterface prints "Ollama not found" and generation then fails to connect

Ollama itself (the binary, not just the Python package) must be installed and on PATH for auto_start to work — see ollama.com/download. If it's already running elsewhere (a different host/port), pass base_url="http://host:port" as a kwarg rather than relying on auto-start.

OllamaInterface generation times out on long responses

generate() streams internally specifically to reset the read-timeout per chunk (see OllamaInterface), so a genuinely slow model shouldn't hit the read timeout mid-response. If you're still timing out, it's most likely the 10-second connect timeout (fixed, not configurable via llm_config) — check that the Ollama server is actually reachable at base_url before generation starts, e.g. via llm.list_models().

GeminiInterface calls feel slow even for small prompts

By design — every call enforces a hardcoded 5-second minimum gap since the previous call on that instance, to stay under Gemini's free-tier rate limit. See GeminiInterface § Rate limiting. This isn't configurable via llm_config; if you need faster back-to-back calls, you'd need to edit _min_request_interval in the source, or use a paid tier and confirm the higher limit warrants it.

Changing llm_config.groq_model has no effect

These three fields exist on the llm_config dataclass but are not read by GroqInterface — each hardcodes its own default. Pass model_name=/region_name= explicitly at construction time instead. See Configuration § Some fields are defined but not wired up.

OllamaInterface ignores tool_choice, or a tool call never comes back

tool_choice is accepted by OllamaInterface.generate/generate_async for signature symmetry with the other six providers, but it is never forwarded anywhere in the request — Ollama's tool-calling API has no equivalent concept, so passing it silently has no effect. If the model never calls a tool despite tools being passed, check that the specific model you're running actually supports tool calling (not every Ollama-served model does) — an unsupported model will just answer normally in prose, and this package treats that as a valid plain-text result rather than an error. See Tool Calling § Ollama forces a non-streaming request.

OllamaInterface.astream() never yields anything when tools is passed

astream() doesn't accept tools, and does not implement the non-streaming tools branch that generate/generate_async use — it only ever calls the streaming/no-tools code paths. Use generate() or generate_async() when you need tool calls on Ollama.

OllamaInterface behaves differently for a missing prompt/messages, or not, depending on tools

Confirmed against the source: without tools, a missing prompt (with no messages given either) raises ValueError straight out of generate/generate_async, uncaught. With tools set, the identical error condition is instead raised inside self._build_messages(...) — but this class has a single except ValueError: raise guard ahead of its generic exception handler that applies to the whole method body, so the ValueError still propagates uncaught either way. In short: this one is not actually a gotcha in practice — both paths raise the same way — but it's worth knowing the two code paths reach that same outcome differently, in case a future change to either path breaks that symmetry.

Still stuck?

If the behavior you're seeing isn't explained by anything above, it's worth checking the specific provider's API page under API Reference — several interfaces (especially Gemini, Mistral, and Ollama) have provider-specific quirks documented inline with the relevant code.

Related pages