Repository: Future-House/paper-qa (language: python)

Issue:
paperqa.Settings does not use local llm
I deployed Qwen2.5-14B-Instruct on my local server and started llm correctly using vllm.
But when I executed the sample code,
```
from paperqa import Settings, ask

local_llm_config = dict(
    model_list=[
        dict(
            model_name="Qwen2.5-14B-Instruct",
            litellm_params=dict(
                model="Qwen2.5-14B-Instruct",
                api_base="http://127.0.0.1:8000/v1",
                api_key="EMPTY",
                temperature=0.7,
                max_tokens=512,
            ),
        )
    ]
)

answer = ask(
    "What manufacturing challenges are unique to bispecific antibodies?",
    settings=Settings(
        llm="Qwen2.5-14B-Instruct",
        llm_config=local_llm_config,
        summary_llm="Qwen2.5-14B-Instruct",
        summary_llm_config=local_llm_config,
    ),
)
```
 I got the following error:
```
Traceback (most recent call last):
  File "/home/huzheyu/project/paper/test/qa.py", line 22, in <module>
    answer = ask(
             ^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/__init__.py", line 102, in ask
    return get_loop().run_until_complete(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 67, in agent_query
    response = await run_agent(docs, query, agent_type, **runner_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 119, in run_agent
    elif tool_selector_or_none := query.settings.make_aviary_tool_selector(agent_type):
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/settings.py", line 719, in make_aviary_tool_selector
    acompletion=self.get_agent_llm().router.acompletion,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/llms.py", line 610, in router
    self._router = litellm.Router(
                   ^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 352, in __init__
    self.set_model_list(model_list)
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4069, in set_model_list
    self._create_deployment(
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 3994, in _create_deployment
    deployment = self._add_deployment(deployment=deployment)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4158, in _add_deployment
    set_client(
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router_utils/client_initalization_utils.py", line 441, in set_client
    _client = openai.AsyncOpenAI(  # type: ignore
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/openai/_client.py", line 319, in __init__
    raise OpenAIError(
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
```

It seems that the settings are invalid, and the program still prioritizes looking for openai_api instead of the local llm settings I passed.
I want to know what caused this.
paperqa.Settings does not use local llm
I deployed Qwen2.5-14B-Instruct on my local server and started llm correctly using vllm.
But when I executed the sample code,
```
from paperqa import Settings, ask

local_llm_config = dict(
    model_list=[
        dict(
            model_name="Qwen2.5-14B-Instruct",
            litellm_params=dict(
                model="Qwen2.5-14B-Instruct",
                api_base="http://127.0.0.1:8000/v1",
                api_key="EMPTY",
                temperature=0.7,
                max_tokens=512,
            ),
        )
    ]
)

answer = ask(
    "What manufacturing challenges are unique to bispecific antibodies?",
    settings=Settings(
        llm="Qwen2.5-14B-Instruct",
        llm_config=local_llm_config,
        summary_llm="Qwen2.5-14B-Instruct",
        summary_llm_config=local_llm_config,
    ),
)
```
 I got the following error:
```
Traceback (most recent call last):
  File "/home/huzheyu/project/paper/test/qa.py", line 22, in <module>
    answer = ask(
             ^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/__init__.py", line 102, in ask
    return get_loop().run_until_complete(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 67, in agent_query
    response = await run_agent(docs, query, agent_type, **runner_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 119, in run_agent
    elif tool_selector_or_none := query.settings.make_aviary_tool_selector(agent_type):
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/settings.py", line 719, in make_aviary_tool_selector
    acompletion=self.get_agent_llm().router.acompletion,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/llms.py", line 610, in router
    self._router = litellm.Router(
                   ^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 352, in __init__
    self.set_model_list(model_list)
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4069, in set_model_list
    self._create_deployment(
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 3994, in _create_deployment
    deployment = self._add_deployment(deployment=deployment)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4158, in _add_deployment
    set_client(
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router_utils/client_initalization_utils.py", line 441, in set_client
    _client = openai.AsyncOpenAI(  # type: ignore
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/openai/_client.py", line 319, in __init__
    raise OpenAIError(
openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
```

It seems that the settings are invalid, and the program still prioritizes looking for openai_api instead of the local llm settings I passed.
I want to know what caused this.


Discussion / hints:
<!-- Greeting -->
Hey, @loilisxka! I'm here to help you with any coding bugs or questions you have. Let's work together to solve this issue!

<!-- Answer -->
It looks like the issue is related to the `OPENAI_API_KEY` environment variable. Even when using a local LLM, the system might still require this variable to be set due to dependencies on libraries like `langchain` or `litellm`.

A similar issue was discussed in a closed thread where the solution was to set the `OPENAI_API_KEY` to a generic value. You can try setting it like this:

```sh
export OPENAI_API_KEY=foo
```

