refactor(cli): replace InquirerPy with an in-house fuzzy picker

InquirerPy was a thin wrapper around prompt_toolkit, which was already
a transitive dependency and is what the existing tests drove the
widget through directly. Removes the InquirerPy (and its pfzy)
dependency and reimplements the same type-to-filter/tab-to-toggle/
enter-to-confirm interaction directly on prompt_toolkit's Application
and KeyBindings primitives in a new fuzzy_picker.py module, matching
every existing test scenario exactly (single-select, multiselect
toggling, filtering across matches).

Along the way, fixed a real bug the port introduced: the cancel
handler returned an empty result, which _fuzzy_pick's calling code
in wizard.py treats as "please pick again," looping forever waiting
for input that was never coming. Ctrl-C/Ctrl-D now raise
KeyboardInterrupt instead, matching how interactive prompts
conventionally cancel.
This commit is contained in:
Krrish Dholakia 2026-07-15 22:12:54 -07:00
parent 9d2ed13355
commit 0ba155fb03
6 changed files with 277 additions and 107 deletions

View file

@ -0,0 +1,165 @@
from dataclasses import dataclass, field
from prompt_toolkit.application import Application
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout import HSplit, Layout, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.styles import Style
_MAX_VISIBLE_ROWS = 15
_STYLE = Style.from_dict(
{
"prompt": "bold",
"highlighted": "reverse",
"hint": "fg:ansibrightblack",
}
)
def _fuzzy_match(query: str, choices: tuple[str, ...]) -> tuple[str, ...]:
"""fzf-style subsequence filter: query chars must appear in order (not necessarily contiguous)
in a choice, case-insensitive. Preserves the choices' original relative order among matches."""
if not query:
return choices
needle = query.lower()
def _matches(choice: str) -> bool:
haystack = choice.lower()
pos = 0
for ch in needle:
pos = haystack.find(ch, pos)
if pos == -1:
return False
pos += 1
return True
return tuple(choice for choice in choices if _matches(choice))
@dataclass
class _PickerState:
all_choices: tuple[str, ...]
multiselect: bool
query: str = ""
cursor: int = 0
selected: set[str] = field(default_factory=set)
@property
def filtered(self) -> tuple[str, ...]:
return _fuzzy_match(self.query, self.all_choices)
def _confirmed_selection(state: _PickerState) -> list[str]:
if state.multiselect:
return [choice for choice in state.all_choices if choice in state.selected]
filtered = state.filtered
return [filtered[state.cursor]] if filtered else []
def _cancel(event: KeyPressEvent, state: _PickerState) -> None:
event.app.exit(exception=KeyboardInterrupt)
def _confirm(event: KeyPressEvent, state: _PickerState) -> None:
event.app.exit(result=_confirmed_selection(state))
def _toggle(event: KeyPressEvent, state: _PickerState) -> None:
if not state.multiselect:
return
filtered = state.filtered
if filtered:
state.selected.symmetric_difference_update({filtered[state.cursor]})
def _move_up(event: KeyPressEvent, state: _PickerState) -> None:
state.cursor = max(0, state.cursor - 1)
def _move_down(event: KeyPressEvent, state: _PickerState) -> None:
state.cursor = min(max(0, len(state.filtered) - 1), state.cursor + 1)
def _backspace(event: KeyPressEvent, state: _PickerState) -> None:
state.query = state.query[:-1]
state.cursor = 0
def _type_char(event: KeyPressEvent, state: _PickerState) -> None:
if event.data and event.data.isprintable():
state.query += event.data
state.cursor = 0
def _build_key_bindings(state: _PickerState) -> KeyBindings:
kb = KeyBindings()
kb.add("c-c")(lambda event: _cancel(event, state))
kb.add("c-d")(lambda event: _cancel(event, state))
kb.add("enter")(lambda event: _confirm(event, state))
kb.add("tab")(lambda event: _toggle(event, state))
kb.add("up")(lambda event: _move_up(event, state))
kb.add("c-p")(lambda event: _move_up(event, state))
kb.add("down")(lambda event: _move_down(event, state))
kb.add("c-n")(lambda event: _move_down(event, state))
kb.add("backspace")(lambda event: _backspace(event, state))
kb.add(Keys.Any)(lambda event: _type_char(event, state))
return kb
def _prompt_text(state: _PickerState, message: str) -> StyleAndTextTuples:
toggle_hint = "tab to toggle, " if state.multiselect else ""
return [
("class:prompt", f"{message}: "),
("", state.query),
("class:hint", f" ({toggle_hint}type to filter, enter to confirm)"),
]
def _visible_window(state: _PickerState, filtered: tuple[str, ...]) -> tuple[int, tuple[str, ...]]:
state.cursor = min(state.cursor, len(filtered) - 1)
start = max(0, min(state.cursor - _MAX_VISIBLE_ROWS + 1, len(filtered) - _MAX_VISIBLE_ROWS))
return start, filtered[start : start + _MAX_VISIBLE_ROWS]
def _choices_text(state: _PickerState) -> StyleAndTextTuples:
filtered = state.filtered
if not filtered:
return [("class:hint", " (no matches)")]
start, visible = _visible_window(state, filtered)
lines: StyleAndTextTuples = []
for offset, choice in enumerate(visible):
index = start + offset
marker = "> " if index == state.cursor else " "
check = ("[x] " if choice in state.selected else "[ ] ") if state.multiselect else ""
style = "class:highlighted" if index == state.cursor else ""
lines.append((style, f"{marker}{check}{choice}\n"))
return lines
def fuzzy_pick(choices: tuple[str, ...], message: str, multiselect: bool) -> list[str]:
"""Interactive type-to-filter picker (fzf-style): type to narrow the list, arrow keys to move
the highlight, Enter to confirm. In multiselect mode, Tab toggles the highlighted item into the
result set and Enter confirms whatever has been toggled (not the bare highlight); Ctrl-C/Ctrl-D
raise KeyboardInterrupt rather than returning an empty result, so a cancel aborts the caller
instead of silently looping back to prompt again.
"""
state = _PickerState(all_choices=choices, multiselect=multiselect)
layout = Layout(
HSplit(
[
Window(content=FormattedTextControl(lambda: _prompt_text(state, message)), height=1),
Window(content=FormattedTextControl(lambda: _choices_text(state))),
]
)
)
app: Application[list[str]] = Application(
layout=layout, key_bindings=_build_key_bindings(state), style=_STYLE, full_screen=False
)
return app.run()
__all__ = ["fuzzy_pick"]

