From 0ba155fb03b1ccd83a7012dd1741b87cb1164fce Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 15 Jul 2026 22:12:54 -0700 Subject: [PATCH] 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. --- .../cli/commands/autoroute/fuzzy_picker.py | 165 ++++++++++++++++++ .../client/cli/commands/autoroute/wizard.py | 20 +-- pyproject.toml | 4 +- .../client/cli/autoroute/test_fuzzy_picker.py | 27 +++ .../proxy/client/cli/autoroute/test_wizard.py | 22 ++- uv.lock | 146 +++++++--------- 6 files changed, 277 insertions(+), 107 deletions(-) create mode 100644 litellm/proxy/client/cli/commands/autoroute/fuzzy_picker.py create mode 100644 tests/test_litellm/proxy/client/cli/autoroute/test_fuzzy_picker.py diff --git a/litellm/proxy/client/cli/commands/autoroute/fuzzy_picker.py b/litellm/proxy/client/cli/commands/autoroute/fuzzy_picker.py new file mode 100644 index 00000000000..6e8d4980b7a --- /dev/null +++ b/litellm/proxy/client/cli/commands/autoroute/fuzzy_picker.py @@ -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"] diff --git a/litellm/proxy/client/cli/commands/autoroute/wizard.py b/litellm/proxy/client/cli/commands/autoroute/wizard.py index 89b53d8f9fd..70f50e76c86 100644 --- a/litellm/proxy/client/cli/commands/autoroute/wizard.py +++ b/litellm/proxy/client/cli/commands/autoroute/wizard.py @@ -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.") diff --git a/pyproject.toml b/pyproject.toml index 2c19bf64b4f..198f2403a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/test_litellm/proxy/client/cli/autoroute/test_fuzzy_picker.py b/tests/test_litellm/proxy/client/cli/autoroute/test_fuzzy_picker.py new file mode 100644 index 00000000000..44f74e66fe0 --- /dev/null +++ b/tests/test_litellm/proxy/client/cli/autoroute/test_fuzzy_picker.py @@ -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") diff --git a/tests/test_litellm/proxy/client/cli/autoroute/test_wizard.py b/tests/test_litellm/proxy/client/cli/autoroute/test_wizard.py index 75431a2c588..62007e42036 100644 --- a/tests/test_litellm/proxy/client/cli/autoroute/test_wizard.py +++ b/tests/test_litellm/proxy/client/cli/autoroute/test_wizard.py @@ -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: diff --git a/uv.lock b/uv.lock index 22e3a3feece..dde4c8a0caf 100644 --- a/uv.lock +++ b/uv.lock @@ -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 = [