This should help bypass the requirement for an actual OpenAI API key and allow your local LLM configuration to be used <sup>[[1]](https://github.com/Future-House/paper-qa/issues/182)</sup>.


<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*

Hey, @dosu I skipped the api check by adding the environment variable `os.environ['OPENAI_API_KEY'] = 'foo'`, but then the program started repeatedly printing
```Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm.set_verbose=True'.
```
What does this mean? Here is the full error
```
Timeout: litellm.Timeout: APITimeoutError - Request timed out.                                                                                                                                                          
           error_str: Request timed out.                                                                                                                                                                                           
           Received Model Group=gpt-4o-2024-08-06                                                                                                                                                                                  
           Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM Max Retries: 3 
```
It seems that the program is still looking for online llm instead of local.
<!-- Answer -->
The error message indicates that `litellm` is timing out while trying to connect to the specified model. This suggests that the configuration might still be pointing to an online model instead of your local setup.

Here are a few things to check:

1. **API Base URL**: Ensure that the `api_base` in your `litellm_params` is correctly set to your local server's address (`http://127.0.0.1:8000/v1`).

2. **Model Name**: Double-check that the `model_name` in your `model_list` matches exactly with what your local server expects.

3. **Network Issues**: Verify that your local server is running and accessible at the specified `api_base` URL. You can test this by sending a simple request to the server to see if it responds.

4. **Verbose Logging**: Enable verbose logging by setting `litellm.set_verbose = True` to get more detailed information about what might be going wrong.

If everything seems correct and the issue persists, it might be worth checking the server logs for any errors or misconfigurations.


<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*

I am pretty sure that there is nothing wrong with the locally deployed LLM. It is providing correct and timely answers when I access it using the Openai package, but paper-QA is not working properly.
I have the same problem. I tried to run with ollama/llama3.2 as per example in the docs. I am getting the same error message, looks like the local model is not being used at all - even though it is specified in the settings as per docs. My error message is:
```APIError: litellm.APIError: APIError: OpenAIException - Connection error.
           Received Model Group=gpt-4o-2024-08-06                                   
           Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM   
           Max Retries: 3
```
> I have the same problem. I tried to run with ollama/llama3.2 as per example in the docs. I am getting the same error message, looks like the local model is not being used at all - even though it is specified in the settings as per docs. My error message is:
> 
> ```
>            Received Model Group=gpt-4o-2024-08-06                                   
>            Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM   
>            Max Retries: 3
> ```

Same problem here, trying to run paperqa locally for the last month and it is pulling gpt-4o-2024-xx-xx model instead of local LLM. I tryed running llama3.1, llama3.2 and I have no problem executing the example from ollama to add documents to storage and ask questions.
Paper-qa seems to connect to ollama server, gpu has the corresponding process, but the error is the one above.
Could someone who has a working example, provide their virtual environment requirement list and IDE description, whether it is a notebook, a plain py or whatever.
@grg-ffb - maybe you can share your setup? I saw that you edited the README file to add ollama in this fork: https://github.com/grg-ffb/paper-qa-ollama
> @grg-ffb - maybe you can share your setup? I saw that you edited the README file to add ollama in this fork: https://github.com/grg-ffb/paper-qa-ollama

Hi @chemical-accuracy,  my code is already merged to paperqa with PR #536 last week. But to add on it: I am hosting llama3.2 and mxbai-embed-large on ollama without any changes to default port or anything. For me also not every api did work. Sometimes I still get the error that openai keys are missing or rate limits are reached. So it seems that for some tasks paperqa still tries to use openai. But as a starting point it should work. 
> I deployed Qwen2.5-14B-Instruct on my local server and started llm correctly using vllm. But when I executed the sample code,
> 
> ```
> from paperqa import Settings, ask
> 
> local_llm_config = dict(
>     model_list=[
>         dict(
>             model_name="Qwen2.5-14B-Instruct",
>             litellm_params=dict(
>                 model="Qwen2.5-14B-Instruct",
>                 api_base="http://127.0.0.1:8000/v1",
>                 api_key="EMPTY",
>                 temperature=0.7,
>                 max_tokens=512,
>             ),
>         )
>     ]
> )
> 
> answer = ask(
>     "What manufacturing challenges are unique to bispecific antibodies?",
>     settings=Settings(
>         llm="Qwen2.5-14B-Instruct",
>         llm_config=local_llm_config,
>         summary_llm="Qwen2.5-14B-Instruct",
>         summary_llm_config=local_llm_config,
>     ),
> )
> ```
> 
> I got the following error:
> 
> ```
> Traceback (most recent call last):
>   File "/home/huzheyu/project/paper/test/qa.py", line 22, in <module>
>     answer = ask(
>              ^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/__init__.py", line 102, in ask
>     return get_loop().run_until_complete(
>            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
>     return future.result()
>            ^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 67, in agent_query
>     response = await run_agent(docs, query, agent_type, **runner_kwargs)
>                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 119, in run_agent
>     elif tool_selector_or_none := query.settings.make_aviary_tool_selector(agent_type):
>                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/settings.py", line 719, in make_aviary_tool_selector
>     acompletion=self.get_agent_llm().router.acompletion,
>                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/llms.py", line 610, in router
>     self._router = litellm.Router(
>                    ^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 352, in __init__
>     self.set_model_list(model_list)
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4069, in set_model_list
>     self._create_deployment(
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 3994, in _create_deployment
>     deployment = self._add_deployment(deployment=deployment)
>                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4158, in _add_deployment
>     set_client(
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router_utils/client_initalization_utils.py", line 441, in set_client
>     _client = openai.AsyncOpenAI(  # type: ignore
>               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/openai/_client.py", line 319, in __init__
>     raise OpenAIError(
> openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
> ```
> 
> It seems that the settings are invalid, and the program still prioritizes looking for openai_api instead of the local llm settings I passed. I want to know what caused this.

From my experience you also need to add the embedding model. If you don't, paperqa will still try to access openai for creating the embeddings and you will get this error if the openai api can't be reached.
So I did a clean install, conda environment with python 3.11 and pip install paper-qa==5.2.0, just to see if it is any different from conda isntall
Execution in vscode jupyter:  

```
import nest_asyncio
nest_asyncio.apply()
from paperqa import Settings, ask
import os

# set up openai key to stop openai lib from complaining 
api_key="sk-no-key-required"
os.environ['OPENAI_API_KEY'] = api_key

# question list
question_list = [
    "test q1?",
    "test q2?",
    "test q3?",
]

model = "ollama/llama3.2" # "ollama/llama3.1" 
embedding = "ollama/mxbai-embed-large" # "nomic-embed-text"

local_llm_config = {
    "model_list": [
        {
            "model_name": model,
            "litellm_params": {
                "model": model,
                "api_base": "http://localhost:11434",
            },
        }
    ]
}

answer = ask(
    question_list[0],
    settings=Settings(
        llm=model,
        llm_config=local_llm_config,
        summary_llm=model,
        summary_llm_config=local_llm_config,
        embedding=embedding,
    ),
)
```

Error that I get is:  

```

           ╭───────────────────────────────── Traceback (most recent call last) ──────────────────────────────────╮
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/paperqa/agents/main.py:240 in              │
           │ run_aviary_agent                                                                                     │
           │                                                                                                      │
           │   237 │   │   │   │   │   )                                                                          │
           │   238 │   │   │   │   │   return env.state.answer, AgentStatus.TRUNCATED                             │
           │   239 │   │   │   │   agent_state.messages += obs                                                    │
           │ ❱ 240 │   │   │   │   for attempt in Retrying(                                                       │
           │   241 │   │   │   │   │   stop=stop_after_attempt(5),                                                │
           │   242 │   │   │   │   │   retry=retry_if_exception_type(MalformedMessageError),                      │
           │   243 │   │   │   │   │   before_sleep=before_sleep_log(logger, logging.WARNING),                    │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/tenacity/__init__.py:443 in __iter__       │
           │                                                                                                      │
           │   440 │   │                                                                                          │
           │   441 │   │   retry_state = RetryCallState(self, fn=None, args=(), kwargs={})                        │
           │   442 │   │   while True:                                                                            │
           │ ❱ 443 │   │   │   do = self.iter(retry_state=retry_state)                                            │
           │   444 │   │   │   if isinstance(do, DoAttempt):                                                      │
           │   445 │   │   │   │   yield AttemptManager(retry_state=retry_state)                                  │
           │   446 │   │   │   elif isinstance(do, DoSleep):                                                      │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/tenacity/__init__.py:376 in iter           │
           │                                                                                                      │
           │   373 │   │   self._begin_iter(retry_state)                                                          │
           │   374 │   │   result = None                                                                          │
           │   375 │   │   for action in self.iter_state.actions:                                                 │
           │ ❱ 376 │   │   │   result = action(retry_state)                                                       │
           │   377 │   │   return result                                                                          │
           │   378 │                                                                                              │
           │   379 │   def _begin_iter(self, retry_state: "RetryCallState") -> None:  # noqa                      │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/tenacity/__init__.py:398 in <lambda>       │
           │                                                                                                      │
           │   395 │                                                                                              │
           │   396 │   def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None:                │
           │   397 │   │   if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result):        │
           │ ❱ 398 │   │   │   self._add_action_func(lambda rs: rs.outcome.result())                              │
           │   399 │   │   │   return                                                                             │
           │   400 │   │                                                                                          │
           │   401 │   │   if self.after is not None:                                                             │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/concurrent/futures/_base.py:449 in result                │
           │                                                                                                      │
           │   446 │   │   │   │   if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:                         │
           │   447 │   │   │   │   │   raise CancelledError()                                                     │
           │   448 │   │   │   │   elif self._state == FINISHED:                                                  │
           │ ❱ 449 │   │   │   │   │   return self.__get_result()                                                 │
           │   450 │   │   │   │                                                                                  │
           │   451 │   │   │   │   self._condition.wait(timeout)                                                  │
           │   452                                                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/concurrent/futures/_base.py:401 in __get_result          │
           │                                                                                                      │
           │   398 │   def __get_result(self):                                                                    │
           │   399 │   │   if self._exception:                                                                    │
           │   400 │   │   │   try:                                                                               │
           │ ❱ 401 │   │   │   │   raise self._exception                                                          │
           │   402 │   │   │   finally:                                                                           │
           │   403 │   │   │   │   # Break a reference cycle with the exception in self._exception                │
           │   404 │   │   │   │   self = None                                                                    │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/paperqa/agents/main.py:247 in              │
           │ run_aviary_agent                                                                                     │
           │                                                                                                      │
           │   244 │   │   │   │   │   reraise=True,                                                              │
           │   245 │   │   │   │   ):                                                                             │
           │   246 │   │   │   │   │   with attempt:  # Retrying if ToolSelector fails to select a tool           │
           │ ❱ 247 │   │   │   │   │   │   action = await agent(agent_state.messages, tools)                      │
           │   248 │   │   │   │   agent_state.messages = [*agent_state.messages, action]                         │
           │   249 │   │   │   │   if on_agent_action_callback:                                                   │
           │   250 │   │   │   │   │   await on_agent_action_callback(action, agent_state)                        │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/aviary/tools/utils.py:175 in __call__      │
           │                                                                                                      │
           │   172 │   │   │   │   # in practice 'tool_calls' shows up too                                        │
           │   173 │   │   │   │   expected_finish_reason.add("stop")                                             │
           │   174 │   │                                                                                          │
           │ ❱ 175 │   │   model_response = await self._bound_acompletion(                                        │
           │   176 │   │   │   messages=MessagesAdapter.dump_python(                                              │
           │   177 │   │   │   │   messages, exclude_none=True, by_alias=True                                     │
           │   178 │   │   │   ),                                                                                 │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:740 in acompletion       │
           │                                                                                                      │
           │    737 │   │   │   │   │   original_exception=e,                                                     │
           │    738 │   │   │   │   )                                                                             │
           │    739 │   │   │   )                                                                                 │
           │ ❱  740 │   │   │   raise e                                                                           │
           │    741 │                                                                                             │
           │    742 │   async def _acompletion(                                                                   │
           │    743 │   │   self, model: str, messages: List[Dict[str, str]], **kwargs                            │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:728 in acompletion       │
           │                                                                                                      │
           │    725 │   │   │   if request_priority is not None and isinstance(request_priority, int):            │
           │    726 │   │   │   │   response = await self.schedule_acompletion(**kwargs)                          │
           │    727 │   │   │   else:                                                                             │
           │ ❱  728 │   │   │   │   response = await self.async_function_with_fallbacks(**kwargs)                 │
           │    729 │   │   │                                                                                     │
           │    730 │   │   │   return response                                                                   │
           │    731 │   │   except Exception as e:                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3044 in                  │
           │ async_function_with_fallbacks                                                                        │
           │                                                                                                      │
           │   3041 │   │   │   │   │   │   )                                                                     │
           │   3042 │   │   │   │   │   )                                                                         │
           │   3043 │   │   │                                                                                     │
           │ ❱ 3044 │   │   │   raise original_exception                                                          │
           │   3045 │                                                                                             │
           │   3046 │   async def async_function_with_retries(self, *args, **kwargs):                             │
           │   3047 │   │   verbose_router_logger.debug(                                                          │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:2898 in                  │
           │ async_function_with_fallbacks                                                                        │
           │                                                                                                      │
           │   2895 │   │   │   │   │   │   Context_Policy_Fallbacks={content_policy_fallbacks}",                 │
           │   2896 │   │   │   │   )                                                                             │
           │   2897 │   │   │                                                                                     │
           │ ❱ 2898 │   │   │   response = await self.async_function_with_retries(*args, **kwargs)                │
           │   2899 │   │   │   verbose_router_logger.debug(f"Async Response: {response}")                        │
           │   2900 │   │   │   return response                                                                   │
           │   2901 │   │   except Exception as e:                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3105 in                  │
           │ async_function_with_retries                                                                          │
           │                                                                                                      │
           │   3102 │   │   │   )                                                                                 │
           │   3103 │   │   │                                                                                     │
           │   3104 │   │   │   # raises an exception if this error should not be retries                         │
           │ ❱ 3105 │   │   │   self.should_retry_this_error(                                                     │
           │   3106 │   │   │   │   error=e,                                                                      │
           │   3107 │   │   │   │   healthy_deployments=_healthy_deployments,                                     │
           │   3108 │   │   │   │   all_deployments=_all_deployments,                                             │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3243 in                  │
           │ should_retry_this_error                                                                              │
           │                                                                                                      │
           │   3240 │   │   │   if (                                                                              │
           │   3241 │   │   │   │   _num_all_deployments <= 1                                                     │
           │   3242 │   │   │   ):  # if there is only 1 deployment for this model group then don't retry         │
           │ ❱ 3243 │   │   │   │   raise error  # then raise error                                               │
           │   3244 │   │                                                                                         │
           │   3245 │   │   # Do not retry if there are no healthy deployments                                    │
           │   3246 │   │   # just raise the error                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3088 in                  │
           │ async_function_with_retries                                                                          │
           │                                                                                                      │
           │   3085 │   │   │   │   │   message=f"This is a mock exception for model={model_group}, to            │
           │        trigger a rate limit error.",                                                                 │
           │   3086 │   │   │   │   )                                                                             │
           │   3087 │   │   │   # if the function call is successful, no exception will be raised and we'll       │
           │        break out of the loop                                                                         │
           │ ❱ 3088 │   │   │   response = await self.make_call(original_function, *args, **kwargs)               │
           │   3089 │   │   │                                                                                     │
           │   3090 │   │   │   return response                                                                   │
           │   3091 │   │   except Exception as e:                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3181 in make_call        │
           │                                                                                                      │
           │   3178 │   │   Handler for making a call to the .completion()/.embeddings() functions.               │
           │   3179 │   │   """                                                                                   │
           │   3180 │   │   model_group = kwargs.get("model")                                                     │
           │ ❱ 3181 │   │   response = await original_function(*args, **kwargs)                                   │
           │   3182 │   │   ## PROCESS RESPONSE HEADERS                                                           │
           │   3183 │   │   await self.set_response_headers(response=response, model_group=model_group)           │
           │   3184                                                                                               │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:879 in _acompletion      │
           │                                                                                                      │
           │    876 │   │   │   )                                                                                 │
           │    877 │   │   │   if model_name is not None:                                                        │
           │    878 │   │   │   │   self.fail_calls[model_name] += 1                                              │
           │ ❱  879 │   │   │   raise e                                                                           │
           │    880 │                                                                                             │
           │    881 │   async def abatch_completion(                                                              │
           │    882 │   │   self,                                                                                 │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:851 in _acompletion      │
           │                                                                                                      │
           │    848 │   │   │   │   await self.async_routing_strategy_pre_call_checks(                            │
           │    849 │   │   │   │   │   deployment=deployment, logging_obj=logging_obj                            │
           │    850 │   │   │   │   )                                                                             │
           │ ❱  851 │   │   │   │   response = await _response                                                    │
           │    852 │   │   │                                                                                     │
           │    853 │   │   │   ## CHECK CONTENT FILTER ERROR ##                                                  │
           │    854 │   │   │   if isinstance(response, ModelResponse):                                           │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/utils.py:1573 in wrapper_async     │
           │                                                                                                      │
           │   1570 │   │   │   │   │   else:                                                                     │
           │   1571 │   │   │   │   │   │   kwargs["model"] = context_window_fallback_dict[model]                 │
           │   1572 │   │   │   │   │   return await original_function(*args, **kwargs)                           │
           │ ❱ 1573 │   │   │   raise e                                                                           │
           │   1574 │                                                                                             │
           │   1575 │   is_coroutine = inspect.iscoroutinefunction(original_function)                             │
           │   1576                                                                                               │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/utils.py:1388 in wrapper_async     │
           │                                                                                                      │
           │   1385 │   │   │   │   │   │   │   ).start()                                                         │
           │   1386 │   │   │   │   │   │   │   return final_embedding_cached_response                            │
           │   1387 │   │   │   # MODEL CALL                                                                      │
           │ ❱ 1388 │   │   │   result = await original_function(*args, **kwargs)                                 │
           │   1389 │   │   │   end_time = datetime.datetime.now()                                                │
           │   1390 │   │   │   if "stream" in kwargs and kwargs["stream"] is True:                               │
           │   1391 │   │   │   │   if (                                                                          │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/main.py:457 in acompletion         │
           │                                                                                                      │
           │    454 │   │   return response                                                                       │
           │    455 │   except Exception as e:                                                                    │
           │    456 │   │   custom_llm_provider = custom_llm_provider or "openai"                                 │
           │ ❱  457 │   │   raise exception_type(                                                                 │
           │    458 │   │   │   model=model,                                                                      │
           │    459 │   │   │   custom_llm_provider=custom_llm_provider,                                          │
           │    460 │   │   │   original_exception=e,                                                             │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/litellm_core_utils/exception_mappi │
           │ ng_utils.py:2033 in exception_type                                                                   │
           │                                                                                                      │
           │   2030 │   │   # don't let an error with mapping interrupt the user from receiving an error          │
           │        from the llm api calls                                                                        │
           │   2031 │   │   if exception_mapping_worked:                                                          │
           │   2032 │   │   │   setattr(e, "litellm_response_headers", litellm_response_headers)                  │
           │ ❱ 2033 │   │   │   raise e                                                                           │
           │   2034 │   │   else:                                                                                 │
           │   2035 │   │   │   for error_type in litellm.LITELLM_EXCEPTION_TYPES:                                │
           │   2036 │   │   │   │   if isinstance(e, error_type):                                                 │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/litellm_core_utils/exception_mappi │
           │ ng_utils.py:358 in exception_type                                                                    │
           │                                                                                                      │
           │    355 │   │   │   │   │   │   )                                                                     │
           │    356 │   │   │   │   │   elif original_exception.status_code == 401:                               │
           │    357 │   │   │   │   │   │   exception_mapping_worked = True                                       │
           │ ❱  358 │   │   │   │   │   │   raise AuthenticationError(                                            │
           │    359 │   │   │   │   │   │   │   message=f"AuthenticationError: {exception_provider} -             │
           │        {message}",                                                                                   │
           │    360 │   │   │   │   │   │   │   llm_provider=custom_llm_provider,                                 │
           │    361 │   │   │   │   │   │   │   model=model,                                                      │
           ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯
           AuthenticationError: litellm.AuthenticationError: AuthenticationError: OpenAIException - Error code: 401
           - {'error': {'message': 'Incorrect API key provided: sk-no-ke******ired. You can find your API key at   
           https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 
           'invalid_api_key'}}                                                                                     
           Received Model Group=gpt-4o-2024-08-06                                                                  
           Available Model Group Fallbacks=None 
           ```



> > I have the same problem. I tried to run with ollama/llama3.2 as per example in the docs. I am getting the same error message, looks like the local model is not being used at all - even though it is specified in the settings as per docs. My error message is:
> > ```
> >            Received Model Group=gpt-4o-2024-08-06                                   
> >            Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM   
> >            Max Retries: 3
> > ```
> 
> Same problem here, trying to run paperqa locally for the last month and it is pulling gpt-4o-2024-xx-xx model instead of local LLM. I tryed running llama3.1, llama3.2 and I have no problem executing the example from ollama to add documents to storage and ask questions. Paper-qa seems to connect to ollama server, gpu has the corresponding process, but the error is the one above. Could someone who has a working example, provide their virtual environment requirement list and IDE description, whether it is a notebook, a plain py or whatever.

Could you please share your Settings configuration? And the corresponding ollama environment configuration required
My running example is python 3.12.3, paperqa 5.0.10. @victorconka  your code works in my setup except I commented out import nest_asyncio and nest_asyncio.apply() as I have this not yet available in my setup. But I experienced that, as no folder containing papers is specified, paperqa checks all available files (including .py files) and produces errors if file is not a paper in format .pdf. It also seems that it is the same in your case - at least the error-messages point in this direction.
 @grg-ffb Thank you so much for trying out my code and providing your setup. I have just set up the environment with python==3.12 (i got 3.12.7), paperqa==5.0.10 and I've finally been able to get this working. I have a papers folder, containing txt files (converted from pdf files), I did not specify anywhere in the code papers folder path, so it is picking up all the txt files but does not seem to read any .py, nevertheless you can specify "paper_directory="./papers" " within ask Settings attribute. 
I get document parsing errors on the first run, but it is not a problem and I still get an answer both in the console and the "answer" object.
By the way, I am running a remote ubuntu 22.04 with NVidia 3090 with ollama installed as described in their wiki:

### Ollama installation
```shell
curl -fsSL https://ollama.com/install.sh | sh  
ollama pull mxbai-embed-large  
ollama pull llama3.1  
ollama pull llama3.2  
``` 

### Directory setup :  
```shell
tree .
.
├── ollama-ask.ipynb
├── papers
│   ├── 1.- paper.txt
│   ├── 2.- paper.txt
│   ├── 3.- paper.txt
│   └── 4.- paper.txt
├── pq.ipynb
└── questions.py
``` 
### pq.ipynb
```python
# jupyter requires asyncio to execute paperqa
import nest_asyncio
nest_asyncio.apply()

from paperqa import Settings, ask

import os
# set up openai key to stop openai lib from complaining 
api_key="sk-no-key-required"
os.environ['OPENAI_API_KEY'] = api_key


# question list
question_list = [
    "test q1?",
    "test q2?",
    "test q3?",
]

from questions import *


model = "ollama/llama3.1" # "ollama/llama3.2" 
embedding = "ollama/mxbai-embed-large" # "nomic-embed-text"

local_llm_config = {
    "model_list": [
        {
            "model_name": model,
            "litellm_params": {
                "model": model,
                "api_base": "http://localhost:11434",
            },
        }
    ]
}

answer = ask(
    question_list[0],
    settings=Settings(
        llm=model,
        llm_config=local_llm_config,
        summary_llm=model,
        summary_llm_config=local_llm_config,
        embedding=embedding,
    ),
)
``` 
P.S. I decided to give a try to the latest paper-qa==5.2.1 version and it has the problem as mentioned in this issue.
Hello,

This issue is a redundant problem related to embedding model. Setting a local embedding model is problematic.

I have no solution for `ask` function but une `Doc().query` method instead. See her : https://github.com/Future-House/paper-qa/issues/541.

Best regards.
<!-- Greeting -->
Hey, @loilisxka! I'm here to help you with any coding bugs or questions you have. Let's work together to solve this issue!

<!-- Answer -->
It looks like the issue is related to the `OPENAI_API_KEY` environment variable. Even when using a local LLM, the system might still require this variable to be set due to dependencies on libraries like `langchain` or `litellm`.

A similar issue was discussed in a closed thread where the solution was to set the `OPENAI_API_KEY` to a generic value. You can try setting it like this:

```sh
export OPENAI_API_KEY=foo
```

This should help bypass the requirement for an actual OpenAI API key and allow your local LLM configuration to be used <sup>[[1]](https://github.com/Future-House/paper-qa/issues/182)</sup>.


<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*

Hey, @dosu I skipped the api check by adding the environment variable `os.environ['OPENAI_API_KEY'] = 'foo'`, but then the program started repeatedly printing
```Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm.set_verbose=True'.
```
What does this mean? Here is the full error
```
Timeout: litellm.Timeout: APITimeoutError - Request timed out.                                                                                                                                                          
           error_str: Request timed out.                                                                                                                                                                                           
           Received Model Group=gpt-4o-2024-08-06                                                                                                                                                                                  
           Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM Max Retries: 3 
```
It seems that the program is still looking for online llm instead of local.
<!-- Answer -->
The error message indicates that `litellm` is timing out while trying to connect to the specified model. This suggests that the configuration might still be pointing to an online model instead of your local setup.

Here are a few things to check:

1. **API Base URL**: Ensure that the `api_base` in your `litellm_params` is correctly set to your local server's address (`http://127.0.0.1:8000/v1`).

2. **Model Name**: Double-check that the `model_name` in your `model_list` matches exactly with what your local server expects.

3. **Network Issues**: Verify that your local server is running and accessible at the specified `api_base` URL. You can test this by sending a simple request to the server to see if it responds.

4. **Verbose Logging**: Enable verbose logging by setting `litellm.set_verbose = True` to get more detailed information about what might be going wrong.

If everything seems correct and the issue persists, it might be worth checking the server logs for any errors or misconfigurations.


<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*

I am pretty sure that there is nothing wrong with the locally deployed LLM. It is providing correct and timely answers when I access it using the Openai package, but paper-QA is not working properly.
I have the same problem. I tried to run with ollama/llama3.2 as per example in the docs. I am getting the same error message, looks like the local model is not being used at all - even though it is specified in the settings as per docs. My error message is:
```APIError: litellm.APIError: APIError: OpenAIException - Connection error.
           Received Model Group=gpt-4o-2024-08-06                                   
           Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM   
           Max Retries: 3
```
> I have the same problem. I tried to run with ollama/llama3.2 as per example in the docs. I am getting the same error message, looks like the local model is not being used at all - even though it is specified in the settings as per docs. My error message is:
> 
> ```
>            Received Model Group=gpt-4o-2024-08-06                                   
>            Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM   
>            Max Retries: 3
> ```

Same problem here, trying to run paperqa locally for the last month and it is pulling gpt-4o-2024-xx-xx model instead of local LLM. I tryed running llama3.1, llama3.2 and I have no problem executing the example from ollama to add documents to storage and ask questions.
Paper-qa seems to connect to ollama server, gpu has the corresponding process, but the error is the one above.
Could someone who has a working example, provide their virtual environment requirement list and IDE description, whether it is a notebook, a plain py or whatever.
@grg-ffb - maybe you can share your setup? I saw that you edited the README file to add ollama in this fork: https://github.com/grg-ffb/paper-qa-ollama
> @grg-ffb - maybe you can share your setup? I saw that you edited the README file to add ollama in this fork: https://github.com/grg-ffb/paper-qa-ollama

Hi @chemical-accuracy,  my code is already merged to paperqa with PR #536 last week. But to add on it: I am hosting llama3.2 and mxbai-embed-large on ollama without any changes to default port or anything. For me also not every api did work. Sometimes I still get the error that openai keys are missing or rate limits are reached. So it seems that for some tasks paperqa still tries to use openai. But as a starting point it should work. 
> I deployed Qwen2.5-14B-Instruct on my local server and started llm correctly using vllm. But when I executed the sample code,
> 
> ```
> from paperqa import Settings, ask
> 
> local_llm_config = dict(
>     model_list=[
>         dict(
>             model_name="Qwen2.5-14B-Instruct",
>             litellm_params=dict(
>                 model="Qwen2.5-14B-Instruct",
>                 api_base="http://127.0.0.1:8000/v1",
>                 api_key="EMPTY",
>                 temperature=0.7,
>                 max_tokens=512,
>             ),
>         )
>     ]
> )
> 
> answer = ask(
>     "What manufacturing challenges are unique to bispecific antibodies?",
>     settings=Settings(
>         llm="Qwen2.5-14B-Instruct",
>         llm_config=local_llm_config,
>         summary_llm="Qwen2.5-14B-Instruct",
>         summary_llm_config=local_llm_config,
>     ),
> )
> ```
> 
> I got the following error:
> 
> ```
> Traceback (most recent call last):
>   File "/home/huzheyu/project/paper/test/qa.py", line 22, in <module>
>     answer = ask(
>              ^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/__init__.py", line 102, in ask
>     return get_loop().run_until_complete(
>            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/asyncio/base_events.py", line 687, in run_until_complete
>     return future.result()
>            ^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 67, in agent_query
>     response = await run_agent(docs, query, agent_type, **runner_kwargs)
>                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/agents/main.py", line 119, in run_agent
>     elif tool_selector_or_none := query.settings.make_aviary_tool_selector(agent_type):
>                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/settings.py", line 719, in make_aviary_tool_selector
>     acompletion=self.get_agent_llm().router.acompletion,
>                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/paperqa/llms.py", line 610, in router
>     self._router = litellm.Router(
>                    ^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 352, in __init__
>     self.set_model_list(model_list)
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4069, in set_model_list
>     self._create_deployment(
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 3994, in _create_deployment
>     deployment = self._add_deployment(deployment=deployment)
>                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router.py", line 4158, in _add_deployment
>     set_client(
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/litellm/router_utils/client_initalization_utils.py", line 441, in set_client
>     _client = openai.AsyncOpenAI(  # type: ignore
>               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>   File "/home/huzheyu/miniconda3/envs/paper/lib/python3.12/site-packages/openai/_client.py", line 319, in __init__
>     raise OpenAIError(
> openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
> ```
> 
> It seems that the settings are invalid, and the program still prioritizes looking for openai_api instead of the local llm settings I passed. I want to know what caused this.

From my experience you also need to add the embedding model. If you don't, paperqa will still try to access openai for creating the embeddings and you will get this error if the openai api can't be reached.
So I did a clean install, conda environment with python 3.11 and pip install paper-qa==5.2.0, just to see if it is any different from conda isntall
Execution in vscode jupyter:  

```
import nest_asyncio
nest_asyncio.apply()
from paperqa import Settings, ask
import os

# set up openai key to stop openai lib from complaining 
api_key="sk-no-key-required"
os.environ['OPENAI_API_KEY'] = api_key

# question list
question_list = [
    "test q1?",
    "test q2?",
    "test q3?",
]

model = "ollama/llama3.2" # "ollama/llama3.1" 
embedding = "ollama/mxbai-embed-large" # "nomic-embed-text"

local_llm_config = {
    "model_list": [
        {
            "model_name": model,
            "litellm_params": {
                "model": model,
                "api_base": "http://localhost:11434",
            },
        }
    ]
}

answer = ask(
    question_list[0],
    settings=Settings(
        llm=model,
        llm_config=local_llm_config,
        summary_llm=model,
        summary_llm_config=local_llm_config,
        embedding=embedding,
    ),
)
```

Error that I get is:  

```

           ╭───────────────────────────────── Traceback (most recent call last) ──────────────────────────────────╮
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/paperqa/agents/main.py:240 in              │
           │ run_aviary_agent                                                                                     │
           │                                                                                                      │
           │   237 │   │   │   │   │   )                                                                          │
           │   238 │   │   │   │   │   return env.state.answer, AgentStatus.TRUNCATED                             │
           │   239 │   │   │   │   agent_state.messages += obs                                                    │
           │ ❱ 240 │   │   │   │   for attempt in Retrying(                                                       │
           │   241 │   │   │   │   │   stop=stop_after_attempt(5),                                                │
           │   242 │   │   │   │   │   retry=retry_if_exception_type(MalformedMessageError),                      │
           │   243 │   │   │   │   │   before_sleep=before_sleep_log(logger, logging.WARNING),                    │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/tenacity/__init__.py:443 in __iter__       │
           │                                                                                                      │
           │   440 │   │                                                                                          │
           │   441 │   │   retry_state = RetryCallState(self, fn=None, args=(), kwargs={})                        │
           │   442 │   │   while True:                                                                            │
           │ ❱ 443 │   │   │   do = self.iter(retry_state=retry_state)                                            │
           │   444 │   │   │   if isinstance(do, DoAttempt):                                                      │
           │   445 │   │   │   │   yield AttemptManager(retry_state=retry_state)                                  │
           │   446 │   │   │   elif isinstance(do, DoSleep):                                                      │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/tenacity/__init__.py:376 in iter           │
           │                                                                                                      │
           │   373 │   │   self._begin_iter(retry_state)                                                          │
           │   374 │   │   result = None                                                                          │
           │   375 │   │   for action in self.iter_state.actions:                                                 │
           │ ❱ 376 │   │   │   result = action(retry_state)                                                       │
           │   377 │   │   return result                                                                          │
           │   378 │                                                                                              │
           │   379 │   def _begin_iter(self, retry_state: "RetryCallState") -> None:  # noqa                      │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/tenacity/__init__.py:398 in <lambda>       │
           │                                                                                                      │
           │   395 │                                                                                              │
           │   396 │   def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None:                │
           │   397 │   │   if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result):        │
           │ ❱ 398 │   │   │   self._add_action_func(lambda rs: rs.outcome.result())                              │
           │   399 │   │   │   return                                                                             │
           │   400 │   │                                                                                          │
           │   401 │   │   if self.after is not None:                                                             │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/concurrent/futures/_base.py:449 in result                │
           │                                                                                                      │
           │   446 │   │   │   │   if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:                         │
           │   447 │   │   │   │   │   raise CancelledError()                                                     │
           │   448 │   │   │   │   elif self._state == FINISHED:                                                  │
           │ ❱ 449 │   │   │   │   │   return self.__get_result()                                                 │
           │   450 │   │   │   │                                                                                  │
           │   451 │   │   │   │   self._condition.wait(timeout)                                                  │
           │   452                                                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/concurrent/futures/_base.py:401 in __get_result          │
           │                                                                                                      │
           │   398 │   def __get_result(self):                                                                    │
           │   399 │   │   if self._exception:                                                                    │
           │   400 │   │   │   try:                                                                               │
           │ ❱ 401 │   │   │   │   raise self._exception                                                          │
           │   402 │   │   │   finally:                                                                           │
           │   403 │   │   │   │   # Break a reference cycle with the exception in self._exception                │
           │   404 │   │   │   │   self = None                                                                    │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/paperqa/agents/main.py:247 in              │
           │ run_aviary_agent                                                                                     │
           │                                                                                                      │
           │   244 │   │   │   │   │   reraise=True,                                                              │
           │   245 │   │   │   │   ):                                                                             │
           │   246 │   │   │   │   │   with attempt:  # Retrying if ToolSelector fails to select a tool           │
           │ ❱ 247 │   │   │   │   │   │   action = await agent(agent_state.messages, tools)                      │
           │   248 │   │   │   │   agent_state.messages = [*agent_state.messages, action]                         │
           │   249 │   │   │   │   if on_agent_action_callback:                                                   │
           │   250 │   │   │   │   │   await on_agent_action_callback(action, agent_state)                        │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/aviary/tools/utils.py:175 in __call__      │
           │                                                                                                      │
           │   172 │   │   │   │   # in practice 'tool_calls' shows up too                                        │
           │   173 │   │   │   │   expected_finish_reason.add("stop")                                             │
           │   174 │   │                                                                                          │
           │ ❱ 175 │   │   model_response = await self._bound_acompletion(                                        │
           │   176 │   │   │   messages=MessagesAdapter.dump_python(                                              │
           │   177 │   │   │   │   messages, exclude_none=True, by_alias=True                                     │
           │   178 │   │   │   ),                                                                                 │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:740 in acompletion       │
           │                                                                                                      │
           │    737 │   │   │   │   │   original_exception=e,                                                     │
           │    738 │   │   │   │   )                                                                             │
           │    739 │   │   │   )                                                                                 │
           │ ❱  740 │   │   │   raise e                                                                           │
           │    741 │                                                                                             │
           │    742 │   async def _acompletion(                                                                   │
           │    743 │   │   self, model: str, messages: List[Dict[str, str]], **kwargs                            │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:728 in acompletion       │
           │                                                                                                      │
           │    725 │   │   │   if request_priority is not None and isinstance(request_priority, int):            │
           │    726 │   │   │   │   response = await self.schedule_acompletion(**kwargs)                          │
           │    727 │   │   │   else:                                                                             │
           │ ❱  728 │   │   │   │   response = await self.async_function_with_fallbacks(**kwargs)                 │
           │    729 │   │   │                                                                                     │
           │    730 │   │   │   return response                                                                   │
           │    731 │   │   except Exception as e:                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3044 in                  │
           │ async_function_with_fallbacks                                                                        │
           │                                                                                                      │
           │   3041 │   │   │   │   │   │   )                                                                     │
           │   3042 │   │   │   │   │   )                                                                         │
           │   3043 │   │   │                                                                                     │
           │ ❱ 3044 │   │   │   raise original_exception                                                          │
           │   3045 │                                                                                             │
           │   3046 │   async def async_function_with_retries(self, *args, **kwargs):                             │
           │   3047 │   │   verbose_router_logger.debug(                                                          │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:2898 in                  │
           │ async_function_with_fallbacks                                                                        │
           │                                                                                                      │
           │   2895 │   │   │   │   │   │   Context_Policy_Fallbacks={content_policy_fallbacks}",                 │
           │   2896 │   │   │   │   )                                                                             │
           │   2897 │   │   │                                                                                     │
           │ ❱ 2898 │   │   │   response = await self.async_function_with_retries(*args, **kwargs)                │
           │   2899 │   │   │   verbose_router_logger.debug(f"Async Response: {response}")                        │
           │   2900 │   │   │   return response                                                                   │
           │   2901 │   │   except Exception as e:                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3105 in                  │
           │ async_function_with_retries                                                                          │
           │                                                                                                      │
           │   3102 │   │   │   )                                                                                 │
           │   3103 │   │   │                                                                                     │
           │   3104 │   │   │   # raises an exception if this error should not be retries                         │
           │ ❱ 3105 │   │   │   self.should_retry_this_error(                                                     │
           │   3106 │   │   │   │   error=e,                                                                      │
           │   3107 │   │   │   │   healthy_deployments=_healthy_deployments,                                     │
           │   3108 │   │   │   │   all_deployments=_all_deployments,                                             │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3243 in                  │
           │ should_retry_this_error                                                                              │
           │                                                                                                      │
           │   3240 │   │   │   if (                                                                              │
           │   3241 │   │   │   │   _num_all_deployments <= 1                                                     │
           │   3242 │   │   │   ):  # if there is only 1 deployment for this model group then don't retry         │
           │ ❱ 3243 │   │   │   │   raise error  # then raise error                                               │
           │   3244 │   │                                                                                         │
           │   3245 │   │   # Do not retry if there are no healthy deployments                                    │
           │   3246 │   │   # just raise the error                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3088 in                  │
           │ async_function_with_retries                                                                          │
           │                                                                                                      │
           │   3085 │   │   │   │   │   message=f"This is a mock exception for model={model_group}, to            │
           │        trigger a rate limit error.",                                                                 │
           │   3086 │   │   │   │   )                                                                             │
           │   3087 │   │   │   # if the function call is successful, no exception will be raised and we'll       │
           │        break out of the loop                                                                         │
           │ ❱ 3088 │   │   │   response = await self.make_call(original_function, *args, **kwargs)               │
           │   3089 │   │   │                                                                                     │
           │   3090 │   │   │   return response                                                                   │
           │   3091 │   │   except Exception as e:                                                                │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:3181 in make_call        │
           │                                                                                                      │
           │   3178 │   │   Handler for making a call to the .completion()/.embeddings() functions.               │
           │   3179 │   │   """                                                                                   │
           │   3180 │   │   model_group = kwargs.get("model")                                                     │
           │ ❱ 3181 │   │   response = await original_function(*args, **kwargs)                                   │
           │   3182 │   │   ## PROCESS RESPONSE HEADERS                                                           │
           │   3183 │   │   await self.set_response_headers(response=response, model_group=model_group)           │
           │   3184                                                                                               │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:879 in _acompletion      │
           │                                                                                                      │
           │    876 │   │   │   )                                                                                 │
           │    877 │   │   │   if model_name is not None:                                                        │
           │    878 │   │   │   │   self.fail_calls[model_name] += 1                                              │
           │ ❱  879 │   │   │   raise e                                                                           │
           │    880 │                                                                                             │
           │    881 │   async def abatch_completion(                                                              │
           │    882 │   │   self,                                                                                 │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/router.py:851 in _acompletion      │
           │                                                                                                      │
           │    848 │   │   │   │   await self.async_routing_strategy_pre_call_checks(                            │
           │    849 │   │   │   │   │   deployment=deployment, logging_obj=logging_obj                            │
           │    850 │   │   │   │   )                                                                             │
           │ ❱  851 │   │   │   │   response = await _response                                                    │
           │    852 │   │   │                                                                                     │
           │    853 │   │   │   ## CHECK CONTENT FILTER ERROR ##                                                  │
           │    854 │   │   │   if isinstance(response, ModelResponse):                                           │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/utils.py:1573 in wrapper_async     │
           │                                                                                                      │
           │   1570 │   │   │   │   │   else:                                                                     │
           │   1571 │   │   │   │   │   │   kwargs["model"] = context_window_fallback_dict[model]                 │
           │   1572 │   │   │   │   │   return await original_function(*args, **kwargs)                           │
           │ ❱ 1573 │   │   │   raise e                                                                           │
           │   1574 │                                                                                             │
           │   1575 │   is_coroutine = inspect.iscoroutinefunction(original_function)                             │
           │   1576                                                                                               │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/utils.py:1388 in wrapper_async     │
           │                                                                                                      │
           │   1385 │   │   │   │   │   │   │   ).start()                                                         │
           │   1386 │   │   │   │   │   │   │   return final_embedding_cached_response                            │
           │   1387 │   │   │   # MODEL CALL                                                                      │
           │ ❱ 1388 │   │   │   result = await original_function(*args, **kwargs)                                 │
           │   1389 │   │   │   end_time = datetime.datetime.now()                                                │
           │   1390 │   │   │   if "stream" in kwargs and kwargs["stream"] is True:                               │
           │   1391 │   │   │   │   if (                                                                          │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/main.py:457 in acompletion         │
           │                                                                                                      │
           │    454 │   │   return response                                                                       │
           │    455 │   except Exception as e:                                                                    │
           │    456 │   │   custom_llm_provider = custom_llm_provider or "openai"                                 │
           │ ❱  457 │   │   raise exception_type(                                                                 │
           │    458 │   │   │   model=model,                                                                      │
           │    459 │   │   │   custom_llm_provider=custom_llm_provider,                                          │
           │    460 │   │   │   original_exception=e,                                                             │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/litellm_core_utils/exception_mappi │
           │ ng_utils.py:2033 in exception_type                                                                   │
           │                                                                                                      │
           │   2030 │   │   # don't let an error with mapping interrupt the user from receiving an error          │
           │        from the llm api calls                                                                        │
           │   2031 │   │   if exception_mapping_worked:                                                          │
           │   2032 │   │   │   setattr(e, "litellm_response_headers", litellm_response_headers)                  │
           │ ❱ 2033 │   │   │   raise e                                                                           │
           │   2034 │   │   else:                                                                                 │
           │   2035 │   │   │   for error_type in litellm.LITELLM_EXCEPTION_TYPES:                                │
           │   2036 │   │   │   │   if isinstance(e, error_type):                                                 │
           │                                                                                                      │
           │ /home/user/miniforge3/envs/pq/lib/python3.11/site-packages/litellm/litellm_core_utils/exception_mappi │
           │ ng_utils.py:358 in exception_type                                                                    │
           │                                                                                                      │
           │    355 │   │   │   │   │   │   )                                                                     │
           │    356 │   │   │   │   │   elif original_exception.status_code == 401:                               │
           │    357 │   │   │   │   │   │   exception_mapping_worked = True                                       │
           │ ❱  358 │   │   │   │   │   │   raise AuthenticationError(                                            │
           │    359 │   │   │   │   │   │   │   message=f"AuthenticationError: {exception_provider} -             │
           │        {message}",                                                                                   │
           │    360 │   │   │   │   │   │   │   llm_provider=custom_llm_provider,                                 │
           │    361 │   │   │   │   │   │   │   model=model,                                                      │
           ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯
           AuthenticationError: litellm.AuthenticationError: AuthenticationError: OpenAIException - Error code: 401
           - {'error': {'message': 'Incorrect API key provided: sk-no-ke******ired. You can find your API key at   
           https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 
           'invalid_api_key'}}                                                                                     
           Received Model Group=gpt-4o-2024-08-06                                                                  
           Available Model Group Fallbacks=None 
           ```



> > I have the same problem. I tried to run with ollama/llama3.2 as per example in the docs. I am getting the same error message, looks like the local model is not being used at all - even though it is specified in the settings as per docs. My error message is:
> > ```
> >            Received Model Group=gpt-4o-2024-08-06                                   
> >            Available Model Group Fallbacks=None LiteLLM Retried: 2 times, LiteLLM   
> >            Max Retries: 3
> > ```
> 
> Same problem here, trying to run paperqa locally for the last month and it is pulling gpt-4o-2024-xx-xx model instead of local LLM. I tryed running llama3.1, llama3.2 and I have no problem executing the example from ollama to add documents to storage and ask questions. Paper-qa seems to connect to ollama server, gpu has the corresponding process, but the error is the one above. Could someone who has a working example, provide their virtual environment requirement list and IDE description, whether it is a notebook, a plain py or whatever.

Could you please share your Settings configuration? And the corresponding ollama environment configuration required
My running example is python 3.12.3, paperqa 5.0.10. @victorconka  your code works in my setup except I commented out import nest_asyncio and nest_asyncio.apply() as I have this not yet available in my setup. But I experienced that, as no folder containing papers is specified, paperqa checks all available files (including .py files) and produces errors if file is not a paper in format .pdf. It also seems that it is the same in your case - at least the error-messages point in this direction.
 @grg-ffb Thank you so much for trying out my code and providing your setup. I have just set up the environment with python==3.12 (i got 3.12.7), paperqa==5.0.10 and I've finally been able to get this working. I have a papers folder, containing txt files (converted from pdf files), I did not specify anywhere in the code papers folder path, so it is picking up all the txt files but does not seem to read any .py, nevertheless you can specify "paper_directory="./papers" " within ask Settings attribute. 
I get document parsing errors on the first run, but it is not a problem and I still get an answer both in the console and the "answer" object.
By the way, I am running a remote ubuntu 22.04 with NVidia 3090 with ollama installed as described in their wiki:

### Ollama installation
```shell
curl -fsSL https://ollama.com/install.sh | sh  
ollama pull mxbai-embed-large  
ollama pull llama3.1  
ollama pull llama3.2  
``` 

### Directory setup :  
```shell
tree .
.
├── ollama-ask.ipynb
├── papers
│   ├── 1.- paper.txt
│   ├── 2.- paper.txt
│   ├── 3.- paper.txt
│   └── 4.- paper.txt
├── pq.ipynb
└── questions.py
``` 
### pq.ipynb
```python
# jupyter requires asyncio to execute paperqa
import nest_asyncio
nest_asyncio.apply()

from paperqa import Settings, ask

import os
# set up openai key to stop openai lib from complaining 
api_key="sk-no-key-required"
os.environ['OPENAI_API_KEY'] = api_key


# question list
question_list = [
    "test q1?",
    "test q2?",
    "test q3?",
]

from questions import *


model = "ollama/llama3.1" # "ollama/llama3.2" 
embedding = "ollama/mxbai-embed-large" # "nomic-embed-text"

local_llm_config = {
    "model_list": [
        {
            "model_name": model,
            "litellm_params": {
                "model": model,
                "api_base": "http://localhost:11434",
            },
        }
    ]
}

answer = ask(
    question_list[0],
    settings=Settings(
        llm=model,
        llm_config=local_llm_config,
        summary_llm=model,
        summary_llm_config=local_llm_config,
        embedding=embedding,
    ),
)
``` 
P.S. I decided to give a try to the latest paper-qa==5.2.1 version and it has the problem as mentioned in this issue.
Hello,

This issue is a redundant problem related to embedding model. Setting a local embedding model is problematic.

I have no solution for `ask` function but une `Doc().query` method instead. See her : https://github.com/Future-House/paper-qa/issues/541.

Best regards.

Fix patch:
diff --git a/README.md b/README.md
index ef8dacff9..f0b9326ea 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ question answering, summarization, and contradiction detection.
   - [Choosing Model](#choosing-model)
     - [Locally Hosted](#locally-hosted)
   - [Changing Embedding Model](#changing-embedding-model)
+    - [Local Embedding Models (Sentence Transformers)](#local-embedding-models-sentence-transformers)
   - [Adjusting number of sources](#adjusting-number-of-sources)
   - [Using Code or HTML](#using-code-or-html)
   - [Using External DB/Vector DB and Caching](#using-external-dbvector-db-and-caching)
@@ -540,6 +541,36 @@ for doc in ("myfile.pdf", "myotherfile.pdf"):
 
 The sparse embedding (keyword) models default to having 256 dimensions, but this can be specified via the `ndim` argument.
 
+#### Local Embedding Models (Sentence Transformers)
+
+You can use a `SentenceTransformerEmbeddingModel` model if you install `sentence-transformers`, which is [a local embedding library](https://sbert.net/) with support for HuggingFace models and more. You can install it by adding the `local` extras.
+
+```sh
+pip install paper-qa[local]
+```
+
+and then prefix embedding model names with `st-`:
+
+```python
+from paperqa import Settings, ask
+
+answer = ask(
+    "What manufacturing challenges are unique to bispecific antibodies?",
+    settings=Settings(embedding="st-multi-qa-MiniLM-L6-cos-v1"),
+)
+```
+
+or with a hybrid model
+
+```python
+from paperqa import Settings, ask
+
+answer = ask(
+    "What manufacturing challenges are unique to bispecific antibodies?",
+    settings=Settings(embedding="hybrid-st-multi-qa-MiniLM-L6-cos-v1"),
+)
+```
+
 ### Adjusting number of sources
 
 You can adjust the numbers of sources (passages of text) to reduce token usage or add more context. `k` refers to the top k most relevant and diverse (may from different sources) passages. Each passage is sent to the LLM to summarize, or determine if it is irrelevant. After this step, a limit of `max_sources` is applied so that the final answer can fit into the LLM context window. Thus, `k` > `max_sources` and `max_sources` is the number of sources used in the final answer.
diff --git a/paperqa/__init__.py b/paperqa/__init__.py
index 534096bb9..422424982 100644
--- a/paperqa/__init__.py
+++ b/paperqa/__init__.py
@@ -18,6 +18,7 @@
     LLMModel,
     LLMResult,
     NumpyVectorStore,
+    SentenceTransformerEmbeddingModel,
     SparseEmbeddingModel,
     embedding_model_factory,
 )
@@ -39,6 +40,7 @@
     "LiteLLMModel",
     "NumpyVectorStore",
     "QueryRequest",
+    "SentenceTransformerEmbeddingModel",
     "Settings",
     "SparseEmbeddingModel",
     "Text",
diff --git a/paperqa/llms.py b/paperqa/llms.py
index 2faf87ba1..30e0f830d 100644
--- a/paperqa/llms.py
+++ b/paperqa/llms.py
@@ -197,6 +197,67 @@ async def embed_documents(self, texts):
         )
         return np.concatenate(all_embeds, axis=1)
 
+    def set_mode(self, mode: EmbeddingModes) -> None:
+        # Set mode for all component models
+        for model in self.models:
+            model.set_mode(mode)
+
+
+class SentenceTransformerEmbeddingModel(EmbeddingModel):
+    """An embedding model using SentenceTransformers."""
+
+    name: str = Field(default="multi-qa-MiniLM-L6-cos-v1")
+    config: dict[str, Any] = Field(default_factory=dict)
+    _model: Any = None
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        try:
+            from sentence_transformers import SentenceTransformer
+        except ImportError as exc:
+            raise ImportError(
+                "Please install paper-qa[local] to use SentenceTransformerEmbeddingModel."
+            ) from exc
+
+        self._model = SentenceTransformer(self.name)
+
+    def set_mode(self, mode: EmbeddingModes) -> None:
+        # SentenceTransformer does not support different modes.
+        pass
+
+    async def embed_documents(self, texts: list[str]) -> list[list[float]]:
+        """
+        Asynchronously embed a list of documents using SentenceTransformer.
+
+        Args:
+            texts: A list of text documents to embed.
+
+        Returns:
+            A list of embedding vectors.
+        """
+        # Extract additional configurations if needed
+        batch_size = self.config.get("batch_size", 32)
+        device = self.config.get("device", "cpu")
+
+        # Update the model's device if necessary
+        if device:
+            self._model.to(device)
+
+        # Run the synchronous encode method in a thread pool to avoid blocking the event loop.
+        embeddings = await asyncio.to_thread(
+            lambda: self._model.encode(
+                texts,
+                convert_to_numpy=True,
+                show_progress_bar=False,  # Disabled progress bar
+                batch_size=batch_size,
+                device=device,
+            ),
+        )
+        # If embeddings are returned as numpy arrays, convert them to lists.
+        if isinstance(embeddings, np.ndarray):
+            embeddings = embeddings.tolist()
+        return embeddings
+
 
 class Chunk(BaseModel):
     model_config = ConfigDict(extra="forbid", frozen=True)
@@ -824,16 +885,62 @@ async def similarity_search(
 
 
 def embedding_model_factory(embedding: str, **kwargs) -> EmbeddingModel:
-    if embedding.startswith("hybrid"):
-        embedding_model_name = "-".join(embedding.split("-")[1:])
-        return HybridEmbeddingModel(
-            models=[
-                LiteLLMEmbeddingModel(name=embedding_model_name),
-                SparseEmbeddingModel(**kwargs),
-            ]
+    """
+    Factory function to create an appropriate EmbeddingModel based on the embedding string.
+
+    Supports:
+    - SentenceTransformer models prefixed with "st-" (e.g., "st-multi-qa-MiniLM-L6-cos-v1")
+    - LiteLLM models (default if no prefix is provided)
+    - Hybrid embeddings prefixed with "hybrid-", contains a sparse and a dense model
+
+    Args:
+        embedding: The embedding model identifier. Supports prefixes like "st-" for SentenceTransformer
+                   and "hybrid-" for combining multiple embedding models.
+        **kwargs: Additional keyword arguments for the embedding model.
+    """
+    embedding = embedding.strip()  # Remove any leading/trailing whitespace
+
+    if embedding.startswith("hybrid-"):
+        # Extract the component embedding identifiers after "hybrid-"
+        dense_name = embedding[len("hybrid-") :]
+
+        if not dense_name:
+            raise ValueError(
+                "Hybrid embedding must contain at least one component embedding."
+            )
+
+        # Recursively create each component embedding model
+        dense_model = embedding_model_factory(dense_name, **kwargs)
+        sparse_model = SparseEmbeddingModel(**kwargs)
+
+        return HybridEmbeddingModel(models=[dense_model, sparse_model])
+
+    if embedding.startswith("st-"):
+        # Extract the SentenceTransformer model name after "st-"
+        model_name = embedding[len("st-") :].strip()
+        if not model_name:
+            raise ValueError(
+                "SentenceTransformer model name must be specified after 'st-'."
+            )
+
+        return SentenceTransformerEmbeddingModel(
+            name=model_name,
+            config=kwargs,
+        )
+
+    if embedding.startswith("litellm-"):
+        # Extract the LiteLLM model name after "litellm-"
+        model_name = embedding[len("litellm-") :].strip()
+        if not model_name:
+            raise ValueError("model name must be specified after 'litellm-'.")
+
+        return LiteLLMEmbeddingModel(
+            name=model_name,
+            config=kwargs,
         )
+
     if embedding == "sparse":
         return SparseEmbeddingModel(**kwargs)
-    if kwargs:  # Only override the default config if there are actually kwargs
-        return LiteLLMEmbeddingModel(name=embedding, config=kwargs)
-    return LiteLLMEmbeddingModel(name=embedding, **kwargs)
+
+    # Default to LiteLLMEmbeddingModel if no special prefix is found
+    return LiteLLMEmbeddingModel(name=embedding, config=kwargs)
diff --git a/pyproject.toml b/pyproject.toml
index f8250d033..7fb6f4ceb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,6 +57,9 @@ datasets = [
 ldp = [
     "ldp>=0.9",  # For alg namespace grouping
 ]
+local = [
+    "sentence-transformers",
+]
 typing = [
     "pandas-stubs",
     "types-PyYAML",
@@ -405,7 +408,7 @@ trailing_comma_inline_array = true
 dev-dependencies = [
     "ipython>=8",  # Pin to keep recent
     "mypy>=1.8",  # Pin for mutable-override
-    "paper-qa[datasets,ldp,typing,zotero]",
+    "paper-qa[datasets,ldp,typing,zotero,local]",
     "pre-commit>=3.4",  # Pin to keep recent
     "pydantic~=2.0",
     "pylint-pydantic",
diff --git a/uv.lock b/uv.lock
index 12b1e0286..e0635ad45 100644
--- a/uv.lock
+++ b/uv.lock
@@ -803,6 +803,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/7a/fc/8709ee90837e94790d8b50db51c7b8a70e86e41b2c81e824c20b0ecfeba7/jiter-0.6.1-cp313-none-win_amd64.whl", hash = "sha256:be7503dd6f4bf02c2a9bacb5cc9335bc59132e7eee9d3e931b13d76fd80d7fda", size = 198919 },
 ]
 
+[[package]]
+name = "joblib"
+version = "1.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 },
+]
+
 [[package]]
 name = "jsonschema"
 version = "4.23.0"
@@ -1089,6 +1098,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
 ]
 
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
+]
+
 [[package]]
 name = "multidict"
 version = "6.1.0"
@@ -1268,6 +1286,126 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/48/6f/129e3c17e3befe7fefdeaa6890f4c4df3f3cf0831aa053802c3862da67aa/numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a", size = 14066202 },
 ]
 
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.4.5.8"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7f/7f/7fbae15a3982dc9595e49ce0f19332423b260045d0a6afe93cdbe2f1f624/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3", size = 363333771 },
+    { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/93/b5/9fb3d00386d3361b03874246190dfec7b206fd74e6e287b26a8fcb359d95/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a", size = 12354556 },
+    { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/77/aa/083b01c427e963ad0b314040565ea396f914349914c298556484f799e61b/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198", size = 24133372 },
+    { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a1/aa/b656d755f474e2084971e9a297def515938d56b466ab39624012070cb773/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3", size = 894177 },
+    { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.1.0.70"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "nvidia-cublas-cu12" },
+]
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.2.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "nvidia-nvjitlink-cu12" },
+]
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 },
+    { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.5.147"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/80/9c/a79180e4d70995fdf030c6946991d0171555c6edf95c265c6b2bf7011112/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9", size = 56314811 },
+    { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.6.1.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "nvidia-cublas-cu12" },
+    { name = "nvidia-cusparse-cu12" },
+    { name = "nvidia-nvjitlink-cu12" },
+]
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 },
+    { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.3.1.170"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "nvidia-nvjitlink-cu12" },
+]
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 },
+    { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.21.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/02/45/239d52c05074898a80a900f49b1615d81c07fceadd5ad6c4f86a987c0bc4/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83", size = 20552510 },
+    { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.4.127"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/06/39/471f581edbb7804b39e8063d92fc8305bdc7a80ae5c07dbe6ea5c50d14a5/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3", size = 100417 },
+    { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 },
+]
+
 [[package]]
 name = "openai"
 version = "1.46.1"
@@ -1352,7 +1490,7 @@ wheels = [
 
 [[package]]
 name = "paper-qa"
-version = "5.2.1.dev9+g8620768.d20241015"
+version = "5.1.2.dev27+ge4d1020.d20241017"
 source = { editable = "." }
 dependencies = [
     { name = "aiohttp" },
@@ -1383,6 +1521,9 @@ datasets = [
 ldp = [
     { name = "ldp" },
 ]
+local = [
+    { name = "sentence-transformers" },
+]
 typing = [
     { name = "pandas-stubs" },
     { name = "types-pyyaml" },
@@ -1413,6 +1554,7 @@ dev = [
     { name = "python-dotenv" },
     { name = "pyzotero" },
     { name = "refurb" },
+    { name = "sentence-transformers" },
     { name = "typeguard" },
     { name = "types-pyyaml" },
     { name = "types-setuptools" },
@@ -1439,6 +1581,7 @@ requires-dist = [
     { name = "pymupdf", specifier = ">=1.24.3" },
     { name = "pyzotero", marker = "extra == 'zotero'" },
     { name = "rich" },
+    { name = "sentence-transformers", marker = "extra == 'local'" },
     { name = "setuptools" },
     { name = "tantivy" },
     { name = "tenacity" },
@@ -1451,7 +1594,7 @@ requires-dist = [
 dev = [
     { name = "ipython", specifier = ">=8" },
     { name = "mypy", specifier = ">=1.8" },
-    { name = "paper-qa", extras = ["datasets", "ldp", "typing", "zotero"] },
+    { name = "paper-qa", extras = ["datasets", "ldp", "typing", "zotero", "local"] },
     { name = "pre-commit", specifier = ">=3.4" },
     { name = "pydantic", specifier = "~=2.0" },
     { name = "pylint-pydantic" },
@@ -2211,6 +2354,77 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/95/cc/109eb8b9863680411ae703664abacaa035820c7755acc9686d5dd02cdd2e/rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", size = 214111 },
 ]
 
+[[package]]
+name = "safetensors"
+version = "0.4.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/46/a1c56ed856c6ac3b1a8b37abe5be0cac53219367af1331e721b04d122577/safetensors-0.4.5.tar.gz", hash = "sha256:d73de19682deabb02524b3d5d1f8b3aaba94c72f1bbfc7911b9b9d5d391c0310", size = 65702 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9a/a5/25bcf75e373412daf1fd88045ab3aa8140a0d804ef0e70712c4f2c5b94d8/safetensors-0.4.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:21f848d7aebd5954f92538552d6d75f7c1b4500f51664078b5b49720d180e47c", size = 392256 },
+    { url = "https://files.pythonhosted.org/packages/08/8c/ece3bf8756506a890bd980eca02f47f9d98dfbf5ce16eda1368f53560f67/safetensors-0.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb07000b19d41e35eecef9a454f31a8b4718a185293f0d0b1c4b61d6e4487971", size = 381490 },
+    { url = "https://files.pythonhosted.org/packages/39/83/c4a7ce01d626e46ea2b45887f2e59b16441408031e2ce2f9fe01860c6946/safetensors-0.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09dedf7c2fda934ee68143202acff6e9e8eb0ddeeb4cfc24182bef999efa9f42", size = 441093 },
+    { url = "https://files.pythonhosted.org/packages/47/26/cc52de647e71bd9a0b0d78ead0d31d9c462b35550a817aa9e0cab51d6db4/safetensors-0.4.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59b77e4b7a708988d84f26de3ebead61ef1659c73dcbc9946c18f3b1786d2688", size = 438960 },
+    { url = "https://files.pythonhosted.org/packages/06/78/332538546775ee97e749867df2d58f2282d9c48a1681e4891eed8b94ec94/safetensors-0.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d3bc83e14d67adc2e9387e511097f254bd1b43c3020440e708858c684cbac68", size = 478031 },
+    { url = "https://files.pythonhosted.org/packages/d9/03/a3c8663f1ddda54e624ecf43fce651659b49e8e1603c52c3e464b442acfa/safetensors-0.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39371fc551c1072976073ab258c3119395294cf49cdc1f8476794627de3130df", size = 494754 },
+    { url = "https://files.pythonhosted.org/packages/e6/ee/69e498a892f208bd1da4104d4b9be887f8611bf4942144718b6738482250/safetensors-0.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6c19feda32b931cae0acd42748a670bdf56bee6476a046af20181ad3fee4090", size = 435013 },
+    { url = "https://files.pythonhosted.org/packages/a2/61/f0cfce984515b86d1260f556ba3b782158e2855e6a318446ac2613786fa9/safetensors-0.4.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a659467495de201e2f282063808a41170448c78bada1e62707b07a27b05e6943", size = 455984 },
+    { url = "https://files.pythonhosted.org/packages/e7/a9/3e3b48fcaade3eb4e347d39ebf0bd44291db21a3e4507854b42a7cb910ac/safetensors-0.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bad5e4b2476949bcd638a89f71b6916fa9a5cae5c1ae7eede337aca2100435c0", size = 619513 },
+    { url = "https://files.pythonhosted.org/packages/80/23/2a7a1be24258c0e44c1d356896fd63dc0545a98d2d0184925fa09cd3ec76/safetensors-0.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a3a315a6d0054bc6889a17f5668a73f94f7fe55121ff59e0a199e3519c08565f", size = 604841 },
+    { url = "https://files.pythonhosted.org/packages/b4/5c/34d082ff1fffffd8545fb22cbae3285ab4236f1f0cfc64b7e58261c2363b/safetensors-0.4.5-cp311-none-win32.whl", hash = "sha256:a01e232e6d3d5cf8b1667bc3b657a77bdab73f0743c26c1d3c5dd7ce86bd3a92", size = 272602 },
+    { url = "https://files.pythonhosted.org/packages/6d/41/948c96c8a7e9fef57c2e051f1871c108a6dbbc6d285598bdb1d89b98617c/safetensors-0.4.5-cp311-none-win_amd64.whl", hash = "sha256:cbd39cae1ad3e3ef6f63a6f07296b080c951f24cec60188378e43d3713000c04", size = 285973 },
+    { url = "https://files.pythonhosted.org/packages/bf/ac/5a63082f931e99200db95fd46fb6734f050bb6e96bf02521904c6518b7aa/safetensors-0.4.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:473300314e026bd1043cef391bb16a8689453363381561b8a3e443870937cc1e", size = 392015 },
+    { url = "https://files.pythonhosted.org/packages/73/95/ab32aa6e9bdc832ff87784cdf9da26192b93de3ef82b8d1ada8f345c5044/safetensors-0.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:801183a0f76dc647f51a2d9141ad341f9665602a7899a693207a82fb102cc53e", size = 381774 },
+    { url = "https://files.pythonhosted.org/packages/d6/6c/7e04b7626809fc63f3698f4c50e43aff2864b40089aa4506c918a75b8eed/safetensors-0.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1524b54246e422ad6fb6aea1ac71edeeb77666efa67230e1faf6999df9b2e27f", size = 441134 },
+    { url = "https://files.pythonhosted.org/packages/58/2b/ffe7c86a277e6c1595fbdf415cfe2903f253f574a5405e93fda8baaa582c/safetensors-0.4.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3139098e3e8b2ad7afbca96d30ad29157b50c90861084e69fcb80dec7430461", size = 438467 },
+    { url = "https://files.pythonhosted.org/packages/67/9c/f271bd804e08c7fda954d17b70ff281228a88077337a9e70feace4f4cc93/safetensors-0.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65573dc35be9059770808e276b017256fa30058802c29e1038eb1c00028502ea", size = 476566 },
+    { url = "https://files.pythonhosted.org/packages/4c/ad/4cf76a3e430a8a26108407fa6cb93e6f80d996a5cb75d9540c8fe3862990/safetensors-0.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd33da8e9407559f8779c82a0448e2133737f922d71f884da27184549416bfed", size = 492253 },
+    { url = "https://files.pythonhosted.org/packages/d9/40/a6f75ea449a9647423ec8b6f72c16998d35aa4b43cb38536ac060c5c7bf5/safetensors-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3685ce7ed036f916316b567152482b7e959dc754fcc4a8342333d222e05f407c", size = 434769 },
+    { url = "https://files.pythonhosted.org/packages/52/47/d4b49b1231abf3131f7bb0bc60ebb94b27ee33e0a1f9569da05f8ac65dee/safetensors-0.4.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dde2bf390d25f67908278d6f5d59e46211ef98e44108727084d4637ee70ab4f1", size = 457166 },
+    { url = "https://files.pythonhosted.org/packages/c3/cd/006468b03b0fa42ff82d795d47c4193e99001e96c3f08bd62ef1b5cab586/safetensors-0.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7469d70d3de970b1698d47c11ebbf296a308702cbaae7fcb993944751cf985f4", size = 619280 },
+    { url = "https://files.pythonhosted.org/packages/22/4d/b6208d918e83daa84b424c0ac3191ae61b44b3191613a3a5a7b38f94b8ad/safetensors-0.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a6ba28118636a130ccbb968bc33d4684c48678695dba2590169d5ab03a45646", size = 605390 },
+    { url = "https://files.pythonhosted.org/packages/e8/20/bf0e01825dc01ed75538021a98b9a046e60ead63c6c6700764c821a8c873/safetensors-0.4.5-cp312-none-win32.whl", hash = "sha256:c859c7ed90b0047f58ee27751c8e56951452ed36a67afee1b0a87847d065eec6", size = 273250 },
+    { url = "https://files.pythonhosted.org/packages/f1/5f/ab6b6cec85b40789801f35b7d2fb579ae242d8193929974a106d5ff5c835/safetensors-0.4.5-cp312-none-win_amd64.whl", hash = "sha256:b5a8810ad6a6f933fff6c276eae92c1da217b39b4d8b1bc1c0b8af2d270dc532", size = 286307 },
+    { url = "https://files.pythonhosted.org/packages/90/61/0e27b1403e311cba0be20026bee4ee822d90eda7dad372179e7f18bb99f3/safetensors-0.4.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:25e5f8e2e92a74f05b4ca55686234c32aac19927903792b30ee6d7bd5653d54e", size = 392062 },
+    { url = "https://files.pythonhosted.org/packages/b1/9f/cc31fafc9f5d79da10a83a820ca37f069bab0717895ad8cbcacf629dd1c5/safetensors-0.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:81efb124b58af39fcd684254c645e35692fea81c51627259cdf6d67ff4458916", size = 382517 },
+    { url = "https://files.pythonhosted.org/packages/a4/c7/4fda8a0ebb96662550433378f4a74c677fa5fc4d0a43a7ec287d1df254a9/safetensors-0.4.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:585f1703a518b437f5103aa9cf70e9bd437cb78eea9c51024329e4fb8a3e3679", size = 441378 },
+    { url = "https://files.pythonhosted.org/packages/14/31/9abb431f6209de9c80dab83e1112ebd769f1e32e7ab7ab228a02424a4693/safetensors-0.4.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b99fbf72e3faf0b2f5f16e5e3458b93b7d0a83984fe8d5364c60aa169f2da89", size = 438831 },
+    { url = "https://files.pythonhosted.org/packages/37/37/99bfb195578a808b8d045159ee9264f8da58d017ac0701853dcacda14d4e/safetensors-0.4.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b17b299ca9966ca983ecda1c0791a3f07f9ca6ab5ded8ef3d283fff45f6bcd5f", size = 477112 },
+    { url = "https://files.pythonhosted.org/packages/7d/05/fac3ef107e60d2a78532bed171a91669d4bb259e1236f5ea8c67a6976c75/safetensors-0.4.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76ded72f69209c9780fdb23ea89e56d35c54ae6abcdec67ccb22af8e696e449a", size = 493373 },
+    { url = "https://files.pythonhosted.org/packages/cf/7a/825800ee8c68214b4fd3506d5e19209338c69b41e01c6e14dd13969cc8b9/safetensors-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2783956926303dcfeb1de91a4d1204cd4089ab441e622e7caee0642281109db3", size = 435422 },
+    { url = "https://files.pythonhosted.org/packages/5e/6c/7a3233c08bde558d6c33a41219119866cb596139a4673cc6c24024710ffd/safetensors-0.4.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d94581aab8c6b204def4d7320f07534d6ee34cd4855688004a4354e63b639a35", size = 457382 },
+    { url = "https://files.pythonhosted.org/packages/a0/58/0b7bcba3788ff503990cf9278d611b56c029400612ba93e772c987b5aa03/safetensors-0.4.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:67e1e7cb8678bb1b37ac48ec0df04faf689e2f4e9e81e566b5c63d9f23748523", size = 619301 },
+    { url = "https://files.pythonhosted.org/packages/82/cc/9c2cf58611daf1c83ce5d37f9de66353e23fcda36008b13fd3409a760aa3/safetensors-0.4.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:dbd280b07e6054ea68b0cb4b16ad9703e7d63cd6890f577cb98acc5354780142", size = 605580 },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "joblib" },
+    { name = "numpy" },
+    { name = "scipy" },
+    { name = "threadpoolctl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ff/91/609961972f694cb9520c4c3d201e377a26583e1eb83bc5a334c893729214/scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", size = 12088580 },
+    { url = "https://files.pythonhosted.org/packages/cd/7a/19fe32c810c5ceddafcfda16276d98df299c8649e24e84d4f00df4a91e01/scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", size = 10975994 },
+    { url = "https://files.pythonhosted.org/packages/4c/75/62e49f8a62bf3c60b0e64d0fce540578ee4f0e752765beb2e1dc7c6d6098/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", size = 12465782 },
+    { url = "https://files.pythonhosted.org/packages/49/21/3723de321531c9745e40f1badafd821e029d346155b6c79704e0b7197552/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", size = 13322034 },
+    { url = "https://files.pythonhosted.org/packages/17/1c/ccdd103cfcc9435a18819856fbbe0c20b8fa60bfc3343580de4be13f0668/scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", size = 11015224 },
+    { url = "https://files.pythonhosted.org/packages/a4/db/b485c1ac54ff3bd9e7e6b39d3cc6609c4c76a65f52ab0a7b22b6c3ab0e9d/scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a", size = 12110344 },
+    { url = "https://files.pythonhosted.org/packages/54/1a/7deb52fa23aebb855431ad659b3c6a2e1709ece582cb3a63d66905e735fe/scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", size = 11033502 },
+    { url = "https://files.pythonhosted.org/packages/a1/32/4a7a205b14c11225609b75b28402c196e4396ac754dab6a81971b811781c/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", size = 12085794 },
+    { url = "https://files.pythonhosted.org/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", size = 12945797 },
+    { url = "https://files.pythonhosted.org/packages/aa/ce/c0b912f2f31aeb1b756a6ba56bcd84dd1f8a148470526a48515a3f4d48cd/scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", size = 10985467 },
+    { url = "https://files.pythonhosted.org/packages/a4/50/8891028437858cc510e13578fe7046574a60c2aaaa92b02d64aac5b1b412/scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", size = 12025584 },
+    { url = "https://files.pythonhosted.org/packages/d2/79/17feef8a1c14149436083bec0e61d7befb4812e272d5b20f9d79ea3e9ab1/scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", size = 10959795 },
+    { url = "https://files.pythonhosted.org/packages/b1/c8/f08313f9e2e656bd0905930ae8bf99a573ea21c34666a813b749c338202f/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", size = 12077302 },
+    { url = "https://files.pythonhosted.org/packages/a7/48/fbfb4dc72bed0fe31fe045fb30e924909ad03f717c36694351612973b1a9/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", size = 13002811 },
+    { url = "https://files.pythonhosted.org/packages/a5/e7/0c869f9e60d225a77af90d2aefa7a4a4c0e745b149325d1450f0f0ce5399/scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", size = 10951354 },
+]
+
 [[package]]
 name = "scipy"
 version = "1.14.1"
@@ -2246,6 +2460,24 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 },
 ]
 
+[[package]]
+name = "sentence-transformers"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "huggingface-hub" },
+    { name = "pillow" },
+    { name = "scikit-learn" },
+    { name = "scipy" },
+    { name = "torch" },
+    { name = "tqdm" },
+    { name = "transformers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/aa/ccd95cc81156e19173d7e102fa96137560b5fbef4f7c640076f201b712d5/sentence_transformers-3.2.0.tar.gz", hash = "sha256:4da78ba340fffc48d60a25415145cbe665216d374c948ec54c3163bd352bcc27", size = 202337 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/78/2835963e7c03865392e88008629d9b81db386fedfcfac006d5c6454b8ecf/sentence_transformers-3.2.0-py3-none-any.whl", hash = "sha256:02dbe96d669d30084ea11af94e7c17895c31748e86f20af4dbcc4ea6522b3506", size = 255175 },
+]
+
 [[package]]
 name = "setuptools"
 version = "75.1.0"
@@ -2335,6 +2567,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 },
 ]
 
+[[package]]
+name = "sympy"
+version = "1.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 },
+]
+
 [[package]]
 name = "tantivy"
 version = "0.22.0"
@@ -2371,6 +2615,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/7f/be/df630c387a0a054815d60be6a97eb4e8f17385d5d6fe660e1c02750062b4/termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", size = 7755 },
 ]
 
+[[package]]
+name = "threadpoolctl"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 },
+]
+
 [[package]]
 name = "tiktoken"
 version = "0.8.0"