View file

@ -3,8 +3,6 @@ from pathlib import Path
import click
import yaml
from InquirerPy import inquirer
from InquirerPy.base.control import Choice
from .... import Client
from .config import (
@ -22,6 +20,7 @@ from .config import (
parse_discovered_models,
validate_config,
)
from .fuzzy_picker import fuzzy_pick
from .process import CONFIG_PATH, secure_create
@ -30,26 +29,19 @@ def _is_interactive() -> bool:
def _fuzzy_pick(models: tuple[DiscoveredModel, ...], prompt_label: str, multiselect: bool) -> list[str]:
"""Type-to-filter picker over a (possibly huge) model pool, using InquirerPy's fzf-style fuzzy prompt.
"""Type-to-filter picker over a (possibly huge) model pool.
A plain numbered table + typed index does not scale past a handful of models -- proxies with
hundreds of model groups made that interaction unusable. This lets the user narrow the pool by
typing a substring instead of scrolling/counting.
Assumes the caller already checked interactivity (run_configure_wizard does, once, up front) --
checking here too would check the wrong thing under test, where InquirerPy is driven through its
own injected input/output rather than the real process stdin.
checking here too would check the wrong thing under test, where the picker is driven through
prompt_toolkit's own injected input/output rather than the real process stdin.
"""
choices = [Choice(value=model.name, name=model.name) for model in models]
toggle_hint = "tab to toggle, " if multiselect else ""
choices = tuple(model.name for model in models)
while True:
result = inquirer.fuzzy(
message=f"{prompt_label}: type to filter, {toggle_hint}enter to confirm",
choices=choices,
multiselect=multiselect,
max_height="70%",
).execute()
selected = result if multiselect else [result]
selected = fuzzy_pick(choices, prompt_label, multiselect)
if selected:
return selected
click.echo("Select at least one model.")

View file

@ -66,7 +66,7 @@ proxy = [
"litellm-enterprise==0.1.50",
"RestrictedPython>=8.1,<9.0",
"rich>=13.9.4,<14.0",
"InquirerPy>=0.3.4,<1.0",
"prompt-toolkit>=3.0.36,<4.0",
"polars>=1.38.1,<2.0",
"soundfile>=0.12.1,<1.0",
"pyroscope-io>=0.8.16,<1.0; sys_platform != 'win32'",
@ -80,7 +80,7 @@ cli = [
"rich>=13.9.4,<14.0",
"pyyaml>=6.0.3,<7.0",
"requests>=2.32.0,<3.0",
"InquirerPy>=0.3.4,<1.0",
"prompt-toolkit>=3.0.36,<4.0",
]
extra_proxy = [
"prisma>=0.11.0,<1.0",

View file

@ -0,0 +1,27 @@
from litellm.proxy.client.cli.commands.autoroute.fuzzy_picker import _fuzzy_match
class TestFuzzyMatch:
def test_empty_query_returns_all_choices_in_order(self):
choices = ("gpt-4o", "claude-opus", "o1")
assert _fuzzy_match("", choices) == choices
def test_exact_substring_matches(self):
choices = ("gpt-4o-mini", "gpt-4o", "claude-opus")
assert _fuzzy_match("gpt-4o", choices) == ("gpt-4o-mini", "gpt-4o")
def test_matches_as_a_subsequence_not_just_a_substring(self):
choices = ("gpt-4o-mini", "claude-opus")
assert _fuzzy_match("g4om", choices) == ("gpt-4o-mini",)
def test_is_case_insensitive(self):
choices = ("GPT-4o-Mini",)
assert _fuzzy_match("gpt4o", choices) == ("GPT-4o-Mini",)
def test_no_match_excludes_the_choice(self):
choices = ("gpt-4o", "claude-opus")
assert _fuzzy_match("xyz", choices) == ()
def test_preserves_original_relative_order_among_matches(self):
choices = ("z-model", "a-model", "m-model")
assert _fuzzy_match("model", choices) == ("z-model", "a-model", "m-model")

View file

@ -6,7 +6,6 @@ import click
import pytest
import yaml
from click.testing import CliRunner
from InquirerPy.base.control import Choice
from prompt_toolkit.application import create_app_session
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import DummyOutput
@ -215,10 +214,10 @@ def _drive_fuzzy_pick(
multiselect: bool,
key_events: List[Tuple[str, float]],
) -> List[str]:
"""Drives the real InquirerPy fuzzy prompt through prompt_toolkit's own test input/output,
"""Drives the real fuzzy-picker widget through prompt_toolkit's own test input/output,
exercising the actual widget (filtering, tab-to-toggle, enter-to-confirm) rather than mocking
it away. asyncio.to_thread propagates the create_app_session context into the worker thread
running _fuzzy_pick's synchronous .execute() call."""
running fuzzy_pick's synchronous Application.run() call."""
async def _run() -> List[str]:
with create_pipe_input() as pipe_input:
@ -267,10 +266,19 @@ class TestFuzzyPickWidget:
)
assert set(result) == {"model-3", "model-15"}
def test_choice_wraps_name_and_value_to_the_same_model_name(self):
model = DiscoveredModel(name="only-model")
choice = Choice(value=model.name, name=model.name)
assert choice.value == choice.name == "only-model"
def test_down_arrow_moves_the_highlighted_match(self):
result = _drive_fuzzy_pick(
self._models(),
"test",
multiselect=False,
key_events=[("model-1", 0.3), ("\x1b[B", 0.1), ("\r", 0.1)],
)
# "model-1" fuzzy-matches model-1, model-10..19; one Down moves off model-1 to model-10
assert result == ["model-10"]
def test_raises_keyboard_interrupt_on_ctrl_c(self):
with pytest.raises(KeyboardInterrupt):
_drive_fuzzy_pick(self._models(), "test", multiselect=False, key_events=[("model-13", 0.3), ("\x03", 0.1)])
class TestRenderAndPromptForModelWrappers:

146
uv.lock generated
View file

@ -10,7 +10,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-07-13T03:38:04.421387Z"
exclude-newer = "2026-07-13T04:54:35.730742Z"
exclude-newer-span = "P3D"
[manifest]
@ -222,9 +222,9 @@ name = "aiologic"
version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sniffio" },
{ name = "sniffio", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "wrapt" },
{ name = "wrapt", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/a7/809482759f40079f4c4328c7318bf569ae25d457f5017aad30a1b9aafedc/aiologic-0.17.0.tar.gz", hash = "sha256:65aa058e858c94cd208badb188e7f00b54dcabb3ba85b34f794db98074d108b9", size = 251625, upload-time = "2026-06-14T12:24:35.367Z" }
wheels = [
@ -516,14 +516,14 @@ name = "aurelio-sdk"
version = "0.0.19"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
{ name = "aiohttp" },
{ name = "colorlog" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "tornado" },
{ name = "aiofiles", marker = "python_full_version < '3.14'" },
{ name = "aiohttp", marker = "python_full_version < '3.14'" },
{ name = "colorlog", marker = "python_full_version < '3.14'" },
{ name = "pydantic", marker = "python_full_version < '3.14'" },
{ name = "python-dotenv", marker = "python_full_version < '3.14'" },
{ name = "requests", marker = "python_full_version < '3.14'" },
{ name = "requests-toolbelt", marker = "python_full_version < '3.14'" },
{ name = "tornado", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/0e/c2e369ad173fb3d76448e46d10beb3dcc53388318933ddf8169a3f21a810/aurelio_sdk-0.0.19.tar.gz", hash = "sha256:14107e7440ff2efd0b4a08c52fb595e7680bd4bc973a0ddfb3b64157c6666b91", size = 15258, upload-time = "2025-03-24T14:37:32.203Z" }
wheels = [
@ -1047,7 +1047,7 @@ name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "humanfriendly" },
{ name = "humanfriendly", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
wheels = [
@ -1059,7 +1059,7 @@ name = "colorlog"
version = "6.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" }
wheels = [
@ -1420,7 +1420,7 @@ name = "culsans"
version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiologic" },
{ name = "aiologic", marker = "python_full_version < '3.13'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" }
@ -2966,7 +2966,7 @@ name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
{ name = "pyreadline3", marker = "python_full_version < '3.14' and sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
wheels = [
@ -3049,19 +3049,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "inquirerpy"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pfzy" },
{ name = "prompt-toolkit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/73/7570847b9da026e07053da3bbe2ac7ea6cde6bb2cbd3c7a5a950fa0ae40b/InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e", size = 44431, upload-time = "2022-06-27T23:11:20.598Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/ff/3b59672c47c6284e8005b42e84ceba13864aa0f39f067c973d1af02f5d91/InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4", size = 67677, upload-time = "2022-06-27T23:11:17.723Z" },
]
[[package]]
name = "isodate"
version = "0.7.2"
@ -3293,10 +3280,10 @@ name = "jsonschema-path"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pathable" },
{ name = "pyyaml" },
{ name = "referencing" },
{ name = "requests" },
{ name = "pathable", marker = "python_full_version < '3.14'" },
{ name = "pyyaml", marker = "python_full_version < '3.14'" },
{ name = "referencing", marker = "python_full_version < '3.14'" },
{ name = "requests", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" }
wheels = [
@ -3767,7 +3754,7 @@ caching = [
{ name = "diskcache" },
]
cli = [
{ name = "inquirerpy" },
{ name = "prompt-toolkit" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "rich" },
@ -3803,12 +3790,12 @@ proxy = [
{ name = "fastapi-sso" },
{ name = "granian" },
{ name = "gunicorn" },
{ name = "inquirerpy" },
{ name = "litellm-enterprise" },
{ name = "litellm-proxy-extras" },
{ name = "mcp" },
{ name = "orjson" },
{ name = "polars" },
{ name = "prompt-toolkit" },
{ name = "pydantic-settings" },
{ name = "pyjwt" },
{ name = "pynacl" },
@ -3981,8 +3968,6 @@ requires-dist = [
{ name = "gunicorn", marker = "extra == 'proxy'", specifier = ">=23.0.0,<24.0" },
{ name = "httpx", specifier = ">=0.28.0,<1.0" },
{ name = "importlib-metadata", specifier = ">=8.0.0,<9.0" },
{ name = "inquirerpy", marker = "extra == 'cli'", specifier = ">=0.3.4,<1.0" },
{ name = "inquirerpy", marker = "extra == 'proxy'", specifier = ">=0.3.4,<1.0" },
{ name = "jinja2", specifier = ">=3.1.6,<4.0" },
{ name = "jsonschema", specifier = ">=4.0.0,<5.0" },
{ name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = ">=2.59.7,<3.0" },
@ -4004,6 +3989,8 @@ requires-dist = [
{ name = "polars", marker = "extra == 'proxy'", specifier = ">=1.38.1,<2.0" },
{ name = "prisma", marker = "extra == 'extra-proxy'", specifier = ">=0.11.0,<1.0" },
{ name = "prometheus-client", marker = "extra == 'proxy-runtime'", specifier = ">=0.20.0,<1.0" },
{ name = "prompt-toolkit", marker = "extra == 'cli'", specifier = ">=3.0.36,<4.0" },
{ name = "prompt-toolkit", marker = "extra == 'proxy'", specifier = ">=3.0.36,<4.0" },
{ name = "pydantic", specifier = ">=2.10.0,<3.0.0" },
{ name = "pydantic-settings", marker = "extra == 'proxy'", specifier = ">=2.14.1,<3.0" },
{ name = "pyjwt", marker = "extra == 'proxy'", specifier = ">=2.13.0,<3.0" },
@ -4481,7 +4468,7 @@ version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594, upload-time = "2024-09-13T19:07:11.624Z" }
wheels = [
@ -4980,14 +4967,14 @@ name = "openapi-core"
version = "0.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "isodate" },
{ name = "jsonschema" },
{ name = "jsonschema-path" },
{ name = "more-itertools" },
{ name = "openapi-schema-validator" },
{ name = "openapi-spec-validator" },
{ name = "typing-extensions" },
{ name = "werkzeug" },
{ name = "isodate", marker = "python_full_version < '3.14'" },
{ name = "jsonschema", marker = "python_full_version < '3.14'" },
{ name = "jsonschema-path", marker = "python_full_version < '3.14'" },
{ name = "more-itertools", marker = "python_full_version < '3.14'" },
{ name = "openapi-schema-validator", marker = "python_full_version < '3.14'" },
{ name = "openapi-spec-validator", marker = "python_full_version < '3.14'" },
{ name = "typing-extensions", marker = "python_full_version < '3.14'" },
{ name = "werkzeug", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/65/ee75f25b9459a02df6f713f8ffde5dacb57b8b4e45145cde4cab28b5abba/openapi_core-0.22.0.tar.gz", hash = "sha256:b30490dfa74e3aac2276105525590135212352f5dd7e5acf8f62f6a89ed6f2d0", size = 109242, upload-time = "2025-12-22T19:19:49.608Z" }
wheels = [
@ -4999,9 +4986,9 @@ name = "openapi-schema-validator"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonschema" },
{ name = "jsonschema-specifications" },
{ name = "rfc3339-validator" },
{ name = "jsonschema", marker = "python_full_version < '3.14'" },
{ name = "jsonschema-specifications", marker = "python_full_version < '3.14'" },
{ name = "rfc3339-validator", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" }
wheels = [
@ -5013,10 +5000,10 @@ name = "openapi-spec-validator"
version = "0.7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonschema" },
{ name = "jsonschema-path" },
{ name = "lazy-object-proxy" },
{ name = "openapi-schema-validator" },
{ name = "jsonschema", marker = "python_full_version < '3.14'" },
{ name = "jsonschema-path", marker = "python_full_version < '3.14'" },
{ name = "lazy-object-proxy", marker = "python_full_version < '3.14'" },
{ name = "openapi-schema-validator", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
wheels = [
@ -5879,15 +5866,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
]
[[package]]
name = "pfzy"
version = "0.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/5a/32b50c077c86bfccc7bed4881c5a2b823518f5450a30e639db5d3711952e/pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1", size = 8396, upload-time = "2022-01-28T02:26:17.946Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/d7/8ff98376b1acc4503253b685ea09981697385ce344d4e3935c2af49e044d/pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96", size = 8537, upload-time = "2022-01-28T02:26:16.047Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
@ -7163,16 +7141,16 @@ name = "redisvl"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "ml-dtypes" },
{ name = "coloredlogs", marker = "python_full_version < '3.14'" },
{ name = "ml-dtypes", marker = "python_full_version < '3.14'" },
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "pydantic" },
{ name = "python-ulid" },
{ name = "pyyaml" },
{ name = "redis" },
{ name = "tabulate" },
{ name = "tenacity" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" },
{ name = "pydantic", marker = "python_full_version < '3.14'" },
{ name = "python-ulid", marker = "python_full_version < '3.14'" },
{ name = "pyyaml", marker = "python_full_version < '3.14'" },
{ name = "redis", marker = "python_full_version < '3.14'" },
{ name = "tabulate", marker = "python_full_version < '3.14'" },
{ name = "tenacity", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/21/33/ab14865a0b2a31b1d003c29e7e8ea3a7a2f2c8ecb24e58e58d606e1f031b/redisvl-0.4.1.tar.gz", hash = "sha256:fd6a36426ba94792c0efca20915c31232d4ee3cc58eb23794a62c142696401e6", size = 77688, upload-time = "2025-02-21T22:51:41.389Z" }
wheels = [
@ -7406,7 +7384,7 @@ name = "rfc3339-validator"
version = "0.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
{ name = "six", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
wheels = [
@ -7855,20 +7833,20 @@ name = "semantic-router"
version = "0.1.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "aurelio-sdk" },
{ name = "colorama" },
{ name = "colorlog" },
{ name = "litellm" },
{ name = "aiohttp", marker = "python_full_version < '3.14'" },
{ name = "aurelio-sdk", marker = "python_full_version < '3.14'" },
{ name = "colorama", marker = "python_full_version < '3.14'" },
{ name = "colorlog", marker = "python_full_version < '3.14'" },
{ name = "litellm", marker = "python_full_version < '3.14'" },
{ name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "regex" },
{ name = "tiktoken" },
{ name = "tornado" },
{ name = "urllib3" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" },
{ name = "openai", marker = "python_full_version < '3.14'" },
{ name = "pydantic", marker = "python_full_version < '3.14'" },
{ name = "pyyaml", marker = "python_full_version < '3.14'" },
{ name = "regex", marker = "python_full_version < '3.14'" },
{ name = "tiktoken", marker = "python_full_version < '3.14'" },
{ name = "tornado", marker = "python_full_version < '3.14'" },
{ name = "urllib3", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/a9/1a689e916e8b280f1fd8fb335cc059be626a22fe4533baa045d32fcd6de5/semantic_router-0.1.15.tar.gz", hash = "sha256:328256ddc3c2b713101ec69561d6585aecbf1198ea3461e1486289d8c3a35288", size = 95605, upload-time = "2026-05-23T12:58:15.444Z" }
wheels = [