@@ -2445,6 +2698,44 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
 ]
 
+[[package]]
+name = "torch"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "filelock" },
+    { name = "fsspec" },
+    { name = "jinja2" },
+    { name = "networkx" },
+    { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "setuptools", marker = "python_full_version >= '3.12'" },
+    { name = "sympy" },
+    { name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux'" },
+    { name = "typing-extensions" },
+]
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/75/9f/cde8b71ccca65d68a3733c5c9decef9adefcfaa692f8ab03afbb5de09daa/torch-2.5.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:499a68a756d3b30d10f7e0f6214dc3767b130b797265db3b1c02e9094e2a07be", size = 906478039 },
+    { url = "https://files.pythonhosted.org/packages/58/27/5bacfb6600209bf7e77ba115656cf7aca5b6ab1e0dc95551eefac2d6e7ec/torch-2.5.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9f3df8138a1126a851440b7d5a4869bfb7c9cc43563d64fd9d96d0465b581024", size = 91843630 },
+    { url = "https://files.pythonhosted.org/packages/78/18/7a2e56e2dc45a433dea9e1bf46a65e234294c9c470ccb4d4b53025f57b23/torch-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b81da3bdb58c9de29d0e1361e52f12fcf10a89673f17a11a5c6c7da1cb1a8376", size = 203117099 },
+    { url = "https://files.pythonhosted.org/packages/47/1b/3dfcc84b383f7b27a41de3251753db077b1e23d3f89a3b294cdd2d86fb7b/torch-2.5.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:ba135923295d564355326dc409b6b7f5bd6edc80f764cdaef1fb0a1b23ff2f9c", size = 64288133 },
+    { url = "https://files.pythonhosted.org/packages/ac/72/d610029ef5cdde3f3aa216e8e75c233b1a91b34af0fc47392b3aa928563a/torch-2.5.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:2dd40c885a05ef7fe29356cca81be1435a893096ceb984441d6e2c27aff8c6f4", size = 906389657 },
+    { url = "https://files.pythonhosted.org/packages/22/c2/d1759641eafdf59cb3a339909e96c842fc0c3579681bb7422acaf4a2c179/torch-2.5.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:bc52d603d87fe1da24439c0d5fdbbb14e0ae4874451d53f0120ffb1f6c192727", size = 91823361 },
+    { url = "https://files.pythonhosted.org/packages/2b/e3/0f2698930d944087c3ef585b71a1a72aa51929877c1ccf35d625bec9bd78/torch-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea718746469246cc63b3353afd75698a288344adb55e29b7f814a5d3c0a7c78d", size = 203064894 },
+    { url = "https://files.pythonhosted.org/packages/56/88/f1ddffd642cf71777dca43621b170d50f13175cdd0b4179e04d6e025b5fb/torch-2.5.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6de1fd253e27e7f01f05cd7c37929ae521ca23ca4620cfc7c485299941679112", size = 64261171 },
+    { url = "https://files.pythonhosted.org/packages/b4/b1/f06261814df00eee07ac8cf697a6f5d79231d9894c996d5985243343518a/torch-2.5.0-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:83dcf518685db20912b71fc49cbddcc8849438cdb0e9dcc919b02a849e2cd9e8", size = 906416128 },
+]
+
 [[package]]
 name = "tqdm"
 version = "4.66.5"
@@ -2466,6 +2757,39 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 },
 ]
 
+[[package]]
+name = "transformers"
+version = "4.45.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "filelock" },
+    { name = "huggingface-hub" },
+    { name = "numpy" },
+    { name = "packaging" },
+    { name = "pyyaml" },
+    { name = "regex" },
+    { name = "requests" },
+    { name = "safetensors" },
+    { name = "tokenizers" },
+    { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/4c/3862b2dd6cdf83b187897bd351da0f7fb74d0df642b03c6f5d06353a3ca0/transformers-4.45.2.tar.gz", hash = "sha256:72bc390f6b203892561f05f86bbfaa0e234aab8e927a83e62b9d92ea7e3ae101", size = 8478357 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f9/9d/030cc1b3e88172967e22ee1d012e0d5e0384eb70d2a098d1669d549aea29/transformers-4.45.2-py3-none-any.whl", hash = "sha256:c551b33660cfc815bae1f9f097ecfd1e65be623f13c6ee0dda372bd881460210", size = 9881312 },
+]
+
+[[package]]
+name = "triton"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "filelock", marker = "python_full_version < '3.13'" },
+]
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/86/17/d9a5cf4fcf46291856d1e90762e36cbabd2a56c7265da0d1d9508c8e3943/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c", size = 209506424 },
+    { url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 },
+]
+
 [[package]]
 name = "typeguard"
 version = "4.3.0"


Test patch:
diff --git a/tests/test_llms.py b/tests/test_llms.py
index e699cc5ae..9cf827bdc 100644
--- a/tests/test_llms.py
+++ b/tests/test_llms.py
@@ -6,7 +6,14 @@
 import litellm
 import pytest
 
-from paperqa import LiteLLMModel
+from paperqa import (
+    HybridEmbeddingModel,
+    LiteLLMEmbeddingModel,
+    LiteLLMModel,
+    SentenceTransformerEmbeddingModel,
+    SparseEmbeddingModel,
+    embedding_model_factory,
+)
 from paperqa.llms import Chunk
 from tests.conftest import VCR_DEFAULT_MATCH_ON
 
@@ -150,3 +157,106 @@ def test_pickling(self, tmp_path: pathlib.Path) -> None:
         assert llm.name == rehydrated_llm.name
         assert llm.config == rehydrated_llm.config
         assert llm.router.deployment_names == rehydrated_llm.router.deployment_names
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_sentence_transformer() -> None:
+    """Test that the factory creates a SentenceTransformerEmbeddingModel when given an 'st-' prefix."""
+    embedding = "st-multi-qa-MiniLM-L6-cos-v1"
+    model = embedding_model_factory(embedding)
+    assert isinstance(
+        model, SentenceTransformerEmbeddingModel
+    ), "Factory did not create SentenceTransformerEmbeddingModel"
+    assert model.name == "multi-qa-MiniLM-L6-cos-v1", "Incorrect model name assigned"
+
+    # Test embedding functionality
+    texts = ["Hello world", "Test sentence"]
+    embeddings = await model.embed_documents(texts)
+    assert len(embeddings) == 2, "Incorrect number of embeddings returned"
+    assert all(
+        isinstance(embed, list) for embed in embeddings
+    ), "Embeddings are not in list format"
+    assert all(len(embed) > 0 for embed in embeddings), "Embeddings should not be empty"
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_hybrid_with_sentence_transformer() -> None:
+    """Test that the factory creates a HybridEmbeddingModel containing a SentenceTransformerEmbeddingModel."""
+    embedding = "hybrid-st-multi-qa-MiniLM-L6-cos-v1"
+    model = embedding_model_factory(embedding)
+    assert isinstance(
+        model, HybridEmbeddingModel
+    ), "Factory did not create HybridEmbeddingModel"
+    assert len(model.models) == 2, "Hybrid model should contain two component models"
+    assert isinstance(
+        model.models[0], SentenceTransformerEmbeddingModel
+    ), "First component should be SentenceTransformerEmbeddingModel"
+    assert isinstance(
+        model.models[1], SparseEmbeddingModel
+    ), "Second component should be SparseEmbeddingModel"
+
+    # Test embedding functionality
+    texts = ["Hello world", "Test sentence"]
+    embeddings = await model.embed_documents(texts)
+    assert len(embeddings) == 2, "Incorrect number of embeddings returned"
+    expected_length = len((await model.models[0].embed_documents(texts))[0]) + len(
+        (await model.models[1].embed_documents(texts))[0]
+    )
+    assert all(
+        len(embed) == expected_length for embed in embeddings
+    ), "Embeddings do not match expected combined length"
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_invalid_st_prefix() -> None:
+    """Test that the factory raises a ValueError when 'st-' prefix is provided without a model name."""
+    embedding = "st-"
+    with pytest.raises(
+        ValueError,
+        match="SentenceTransformer model name must be specified after 'st-'.",
+    ):
+        embedding_model_factory(embedding)
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_unknown_prefix() -> None:
+    """Test that the factory defaults to LiteLLMEmbeddingModel when an unknown prefix is provided."""
+    embedding = "unknown-prefix-model"
+    model = embedding_model_factory(embedding)
+    assert isinstance(
+        model, LiteLLMEmbeddingModel
+    ), "Factory did not default to LiteLLMEmbeddingModel for unknown prefix"
+    assert model.name == "unknown-prefix-model", "Incorrect model name assigned"
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_sparse() -> None:
+    """Test that the factory creates a SparseEmbeddingModel when 'sparse' is provided."""
+    embedding = "sparse"
+    model = embedding_model_factory(embedding)
+    assert isinstance(
+        model, SparseEmbeddingModel
+    ), "Factory did not create SparseEmbeddingModel"
+    assert model.name == "sparse", "Incorrect model name assigned"
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_litellm() -> None:
+    """Test that the factory creates a LiteLLMEmbeddingModel when 'litellm-' prefix is provided."""
+    embedding = "litellm-text-embedding-3-small"
+    model = embedding_model_factory(embedding)
+    assert isinstance(
+        model, LiteLLMEmbeddingModel
+    ), "Factory did not create LiteLLMEmbeddingModel"
+    assert model.name == "text-embedding-3-small", "Incorrect model name assigned"
+
+
+@pytest.mark.asyncio
+async def test_embedding_model_factory_default() -> None:
+    """Test that the factory defaults to LiteLLMEmbeddingModel when no known prefix is provided."""
+    embedding = "default-model"
+    model = embedding_model_factory(embedding)
+    assert isinstance(
+        model, LiteLLMEmbeddingModel
+    ), "Factory did not default to LiteLLMEmbeddingModel"
+    assert model.name == "default-model", "Incorrect model name assigned"